|
CS 4513 Project 4Dragonfly Wings - Extending the Dragonfly Game Engine with NetworkingDue date: Sunday, February 28th, by 11:59pm |
You will extend the Dragonfly game engine with network support and create a two player, 2d shoot-em up game using your new Dragonfly.
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.
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.
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:
Program flow is then:
onEvent()
.
Since the Host/Client had registered interest in network events,
they then are notified a complete network message has arrived.
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: the object_type
, as a string
(e.g., "Saucer")
+ else for delete or update, then next is: the object_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 strings
Note, 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.
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.
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:
Assignments are to be submitted electronically on the day due.
All submissions must include the following:
A source code package:
.h
files.
Game code for Saucer Shoot 2.
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).
Usezip
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 Wingsand select an available slot. Demonstrations will be:
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.
Send all questions to the TA mailing list (cs4513-staff at cs.wpi.edu).