Home of Ebyan Alvarez-Buylla

Cardinal Movement Relative to Player Screen Position

Cardinal Movement Relative to Player Screen Position

Here’s something you’re likely to have run across: the player character is in the center of the screen, and you want it to move in the cardinal direction you click, relative to its position—clicking above it moves north, below south, to the right east, and to the left west.

I’ve implemented this control scheme in my current project, settling on the following technique after a less-than-elegant implementation relying heavily on if-elses:

In order to detect the desired direction, the screen must be separated into four orthogonal quadrants for each of the cardinal direction so that the topmost area corresponds to North, bottommost to South, leftmost to West, and rightmost to East. This can be accomplished by placing clickable elements that detect their own clicks (feasible in Flash or other systems that allow you to easily define arbitrarily-shaped clickable regions), a series of cumbersome branching conditions to detect which quadrant the user is clicking (my original approach), or a more elegant approach that exploits the mathematical properties of these quadrants (illustrated below).

Assuming the function used to move your character takes an x/y-offset from the current position, something like move(xOffset, yOffset), and, for simplicity’s sake, assuming a discrete tile-based system that is integer-indexed such that your map is stored in a 2D array of tiles where an x-offset of -1 to 1 moves West and East, respectively, and a y-offset of -1 to 1 moves North and South, in pseudocode:


function handleClick(screenX, screenY)
{
    deltaX = screenX - playerX;
    deltaY = screenY - playerY;
    absX = Math.abs(deltaX);
    absY = Math.abs(deltaY);

    // If the absolute value of the difference of X is
    // higher than Y's, we're either requesting a left
    // or right click.
    if(absX > absY)
    {
        // Differentiate between West (-1) and East (1)
        // by extracting the sign by dividing by the
        // absolute value.
        move(deltaX / absX, 0);
    }
    // If the absolute value of the difference of Y is
    // higher or equal (arbitrary tiebreaker) than X,
    // we're either requesting a top or bottom click.
    else
    {
        // Differentiate between North (-1) and
        // South (1) by extracting the sign by dividing
        // by the absolute value.
        move(0, deltaY / absY);
    }
}

How does it work?

The first step, regardless of approach, is to put together a vector that describes the relation of the click to the player character’s screen position. This vector, denoted above by (deltaX, deltaY), is then tested to detect in which orthogonal quadrant it lies. If we were working with an arbitrary directional system, we could feed this vector (probably normalized) directly into the move() function, and if we were working with an 8-way movement system, we might be forced to rely on arctangent calculations to determine the angle.

For cardinal movement, however, we can begin with the less-than-elegant brute force approach I mentioned earlier, which goes something like this (replacing “greater” by “greater than or equal to” is trivial but necessary to break ties where applicable):

  • If the deltaY is negative, we are clicking on the top half of the screen, which means either North, East, or West, but no South.
    • If the deltaX is positive, we are clicking on the top-right quadrant of the screen, which means either North or East, but no West.
      • To differentiate between North and East, we see which is bigger: if the absolute value of deltaY is greater than the absolute value of deltaX, then it’s North, else East.
    • If the deltaX is negative, we are clicking on the top-left quadrant of the screen, which means either North or West, but no East.
      • To differentiate between North and West, we see which is bigger: if the absolute value of deltaY is greater than the absolute value of deltaX, then it’s North, else West.
  • If the deltaY is positive, we are clicking on the bottom half of the screen, which means either South, East, or West, but no North.
    • If the deltaX is positive, we are clicking on the bottom-right quadrant of the screen, which means either South or East, but no West.
      • To differentiate between South and East, we see which is bigger: if the absolute value of deltaY is greater than the absolute value of deltaX, then it’s South, else East.
    • If the deltaX is negative, we are clicking on the bottom-left quadrant of the screen, which means either South or West, but no East.
      • To differentiate between South and West, we see which is bigger: if the absolute value of deltaY is greater than the absolute value of deltaX, then it’s South, else West.

A crude approach revealing one nugget of wisdom: comparing the absolute values of the vector components is at the core of each of these checks. So we factor it out, checking firstly whether we are moving more horizontally than vertically, then we divide the vector components by their absolute value to pass in either -1 or 1 into the move() call.

See it in action

Here is the code in action (mouseover to show direction and values):

playerX: 290
screenX:
deltaX:
playerY: 290
screenY:
deltaY:

Here is the JavaScript/jQuery code for the demo above:


$(function()
{
    var playerX = 290;
    var playerY = 290;
    var canvas = $('#cardinal');

    // Swap for mouseclick if desired in a game
    canvas.mousemove(function(event)
    {
        var canvasOffset = canvas.offset();
        var screenX = Math.round(event.pageX - canvasOffset.left);
        var screenY = Math.round(event.pageY - canvasOffset.top);
        var deltaX = screenX - playerX;
        var deltaY = screenY - playerY;
        var absX = Math.abs(deltaX);
        var absY = Math.abs(deltaY);

        // Updates the screen numbers, may not be relevant in a game application
        update(screenX, screenY, deltaX, deltaY);

        if(absX > absY)
        {
            move(deltaX / absX, 0);
        }
        else
        {
            move(0, deltaY / absY);
        }
    });

    function update(screenX, screenY, deltaX, deltaY)
    {
        $('#screenX strong').text(screenX);
        $('#screenY strong').text(screenY);
        $('#deltaX strong').text(deltaX);
        $('#deltaY strong').text(deltaY);
    }

    function move(xOffset, yOffset)
    {
        var direction = 'move(' + xOffset + ', ' + yOffset + '); '
        if(yOffset == -1) direction += '<strong>North</strong>';
        else if(yOffset == 1) direction += '<strong>South</strong>';
        else if(xOffset == -1) direction += '<strong>West</strong>';
        else if(xOffset == 1) direction += '<strong>East</strong>';
        $('#move').html(direction);
    }
});