With the core Standoff application under my belt, I was finally able to move on to the networking portion of the game’s development. I gravitated towards working with network sockets in part due to my background using them at my previous job. The Standoff application never necessitated a complex implementation of networking - a simple buffer of bits suffices to transmit any message between client and server. Messages rarely exceed a byte in size, and only when asked to display a game name of considerable length. The User Datagram Protocol (UDP) provides the kind of low-latency gameplay that we have come to expect from networked games, additionally serving as a flexible and low-level foundation for future iterations of my model.
Luckily, I was able to locate another tutorial series to aid in my development process - this time provided by the blog “Gaffer on Games” (http://gafferongames.com/networking-for-game-programmers/). Glenn Fiedler’s tutorials feature an in-depth look at the construction of a platform-agnostic model for virtual connection, which I distilled into the raw client-server approach featured in the completed Standoff application.
// basic serialization
unsigned char* packet = new unsigned char[MAX_PACKET_SIZE + HEADER_SIZE];
packet[0] = (unsigned char)(mProtocolId >> 24);
packet[1] = (unsigned char)((mProtocolId >> 16) & 0xFF);
packet[2] = (unsigned char)((mProtocolId >> 8) & 0xFF);
packet[3] = (unsigned char)((mProtocolId) & 0xFF);
std::memcpy(&packet[HEADER_SIZE], data, MAX_PACKET_SIZE);
bool ret_val =
mSocket.send(to_address, packet, MAX_PACKET_SIZE + HEADER_SIZE);
// basic deserialization
unsigned char* packet = new unsigned char[MAX_PACKET_SIZE + HEADER_SIZE];
int bytes_read =
mSocket.receive(from_address, packet, MAX_PACKET_SIZE + HEADER_SIZE);
std::memcpy(data, &packet[HEADER_SIZE], MAX_PACKET_SIZE);
I divided my networking module into three parts: at the top-level, the ‘ConnectHandler’ class presides over startup and shutdown of the connection process, in addition to performing a set of serialization and deserialization transformations on every sent message. A ConnectHandler_c object owns an ‘LSocket’, a wrapper class designed in the style of the ‘LTexture’ class used in my project’s graphics module. An LSocket contains platform-specific implementations for the raw network socket, and encapsulates the socket’s life cycle as well as capabilities such as sending and receiving. Last but not least is the ‘Address’ class, which stores any data required to connect with a particular device - be it client or server. Development for each of these parts relied in no small part on the aforementioned tutorials by Fiedler. His posts are more informative than anything I could write on the subject, with the first two entries of his Networking for Game Programmers series proving the most informative.

The next step of my development process involved the construction of both client and server endpoints for my networking pipeline. These new modules were designed to leverage the completed ConnectHandler_c in order to establish a network connection between two separate instances of the Standoff application. ???
enum Request_e
{
INVALID = 0,
CREATE_GAME = 1,
FIND_GAMES = 2,
JOIN_GAME = 3,
MOVE_ACTION = 4,
SHOOTOUT = 5
};
Communication between the server and each client is facilitated via delivery and receipt of “requests”. The ‘StandoffClient’ class only deals in messages of one of 5 types, as delineated by the Request_e enumeration. Each bitstream message begins with an integer representing the type of request that it contains. Upon receiving a message, the ‘StandoffServer’ class reacts in one of the following ways:
-
CREATE_GAME: push a new game onto the list of active games, with the address of the sender linked to Player 1. Additionally, StandoffServer_c assigns each game a unique ID upon creation.
-
FIND_GAMES: for each active game, reply to the sender with a message containing the game’s relevant data.
-
JOIN_GAME: if the message contains the ID of an open game, close that game and link the address of the sender to Player 2.
-
MOVE_ACTION: deserialize the move and check it against the saved game state, applying it on a successful validation; pass the message along to the game’s other player.
-
SHOOTOUT: call a shootout on the server’s stored copy of the sender’s game; pass the message along to the game’s other player.
Available actions correspond with the enumerators of Request_e, with the exception of the INVALID request identifier.
// from: StandoffApp_c::update()
switch (mCurrentGame.getCurrentMove().mCurrentAction)
{
case Game_n::MOVE :
case Game_n::DEPLOY :
case Game_n::ROTATE :
{
data[0] = 4; // Request_e::MOVE_ACTION
data[1] = mCurrentGame.getCurrentMove().mPrevPosition.first;
data[2] = mCurrentGame.getCurrentMove().mPrevPosition.second;
data[3] = mCurrentGame.getCurrentMove().mMovedPiece->getPosition().first;
data[4] = mCurrentGame.getCurrentMove().mMovedPiece->getPosition().second;
data[5] =
static_cast<int>(mCurrentGame.getCurrentMove().mMovedPiece->getDirection());
send_flag = true;
break;
}
case Game_n::SHOOTOUT :
{
data[0] = 5; // Request_e::SHOOTOUT
send_flag = true;
break;
} }
With the client now built as a layer around the original ‘StandoffApp’ class, I needed to implement some changes to bring the old application logic up to speed. In order to facilitate a client’s communication with the server, I added a methods for sending and receiving request messages - StandoffApp_c::update and listen, respectively. The StandoffApp only needs to handle MOVE_ACTION and SHOOTOUT messages, as the remaining messages are only used prior to a game’s start. Since move, deploy, and rotate actions are all stored identically in the form of a ‘Move’ structure, the server can apply each MOVE_ACTION request in the same way, resulting in cleaner code all around.
// from: StandoffApp_c::listen()
if (mConnectHandler.receiveData(mServerAddress, data) > 0)
{
receive_complete = true;
switch(static_cast<ConnectHandler_n::Request_e>(data[0]))
{
case ConnectHandler_n::MOVE_ACTION :
{
std::pair<int, int> start_position =
std::make_pair((int)data[1], (int)data[2]);
const std::vector<Game_n::PiecePtr>& pieces =
(mCurrentGame.mCurrentTurn % 2) ? mCurrentGame.getPlayer1Pieces()
: mCurrentGame.getPlayer2Pieces();
std::vector<Game_n::PiecePtr>::const_iterator it;
for (it = pieces.begin(); it != pieces.end(); ++it)
{
if ((*it)->getPosition() == start_position
&& (*it)->getPlayState() != Piece_n::DEAD)
{
mCurrentGame.setCurrentPiece(*it);
mCurrentGame.move(std::make_pair((int)data[3], (int)data[4]));
mCurrentGame.rotate(static_cast<Piece_n::Direction_e>(data[5]));
mCurrentGame.nextPlayer();
}
}
break;
}
case ConnectHandler_n::SHOOTOUT :
{
mCurrentGame.shootout2();
mCurrentGame.nextPlayer();
break;
} } }
Given the types of issues common to most networked multiplayer games, I was understandably worried about the security of my application, especially because of my intent to distribute Standoff amongst my friends. Based on my decision to use a bit buffer for message transmission, my main concern was the possibility of a buffer overflow allowing for an attacker to seize control of the program’s execution. [3]