Back to Top

RPG Tutorials

Coding a smooth player movement snippet

tutorial2

Let's get straight to the point: movement code is complicated! In fact, you could get away with a simple snippet, but the movement would be terrible! And players will quickly punish developers who are trying to sell them unpolished games.


 

Nevertheless, you shouldn't get discouraged if the code below looks a bit too complex; it's fully standalone, so you can use it in your own games without worrying about its inner workings.


 

var trpg_player_speed = 10;

var trpg_fwdbk_accel = 4;

var trpg_side_accel = 2;

var trpg_friction = 1.5;

var trpg_camera_h = 12;

var trpg_camera_h_frict = 0.95;

var trpg_camera_v = 8;

var trpg_camera_v_frict = 0.8;

var trpg_players_health = 100;

var trpg_players_armor = 100;


 

The lines above define a bunch of variables that are used to set up player's speed, acceleration, friction, camera acceleration, camera friction, health and armor. We don't need to worry about them for now.


 

STRING* key_forward = "w";

STRING* key_backward = "s";

STRING* key_left = "a";

STRING* key_right = "d";

STRING* key_run = "shiftl";

STRING* step_wav = "step.wav";


 

The string definitions above set up the default movement keys. The code is written in a way that allows you to redefine them later on. I have chosen the often-used W, S, A, D combination for movement and shift for running. The last string defines a step.wav sound effect that will be triggered every time the player makes a step.


 

action t_rpg_player()

{

player = my;

my.ambient = 50;

set (my, FLAG2);

var forward_on, backward_on, right_on, left_on, run_on, anim_percentage, attack_percentage;

var camera_h_speed = 0, camera_v_speed = 0;

VECTOR horizontal_speed, vertical_speed, temp;

vec_set(horizontal_speed.x, nullvector);

vec_set(vertical_speed.x, nullvector);

my.emask |= (ENABLE_IMPACT | ENABLE_ENTITY);


Player's action assigns the predefined "player" pointer to the player entity, and then sets its ambient to 50%. We also set player's "flag2", which can be used in the future by player's enemies to uniquely identify it. Then, we define a bunch of variables that tell us if a particular key is being pressed, two variables that will be used to animate player's "attack" sequence, and two variables that set the horizontal and vertical camera speeds.


 

We also define three vectors, and then we initialize the first two. Finally, we make sure that the player entity has its event mask set up in a way that makes it sensitive to impact with level objects and other entities (enemies, trees, etc).


 

while(trpg_players_health > 0)

{

forward_on = 0;

backward_on = 0;

right_on = 0;

left_on = 0;

run_on = 0;

if(key_pressed(key_for_str(key_forward)))

forward_on = 1;

if(key_pressed(key_for_str(key_backward)))

backward_on = 1;  

if(key_pressed(key_for_str(key_left)))

left_on = 1;

if(key_pressed(key_for_str(key_right)))

right_on = 1;  

if(key_pressed(key_for_str(key_run)))

run_on = 1;    


The loop above will run for as long as player's health is greater than zero. The code resets the key statuses at the beginning of each frame. Then, it checks if one of the movement keys is pressed or not, setting the corresponding variables to 1 (or not).


 

vec_set (camera.x, vector(-100, 0, 75));

vec_rotate (camera.x, player.pan);

vec_add (camera.x, player.x);

camera.pan -= accelerate (camera_h_speed, trpg_camera_h * (mouse_force.x), trpg_camera_h_frict);

camera.tilt += accelerate (camera_v_speed, trpg_camera_v * (mouse_force.y), trpg_camera_v_frict);

my.pan = camera.pan;


The code above controls the camera, which will be placed 100 units behind the player and 75 units above its origin. The camera is rotated and placed behind the player at all times; its pan and tilt angles are accelerated depending on the speed with which the player moves the mouse.


 

horizontal_speed.x = (horizontal_speed.x > 0) * maxv(horizontal_speed.x - time_step * trpg_friction, 0) + (horizontal_speed.x < 0) * minv(horizontal_speed.x + time_step * trpg_friction, 0);

if(forward_on)

{

horizontal_speed.x += time_step * trpg_fwdbk_accel;

horizontal_speed.x = minv(horizontal_speed.x, time_step * trpg_player_speed  * (1 + run_on));

}    

if(backward_on)

{

horizontal_speed.x -= time_step * trpg_fwdbk_accel;

horizontal_speed.x = maxv(horizontal_speed.x, -(time_step * trpg_player_speed * (1 + run_on)));

}    

horizontal_speed.y = (horizontal_speed.y > 0) * maxv(horizontal_speed.y - time_step * trpg_friction, 0) + (horizontal_speed.y < 0) * minv(horizontal_speed.y + time_step * trpg_friction, 0);

if(left_on)

{

horizontal_speed.y += time_step * trpg_side_accel;

horizontal_speed.y = minv(horizontal_speed.y, time_step * trpg_player_speed * (1 + run_on));

}

if(right_on)

{

horizontal_speed.y -= time_step * trpg_side_accel;

horizontal_speed.y = maxv(horizontal_speed.y, -(time_step * trpg_player_speed * (1 + run_on)));

}    


Player's horizontal movement code makes use of acceleration and friction; I have used a single, albeit complex line of code to implement it. The next four sections of code are very similar; basically, if one of the movement keys is pressed, we set its horizontal speed component, adding acceleration to it and doubling the speed if the "run" key is being held down at the moment.


 

move_friction = 0;

vec_set(temp.x, my.x);

temp.z -= 10000;

my.z -= c_trace(my.x, temp.x, IGNORE_ME | IGNORE_PASSABLE | USE_BOX) - 30;

if((forward_on) + (backward_on) + (left_on) + (right_on))

{

anim_percentage += 0.5 * (1 + run_on) * trpg_player_speed * time_step;

ent_animate(my, "walk", anim_percentage, ANM_CYCLE);

}

else

{

if (mouse_left)

{

attack_percentage += 20 * time_step;

ent_animate(my, "attack", attack_percentage, ANM_CYCLE);

attack_percentage = minv(62, attack_percentage);

}

else

{

attack_percentage = 0;

anim_percentage += 1 * time_step;

ent_animate(my, "stand", anim_percentage, ANM_CYCLE);

}

}


The code above disables friction each frame, allowing our player to glide along the level blocks without getting stuck. Then, we trace 30 units below player's feet to ensure that its model doesn't float above the ground. If one or more movement keys are pressed, we play the "walk" animation; otherwise, if the left mouse button is played, we play the "attack" animation. Finally, if the player isn't moving at all, we play its "stand" animation.


my.skill80 += c_move (my, horizontal_speed.x , vertical_speed.x, IGNORE_PASSABLE | GLIDE);

if (my.skill80 > 105)

{

SND_CREATE_STATIC(rpg_step, step_wav);

snd_play(rpg_step, 20, 0);

my.skill80 = 0;

}

wait(1);

}

my.skill80 = 0;

camera.z -= 30;

camera.roll = 40;

while (my.skill80 < 100)

{

ent_animate(my, "death", my.skill80, NULL);

my.skill80 += 2 * time_step;

wait (1);

}

set (my, PASSABLE); // the corpse will be passable from now on

}


The last code block in this tutorial moves the player in the direction given by the horizontal and vertical movement vectors. The player will ignore passable entities, because it's supposed to be able to pass through grass models, for example.


 

The c_move instruction returns the distance that was covered by the entity during the last engine frame; we keep adding the result to my.skill80, and then, when the value exceeds 105 (an experimental value) we create a footstep sound effect. The last few lines of code are run when the player dies; its skill80 is reset, because it will be used for the "death" animation, the camera height and roll angle will be set to weird looking values, and then the "death" animation will be played. Finally, player's dead corpse will be made passable.


 

I'm glad to see that you've made it this far! I realize that some of the code may not make too much sense now, but I promise that the following tutorial will be easier to understand.