[Dragonfly]

CS 4513 Project 4

Dragonfly Wings - Extending the Dragonfly Game Engine with Networking

Due date: Friday, May 2nd, 11:59pm


Top | Synopsis | Overview | Details | 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


Overview

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.


Details

If you have not already, 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 modification you will make called Saucer Shoot 2.

Design of Dragonfly Networking

You will create two classes in support of Dragonfly networking: 1) a network manager and 2) a network event. While these classes will not technically be part of the Dragonfly engine (the engine is an immutable library, libdragonfly.a, unless you have implemented the engine from scratch already), the new network classes can easily be accessed through an additional library (e.g., libnetwork.a) you make and, hence, will behave as if they are part of the engine.

Network Manager

The Network Manager handles the internals of a game's network connection. As such, both a game client and a game server have a Network Manager. Like other Dragonfly managers, the NetworkManager inherits from the Manager base class and is a singleton. Its main private attribute is a connected socket. Since the requirements for this project are only for a 2-player game, only 1 socket is required. The class header file is provided below:

// 
// NetworkManager.h
// 
// Manage network connections to/from engine. 
// 

class NetworkManager : 
public Manager {

 private:
  NetworkManager();                       // Private since a singleton.
  NetworkManager(NetworkManager const&);  // Don't allow copy.
  void operator=(NetworkManager const&);  // Don't allow assignment.
  int sock;                               // Connected network socket.
  
 public:

  // Get the one and only instance of the NetworkManager.
  static NetworkManager &getInstance();

  // Start up NetworkManager.
  int startUp();

  // Shut down NetworkManager.
  void shutDown();

  // Accept only network events.
  // Returns false for other engine events.
  bool isValid(string event_type);

  // Block, waiting to accept network connection.
  int accept(string port = DRAGONFLY_PORT);

  // Make network connection.
  // Return 0 if success, else -1.
  int connect(string host, string port = DRAGONFLY_PORT);

  // Close network connection.
  // Return 0 if success, else -1.
  int close();

  // Send buffer to connected network.
  // Return 0 if success, else -1.
  int send(void *buffer, int bytes);

  // Receive from connected network (no more than bytes).
  // Return number of bytes received, else -1 if error.
  int receive(void *buffer, int bytes);

  // Check if network data.
  // Return amount of data (0 if no data), -1 if not connected or error.
  int isData();

  // Return true if network connected, else false.
  bool isConnected();

  // Return socket.
  int getSocket();
};

Upon startup(), the NetworkManager initializes the socket to -1 (indicating the NetworkManager is not yet connected). Upon shutdown(), if the socket is connected (greater than 0), it is closed via the system close() call. The isConnected() method checks if the socket is greater than 0, returning true of so, else returning false.

The accept() method undertakes setting up and then waiting for a TCP/IP socket connection from a client, somewhere else on the Internet. Typical of most Internet servers, the NetworkManager listens on a ``well-known'' port (for most network games, this is defined by the game developers) that the client uses when connecting. It also assumes an external naming service provides the IP address and/or hostname where the server (game host) is running. Inside accept(), the following steps are done:

  1. Create an unconnected socket to which a remote client can connect. A relevant system call is socket().
  2. Bind to the server's local address and port so a client can connect. A relevant system call is bind().
  3. Put the socket in a state to accept the other ``half'' of an Internet connection. A relevant system call is listen().
  4. Wait (block) until the socket connected. A relevant system call is accept().

The complimentary method, connect(), is invoked by the client to connect to the server. As such, it takes in the server's hostname and port as strings. When invoked, connect() does the following steps:

  1. Lookup the host name, translating it into the necessary system structure for connections. A relevant system call is getaddrinfo().
  2. Create a socket used by the client to connect to the server. A relevant system call is socket().
  3. Connect to the server. A relevant system call is connect().

The method isConnected() can be used by either the client or the server to tell if the NetworkManager has a connected socket. It returns true if the socket (sock) is greater than 0. The method getSocket() actually returns the socket.

The method send() sends data through the connected socket (using the send() system call).

The method receive() receives data from the connected socket, using the recv() system call). Receive is non-blocking, so if their is no data pending, the method does not block but returns. The receive() method has a "peek" option to read the data but leave the data in the socket.

Network Event

The network event is a "message" that is communicated by the NetworkManager to all Objects that have registered interest in a network event. Implementation of the EventNetwork class is fairly straightforward as the network event class is mostly a container. The proposed design, shown below, does not include the actual network data (although it could) since that is assumed to be pulled from the NetworkManager by any interested game Objects.

//                                                                              
// A "network" event, generated when a network packet arrives.                  
//                                                                              

#ifndef __EVENT_NETWORK_H__
#define __EVENT_NETWORK_H__

#include "Event.h"
  
#define NETWORK_EVENT "__network__"

class EventNetwork : public Event {
  
 private:
  int bytes;                    // number of bytes available                    
  
 public:
  // Default constructor.                                                       
  EventNetwork();

  // Create object with initial bytes.                                          
  EventNetwork(int initial_bytes);

  // Set number of bytes available.                                             
  void setBytes(int new_bytes);

  // Get number of bytes available.                                             
  int getBytes();
};

#endif // __EVENT_NETWORK_H__         

Network Interactions

If the NetworkManager was actually implemented in the engine, it would be providing notification of all network events - i.e., when network data arrived, it would generate an EventNetwork for all Objects that had registered. However, for this project, since the NetworkManager is implemented in game code, there needs to be a game object running on both the client and host that generates network events. This can be most easily done by a "sentry" object (call it Sentry) that is inherited from Object and registers for step events. Every step, Sentry polls the network manager and when network data has arrived since the last step, it generates network events by calling the NetworkManager's onEvent() method. Since the Host/Client had registered interest in network events, it is then notified network data has arrived.


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 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. Nukes are also optional. The game can start right into the gameplay and end right when a Hero is destroyed. 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 server, and 3) how inconsistencies between client and server game states are resolved. Note, for the game architecture, one of the players can interact on the server (effectively, this player's computer is hosting the game), 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(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() in order to serialize and deserialize any game-specific data. 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.
string toString(int i);

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

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

// Convert boolean to string, returning string.
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(string str, string find);

The toString() functions are self-explanatory. The match() method 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. The functions atoi() and atof() can be used to convert the resulting strings to numbers, 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, I encourage you 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!

Since Dragonfly uses curses, errors written to standard output (the screen), won't be displayed properly. Instead, you should print messages to the Dragonfly logfile. This is done with the LogManager's writeLog(). The writeLog() method has printf()-style variable argument formatting. The LogManager is a singleton, so you need to call getInstance() to use.

Remember that TCP is a stream-based protocol. As such, while 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.

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()).

If using TCP, remember TCP is a stream-oriented protocol, a client (or server) can receive part of a message (say, a serialized Object) even though the server sends only complete messages. You'll often 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.

Sockets are by default blocking, meaning if a process reads from them and there is no data available, the process blocks until there is data. Non-blocking behavior can be achieved with the MSG_DONTWAIT flag for a recv() call.

The ioctl() call can be used to see the number of bytes available in a socket before reading using the FIONREAD parameter as the second argument (e.g., ioctl(sock, FIONREAD, &bytes)).

By default, a recv() call that retrieves data from a socket removes it from the OS 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.

For namespace reasons, the syntax :: may be needed in front of system calls (e.g., ::send(...)).

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.

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.

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.

Important! Before you are ready to submit, make sure your game system works on the CCC machines.

All submissions must include the following:

Use tar with gzip to archive your files. For example:

        mkdir lastname-proj4
        cp * lastname-proj4 /* copy all the files you want to submit */
        tar czvf proj4-lastname.tgz lastname-proj4  /* bundle & compress */
Submit your assignment (proj4-lastname.tgz):
        /cs/bin/turnin submit cs4513 project4 proj4-lastname.tgz

If you need more information, see Using the turnin Program for additional help with turnin.


Grading

An approximate breakdown of grades is as follows:

Grading Guidelines
Component
Percent
Network Support
25%
Saucer Shoot 2
50%
Experiments
20%
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 barely 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, crashes consistently, or is 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 | Overview | Details | 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).