Extending the Dragonfly Game Engine with Networking
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. This document provides guidlines for designing and implementing network support for Dragonfly from the user level (i.e., as game programmer code).
If you have not already, you might consider working through the Dragonfly tutorial available online through the Dragonfly Web page. Doing so will help you setup your development environment, as well as provide some background on Dragonfly syntax and structure.
Recommended are three classes in support
of Dragonfly networking: 1) a network
manager, 2) a network event, and 3) a sentry to
support network interaction. While these classes will not technically
be part of the Dragonfly engine (the engine
is an immutable library, unless you have the source from implementing
the engine from scratch), the new network classes can easily be
accessed through an additional library
libnetwork.a) you make and, hence, will behave as
if they are part of the engine.
Since the added classes are intended to function as part of the
engine, they should be placed
This requires code outside of the namespace (e.g., game code) to
df:: to access elements inside the namespace
(e.g., df::NetworkManager::getInstance()). Typically
large, 3rd-party libraries (such as a game engine) use namespaces to
help developers avoid conflicts in names their own code may use with
names the libraries use. The Dragonfly
namespace is meant to prevent potential name conflicts with game code.
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 class inherits from the Manager base class and is a singleton. Its main private attribute is a connected socket. To support a 2-player game (or a cloud-based game engine), only 1 socket is required. The class header file is provided below:
startup(), the NetworkManager initializes the
socket to -1 (indicating the NetworkManager is not yet connected).
shutdown(), if the socket is connected (greater than
0), it is closed via the system
isValid() is used by the Manager class to
determine if the derived manager (the NetworkManager, in this case)
accepts the event in question. It should return
only for network events (see the EventNetwork class below).
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.
The default can be specified via
assumption is also that an external naming service provides the IP
address and/or hostname where the server (game host) is running.
accept(), the following steps are done:
The complimentary method,
connect(), is invoked by the
client to connect to the server. As such, it takes as parameters the
server's hostname and port as
connect() does the following steps:
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
getSocket() actually returns the socket.
send() sends data through the connected socket
send() system call).
receive() receives data from the connected socket,
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.
isData() returns the amount of data (in
bytes) that is waiting in the network socket, leaving any data there
intact. If there is no data, zero is returned.
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 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.
The constructors should set the event type
setType() in the base Event class)
The other methods are to set and get the number of bytes, respectively.
If the NetworkManager was actually implemented in the engine, the engine would provided notification of all network events - i.e., when network data arrived, the engine 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 (called Sentry) that is derived from Object and registers for step events. Every step, Sentry polls the network manager and when network data has arrived since the last step generates network events. Since the Host/Client had registered interest in network events, it is then notified that network data has arrived.
The constructor registers for step events
registerInterest()). It should also be spectral
SPECTRAL) and invisible
eventHandler() checks if the event is a step event
and, if so, invokes the
doStep() method checks the socket to see if there
is at least an
int worth of data. If so, it expects
int to reflect the number of bytes in a complete
message (including this
intdoStep() generates a network event by calling
onEvent() is actually defined in the base Manager
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
writeLog() method has
variable argument formatting. Verbosity of logging can also be
setLogLevel() to set the log level and
an initial integer to
writeLog() to indicate the level of
the message. The LogManager is a singleton, so you need to
getInstance() to use it.
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. On Linux and Mac,
recv() call can be made
non-blocking behavior with the
MSG_DONTWAIT flag and the
socket made non-blocking with an
O_NONBLOCK. On Windows, a socket can be made
non-blocking with an
ioctlsocket() call with FIONBIO.
On Linux and Mac, 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.,
FIONREAD, &bytes)). On Windows, the equivalent call is
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
can be used to retrieve the data but leave it in the buffer for
Unlike many system calls, socket code works nearly the same on Windows, Mac and Linux. However, if trying to make the NetworkManager work on all three platforms (optional, not required), the following are Linux/Mac specific:And the following are Windows specific:
For namespace reasons, the syntax
:: may be needed
in front of system calls (e.g.,
Compilation errors such as "Redeclaration of class
ClassName..." (with the actual class name instead of ClassName)
typically occur if the header file, say
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
#define statements are identical.
Remember to error check all system calls
send()). Where appropriate, messages should be
written to the Dragonfly logfile (using the
Send all questions to the TA mailing list (cs4513-staff at cs.wpi.edu).