ID:34835
 
A BYONDscape Classic!


Contents

Player movement is one of the defining elements of any game experience. It is one of the first things players will notice, and if handled poorly it will drive players away in spite of how rich the game may be in other aspects.

This article describes several methods to enhance and control player movement.

Controlling Movement Speed

Controlling the speed that players move is a great concern. Without any control, players will zip around the map so quickly that players with slower connections may not even notice them passing. A poor movement control scheme will result in jerky movement and a bad playing experience.

Movement Timer

One of the simplest ways to control player movement is with a movement timer. Each time the player tries to move, the program tests the timer to see if enough time has passed since the player last moved.
mob
var
move_delay = 5 // how many ticks the player must wait between movements
tmp // these vars are not saved
move_time = 0 // the earliest time the mob may move

Move()
if(world.time < move_time) // not enough time passed
return

// set the move_time for move_delay ticks from now
move_time = world.time + move_delay
return ..() // do the default Move() proc and return what it returns

Most people want to override mob/Move() to slow the player as in the example above. If something external happens to move the player (like stepping on a teleporter) before the timer expires, that movement will be blocked as well. I prefer to override client/Move(), so that movement is only delayed if the player tries to move with the arrow keys. If some other force moves the player, it will move them without regard to whether the player moved herself recently. On the downside, the designer must be careful to limit non-player mobs within the AI code.

It only requires a couple minor modifications to change the above mob style movement timer code for client style speed control.

mob
var
move_delay = 5 // how many ticks the player must wait between movements
tmp // these vars are not saved
move_time = 0 // the earliest time the mob may move

client
Move()
if(world.time < mob.move_time) // not enough time passed
return

// set the move_time for move_delay ticks from now
mob.move_time = world.time + mob.move_delay
return ..() // do the default Move() proc and return what it returns

Movement Accumulator

Another method of controlling movement speed is to test an accumulator within some sort of life cycle proc. A life cycle proc loops periodically throughout the lifespan of a mob. Deadron's Event Loop library is an excellent example of a life cycle proc. This article will use simpler mob-specific life cycles for demonstration.

Each time through the life cycle, the accumulator is increased. When the accumulator exceeds a predefined amount, the accumulator drops by that amount and the player is allowed to move.

var
const // constants
ACTION_RATE = 30 // how high the accumulator must go to allow movement
NO_ACTION = 0
mob
var
action_speed = 5 // rate the mob's accumulator increases
tmp
action = NO_ACTION // stores the direction the player wants to move
action_count = 0 // the movement accumulator

New()
..() // perform the default New()
spawn(1) lifecycle() // begin the lifecycle loop

proc
lifecycle()
action_count += action_speed
if(action_count >= ACTION_RATE)
action_count -= ACTION_RATE // reduce the accumulator
// perform movement/action here
// I will provide more interesting methods
// of movement later in this article
if(action)
step(src,action)

action = NO_ACTION // reset the action
spawn(1) lifecycle() // repeat the lifecycle in one tick

// override client directions to use the action
client
North()
mob.action = NORTH
South()
mob.action = SOUTH
East()
mob.action = EAST
West()
mob.action = WEST
Northeast()
mob.action = NORTHEAST
Northwest()
mob.action = NORTHWEST
Southeast()
mob.action = SOUTHEAST
Southwest()
mob.action = SOUTHWEST

The accumulator provides finer control over movement speed than a timer does. Timers are limited to even tick rates, meaning that if you speed up a mob by just one level, they move a full tick faster. With an accumulator, a slightly faster mob gets an advantage over time. By using a larger ACTION_RATE, the programmer may create more degrees of control.

Example: Mob 1 has a speed of 10 and Mob 2 has a speed of 11. The ACTION_RATE is 20.
Tick   Mob 1     Mob 2  
1 10 11
2 20 22 both mobs act
0 2
3 10 13
4 20 24 both mobs act
0 4
.
.
.
9 10 19
10 20 30 both mobs act
0 10
11 10 21 mob 2 moves ahead
10 1 of mob 1!
12 20 12 mob 1 acts
0 12
13 10 22 mob 2 acts
and so on...

Enhancing Movement

The rest of this article covers some enhanced styles of movement for BYOND games.

Animation Timer

One thing that annoys many BYOND players is the way movement animations only flash a frame or two before the next step if the mob is moving too fast to play the entire movement animation. The animation timer works just like a movement timer, but it operates independently of the movement timer. A player may run as fast as she wants, but the animation will not repeat frames unless the previous animation is complete.

You can see an example of an animation timer in the Halloween Edition of Tanks. Join the green team and watch how the blob always flows properly, no matter how fast you move.

To use animation timers, forget about the built-in movement states altogether. They are handy for beginner projects, but this method bypasses it completely. The following code requires that mobs have an icon state called "walk" for the animation of the moving mob.

mob
var
tmp // these vars are not saved
anim_delay = 4 // how many frames in the "walk" icon state
anim_time = 0 // the earliest time the animation may play

Move()
var/old_loc = loc // remember where the mob started
. = ..() // perform the default move and store the result in .

// prevent flick()ing the "walk" state if the mob didn't move
if(loc != old_loc)
if(world.time >= anim_time) // check the animation timer
// set the anim_time for anim_delay ticks from now
anim_time = world.time + anim_delay
flick("walk",src)
return . // This line is here for clarity. DM automatically returns .

Pausing To Turn

The default movement routines move your mob immediately in the direction of the arrow key you press. Press down, your mob turns south and takes a step. Press left, your mob turns west and takes a step. Sometimes it would be nice to face a direction without automatically taking a step.

For example, your mob is facing south. Pressing the down arrow moves the mob south as normal. If you press the left arrow, the mob faces west but does not move. The next time you press the left arrow, the mob moves west. SpaceTug has a mode to enable this style of movement.

It's easy to override the client directional proc so that when you press a direction your mob is not facing, it turns the mob that direction without moving.

client
North()
if(mob.dir != NORTH) // if the mob isn't facing north
mob.dir = NORTH // face north
else // otherwise
return ..() // do the default
South()
if(mob.dir != SOUTH)
mob.dir = SOUTH
else
return ..()

.
.
.
and so on, redefining each of the client directional procs. (East(), West(), Northeast(), Northwest(), Southeast(), Southwest())

It is my motto that if you find yourself retyping the same thing over and over again with minor changes, there is a shorter way to do it. In this case, you could override client.Move().

client/Move(Loc)    // player is trying to move to Loc
// get the direction between the mob and the new loc
var/Dir = get_dir(mob,Loc)
if(mob.dir != Dir) // if the mob isn't facing the new Loc
mob.dir = Dir // turn to face it
else // otherwise
return ..() // do the default

Rotational Movement

Rotational movement is a slightly more complex movement style that allows the player to rotate her mob by pressing the left and right arrow keys and step forward or backward by pressing the up and down arrows respectively. Tanks provides a good example of rotational movement.
client
North()
step(mob,mob.dir) // make the mob step forward.
South()
// find the turf behind the mob
var/turf/T = get_step(mob, turn(mob.dir,180))
mob.Move(T,mob.dir) // move to T, but keep facing the current direction
East()
// turn the mob 45 degrees clockwise
mob.dir = turn(mob.dir, -45)
West()
// turn the mob 45 degrees counterclockwise
mob.dir = turn(mob.dir, 45)
// override the diagonals so they do nothing
Northeast()
Northwest()
Southeast()
Southwest()

Strafing

Now you know how to rotate and step forward and backward. Why not step sideways? Sideways movement in video games is also called strafing. Nano Wars provides an example of strafing movement.

This snippet will make the mob step left or right by pressing the south (down) diagonals. Just replace the Southeast() and Southwest() procs in the Rotational Movement snippet with these:

Southeast()
// find the turf right of the mob
var/turf/T = get_step(mob, turn(mob.dir,-90))
mob.Move(T,mob.dir) // move to T, but keep facing the current direction
Southwest()
// find the turf left of the mob
var/turf/T = get_step(mob, turn(mob.dir,90))
mob.Move(T,mob.dir) // move to T, but keep facing the current direction

You can even make the North diagonals turn and step forward in a single action.

Northeast()
// turn the mob 45 degrees clockwise
mob.dir = turn(mob.dir, -45)
step(mob,mob.dir) // make the mob step forward
Northwest()
// turn the mob 45 degrees counterclockwise
mob.dir = turn(mob.dir, 45)
step(mob,mob.dir) // make the mob step forward

Acceleration (Vehicle-like Movement)

Vehicle style movement is a little more complex. It should be a rotational system that allows the up and down arrows to modify the vehicle's speed.

This snippet uses an action cycle (movement accumulator) to provide these controls:

Numeric Keypad
7
Home
forward &
turn left
8
Up Arrow
forward
 
9
Pg Up
forward &
turn right
What do all these mean?
forward - accelerate in the direction the vehicle is facing.
reverse - accelerate away from the direction the vehicle is facing.
brake - decelerate to 0 velocity
4
Left Arrow
turn left
5
 
brake
6
Right Arrow
turn right
1
End
reverse &
turn left
2
Down Arrow
reverse
 
3
Pg Dn
reverse &
turn right

Here is a snippet that makes it possible.

var
const // constants
ACTION_RATE = 30 // how high the accumulator must go to allow movement
FORWARD_ACTION = 1
REVERSE_ACTION = -1

mob
var
action_speed = 0 // rate the mob's accumulator increases
max_forward = 10 // maximum forward speed
max_reverse = 5 // maximum reverse speed
acceleration = 1 // acceleration/deceleration rate
tmp
action = FORWARD_ACTION // the direction the player wants to move
action_count = 0 // the movement accumulator

New()
..() // perform the default New()
spawn(1) lifecycle() // begin the lifecycle loop

proc
lifecycle()
action_count += action_speed
if(action_count >= ACTION_RATE)
action_count -= ACTION_RATE // reduce the accumulator
// perform movement/action here
if(action == FORWARD_ACTION)
step(src,dir)
else if(action == REVERSE_ACTION)
var/turf/T = get_step(src, turn(dir,180))
// move to T, but keep facing the current direction
Move(T,dir)

// we do NOT reset the action in a vehicle lifecycle
spawn(1) lifecycle() // repeat the lifecycle in one tick
accelerate(direction)
if(action == -(direction))
action_speed -= acceleration
if(action_speed < 0)
action_speed *= -1 // make speed positive
action = direction // reverse direction
else
action_speed += acceleration
action = direction
if(direction==FORWARD_ACTION)
action_speed = min(max_forward, action_speed)
else
action_speed = min(max_reverse, action_speed)

// override client directions to use the action lifecycle
client
Center()
mob.action_speed -= mob.acceleration
// make sure it's not less than 0
mob.action_speed = max(0,mob.action_speed)
North()
mob.accelerate(FORWARD_ACTION)
South()
mob.accelerate(REVERSE_ACTION)
East()
// turn the mob 45 degrees clockwise
mob.dir = turn(mob.dir, -45)
West()
// turn the mob 45 degrees counterclockwise
mob.dir = turn(mob.dir, 45)
Northeast()
// turn the mob 45 degrees clockwise
mob.dir = turn(mob.dir, -45)
mob.accelerate(FORWARD_ACTION)
Northwest()
// turn the mob 45 degrees counterclockwise
mob.dir = turn(mob.dir, 45)
mob.accelerate(FORWARD_ACTION)
Southeast()
// turn the mob 45 degrees clockwise
mob.dir = turn(mob.dir, -45)
mob.accelerate(REVERSE_ACTION)
Southwest()
// turn the mob 45 degrees counterclockwise
mob.dir = turn(mob.dir, 45)
mob.accelerate(REVERSE_ACTION)

Combining Movement Styles in One Program

Some games have a variety of movement styles the programmer will want to include. Perhaps you want vehicles to work one way while pedestrians move differently, or you just want to allow players to choose their preference from a variety of styles. The key to sorting out multiple movement styles is in the client directional procs.

Here is an example snippet from Darke Dungeon, which allows players to select their preference from quick movement (traditional BYOND movement), directional movement (as in Pausing to Turn above), and rotational movement (with strafing.) Darke Dungeon uses a variation the movement accumulator method outlined above. The lifecycle() and constant declarations are not important to understand the theory presented here.

client
var
movemode = QUICK_MOVEMODE // default to quick movemode

Move(Loc,Dir)
// this proc processes the direction of an arrowkey pressed by the player
switch(movemode)
if(QUICK_MOVEMODE) // like basic BYOND movement
mob.dir = Dir // turn to the new direction
mob.action = MOVE_ACTION // and move

if(DIRECTIONAL_MOVEMODE) // pause after turning in a direction
// if the mob is already facing this dir
if(mob.dir == Dir)
mob.action = MOVE_ACTION // move the mob
else // otherwise
mob.dir = Dir // turn to face this direction

if(ROTATIONAL_MOVEMODE) // rotating and strafing move style
// decide what to do based on dir pressed
switch(Dir)
if(NORTH) // up arrow
// move the mob
mob.action = MOVE_ACTION
if(SOUTH) // down arrow
// move backwards
mob.action = MOVE_BACK_ACTION
if(EAST) // right arrow
// turn clockwise
mob.dir = turn(mob.dir, -45)
if(WEST) // left arrow
// turn ccw
mob.dir = turn(mob.dir, 45)
if(NORTHEAST, NORTHWEST) // either diagonal up
// turn around
mob.dir = turn(mob.dir, 180)
if(SOUTHEAST) // diagonal down right
// strafe right
mob.action = MOVE_RIGHT_ACTION
if(SOUTHWEST) // diagonal down left
// strafe left
mob.action = MOVE_LEFT_ACTION

verb
movemode()
set desc = "Toggle movement modes."
// increase movemode and keep it within bounds (0 - 2)
if(++movemode > 2) movemode = 0

// tell the player which movemode he is in
switch(movemode)
if(QUICK_MOVEMODE)
src << "Movemode: Quick"
if(DIRECTIONAL_MOVEMODE)
src << "Movemode: Directional"
if(ROTATIONAL_MOVEMODE)
src << "Movemode: Rotational"

Closing Notes

This is far from an exhaustive list of movement controls and enhancements. One of the best things about programming is that there are multiple ways of solving any problem. You can mix and match elements from this article or use the concepts herein to design entirely new movement systems.
do you think this will help my speed abusers problem??
Evil-D123 wrote:
do you think this will help my speed abusers problem??

Yes, since the movement timer is server side, it doesn't matter how fast or how many times the abuser sends a movement command, the mob will only move when its time has come.

If you want to protect all your commands, you may want to consider something like Gughunter's Simple Locks or Jtgibson's Action Locks.
Sorry for bringing up this old post, but I think it would be relevant to note that the text is unreadable for the code.
Highlighting solves all problems.
I'm sure that's exactly what they intended upon the original posting of this article.