Updating All Players
Whereas the local player’s
input is processed in the cApp::frame function, the
update_players (which you saw in the code in the previous section) processes the
players according to their respective states.
Unlike the server's
update_players function, the client's update_players function is simple.
The client is allowed to move players based only on their last known positions,
directions, and elapsed time since their last update.
Remember, only the server can
clear the weapon-swinging and being-hurt states, so
the client has nothing to do at this point except update the various animations
to
show the player what is going on:
void cApp::update_players()
{
// process all active player movements
for(long i = 0; i < MAX_PLAYERS; i++)
{
if(! m_players[i].connected)
continue;
long elapsed = timeGetTime() - m_players[i].last_update_time;
if(m_players[i].last_state == STATE_MOVE)
{
// process player movement state
float speed = elapsed / 1000.0f * m_players[i].speed;
float x_move = sin(m_players[i].direction) * speed;
float z_move = cos(m_players[i].direction) * speed;
// check for movement collisions - can not walk past anything blocking path
if(m_nodetree_mesh.is_ray_intersect(
m_players[i].x_pos, m_players[i].y_pos + 16.0f, m_players[i].z_pos,
m_players[i].x_pos + x_move, m_players[i].y_pos + 16.0f, m_players[i].z_pos + z_move,
NULL))
{
x_move = z_move = 0.0f;
}
// update coordinates
EnterCriticalSection(&m_update_cs);
m_players[i].x_pos += x_move;
m_players[i].y_pos = 0.0f;
m_players[i].z_pos += z_move;
m_players[i].last_update_time = timeGetTime(); // reset time
if(m_players[i].last_anim != ANIM_WALK)
{
m_players[i].last_anim = ANIM_WALK;
m_players[i].body.set_anim_set(&m_char_anim, "Walk", timeGetTime()/32);
}
LeaveCriticalSection(&m_update_cs);
}
else if(m_players[i].last_state == STATE_IDLE)
{
// set new animations as needed
if(m_players[i].last_anim != ANIM_IDLE)
{
EnterCriticalSection(&m_update_cs);
m_players[i].last_anim = ANIM_IDLE;
m_players[i].body.set_anim_set(&m_char_anim, "Idle", timeGetTime()/32);
LeaveCriticalSection(&m_update_cs);
}
}
else if(m_players[i].last_state == STATE_SWING)
{
if(m_players[i].last_anim != ANIM_SWING)
{
EnterCriticalSection(&m_update_cs);
m_players[i].last_anim = ANIM_SWING;
m_players[i].body.set_anim_set(&m_char_anim, "Swing", timeGetTime()/32);
LeaveCriticalSection(&m_update_cs);
}
}
else if(m_players[i].last_state == STATE_HURT)
{
if(m_players[i].last_anim != ANIM_HURT)
{
EnterCriticalSection(&m_update_cs);
m_players[i].last_anim = ANIM_HURT;
m_players[i].body.set_anim_set(&m_char_anim, "Hurt", timeGetTime()/32);
LeaveCriticalSection(&m_update_cs);
}
}
}
}
Character animations are updated
only if they differ from the last known animation.
The sPlayer::last_anim variable tracks the last known animation, although the
various ANIM_* macros define which animations to play.
Other
Function:
int WINAPI WinMain(HINSTANCE inst, HINSTANCE, LPSTR cmd_line, int cmd_show)
{
DWORD pos_x = (get_screen_width() - CLIENT_WIDTH) / 2;
DWORD pos_y = (get_screen_height() - CLIENT_HEIGHT) / 4;
build_window(inst, "ClientClass", "Network Client Demo",
WS_BORDER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU,
pos_x, pos_y, CLIENT_WIDTH, CLIENT_HEIGHT);
cApp app;
app.run();
return 0;
}
/****************************************************************************************************/
cApp::cApp()
{
// clear class data
m_adapter_guid = NULL;
m_host_ip[0] = '\0';
m_player_name[0] = '\0';
m_players = NULL;
m_num_players = 0;
m_cam_angle = 0.0f;
m_font = NULL;
// set global pointer
g_app = this;
g_adapter = &m_adapter;
InitializeCriticalSection(&m_update_cs);
}
//////////////////////////////////////////////////////////////////////////////////////////////
bool cApp::init()
{
if(! select_adapter())
return false;
if(! init_game())
{
show_error_msg(false, "Unable to initialize game.");
return false;
}
if(! join_game())
{
show_error_msg(false, "Unable to connect to server.");
return false;
}
return true;
}
//////////////////////////////////////////////////////////////////////////////////////////////
void cApp::shutdown()
{
delete[] m_players;
m_players = NULL;
DeleteCriticalSection(&m_update_cs);
}
//////////////////////////////////////////////////////////////////////////////////////////////
bool cApp::select_adapter()
{
// hide main window
ShowWindow(g_hwnd, SW_HIDE);
m_adapter.init();
// do not continue if quit selected
if(! DialogBox(get_window_inst(), MAKEINTRESOURCE(IDD_CONNECT), g_hwnd, connect_dialog_proc))
return false;
// show main window
ShowWindow(g_hwnd, SW_SHOW);
return true;
}
//////////////////////////////////////////////////////////////////////////////////////////////
BOOL CALLBACK connect_dialog_proc(HWND dlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
static HWND adapter_wnd;
switch(msg)
{
case WM_INITDIALOG:
adapter_wnd = GetDlgItem(dlg, IDC_ADAPTERS);
// add adapter names to list
for(long i = 0; i < g_adapter->get_num_adapters(); i++)
{
char text[256];
g_adapter->get_name(i, text);
insert_string_to_combo(adapter_wnd, i, text);
}
// select first adapter in list
set_combo_cur_sel(adapter_wnd, 0);
// clear fields
SetWindowText(GetDlgItem(dlg, IDC_HOSTIP), "192.168.0.100");
SetWindowText(GetDlgItem(dlg, IDC_NAME), "");
return TRUE;
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDC_OK:
{
int sel = (int) get_combo_cur_sel(adapter_wnd);
// make sure an adapter was selected
if(sel == LB_ERR)
break;
char name[32], ip[16];
// make sure there is an ip entered
GetWindowText(GetDlgItem(dlg, IDC_HOSTIP), ip, sizeof(ip));
if(ip[0] == '\0') // ip is NULL
break;
// make sure there is a name entered
GetWindowText(GetDlgItem(dlg, IDC_NAME), name, sizeof(name));
if(name[0] == '\0') // name is NULL
break;
g_app->set_info(g_adapter->get_guid(sel), ip, name);
EndDialog(dlg, TRUE);
return TRUE;
}
case IDC_CANCEL:
EndDialog(dlg, FALSE);
return TRUE;
}
}
return FALSE;
}
//////////////////////////////////////////////////////////////////////////////////////////////
void cApp::set_info(GUID* adapter_guid, const char* host_ip, const char* player_name)
{
m_adapter_guid = adapter_guid;
strcpy(m_host_ip, host_ip);
strcpy(m_player_name, player_name);
}
//////////////////////////////////////////////////////////////////////////////////////////////
bool cApp::init_game()
{
create_display(g_hwnd, CLIENT_WIDTH, CLIENT_HEIGHT, 16, true, true);
set_perspective(D3DX_PI/4, 1.3333f, 1.0f, 10000.0f);
ShowCursor(FALSE);
create_font(&m_font, "Arial", 16, true, false);
m_input.create(g_hwnd, get_window_inst());
m_keyboard.create_keyboard(&m_input);
m_mouse.create_mouse(&m_input, true);
if(! m_terrain_mesh.load("..\\Data\\Arena.x", "..\\Data\\"))
return false;
m_nodetree_mesh.create(&m_terrain_mesh, QUADTREE, 256, 32);
// load the meshes and animations
if(! m_char_mesh.load("..\\Data\\Warrior.x", "..\\Data\\"))
return false;
if(! m_weapon_mesh.load("..\\Data\\Sword.x", "..\\Data\\"))
return false;
if(! m_char_anim.load("..\\Data\\Warrior.x", &m_char_mesh))
return false;
m_char_anim.set_loop(true, "Walk");
m_char_anim.set_loop(true, "Idle");
m_char_anim.set_loop(false, "Swing");
m_char_anim.set_loop(false, "Hurt");
m_cam_angle = 0.0f;
m_players = new sPlayer[MAX_PLAYERS];
// setup player data
for(long i = 0; i < MAX_PLAYERS; i++)
{
m_players[i].body.create(&m_char_mesh);
m_players[i].weapon.create(&m_weapon_mesh);
m_players[i].weapon.attach_to_object(&m_players[i].body, "Bip01_R_Finger11");
m_players[i].weapon.rotate(1.57f, 0.0f, 0.0f);
}
m_num_players = 1;
// setup local player structure
m_players[0].connected = true;
m_players[0].direction = 0.0f;
m_players[0].x_pos = 0.0f;
m_players[0].y_pos = 0.0f;
m_players[0].z_pos = 0.0f;
m_players[0].speed = 0.0f;
m_players[0].last_state = STATE_IDLE;
return true;
}
//////////////////////////////////////////////////////////////////////////////////////////////
bool cApp::join_game()
{
// initialize network and try to connect to host
m_client.init();
return m_client.connect(m_adapter_guid, m_host_ip, 9123, m_player_name, "RPGGAME", NULL);
}
//////////////////////////////////////////////////////////////////////////////////////////////
void cApp::render_scene()
{
// center camera on player using camera angle
float x_eye = m_players[0].x_pos + cos(m_cam_angle) * 300.0f;
float y_eye = m_players[0].y_pos + 100.0f;
float z_eye = m_players[0].z_pos + sin(m_cam_angle) * 300.0f;
m_camera.point(x_eye, y_eye, z_eye, m_players[0].x_pos, m_players[0].y_pos, m_players[0].z_pos);
set_display_camera(&m_camera);
cFrustum frustum;
frustum.create(0.0f);
clear_display(0, 1.0f);
if(begin_display_scene())
{
// draw the terrain
enable_zbuffer();
m_nodetree_mesh.render(&frustum, 0.0f);
// draw all actiive and in view characters
for(long i = 0; i < MAX_PLAYERS; i++)
{
if(! m_players[i].connected)
continue;
float radius;
m_players[i].body.get_bounds(NULL, NULL, NULL, NULL, NULL, NULL, &radius);
// bounds check if player in view
if(frustum.is_sphere_in(m_players[i].x_pos, m_players[i].y_pos, m_players[i].z_pos, radius))
{
// position character and rotate
m_players[i].body.move(m_players[i].x_pos, m_players[i].y_pos, m_players[i].z_pos);
m_players[i].body.rotate(0.0f, m_players[i].direction, 0.0f);
// render body and weapon
m_players[i].body.update_anim(timeGetTime()/32, true);
m_players[i].body.render();
m_players[i].weapon.render();
}
}
end_display_scene();
}
present_display();
}
The
Client’s Full Glory
The hard work is over! The only
requirements for running the client are processing
the local player’s input and updating the players. Now, all you have to do is
spruce up your project with some 3-D graphics, and you’ll almost have a game.
The graphics portion of the
client application uses the Graphics Core to draw the
various connected players in the game. You use a NodeTree object to render the
game’s level. The client loads all meshes when the application class is
initialized. As
previously mentioned, all players receive an assigned mesh to represent their
characters
and weapons. Animations are also used and are set by the various update
messages.
You limit rendering of a scene
to 30 times a second, and to ensure that everything
runs as quickly as possible, you use a viewing frustum to render the level and
to clip
unseen characters out of the rendering loop.
To wrap up the Client
application, you deal with the different kinds of application
code, such as selecting an adapter and connecting to the server.
Download source and solution