[Dragonfly]

CS 4513 Project 4

Dragonfly Wings - Extending the Dragonfly Game Engine with Networking

Due date: Sunday, February 28th, by 11:59pm


Top | Synopsis | Details | Networking | Saucer Shoot 2 | Hints | Experiments | Hand In | Grading

Synopsis

You will extend the Dragonfly game engine with network support and create a two player, 2d shoot-em up game using your new Dragonfly.

Goals

Objectives


Details

Dragonfly is a text-based game engine, primarily designed to teach about game engine development. While it is a full-featured game engine for stand-alone computer game development, it does not provide any support for networking. In this project, you will remedy that by designing and implementing network support ("wings") for Dragonfly. Once networking is implemented and integrated into the engine, you will extend a single-player game using a traditional client-server game architecture to become a fully-distributed, two player, networked game.

You should work through Dragonfly tutorial available online through the Dragonfly Web page. Doing so will help you setup your development environment, as well as provide necessary background on game programming with Dragonfly. Furthermore, the tutorial game, Saucer Shoot, will serve as the basis for the two player game you will create, called Saucer Shoot 2.

You should next design, implement and test classes in support of Dragonfly networking. Refer to the online document Dragonfly Networking for details.


Design of Saucer Shoot 2

Saucer Shoot 2 - the two player, networked version of Saucer Shoot - can, and should, make extensive use of the existing Saucer Shoot game, taking advantage of game sprites and sounds and game objects provided. The base requirement must include the core gameplay (i.e., ships shooting saucers with starfield and points). Thus, the GameStart screen and the GameOver animation are not required. The game can start right into the gameplay and end right when a Hero is destroyed. Nukes are also optional. Note, including these optional elements can count towards the Miscellaneous points in the grading section.

For functionality, what Saucer Shoot 2 requires is for two players to play Saucer Shoot simultaneously from different, independent computers (both computers connected to the Internet). You can use some creativity in providing new gameplay (e.g., competitive or cooperative), with a suggested change to have both players on the same side shooting at the same Saucers, but competing for points. A possible screenshot showing competitive Heroes on the same side is shown below.

Saucer Shoot 2

The actual design and implementation of Saucer Shoot 2 is up to you. There are many decisions that can be made in implementing an architecture for a multiplayer game such as Saucer Shoot 2, including: 1) what Objects are synchronized and how often, 2) how player actions on the client are transmitted to the host, and 3) how inconsistencies between client and host game states are resolved. Note, for the game architecture, one of the players can play on the host where the other player is the client that connects.

A significant task in synchronizing states in a network game (and in other distributed systems) is transferring data that needs to be synchronized between nodes. For a network game, and many other object-oriented systems, this means synchronizing the attributes of objects across computer systems. This is often done through serializing (also known as marshalling), where an object's state is translated into a format that can be transmitted across a network connection and reconstructed on another computer. Dragonfly provides several built-in methods for the Object class to make this easier, shown below:

//
// Object class methods to support serialization
//

// Serialize Object attributes to single string.
// e.g., "id:110,is_active:true, ...                                              
// Only modified attributes are serialized (unless all is true).                  
// Clear modified[] array.                                                        
virtual string serialize(bool all = false);                                       

// Deserialize string to become Object attributes.                                
// Return 0 if no errors, else -1.                                                
virtual int deserialize(string s);    

// Return true if attribute modified since last serialize.                      
bool isModified(enum ObjectAttribute attribute);

The method serialize() produces a string of Object attributes in key:value pairs separated by commas. For example, the attribute and value for the Object id is represented as "id:110,". By default, the serialization string returned contains the attributes that have been modified since the last call to serialize(), unless invoked with the boolean all as true.

The counterpart method, deserialize(), takes in a string produced by serialize() (presumably, on a separate computer) and parses it into the resulting key:value pairs, setting all Object attributes as appropriate.

The method isModified() queries individual methods to see if they have been modified or not (e.g., isModified(df::ID)), returning true if the Object id has changed since the last call to seralize(). When an Object is first created, all attributes indicate as having been modified.

Any derived classes (e.g., game programmer objects, such as Saucers) need to implement their own versions of serialize() and deserialize() if there is any game-specific data to serialize and deserialize. They can (and should) call the parent class (de)serialize(). The utility functions below, provided by Dragonfly, may be useful when making a derived Object in a network game.

// Convert integer to string, returning string.
std::string toString(int i);

// Convert float to string, returning string.
std::string toString(float f);

// Convert character to string, returning string.
std::string toString(char c);

// Convert boolean to string, returning string.
std::string toString(bool b);

// Match key:value pair in string in str, returning value.
// If str is empty, use previously parsed string str.
// Return empty string if no match.
string match(std::string str, std::string find);

The toString() functions are self-explanatory. The match() function looks for exactly one key in an input string, returning the associated key paired with it as a string. The first call to match() should be made with the serialized string, whereupon match() parses the string and stores the key:value pairs internally (as static variables). Subsequent calls to match() should be invoked with an empty string as the first argument, matching each of the attributes as a key until done parsing (wherin match() returns an empty string). The functions atoi() and atof() can be used to convert the resulting strings to numbers, int and float, respectively, if needed.

There are many choices as to how and when to serialize, send, receive and then deserialize game objects. However, some suggestions that are relevant for many network games, and are certainly relevant for this project, are as follows:


Hints

For asking questions on Dragonfly, you are encouraged to use the Dragonfly Q&A forum. There, you can ask and answer questions, comment and vote for the questions of others and their answers. Both questions and answers can be revised and improved. Questions are tagged with the relevant keywords to simplify future access and organize the accumulated material - you might be able to find the answer to your question before you ask it!

Dragonfly has a built-in logfile that can be helpful for development and debugging. Printing to the Dragonfly logfile is done with the LogManager's writeLog() method. The writeLog() method has printf()-style variable argument formatting. The LogManager is a singleton, so you need to call getInstance() to use it.

If using TCP, remember TCP is a stream-oriented protocol. This means a Host may intend to transmit a serialized Object as a single message, it may be chunked such that the Client only gets part of the message at a time. TCP provides the entire message eventually, in order, but it does not guarantee providing the data in the same chunk size in which it was transmitted. You'll need to be sure a complete message has arrived before processing. If using UDP, message boundaries are preserved, but remember that messages may be lost.

By default, a recv() call that retrieves data from a socket removes it from the operating system buffer such that subsequent reads do not get the same data. The MSG_PEEK can be used to retrieve the data but leave it in the buffer for subsequent reads.

Compilation errors such as "Redeclaration of class ClassName..." (with the actual class name instead of ClassName) typically occur if the header file, say ClassName.h, has been included from multiple source files. This error occurs most often for utility-type classes (and functions) that are used by multiple other classes (e.g., the NetworkManager). The fix is typically to use conditional compilation directives for the compiler pre-processor. If conditional compilation directives are in use, they should be checked that the names used in the #ifndef and #define statements are identical.

Remember to error check all system calls (e.g., send()). Where appropriate, messages should be written to the Dragonfly logfile (using the LogManager writeLog()).

To smooth out unexplained visual "glitches" in game state synchronization, it may be effective to occasionally synchronize all Objects on the client with those on the host.

For convenience in development, consider testing connections via localhost before actually testing with separate computers. In so doing, you may want to limit Dragonfly from capturing keyboard input unless the mouse is active in the game Window. This can be done as in the below code snippet:

// Check if mouse outside game window.
sf::RenderWindow *p_win = df::GraphicsManager::getInstance().getWindow();
sf::Vector2i lp = sf::Mouse::getPosition(*p_win);
if (lp.x > df::Config::getInstance().getWindowHorizontalPixels() ||       
    lp.x < 0 ||
    lp.y > df::Config::getInstance().getWindowVerticalPixels() ||
    lp.y < 0) {
  // Outside window so don't capture input.
} else {
  // Inside window so capture input.
}
                    

It can be helpful for various aspects of the game to know if the game is in "Host" or "Client" mode. This can be done by having a singleton class that sets the role (host/client) when initialized and can be later queried for the role during gameplay. A possible design (header file) for such a class is below.

//                                                                                 
// Role class                                                                      
//                                                                                 
// Indicate whether game is Host or Client.                                        
//                                                                                 

#ifndef __ROLE_H__
#define __ROLE_H__

class Role {

 private:
  Role();                       // Private since a singleton.                      
  Role (Role const&);           // Don't allow copy.                               
  void operator=(Role const&);  // Don't allow assignment.                         
  bool is_host;                 // True if hosting game.                           

 public:
  // Get the one and only instance of the Role.                                    
  static Role &getInstance();

  // Set host.                                                                     
  void setHost(bool is_host = true);

  // Return true if host.                                                          
  bool isHost() const;
};
                        
#endif // __ROLE_H__                                

Although not part of this assignment, you can learn a lot about game programming, and programming in general, by making your own game engine. If you are interested in building your own Dragonfly, you might consider the book, Dragonfly - Program a Game Engine from Scratch.

The slide deck for project 4 has additional background information on Dragonfly that is not in this writeup.


Experiments

After you have successfully implemented (and tested!) your Dragonfly network extensions and two-player Saucer Shoot 2 game, you then design experiments to measure: 1) the network data rate from server to client, 2) the network data rate from client to server, 3) the in-game round trip time. Note! If you with one client running over X, you want to measure the network game traffic, not the display traffice (i.e., ignore the X traffic).

For all measurements, you need to consider in-game aspects, such as data rate over time and gameplay during measurements. In other words, the amount of network traffic will depend upon the number of Objects in the game world (e.g., more and more saucers as time progresses) and the player actions (e.g., frantic moving and shooting) may depend upon the same.

To measure the network data rates, consider instrumenting your code to write data out to a logfile for each packet sent/received. Analysis would then provide information on packet sizes, packet rates and bitrates. Provide at least one graph of network bitrate (e.g., Kb/s) over time.

To measure in-game round trip time, consider timing from when a player inputs a key until the result of that action is drawn to the screen. Logfile messages placed at the right points in the client-side code should be able to help ascertain this. Multiple measurements should be provided, with analysis on the average and standard deviation, as well as the minimum and maximum. The system call gettimeofday() can be used to obtain the system time.

When your experiments are complete, you must turn in a brief (1-2 page) write-up with the following sections:

  1. Design - describe your experiments, including: a) how you instrumented/measured your system, b) how many runs you performed; c) what the system conditions were like; d) and any other details you think are relevant.
  2. Results - depict your results clearly using a series of tables or graphs. Provide statistical analysis where appropriate.
  3. Analysis - interpret the results. Briefly describe what the results mean, including scalability to more players and playability over networks, and what you think is happening and any subjective analysis you wish to provide.


Hand In

Assignments are to be submitted electronically on the day due.

All submissions must include the following:

Before submitting, "clean" your code (i.e., do a "make clean") removing the binaries (executables and .o files).

Use zip to archive your files. For example:
// put everything in it's own directory
mkdir lastname-proj4

// copy all the files you want to submit
cp * lastname-proj4

// package and compress
zip -r proj4-lastname.zip lastname-proj4

To submit your assignment (proj4-lastname.zip), log into the Instruct Assist website:

https://ia.wpi.edu/cs4513/

Use your WPI username and password for access. Visit:

Tools → File Submission

Select "Project 4" from the dropdown and then "Browse" and select your assignment (proj4-lastname.zip).

Make sure to hit "Upload File" after selecting it!

If successful, you should see a line similar to:

 Creator    Upload Time             File Name        Size    Status   Removal
 Claypool 2016-02-21 12:11:23  proj4-claypool.zip   2508 KB  On Time  Delete

IMPORTANT!

After submitting your project, you must arrange a time to provide a demonstration with the TA. On the Instruct Assist website, visit:

Tools → Demonstrations - List → Project 4 - Dragonfly Wings
and select an available slot. Demonstrations will be:


Grading

A grading guide provides a detailed point breakdown for the individual project components.

An approximate breakdown of grades is as follows:

Grading Guidelines
Component
Percent
Network Support
30%
Saucer Shoot 2
60%
Experiments
5%
Miscellaneous
5%

The "Networking Support" category primarily includes socket-based code that integrates with Dragonfly as a Manager.

The "Saucer Shoot 2" category includes integrating the networking aspects, including distributed synchronization of Objects, into Saucer Shoot. It also includes enhancing the gameplay of Saucer Shoot to incorporate a second player.

The "Miscellaneous" category is for flexibility in assigning points across the networking support and the game. Extra networking features (e.g., multiple sockets, TCP and UDP, multicast), game enhancements (e.g., GameOver, GameStart and Nukes, UI for Host/Client) or experiments (e.g., range of system conditions such networking, end-host, gameplay types) will produce points here. If everything is done to a basic, minimal level, there will be no points earned in this category.

Below is a general grading rubric:

90-100 The submission clearly exceeds requirements. The functionality is fully implemented and is provided in a robust, bug-free fashion. Full client-host synchronization is evident in the game. Gameplay is effective and fun for two players. All code is well-structured and clearly commented. Experiments effectively test all required measurements. Experimental writeup has the three required sections, with each clearly written and the results clearly depicted.

89-80 The submission meets requirements. The basic functionality is implemented and runs as expected without any critical bugs. Client-host synchronization is effective, but there may be occasional visual glitches that are not critical to gameplay. Gameplay is effective for two players. Code is well-structured and clearly commented. Experimental writeup has the three required sections, with details on the methods used and informative results.

79-70 The submission minimally meets requirements. Functionality is mostly implemented, but may not be fully implemented and/or may not run as expected. Client-host synchronization provides occasional visual glitches, some may be critical to gameplay. Gameplay supports two players, but to a limited extent. Code is only somewhat well-structured and commented. Experiments are incomplete and/or the writeup does not provide clarity on the methods or results.

69-60 The project fails to meet requirements in some places. Networking support is missing critical functionality or robustness. The engine may crash occasionally. The game does not support complete or robust gameplay for two players. Code is lacking in structure or comments. Experiments are incomplete and the writeup does not provide clarity on the methods or results.

59-0 The project does not meet core requirements. The networking extensions cannot compile, crash consistently, or are lacking many functional features. The game does not compile or does not support two player interaction. Code is only lacking structure and comments. Experiments are incomplete with a minimal writeup.


Top | Synopsis | Details | Networking | Saucer Shoot 2 | Hints | Experiments | Hand In | Grading

Return to 4513 Home Page

Send all questions to the TA mailing list (cs4513-staff at cs.wpi.edu).