Controlling Players and Characters(41)
download source and solution
Demonstrating Characters with the Chars Demo
All your hard work is about to pay off with a demonstration of the character and
spell controllers seen in this chapter.
Upon executing the program, you see the scene shown in following snap.
In the Chars demo, you take control of the PC, using the arrow keys to turn and
move him. The controls are straightforward—use the space bar to interact with the
closest character (either to speak to an NPC or to attack a monster). Pressing the
number keys 1 through 3 casts a few spells at the closest monster.
Each character in the game demonstrates a single artificial intelligence. Speaking
to another character conveys which artificial intelligence a particular character uses
(except for monsters, which either stand still or follow the player character). It’s
best to quickly dispatch the monsters before they take your player character out.
Everything in the Chars demo has been explained in this chapter. A script class
determines which characters to place in the map during startup (as detailed in the
startup script) and what each character does or says when spoken to.
The demo’s action template, default.mla, contains a number of script actions that
directly modify a character’s type, artificial intelligence, position, and direction.
Adding characters to the world is as easy as using an add character script action,
and from there, you modify the character’s attributes accordingly.
As for the main application, the system core’s cApp class is being used to
control the flow of the demo; each frame update is regulated to 33-millisecond
lapses, giving a 30-frames-per-second update rate. At each and every frame, keyboard
input is read in and stored, waiting to be used during the PC update function.
A fixed camera renders out the action, with each character fully animated
inside a single level (both characters and the level represented by meshes).
The code to the demo is well commented, so enjoy exploring it, and find out how
quickly you can create characters running around in your game project. Be sure to
check out the scripts and script action template using the Mad Lib Script editor, as
well as the items and character definitions using the MIL and MCL Editors.
Main Routine Source:
WinMain.h:
#define WINMAIN_H
#include "core_framework.h"
#include "core_input.h"
#include "text_window.h"
#include "char_ics.h"
#include "char.h"
#include "script.h"
#include "spell.h"
class cApp;
/*************************************************************************************************/
class cGameCharController : public cCharController
{
private :
cApp* m_app;
private :
virtual void pc_update(sCharacter* character, long elapsed,
float * x_move, float * y_move, float * z_move);
virtual bool validate_move(sCharacter* character,
float * x_move, float * y_move, float * z_move);
public :
void set_data(cApp* app)
{
m_app = app;
}
};
/*************************************************************************************************/
class cGameScript : public cScript
{
friend cApp;
private :
BOOL m_flags[256];
cApp* m_app;
cInputDevice* m_keyboard;
cGameCharController* m_gc_controller;
long m_num_route_points;
sRoutePoint* m_route;
cTextWindow m_text_window;
ID3DXFont* m_font;
///////////////////////////////////////////////////////////////////////////////// /
public :
cGameScript()
{
m_app = NULL;
m_keyboard = NULL;
m_gc_controller = NULL;
m_route = NULL;
m_font = NULL;
ZeroMemory(m_flags, sizeof (m_flags));
}
~cGameScript()
{
delete[] m_route;
}
void set_data(cApp* app, cInputDevice* keyboard, cGameCharController* gc_controller, ID3DXFont* font)
{
m_app = app;
m_keyboard = keyboard;
m_gc_controller = gc_controller;
m_font = font;
m_text_window.create(m_font);
}
///////////////////////////////////////////////////////////////////////////////// /
private :
virtual void release()
{
delete[] m_route;
m_route = NULL;
m_num_route_points = 0;
}
virtual sScriptInfo* process(sScriptInfo* info)
{
switch (info->action_index)
{
case 0: return script_end(info);
case 1: return script_if_flag_then(info);
case 2: return script_else(info);
case 3: return script_endif(info);
case 4: return script_set_flag(info);
case 5: return script_show_msg(info);
case 6: return script_add_char(info);
case 7: return script_remove_char(info);
case 8: return script_show_char_msg(info);
case 9: return script_set_char_type(info);
case 10: return script_set_char_ai(info);
case 11: return script_set_char_distance(info);
case 12: return script_set_char_bound(info);
case 13: return script_set_target_char(info);
case 14: return script_set_no_target(info);
case 15: return script_create_route(info);
case 16: return script_add_point(info);
case 17: return script_assign_route(info);
case 18: return script_move_char(info);
case 19: return script_set_char_script(info);
}
return NULL; // error executing
}
///////////////////////////////////////////////////////////////////////////////// /
private :
sScriptInfo* script_end(sScriptInfo* info)
{
return NULL; // force end of processing
}
sScriptInfo* script_else(sScriptInfo* info)
{
return info->next;
}
sScriptInfo* script_endif(sScriptInfo* info)
{
return info->next;
}
sScriptInfo* script_set_flag(sScriptInfo* info)
{
m_flags[info->entries[0].long_value % 256] = info->entries[1].bool_value;
return info->next;
}
sScriptInfo* script_add_char(sScriptInfo* info)
{
m_gc_controller->add_char(info->entries[0].long_value,
info->entries[1].long_value,
info->entries[2].selection,
info->entries[3].selection,
info->entries[4].float_value,
info->entries[5].float_value,
info->entries[6].float_value,
info->entries[7].float_value);
return info->next;
}
sScriptInfo* script_remove_char(sScriptInfo* info)
{
m_gc_controller->remove(info->entries[0].long_value);
return info->next;
}
sScriptInfo* script_set_char_type(sScriptInfo* info)
{
m_gc_controller->set_char_type(info->entries[0].long_value, info->entries[1].selection);
return info->next;
}
sScriptInfo* script_set_char_ai(sScriptInfo* info)
{
m_gc_controller->set_char_ai(info->entries[0].long_value, info->entries[1].selection);
return info->next;
}
sScriptInfo* script_set_char_distance(sScriptInfo* info)
{
m_gc_controller->set_char_distance(info->entries[0].long_value, info->entries[1].float_value);
return info->next;
}
sScriptInfo* script_set_char_bound(sScriptInfo* info)
{
m_gc_controller->set_char_bound(info->entries[0].long_value,
info->entries[1].float_value,
info->entries[2].float_value,
info->entries[3].float_value,
info->entries[4].float_value,
info->entries[5].float_value,
info->entries[6].float_value);
return info->next;
}
sScriptInfo* script_set_target_char(sScriptInfo* info)
{
m_gc_controller->set_target_char(info->entries[0].long_value, info->entries[1].long_value);
return info->next;
}
sScriptInfo* script_set_no_target(sScriptInfo* info)
{
m_gc_controller->set_target_char(info->entries[0].long_value, -1);
return info->next;
}
sScriptInfo* script_create_route(sScriptInfo* info)
{
delete[] m_route;
m_route = NULL;
m_num_route_points = 0;
m_num_route_points = info->entries[0].long_value;
m_route = new sRoutePoint[m_num_route_points];
return info->next;
}
sScriptInfo* script_add_point(sScriptInfo* info)
{
long route_index = info->entries[0].long_value;
m_route[route_index].pos_x = info->entries[1].float_value;
m_route[route_index].pos_y = info->entries[2].float_value;
m_route[route_index].pos_z = info->entries[3].float_value;
return info->next;
}
sScriptInfo* script_assign_route(sScriptInfo* info)
{
m_gc_controller->set_char_route(info->entries[0].long_value, m_num_route_points, m_route);
return info->next;
}
sScriptInfo* script_move_char(sScriptInfo* info)
{
m_gc_controller->move_char(info->entries[0].long_value,
info->entries[1].float_value,
info->entries[2].float_value,
info->entries[3].float_value);
return info->next;
}
sScriptInfo* script_set_char_script(sScriptInfo* info)
{
m_gc_controller->set_char_script(info->entries[0].long_value, info->entries[1].text);
return info->next;
}
///////////////////////////////////////////////////////////////////////////////// /
private :
sScriptInfo* script_if_flag_then(sScriptInfo* info);
sScriptInfo* script_show_msg(sScriptInfo* info);
sScriptInfo* script_show_char_msg(sScriptInfo* info);
void render_scene_and_msg();
};
/*************************************************************************************************/
class cApp : public cFramework
{
friend class cGameScript;
friend class cGameCharController;
private :
cCamera m_camera;
cInput m_input;
cInputDevice m_keyboard;
cInputDevice m_mouse;
cMesh m_terrain_mesh;
cObject m_terrain_object;
cGameCharController m_gc_controller;
cSpellController m_spell_controller;
cGameScript m_game_script;
sItem m_mil[1024];
ID3DXFont* m_font;
public :
bool init();
bool frame();
long get_input();
bool check_intersect( float x_start, float y_start, float z_start,
float x_end, float y_end, float z_end);
};
#endif
WinMain.cpp:
#include "core_graphics.h"
#include "char.h"
#include "script.h"
#include "text_window.h"
#include "tool.h"
#include "WinMain.h"
#define PRESS_UP 1
#define PRESS_RIGHT 2
#define PRESS_DOWN 4
#define PRESS_LEFT 8
#define PRESS_SPACE 16
#define PRESS_1 32
#define PRESS_2 64
#define PRESS_3 128
#define CLIENT_WIDTH 800
#define CLIENT_HEIGHT 600
cApp g_app;
// Global names of character meshes
PCSTR g_char_mesh_names[] = {
"..\\Data\\Warrior.x", // Mesh # 0
"..\\Data\\Yodan.x" // Mesh # 1
};
sCharAnimInfo g_char_anim_infos[] = {
{ "Idle", true },
{ "Walk", true },
{ "Swing", false },
{ "Spell", false },
{ "Swing", false },
{ "Hurt", false },
{ "Die", false },
{ "Idle", true }
};
PCSTR g_spell_mesh_names[] = {
"..\\Data\\Fireball.x",
"..\\Data\\Explosion.x",
"..\\Data\\Groundball.x",
"..\\Data\\ice.x",
"..\\Data\\bomb.x",
};
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, "CharClass", "Characters Demo",
WS_BORDER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU,
pos_x, pos_y, CLIENT_WIDTH, CLIENT_HEIGHT);
g_app.run();
return 0;
}
/*************************************************************************************************/
void cGameCharController::pc_update(sCharacter* character, long elapsed,
float * x_move, float * y_move, float * z_move)
{
if (character->id != CHAR_PC)
return ;
float speed = elapsed/1000.0f * get_speed(character);
// rotate character
long action = m_app->get_input();
if (action & PRESS_RIGHT)
{
character->direction += (elapsed/1000.0f * 8);
character->action = CHAR_MOVE;
}
if (action & PRESS_LEFT)
{
character->direction -= (elapsed/1000.0f * 8);
character->action = CHAR_MOVE;
}
// walk forward
if (action & PRESS_UP)
{
*x_move = sin(character->direction) * speed;
*z_move = cos(character->direction) * speed;
character->action = CHAR_MOVE;
}
sCharacter* char_ptr;
float x_diff, y_diff, z_diff, dist;
// attack a nearby monster or process NPC script
if (action & PRESS_SPACE)
{
for (char_ptr = get_root_char(); char_ptr != NULL; char_ptr = char_ptr->next)
{
// only check other characters
if (char_ptr->id == character->id)
continue ;
x_diff = fabs(char_ptr->pos_x - character->pos_x);
y_diff = fabs(char_ptr->pos_y - character->pos_y);
z_diff = fabs(char_ptr->pos_z - character->pos_z);
dist = x_diff * x_diff + y_diff * y_diff + z_diff * z_diff;
// only check characters within 1000.0 units distance
if (dist > 10000.0f)
continue ;
if (char_ptr->script_filename[0])
m_app->m_game_script.execute(char_ptr->script_filename);
else
{
// turn toward victim
x_diff = char_ptr->pos_x - character->pos_x;
z_diff = char_ptr->pos_z - character->pos_z;
character->direction = atan2(x_diff, z_diff);
character->victim = char_ptr;
char_ptr->attacker = character;
m_app->m_gc_controller.set_char_action(character, CHAR_ATTACK, 0);
}
break ;
}
}
long spell_index = 0;
// cast spells
if (action & PRESS_1 || action & PRESS_2 || action & PRESS_3)
{
// get spell index to cast
if (action & PRESS_1) spell_index = 0;
if (action & PRESS_2) spell_index = 1;
if (action & PRESS_3) spell_index = 2;
float spell_max_dist = m_app->m_spell_controller.get_spell(spell_index)->max_dist;
// search for closest monster
for (char_ptr = get_root_char(); char_ptr != NULL; char_ptr = char_ptr->next)
{
if (char_ptr->type == CHAR_MONSTER)
{
x_diff = fabs(char_ptr->pos_x - character->pos_x);
y_diff = fabs(char_ptr->pos_y - character->pos_y);
z_diff = fabs(char_ptr->pos_z - character->pos_z);
dist = x_diff * x_diff + y_diff * y_diff + z_diff * z_diff;
if (dist <= (spell_max_dist * spell_max_dist))
{
character->spell_index = spell_index;
character->target_type = CHAR_MONSTER;
character->target_x = char_ptr->pos_x;
character->target_y = char_ptr->pos_y;
character->target_z = char_ptr->pos_z;
// turn toward victim
x_diff = char_ptr->pos_x - character->pos_x;
z_diff = char_ptr->pos_z - character->pos_z;
character->direction = atan2(x_diff, z_diff);
m_app->m_gc_controller.set_char_action(character, CHAR_SPELL, 0);
break ;
}
}
}
}
}
///////////////////////////////////////////////////////////////////////////////// //
bool cGameCharController::validate_move(sCharacter* character,
float * x_move, float * y_move, float * z_move)
{
// check against terrain mesh for collision
return ! m_app->check_intersect(character->pos_x, character->pos_y + 2.0f, character->pos_z,
*x_move + character->pos_x, *y_move + character->pos_y + 2.0f, *z_move + character->pos_z);
}
/*************************************************************************************************/
void cGameScript::render_scene_and_msg()
{
clear_display(0, 1.0);
if (begin_display_scene())
{
enable_zbuffer();
g_app.m_terrain_object.render();
g_app.m_gc_controller.render(-1, NULL, 0);
g_app.m_spell_controller.render(NULL, 0);
m_text_window.render(NULL, 0);
end_display_scene();
}
present_display();
}
///////////////////////////////////////////////////////////////////////////////// //
sScriptInfo* cGameScript::script_if_flag_then(sScriptInfo* info)
{
bool skip;
// see if a flag matches second entry
if (m_flags[info->entries[0].long_value % 256] == info->entries[1].bool_value)
skip = false ;
else
skip = true ;
// At this point, Skip states if the script actions need to be skipped due to a conditional
// if..then statement.
//
// Actions are further processed if skip = false, looking for an else to flip the skip mode,
// or an endif to end the conditional block.
info = info->next;
while (info)
{
if (info->action_index == 2) // if else, flip skip mode.
skip = !skip;
else if (info->action_index == 3) // break on end if
return info->next;
// Process script function in conditional block, making sure to skip actions when condition not met.
if (skip)
info = info->next;
else
{
if ((info = process(info)) == NULL)
return NULL;
}
}
return NULL; // end of script reached
}
///////////////////////////////////////////////////////////////////////////////// //
sScriptInfo* cGameScript::script_show_msg(sScriptInfo* info)
{
m_text_window.set_text(info->entries[0].text, COLOR_WHITE);
m_text_window.move(10, 10, CLIENT_WIDTH-20, 0, -1, -1, COLOR_BLACK, COLOR_ARGENTINE);
render_scene_and_msg();
// wait for a key press
m_keyboard->acquire();
m_keyboard->m_locks[KEY_SPACE] = true ;
m_keyboard->set_key_state(KEY_SPACE, false );
while (1)
{
m_keyboard->read();
if (m_keyboard->get_key_state(KEY_SPACE))
break ;
}
m_keyboard->m_locks[KEY_SPACE] = true ;
m_keyboard->set_key_state(KEY_SPACE, false );
return info->next;
}
///////////////////////////////////////////////////////////////////////////////// //
sScriptInfo* cGameScript::script_show_char_msg(sScriptInfo* info)
{
D3DXMATRIX mat_world, mat_view, mat_proj;
D3DXMatrixIdentity(&mat_world);
get_display_view_matrix(&mat_view);
get_display_proj_matrix(&mat_proj);
D3DVIEWPORT9 viewport;
get_display_viewport(&viewport);
// get the character's coordinates
float max_y;
sCharacter* character = m_gc_controller->get_char(info->entries[1].long_value);
character-> object .get_bounds(NULL, NULL, NULL, NULL, &max_y, NULL, NULL);
// project the 3D coordinates in 2D coordinates
D3DXVECTOR3 target_vec;
D3DXVECTOR3 source_vec(character->pos_x, character->pos_y + max_y, character->pos_z);
D3DXVec3Project(&target_vec, &source_vec, &viewport, &mat_proj, &mat_view, &mat_world);
m_text_window.set_text(info->entries[0].text, D3DCOLOR_RGBA(255, 255, 0, 255));
m_text_window.move(10, 10, CLIENT_WIDTH-20, 0, target_vec.x, target_vec.y,
D3DCOLOR_RGBA(0, 30, 60, 255), COLOR_ARGENTINE);
// display the window while waiting for a keypress
m_keyboard->acquire();
m_keyboard->m_locks[KEY_SPACE] = true ;
m_keyboard->set_key_state(KEY_SPACE, false );
while (1)
{
m_keyboard->read();
if (m_keyboard->get_key_state(KEY_SPACE))
break ;
render_scene_and_msg();
}
m_keyboard->m_locks[KEY_SPACE] = true ;
m_keyboard->set_key_state(KEY_SPACE, false );
return info->next;
}
/*************************************************************************************************/
bool cApp::init()
{
create_display(g_hwnd, CLIENT_WIDTH, CLIENT_HEIGHT, 16, true , true );
set_perspective(D3DX_PI/4, 1.3333f, 1.0f, 10000.0f);
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 );
m_terrain_mesh.load("..\\Data\\World.x", "..\\Data\\");
m_terrain_object.create(&m_terrain_mesh);
// load the master item list
ZeroMemory(m_mil, sizeof (m_mil));
FILE* fp;
if ((fp = fopen("..\\Data\\Default.mil", "rb")) == NULL)
return false ;
fread(m_mil, 1, sizeof (m_mil), fp);
fclose(fp);
m_spell_controller.init("..\\Data\\Default.msl",
array_num(g_spell_mesh_names), g_spell_mesh_names,
"..\\Data\\");
m_gc_controller.init(m_font, "..\\Data\\Default.mcl",
m_mil, m_spell_controller.get_spell_list(),
array_num(g_char_mesh_names), g_char_mesh_names,
"..\\Data\\", "..\\Data\\",
array_num(g_char_anim_infos), g_char_anim_infos);
m_spell_controller.attach(&m_gc_controller);
m_gc_controller.attach(&m_spell_controller);
m_gc_controller.set_data( this );
// add the character player
m_gc_controller.add_char(0, 0, CHAR_PC, CHAR_STAND, 0.0f, 0.0f, 0.0f, 3.14f);
// process the startup script
m_game_script.set_data( this , &m_keyboard, &m_gc_controller, m_font);
m_game_script.execute("..\\Data\\Startup.mls");
return true ;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
bool cApp::frame()
{
static DWORD update_counter = timeGetTime();
// lock to 30fps
if (timeGetTime() < update_counter + 33)
return true ;
update_counter = timeGetTime();
m_keyboard.acquire();
m_keyboard.read();
// exit if ESC pressed
if (m_keyboard.get_key_state(KEY_ESC))
return false ;
m_gc_controller.update(33);
m_spell_controller.update(33);
m_camera.point(0.0f, 700.0f, -700.0f, 0.0f, 0.0f, 0.0f);
set_display_camera(&m_camera);
clear_display(0, 1.0f);
if (begin_display_scene())
{
enable_zbuffer();
m_terrain_object.render();
m_gc_controller.render(-1, NULL, 0);
m_spell_controller.render(NULL, 0);
static sCharacter* character = m_gc_controller.get_char(0);
char stats[128];
sprintf(stats, "HP: %ld / %ld\r\nMP: %ld / %ld",
character->health_points, character->char_def.health_points,
character->mana_points, character->char_def.mana_points);
draw_font(m_font, stats, 2, 2, 0, 0, COLOR_WHITE, DT_LEFT);
end_display_scene();
}
present_display();
return true ;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
long cApp::get_input()
{
long action = 0;
if (m_keyboard.get_key_state(KEY_UP) || m_keyboard.get_key_state(KEY_W))
action |= PRESS_UP;
if (m_keyboard.get_key_state(KEY_RIGHT) || m_keyboard.get_key_state(KEY_D))
action |= PRESS_RIGHT;
if (m_keyboard.get_key_state(KEY_DOWN) || m_keyboard.get_key_state(KEY_S))
action |= PRESS_DOWN;
if (m_keyboard.get_key_state(KEY_LEFT) || m_keyboard.get_key_state(KEY_A))
action |= PRESS_LEFT;
if (m_keyboard.get_key_state(KEY_SPACE))
{
action |= PRESS_SPACE;
m_keyboard.m_locks[KEY_SPACE] = true ;
m_keyboard.set_key_state(KEY_SPACE, false );
}
if (m_keyboard.get_key_state(KEY_1))
{
action |= PRESS_1;
m_keyboard.m_locks[KEY_1] = true ;
m_keyboard.set_key_state(KEY_1, false );
}
if (m_keyboard.get_key_state(KEY_2))
{
action |= PRESS_2;
m_keyboard.m_locks[KEY_2] = true ;
m_keyboard.set_key_state(KEY_2, false );
}
if (m_keyboard.get_key_state(KEY_3))
{
action |= PRESS_3;
m_keyboard.m_locks[KEY_3] = true ;
m_keyboard.set_key_state(KEY_3, false );
}
return action;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
bool cApp::check_intersect( float x_start, float y_start, float z_start,
float x_end, float y_end, float z_end)
{
for (sMeshInfo* mesh_info = m_terrain_mesh.get_root_mesh(); mesh_info != NULL; mesh_info = mesh_info->m_next)
{
if (is_ray_intersect_mesh(mesh_info->m_d3d_mesh, x_start, y_start, z_start, x_end, y_end, z_end, NULL))
return true ;
}
return false ;
}