Working with Game Clients
The client application
(referred to as the client) is the conduit between the gaming
server and the player. The client accepts the user’s input and forwards it to
the server.
Between updates from the server, the client updates itself based on what little
information
it has—the player’s movement, other players’ movements, NPC actions, and so on.
The client uses graphics,
sound, and input-processing to work its magic. However,
if you were to strip away the graphics and sound, you would be left with a
rather
bland application. This “dumb” client structure might look unworthy, but believe
me, it will work perfectly for your game project.
To use the Client application,
you can follow these steps:
1. Locate and run the Client
application. The Connect to Server dialog box
(shown in Following snap) appears.
|
Besides
picking an adapter and entering a player name, you’ll need to know the
server’s IP address in order to connect and play the game. |
2. In the Connect to Server
dialog box, enter the host’s IP address, select an
adapter, and enter your player’s name.
3. Click OK to begin the game
and connect to the server.
The client works almost
identically to the server in some respects, the first of which
is dealing with players.
Handling Player Data
The client, much like the
server, uses an sPlayer structure that contains the information
about each connected player in the game. This time, however, information is
needed to track the 3-D object for drawing the player (as well as the weapon)
and
the player animation being played. Other than that, you can see many
similarities
between the sPlayer structure being used by the client and server. Take a look
at
the declaration of the client’s sPlayer structure (along with supporting
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
#define ANIM_IDLE 1
#define ANIM_WALK 2
#define ANIM_SWING 3
#define ANIM_HURT 4
typedef struct sPlayer
{
bool connected;
DPNID player_id;
long last_state;
long last_update_time;
long latency;
float x_pos, y_pos, z_pos;
float direction;
float speed;
cObject body;
cObject weapon;
long last_anim;
///////////////////////////////////////////////////////////////
sPlayer()
{
connected = false;
player_id = 0;
last_anim = 0;
last_update_time = 0;
}
} *sPlayerPtr;
Again, an array of sPlayer structures is allocated to hold the player
information.
Each player is allowed to use a separate Graphics Core object for the
character’s
body and weapon mesh. The local player uses the first element in the player data
array (defined as m_players in the application class), although joining players
are
stuffed into the first empty slot found.
#define MSG_CREATE_PLAYER 1
#define MSG_GET_PLAYER_INFO 2
#define MSG_DESTROY_PLAYER 3
#define MSG_STATE_CHANGE 4
typedef struct sMsgHeader
{
long type; // type of message (MSG_*)
long size; // size of data to send
DPNID player_id; // player performing action
} *sMsgHeaderPtr;
typedef struct sMsg // The message queue message structure
{
sMsgHeader header;
char data[512];
} *sMsgPtr;
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;
typedef struct sStateChangeMsg
{
sMsgHeader header;
long state; // State message (STATE_*)
float x_pos, y_pos, z_pos;
float direction;
float speed;
long latency;
} *sStateChangeMsgPtr;
/****************************************************************************************************/
class cApp : public cFramework
{
private:
CRITICAL_SECTION m_update_cs;
cInput m_input;
cInputDevice m_keyboard;
cInputDevice m_mouse;
cMesh m_terrain_mesh;
cNodeTreeMesh m_nodetree_mesh;
cMesh m_char_mesh;
cMesh m_weapon_mesh;
cAnimation m_char_anim;
cCamera m_camera;
float m_cam_angle;
cNetworkAdapter m_adapter;
cClient m_client;
GUID* m_adapter_guid;
char m_host_ip[16];
char m_player_name[32];
long m_num_players;
sPlayer* m_players;
ID3DXFont* m_font;
///////////////////////////////////////////////////////////////////////////////////////////
public:
cApp();
virtual bool init();
virtual bool frame();
virtual void shutdown();
bool receive(const DPNMSG_RECEIVE* msg);
void set_info(GUID* adapter_guid, const char* host_ip, const char* player_name);
void set_local_player(DPNID player_id);
private:
bool select_adapter();
bool init_game();
bool join_game();
void update_all_players();
void render_scene();
bool send_network_msg(void* msg, long send_flags);
long get_player_index(DPNID player_id);
void create_player(const sMsg* msg);
void destroy_player(const sMsg* msg);
void change_player_state(const sMsg* msg);
};
BOOL CALLBACK connect_dialog_proc(HWND dlg, UINT msg, WPARAM wParam, LPARAM lParam);
As the application class for the
client is initialized, all character and weapon
meshes are loaded and assigned to each of the player data structures. This is
your
first chance to customize your network game; by loading different meshes, you
can
have each player appear differently. For example, one character can be a
warrior,
another character a wizard, and so on.
A list of animations is also
loaded. Those animations represent the various states of
players: a walking animation, standing still (idle), swinging a weapon, and
finally a
hurt animation. Those animations are set by the update_all_players function,
which you
see in a bit in the section “Updating Local Players.”
One extra tidbit in the sPlayer
structure is a DirectPlay identification number.
Clients normally don’t have access to their identification numbers; those are
left
for the server to track. However, clients are designed so that their
identification
numbers track all players, and in order to start playing, all clients must
request
their identification number from the server.
When a game message is received
from the server, the client application scans
through the list of connected players. When the player identification number
from
the local list of players and from the server is matched, the client knows
exactly
which player to update.
The client uses a function
called get_player_index to scan the list of players and return
the index number of the matching player (or -1 if no such match is found):
long cApp::get_player_index(DPNID player_id)
{
// scan list looking for match
for(long i = 0; i < MAX_PLAYERS; i++)
{
if(m_players[i].player_id == player_id && m_players[i].connected)
return i;
}
return -1; // no found in list
}
From now on, the client will
always use the get_player_index function to determine
which player to update. If a player is not found in the list but is known to
exist, the
client must send a MSG_GET_PLAYER_INFO message, which requests the player’s
information
from the server. In response, the server will return a create-player message to
the
requesting client.
But
I’m getting a little ahead of myself, so let’s slow things down a bit. Much like
the server, the client uses the Network Core to handle network communications.
Now, take a look at the client component I’m using for the client application.