/ #mechanics 

Movement: Controls

There are roughly two main approaches to letting players control the movement of entities in a game world: direct control and indirect control.

These different control schemes are perhaps best exemplified by looking at a couple of genres other than Turn-Based Strategy. Specifically, First-Person Shooter (FPS) and Real-Time Strategy (RTS):

  • FPS: Arrow or WASD keys are directly translated into movement in the same direction for the player character (i.e. left moves you left, right moves you right, etc.). Likewise, these games map mouse axes directly to control of the camera (i.e. moving up on the y-axis looks up, down looks down, left on the x-axis looks left, right looks right).
  • RTS: The player directly controls a cursor with the mouse, but the player controls the units in the game indirectly. Typically players use the mouse cursor and a click to select a unit, then do right-click at the point where they want the unit to move. The unit then independently makes its way to the target location (if possible). Interestingly the mouse cursor is directly controlled with the mouse, so indirect control is not “purely” indirect. Instead, think of it as an additional layer of abstraction between the player and the units being controlled.

I think these two different types of control produce profoundly different experiences for the player:

  • Direct control: I AM the character.
  • Indirect control: I COMMAND the character.

This idea can easily be seen in turn-based games as well. Roguelikes tend to you use a direct control scheme where you use the arrows to move around and bump into other characters to attack them. Roguelikes are roleplaying games where you control a single character, so using a direct control scheme that lets the player feel ” I AM the character” makes perfect sense. Contrast this with strategy games where you tend to have many units to control. Here an indirect control scheme is used to let you control the many different units, generating a feeling of ” I COMMAND all these characters”.

The following demo tries to illustrate the two different types of control. The X key switches between four modes: 1. Orthogonal with direct control 1. Diagonal with direct control (hold two arrows to move diagonally) 1. Orthogonal with indirect control 1. Diagonal with indirect control

In the indirect control modes, you use the arrows to move your cursor and the C key to move to the cursor.

The obvious lessons to take from this demo are:

  • Direct control is probably the easiest for the player and the programmer.
  • Indirect control is more work for both.
    • For the player, especially with keyboard controlled single-step movements like in this demo (just count the keypresses and compare with direct control to see).
    • But, indirect control can become more manageable if you also introduce AI techniques like pathfinding, which would let the player move a unit over longer distances without having to plan each step.
  • One peculiarity of the demo is the improved accuracy and control for diagonal movement with indirect control due to the crappy direct diagonal controls from PICO-8’s limited inputs. Perhaps the general lesson there is that indirect control can sometimes give you more control than your input devices naturally allow. This extra control is thanks to the layer of abstraction over the basic inputs, where one can reinterpret inputs in any creative ways imaginable (e.g. edge scrolling in TBS/RTS games).

Code Example

This section highlights the essential bits of the code for this mechanic.

The full code for the demo above is available at https://gitlab.com/tbs-mechanics/movement_control. I built the demo with PICO-8, so here’s an API Reference which should clarify anything that’s not vanilla Lua.

Data

The demo is based off Movement: Grid-based, so all of the basic movement data is present again.

function _init()
  -- Normal movement stuff here...
 
  -- Cursor for indirect control modes
  -- cursor is reserved word in PICO-8, so _cursor
  _cursor={}
  _cursor.on=false
  _cursor.x=1
  _cursor.y=1
  -- Cursor animation
  _cursor.frame=1
  _cursor.frames={}
  for i=4,8 do
   add(_cursor.frames,i)
   add(_cursor.frames,i)
  end
 
  -- Mode switching stuff here...
 
  -- Map setup, constants, config...
end

The _cursor.frame bits are optional - they are present to give the cursor animation to make it a bit more visible.

Update Function

For this demo, there are four different update functions for the various movement modes. However, the direct control modes are the same as in Movement: Grid-based, so I will not repeat them here.

For indirect orthogonal movement:

function update_indirect_orthogonal()
  -- make cursor visible and center on player when entering this mode
  if not _cursor.on then
   _cursor.on=true
   _cursor.x=player.x
   _cursor.y=player.y
  end
 
  if btn(0) then
    -- cursor left
    _cursor.x=min(map_w-1,max(0,player.x-1))
    _cursor.y=player.y
  elseif btn(1) then
    -- cursor right
    _cursor.x=min(map_w-1,max(0,player.x+1))
    _cursor.y=player.y
  elseif btn(2) then
    -- cursor up
    _cursor.x=player.x
    _cursor.y=min(map_h-1,max(0,player.y-1))
  elseif btn(3) then
    -- cursor down
    _cursor.x=player.x
    _cursor.y=min(map_h-1,max(0,player.y+1))
  elseif btn(4) then
    -- move player to cursor
    player.x=_cursor.x
    player.y=_cursor.y
  end
end

The main thing to note in the orthogonal case is that if we receive input on the X-axis, we reset the Y-axis to the player coordinate and vice-versa.

For indirect diagonal movement:

function update_indirect_diagonal()
  -- make cursor visible and center on player when entering this mode
  if not _cursor.on then
    _cursor.on=true
    _cursor.x=player.x
    _cursor.y=player.y
  end
 
  if player.delay>0 then
    player.delay-=1
    return
  end
 
  if btn(0) and player.x-_cursor.x<1 then
    -- cursor left
    _cursor.x=min(map_w-1,max(0,_cursor.x-1))
    player.delay=key_delay
  elseif btn(1) and player.x-_cursor.x>-1 then
    -- cursor right
    _cursor.x=min(map_w-1,max(0,_cursor.x+1))
    player.delay=key_delay
  elseif btn(2) and player.y-_cursor.y<1 then
    -- cursor up
    _cursor.y=min(map_h-1,max(0,_cursor.y-1))
    player.delay=key_delay
  elseif btn(3) and player.y-_cursor.y>-1 then
    -- cursor down
    _cursor.y=min(map_h-1,max(0,_cursor.y+1))
    player.delay=key_delay
  elseif btn(4) then
    -- move player to cursor
    player.x=_cursor.x
    player.y=_cursor.y
  end
end
Author

Matt Van Der Westhuizen

Back-end service developer at Ubisoft Blue Byte by day - wannabe game designer & developer by night.