The Game Developer's Guidepost

The Game Developer's Guidepost

Your guide to learning game development

Programming Simple 2D Games in D

Chapter 3: Basic Player Character Movement

In this chapter things will start looking more like a real video game. We're going to add a character to the game that the player will control. The character will move around the game window in response to player input. Before we begin, go ahead and remove the if statement from last chapter that changed the background color when a key was down. Now add the following below the include statements:

struct Vect2
{
    float x, y;
}

struct Entity
{
    Vect2 pos;
    Vect2 vel;
    int width, height;
}

Here we define two data types, the first of which is a two-dimensional vector where each component is a float. Vector math (and linear algebra in general) is exceptionally useful in game programming, even for games as simple as this one. We’ll make prodigious use of vector math throughout this entire tutorial series, so I highly recommend you familiarize yourself with the topic if you haven’t already.

After our 2D vector definition we then define an Entity struct. In game development the term “entity” is commonly used to refer to any interactive object that inhabits a game world. Since we’re using such a broad term to refer to this data type, it should come as no surprise that we'll use instances of the Entity struct to represent everything that occupies the game world, including the player character.

The "pos" member represents the position of a given entity. More precisely, it determines how far the entity is from the origin of the game world by our unit of measure. For this game, the origin of the world is at the top-left of the window and our units will be in pixels. Ideally we would use meters as our unit of measure in order to decouple the game logic from the rendering code. However, I’ve opted to just use pixels for this game to make certain calculations easier to explain. The second member stores the current velocity of the entity. Each entity also has width and height members. These determine the size of the rectangular bounds that surround the entity. This rectangle is called a "bounding box" and will be used in later chapters to check for collisions between entities.

Before we instantiate our player character entity we need to decide how we want to manage the state of our game. By “state” I’m referring to all the mutable data that will persist from one frame to the next. It would be easier for us if all our state was bundled together, making it simple to pass around to functions that need access. If you think about it, our GameInput struct from last chapter counts as persistent state. Let's broaden GameInput to hold all the data for the game in one place. Rename GameInput to GameState and add a new Entity member called "player". The old GameInput struct should now look like the following:

struct GameState
{
    bool[INPUT_KEY_TOTAL] isKeyDown;
    int[INPUT_KEY_TOTAL]  keycode;

    Entity player;
}

Delete the code just before the game loop where we previously created a GameInput instance and filled out its keycode array. In its place add the following code:

    GameState s;
    initGame(&s);

Here we instead create a new GameState instance simply called "s" and pass it to the initGame function. What initGame function you may ask? We'll get to that shortly, but first let's edit the event loop to refer to "s" rather than "input," like so:

                    foreach (i; 0 .. INPUT_KEY_TOTAL)
                    {
                        if (s.keycode[i] == evt.key.keysym.sym)
                        {
                            s.isKeyDown[i] = evt.key.state == SDL_PRESSED;
                        }
                    }

Now let's add a function to initialize our game state. Add the following above the main function (or below, it's up to you):

void initGame(GameState* s)
{
    s.keycode[INPUT_KEY_LEFT]   = SDLK_a;
    s.keycode[INPUT_KEY_RIGHT]  = SDLK_d;
    s.keycode[INPUT_KEY_UP]     = SDLK_w;
    s.keycode[INPUT_KEY_DOWN]   = SDLK_s;
    s.keycode[INPUT_KEY_RETRY]  = SDLK_RETURN;

    s.player.width  = 32;
    s.player.height = 64;
    s.player.pos.x = windowWidth / 2;
    s.player.pos.y = windowHeight / 2;
}

Here we begin by setting the default keyboard bindings like we did in the last chapter. We then set the width and height of the player character (in pixels) and position it in the center of the game window. For now that's all we need to initialize our player character. But wouldn't it be nice if we could see it on the screen? In later chapters we'll introduce how to render sprites to the screen, but for now we'll simply draw a colored rectangle around the entity's bounds. This isn't very flashy, but it's a good place to start. Add the following code for this purpose:

void renderEntity(SDL_Renderer* renderer, Entity* e, ubyte r, ubyte g, ubyte b)
{
    SDL_SetRenderDrawColor(renderer, r, g, b, 255);

    SDL_Rect rect;
    rect.x = cast(int)e.pos.x - (e.width / 2);
    rect.y = cast(int)e.pos.y - (e.height / 2);
    rect.w = e.width;
    rect.h = e.height;

    SDL_RenderFillRect(renderer, &rect);
}

This function is very simple, but let's break it down anyway. It takes the rendering context returned by SDL2, a pointer to the entity we wish to draw, and the color to draw the entity in RGB values. Inside the function, we first set the color we wish SDL2 to use when drawing by calling SDL_SetRenderDrawColor and pass in our RGB values, much as we did when clearing the screen in earlier chapters. We'll set the alpha channel to 255, which means fully opaque, as this game won't feature any transparent entities. Next we make an instance of an SDL_Rect which describes the pixels on the screen we wish SDL_RenderFillRect to fill with the draw color we just set.

To render an entity's bounds we need a clear understanding of how it relates to the entity's position vector. For example, the position could be relative to the top-left of the entity's bounding box. We could in fact choose any arbitrary anchor point we wish, but doing so will affect how we convert the bounds of an entity to the SDL_Rect that will be rendered inside the window. For this game I’ve opted for the position to correspond to the center of an entity's bounds. But the x and y coordinates of an SDL_Rect correspond to the top-left of a rectangle, not the center of a rectangle as we're using. So how do we convert between these two different representations of the same shape? Fortunately this is very easy; we subtract half the bounds of our entity from its center (the "pos" vector) to find the top-left corner of its bounds and set the x and y members of the SDL_Rect to the result. Then all we have to do is set the w and h members of the SDL_Rect to the width and height of our entity's bounding box.

Now that we have the means, we only need to call renderEntity to draw the player character. The following should be added just before our call to SDL_RenderPresent:

        renderEntity(renderer, &s.player, 0, 0, 255);

If you compile and run the game now, you should see a blue rectangle at the center of the screen. Pretty nice, but we promised player movement! The simplest form of player movement can be done by adding the following below our event loop but before our rendering code:

        float playerSpeed = 0.01f;
        s.player.vel = Vect2(0, 0);
        Vect2 playerAccel = Vect2(0.0f, 0.0f);
        if (s.isKeyDown[INPUT_KEY_LEFT])
        {
            playerAccel.x = -playerSpeed;
        }
        if (s.isKeyDown[INPUT_KEY_RIGHT])
        {
            playerAccel.x = playerSpeed;
        }

        if (s.isKeyDown[INPUT_KEY_UP])
        {
            playerAccel.y = -playerSpeed;
        }
        if (s.isKeyDown[INPUT_KEY_DOWN])
        {
            playerAccel.y = playerSpeed;
        }

        s.player.vel.x += playerAccel.x;
        s.player.vel.y += playerAccel.y;
        s.player.pos.x += s.player.vel.x;
        s.player.pos.y += s.player.vel.y;

Here we check to see if a direction key is pressed on the keyboard and set the acceleration for the player character to one percent of a pixel in the appropriate direction each frame. We then set the player character's velocity to this acceleration and then add the velocity to the player character's position, moving it across the screen. The trouble with this code is the player character will move as fast as the computer can process each frame. You can imagine this would lead to the player character moving faster on some computers and slower on others. This is certainly undesirable; we want the game to behave the same for each player regardless of the computer on which it’s run. But we'll work on fixing that in the next chapter. For now, try adjusting the value of "playerSpeed" if the player character moves too fast or slow to control well.

Chapter Challenges

  1. Try making the position of an entity correspond with the top-left of their bounding box. This will require you to adjust both the rendering code and the code used to center the player character on the screen. Try doing the same with the bottom-right of an entity’s bounding box.

Chapter Resources