2D Character Movement

I have been having trouble finding good resources on a relatively simple, clean, and smooth process for top-down 2D character movement in a video game.  I'm working on a side project inspired mostly by Legend of Zelda: Oracle of Ages, and am trying to write my own similar-ish game.

Movement and collision detection are two of the most fundamental aspects of small games and the manner in which they work can greatly affect game play and the code structure.  Of course, there is not one correct way in which to write this, but what I have come up with works pretty well and I wanted to document it.

My game makes use of axis-aligned bounding boxes (AABBs) for collision detection, and instead of using a tile-based approach for terrain models it in a series off AABB "brushes" similar to how Hammer editor does theirs.  Of course everything is 2D and axis-aligned.  What this gives me is the ability to have terrain features at sizes other than standard tile-size, while giving terrain features the same collision meshes as any other feature.  This was something I wanted to add to the game to make it a bit different, and increase my capabilities.

The main character, Dave, has his own AABB and a position (int x, int y) in world coordinates.  In order to achieve movement I need to shift his position over time to user input, but how to exactly go about doing that while making it smooth and game-friendly is the real challenge.

The simplest control scheme is something as follows:

//detect player input
boolean moveEast = in.isKeyDown(Input.KEY_D);
boolean moveWest = in.isKeyDown(Input.KEY_A);
boolean moveNorth = in.isKeyDown(Input.KEY_W);
boolean moveSouth = in.isKeyDown(Input.KEY_S);
if moveNorth
    pos.y -= SPEED;
else if moveEast
    pos.x += SPEED;
else if moveSouth
    pos.y += SPEED;
else
    pos.x -= SPEED;
end

The above set of code is rather straightforward; you check each button pressed and shift the player according to how it was pressed.  This works in a rather rudimentary way, but presents several problems.

First, if the player presses both North and South simultaneously, they cancel each other out.  This is what one wants, sure, but the way the code handles the case is not terribly elegant.  I want to know that either north or south gets evaluated, and never both.

Second, the player essentially moves twice as fast when moving diagonally than when moving horizontally or vertically.  It makes the player's movement look weird if he accelerates when moving around corners.  Ideally, the player should move with the same total speed in both cases.

In the ideal case, if the player is moving horizontally with speed 1.0, then he should have a total speed of 1.0 when moving diagonally.  The diagonal speed is spd = sqrt(spd_x^2 + spd_y^2).  Since spd_x is going to be the same as spd_y, we determine that we want to slow down axially to 1/sqrt(2) of what we move at horizontally, or 0.7071 * SPEED.

In a world without floating points, the next best thing will have to do.  I have a base speed of 3 pixels per tick and a diagonal speed of 2 pixels per tick.  This gives me a ratio of 0.6666, which works well enough.

Here is the updated code set:

boolean moveEast = in.isKeyDown(Input.KEY_D);
boolean moveWest = in.isKeyDown(Input.KEY_A);
boolean moveNorth = in.isKeyDown(Input.KEY_W);
boolean moveSouth = in.isKeyDown(Input.KEY_S);
 int numDirsPressed = ((moveEast ^ moveWest) ? 1 : 0) + ((moveNorth ^ moveSouth) ? 1 : 0);
 if(numDirsPressed > 0) {
    walkCounter++;
    // horizontal movement
    if (moveEast ^ moveWest) {
        pos.plus(moveEast ? speed : -speed, 0);
    }
    // vertical movement
    if (moveNorth ^ moveSouth) {
        pos.plus(0, moveSouth ? speed : -speed);
    }
}

The next issue to discuss involves collisions.  Movement should be hampered when running into things, and here is where the movement code actually gets interesting and infinitely frustrating.  As mentioned, I have a lot of objects modeled as AABBs, which I can test in collision against my player.  My strategy is to have the player move and then check for collision, shunting the player back in the direction he came from.  Again, this works fine in a rudimentary way, but there are numerous problems.

First, we get the problem of catching corners.  This occurs when you want the player to go down a hallway as wide as he is.  Naturally, in a game based off of its tile-predecessors, there are going to be many corridors exactly as wide as the player model.   Low and behold, if I am walking sideways to enter the corridor there is a good chance that with a 64 pixel character width I will not perfectly line up.  If I shift 3 pixels with every tick it is going to be even harder to line up.

The solution I employed was to use a tactic from the Zelda games.  Whenever I move horizontally or vertically, I will shift the player towards aligning with an 16-pixel spacing.  This makes it much easier to get through those doors, because you are going to naturally align as you move towards them.  It is important to note that the protuberance is small, and I never noticed it in the Zelda games until it was pointed out to me.

The second problem I had to deal with involved sprites, and what animation to play.  Before I can completely explain this, I need to mention that my player has a direction in which he is facing, such that the appropriate sprite is displayed.  Determining this direction is also non-trivial, particularly when the player can move in more than one direction at once, and I don't have diagonally-facing sprites.  The simplest solution I could come up with was to only change the direction I am facing if I press a new direction and am no longer pressing the direction I am currently facing.  Coding this up is a bit confusing, and is included farther below.

As far as pushing goes, I have a boolean variable, isPushing, which determines whether or not the pushing sprite is shown.  I want the player to be shown pushing against the wall when he tried to run into it.  One would think to simply implement the pushing sprite whenever one collides with an AABB, but this does not produce the desired result when facing sideways while moving along a wall and strafing into it.  Since the direction that is being faced is sideways compared to the wall, and the player does move along the wall, it makes no sense to show the image of the player pushing against nothing while moving along the wall.  Instead I want to merely keep the walking sprite.  This was accomplished with some boolean variables, and by sampling the terrain around me before moving the character.

My final code set is as follows, note that isPushing is still activated by running into something:

//check for collisions along border
boolean collN = C.checkPointCollision(pos.x, pos.y - HALF_SPRITE_HEIGHT - 1);
boolean collE = C.checkPointCollision(pos.x + HALF_SPRITE_WIDTH + 1, pos.y);
boolean collS = C.checkPointCollision(pos.x, pos.y + HALF_SPRITE_HEIGHT + 1);
boolean collW = C.checkPointCollision(pos.x - HALF_SPRITE_WIDTH - 1, pos.y);
// collision detectors tingle
boolean willCollide = (moveNorth && collN || moveEast && collE 
    || moveSouth && collS || moveWest && collW);
// if pressing same direction as currently facing
boolean pressingDir = ((dir == Utils.DIR_NORTH && moveNorth) ||
    (dir == Utils.DIR_EAST && moveEast) ||
    (dir == Utils.DIR_SOUTH && moveSouth) ||
    (dir == Utils.DIR_WEST && moveWest));
int numDirsPressed = ((moveEast ^ moveWest) ? 1 : 0) + ((moveNorth ^ moveSouth) ? 1 : 0);
int speed = (isPushing ? PUSH_SPEED : 
    (numDirsPressed == 2 ? WALK_DIAG_SPEED :  WALK_STRAIGHT_SPEED)); //determine speed 
//restrict movement to the direction we are facing
if(willCollide && numDirsPressed > 1){
    if(collN && dir != Utils.DIR_NORTH)
        moveNorth = false;
    if(collE && dir != Utils.DIR_EAST)
        moveEast = false;
    if(collS && dir != Utils.DIR_SOUTH)
        moveSouth = false;
    if(collW && dir != Utils.DIR_WEST)
        moveWest = false;
}
if(numDirsPressed > 0) {
    walkCounter++;
    // horizontal movement
    if (moveEast ^ moveWest) {
        pos.plus(moveEast ? speed : -speed, 0);
    if (numDirsPressed == 1 && pos.y % 16 != 0)
        pos.plus(0, pos.y % 16 < 8 ? -1 : 1); //movement correction
    if (!pressingDir)
        dir = (moveWest ? Utils.DIR_WEST : Utils.DIR_EAST); //face a new direction
    }
    // vertical movement
    if (moveNorth ^ moveSouth) {
        pos.plus(0, moveSouth ? speed : -speed);
    if (numDirsPressed == 1 && pos.x % 16 != 0)
        pos.plus(pos.x % 16 < 8 ? -1 : 1, 1);  //movement correction
    if (!pressingDir)
        dir = (moveSouth ? Utils.DIR_SOUTH : Utils.DIR_NORTH);  //face a new direction
    }
} else {
     //no longer walking
     walkCounter = 0;
}

Voila.  What follows this is simply a collision detection routine where I collide the player off of the AABBs in the environment.  If I hit an AABB I do not necessarily go to the pushing sprite, but instead only activate it if the player's AABB is in the voronoi region of the collided AABB's side (see the N tutorial).  This prevents the player from showing the pushing sprite when barely clipping a block's edge.

Hopefully this post is useful to someone.  I know I spent quite a while trying to find good online references, but never really found a good through explanation.  If you bug me about it I could be convinced to put up some explanatory images to further describe what is going on.  Also, please let me know if you have some neat tutorials on game programming that go further in depth than the basic stuff.

Until next time.

-Tim

One thought on “2D Character Movement

  1. Pingback: Tim » Tile Based Game Overview

Comments are closed.