Description
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
- Gain experience using fundamental networking code.
- Acquire familiarity with networking for a game engine.
- Understand the implications of distributing a shared, virtual world on multiple computers.
- Realize the implementation of a distributed system.
Objectives
- Implement networking (TCP/IP) socket code from scratch.
- Design and implemented network functionality for a game engine.
- Implement the distribution of state in a virtual world.
- Extend a single player game to be a two player, networked game with a client-server architecture.
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](https://web.cs.wpi.edu/~cs4513/c16/projects/proj4/saucer-shoot-2.png)
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:
- Only synchronize “important” Objects and associated events. For example, a player’s Hero ship being destroyed is an important (perhaps, the most important) event and should be serialized across computers. Stars, on the other hand, only provide decoration and, as such, do not need to be synchronized at all. Some guidelines are shown in the table below:
Synchronize Don’t Synchronize Saucer creation/destruction Stars Bullet creation/destruction Object movement that velocity handles Hero creation/destruction Explosions Points increase Reticle Above Object position changes Some of the guidelines above are subtle, such as the creation of Explosions. These could be handled by creating an Explosion on the host, then synchronizing with the client. However, it can also be handled by synchronizing the destruction of the Saucer (which must be done, anyway) where the destructor in each Saucer (e.g.,
~Saucer()
) creates it’s own Explosion object. Both client and host would then automatically destroy the Explosion when its animation finished, obviating the need for synchronization.Note, in theory, having the same random seeds on client and host could mean that even random events (controlled by
rand()
) do not have to be synchronized, but in practice, this requires event actions to take place at specific game clock times. Such timed delivery of events is not currently supported by Dragonfly. - There are at least two options for incorporating player input on the client. The first option is to update the player’s ship on the client and then synchronize this Object with the host. However, this requires the host, having the authoritative representation of the game world, to have a way “roll back” an action that is not allowed (say, because the player’s ship was blocked by an opponent). Instead, the second, recommended option is to capture keystrokes and mouse actions normally at the client, but then send the actions to the host. The host then receives the data, generates network events, where the host re-generates appropriate keyboard and mouse events for the client Hero using the host’s authoritative game world.
- A recommended design includes creating a Host object (derived from Object) that runs on the server computer and a Client object (also derived from Object) that runs on the client. Both register for interest in step events and network events. A Sentry object (also derived from Object) runs on both the client and host and registers for interest in step events.
Using this design, the two-player game would startup as follows:
- Start Dragonfly
- GameManager startUp()
- Start NetworkManager
- NetworkManager startUp()
- Start Sentry
- new Sentry()
- If “host” then Start Host
- new Host
- Else Start Client
- new Client
Program flow is then:
- The host game is started first, whereupon the Host (using the NetworkManager) readies the computer for a connection, while the Client (also using the NetworkManager) starts afterward and connects to the Host.
- Once connected, the Host creates the initial game Objects. These can either be synchronized with the Client (sent automatically) or the Client can create the same initial game Objects.
- Each game step, the Sentry polls the network manager and if a complete message has arrived since the last step, it generates network events by calling NetworkManager
onEvent()
. Since the Host/Client had registered interest in network events, they then are notified a complete network message has arrived. - In general, each game step, the Host checks all game objects to see which ones are new (their Object id’s are modified) and need to be synchronized via the NetworkManager.
- When there is a network event, the Client receives any serialization data over the network, updating Objects as appropriate.
- The Client registers for interest in the keyboard and mouse (with the InputManager) and sends appropriate input to the Host.
- The Host receives input sent by the Client, generating network events to game objects (e.g., the Client Hero) to handle, as appropriate.
- Start Dragonfly
- The Client may only be communicating player input (e.g., keystrokes) to the Host, but the Host needs to communicate at least several types of messages (e.g., add object, update object, destroy object, game over). A message format (a core component of any client-server protocol) should be designed ahead of time, for both Host-to-Client communication and Client-to-Host communication. Message types can be setup as an
enum
. A suggested format for using them is:Header:
+size
: the entire message size, in bytes, as an integer
+message_type
: the enum message type (effectively an integer)For Host-to-Client:
+ if add object, then next is: theobject_type
, as a string (e.g., “Saucer”)
+ else for delete or update, then next is: theobject_id
, as an integer
Body:
+ For add and update, this is the serialized string.For Client-to-Host:
+ if keyboard, then next is: the key value as a string
+ else it’s a click mouse so the next is: mouse-x and mouse-y as stringsNote, mouse movements do not need to be sent from the Client to the Host, only when there is a mouse click.
By having the size of the message first, the Sentry can “peek” at the network data, not pulling it from the socket until there are at least
size
bytes available. At that time, at least one message is complete and can be processed.After pulling the message from the socket (via the NetworkManager
receive()
), the Host or Client can subsequently check the message type and take appropriate actions.
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:
- 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.
- Results – depict your results clearly using a series of tables or graphs. Provide statistical analysis where appropriate.
- 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:
- A source code package:
- All code necessary to build your game engine modification (e.g., the NetworkManager). Note! Make sure your code is well-structured and commented. Failure to do so will result in a loss of points.
- Any other support files, including
.h
files. - Note! You do not need to include the SFML libraries or header files nor the Dragonfly engine or header files.
- A Makefile/Project file for building your game engine modification.
- Game code for Saucer Shoot 2.
- All code necessary to build your game. Note! Make sure your code is well-structured and commented. Failure to do so will result in a loss of points.
- Sprites needed for the game (even if these are the standard ones used in the tutorial).
- A Makefile/Project file for building your game.
- A README.txt file explaining: platform, how to compile. The README must also explain how to run your game. Note, the README must also include the one place to look for extra points to be assigned (see gradinge guide. Be sure to provide anything else needed to understand (and grade) your project.
- An EXPERIMENT.pdf file with your experiment writeup. Format must be pdf.
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:
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:
- In the Zoo lab (unless arranged in advance to be elsewhere)
- Done within 15 a minute slot
- Will be done on computers there (remotely logging in as needed) OR on laptop(s) students bring in
- Will include code walkthrough of Network Manager functionality
- Show successful start up of the games (Host and Client)
- Include robust, two-player gameplay, and graceful shutdown
Grading
A grading guide provides a detailed point breakdown for the individual project components.
An approximate breakdown of grades is as follows:
Grading Guidelines | |
---|---|
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.