Storing Player Information
Players in the game are only
allowed to move around and swing their weapons (hitting
other players). The server will want to track every player’s current state
(walking,
standing still, swinging their weapons, or being hurt), the coordinates in the
world,
the direction they are facing, and the speed they are walking (if they are
walking).
This player data is stored
inside a structure called sPlayer. Because all connected
players in the game need their own set of unique data, an array of sPlayer
structures
are allocated to store the information. Both the number of player structures
to allocate and the number of players to allow to join the game session are
stored
in the macro MAX_PLAYERS, which is currently set to 8.
The sPlayer structure is as
follows (with supporting state definition macros):
#define MAX_PLAYERS 8 // Maximum number of players allowed to be connected at once
#define STATE_IDLE 1
#define STATE_MOVE 2
#define STATE_SWING 3
#define STATE_HURT 4
typedef struct sPlayer
{
bool connected;
char name[256];
DPNID player_id;
long last_state;
long last_update_time;
long latency;
float x_pos, y_pos, z_pos;
float direction;
float speed;
///////////////////////////////////////////////////////////////
sPlayer()
{
ZeroMemory(this, sizeof(*this));
}
} *sPlayerPtr;
There’s not much to the sPlayer
structure; you have a flag if the player is connected,
the name of the player, the player’s DirectPlay identification number, the
player’s current state (as defined by the state macros), time of last state
change,
network latency value, the player’s coordinates, direction, and walking speed.
The variables in sPlayer are
self-explanatory, except for latency. Remember that
latency is the delay resulting from network transmission. By storing the time it
takes
for a message to go from the server to the client (and vice versa), time-based
calculations
become more synchronized between the server and client.
Speaking of time-based
calculations, that's the purpose of the
last_update_time variable. Whenever
the server updates all players, it needs to know the time that has elapsed
between
updates. Every time a player state is changed (from the client), the
last_update_time variable is set
to the current time (minus the latency time).
Time is also used to control
actions. If a player swings a weapon, the server refuses
to accept further state changes from the client until the swing weapon state is
cleared. How does the state clear? After a set amount of time, that’s how! After
one
second passes, the update player cycle clears the player’s state back to idle,
allowing
the client to begin sending new state-change messages.
On the subject of sending
messages, take a look at how the server deals with the
incoming network messages.
Handling Messages
You’ve already seen DirectPlay
messages in action, but now you focus on the game
action messages (state changes). Because DirectPlay has only three functions of
interest when handling incoming network messages (create_player, destroy_player,
and
receive), the server will need to convert the incoming networking message to
messages
more suited to game-play.
The server receives messages
from clients via the DirectPlay network’s receive function.
Those messages are stored in the pReceiveData buffer contained within the
DPNMSG_RECEIVE structure passed to the receive function. That buffer is cast
into a
more usable game message, which is stuffed into the game message queue.
The server game code doesn’t
deal directly with network messages. Those are handled
by a small subset of functions that take the incoming messages and convert
them into game messages (which are entered into the message queue). The server
game code works with those game messages.
Because there can be many
different types of game messages, a generic message
container structure is needed. Each message starts with a header that stores
the type of message, the total size of the message data (in bytes) including the
header, and a DirectPlay player identification number that is usually set to the
player sending the message.
I’ve taken the liberty of
separating the header into another structure, making
it possible to reuse the header in every game message:
#define
MESSAGES_PER_FRAME 64 // number of messages to process per frame
#define MAX_MESSAGES 1024 // number of message to allocate for message queue
typedef struct
sMsgHeader
{
long type; // type of message (MSG_*)
long size; // size of data to send
DPNID player_id; // player performing action
} *sMsgHeaderPtr;
Because there can be many
different game messages, you first need a generic message
container capable of holding all the different game messages. This generic
message container is a structure as follows:
typedef struct
sMsg // the message queue message structure
{
sMsgHeader header;
char data[512];
} *sMsgPtr;
Pretty basic, isn’t it? The
sMsg structure needs to contain only the message header
and an array of chars used to store the specific message data. To use a specific
message,
you can cast the sMsg structure into another structure to access the data.
For example, here is a
structure that represents a state-change message:
typedef struct
sStateChangeMsg
{
sMsgHeader header;
long state; // State message (STATE_*)
float x_pos, y_pos, z_pos;
float direction;
float speed;
long latency;
} *sStateChangeMsgPtr;
To cast the sMsg structure
that contains a state-change message into a usable
sStateChangeMsg structure, you can use this code bit:
sMsg Msg; //
Assuming message contains data
sStateChangeMsg *scm = (sStateChangeMsg*) Msg;
// Access
state-change message data
scm->state = STATE_IDLE;
scm->direction = 1.57f;
In addition to the
state-change message, the following message structures are used
in the network game:
typedef struct
sCreatePlayerMsg
{
sMsgHeader header;
float x_pos, y_pos, z_pos; // create player coordinates
float direction;
} *sCreatePlayerMsgPtr;
typedef struct sRequestPlayerInfoMsg
{
sMsgHeader header;
DPNID request_player_id; // which player to request
} *sRequestPlayerInfoMsgPtr;
typedef struct sDestroyPlayerMsg
{
sMsgHeader header;
} *sDestroyPlayerMsgPtr;
Each message also has a
related macro that both the server and client use. Those
message macros are the values store in the sMsgHeader::type variable. Those
message
type macros are as follows:
#define
MSG_CREATE_PLAYER 1
#define MSG_SEND_PLAYER_INFO 2
#define MSG_DESTROY_PLAYER 3
#define MSG_STATE_CHANGE 4
You see each message in action
in the sections “Processing Game Messages” and
“Working with Game Clients,” later in this chapter, but for now, check out how
the
server maintains these game-related messages.