HalfApped
Android Development and other happenings

Game Development with D

D along with the Dgame package allow for quick game prototyping and development, epecially when compared to C++ and its equivalent graphics packages. This tutorial will show you how to create a simple game engine and implementation using the engine.

Starting Template

Download the template off of Gitgub to begin. You'll need the latest D language runtime, as well as the library dependencies listed in the repository's readme file. Once you setup your project, compile it using dub, and then try running it. If all goes well you should see the a window with our player.

Lets take a look at the basic structure of the code.

Directory structure
project
    - source
        - engine
            - enum
                - direction.d
                - gamestate.d
            - object
                - player.d
            - endgame.d
            - game.d
            - inputhandler.d
            - menu.d
        - app.d
        - engine.d
    - bin
        -res
            -sprite
                - player.png

Our program entry point is app.d. All it does is define the engine object and execute run. The engine.d file is where the first of our game logic begins. Here we define our various game display states (menu, game, endgame) and then also our overall game loop. Each of the states also contains its own loop which is dependent on the GameState variable.

engine.d
class Engine {
    private static const int WINDOW_HEIGHT = 680;
    private static const int WINDOW_WIDTH = 480;
    private static const Color4b BG_COLOR = Color4b(61, 92, 92, 0);
    
    private static Window window;
    
    private GameState gameState;
    
    this() {
        window = Window(WINDOW_WIDTH, WINDOW_HEIGHT, "DeeGame");
        window.setVerticalSync(Window.VerticalSync.Enable);
        window.setClearColor(BG_COLOR);
    }
    
    public void run() {
        Menu menu = new Menu(window);
        Game game = new Game(window);
        EndGame endGame = new EndGame(window);
        
        gameState = GameState.MENU;
        while(gameState != GameState.QUIT) {
            menu.run(gameState);
            game.run(gameState);
            endGame.run(gameState);
        }
    }
}

The various game states each implement a run method which handles the individual logic for each step. For our menu and endgame states this means displaying the menu text, and handling the input. For our game state, we handle all of the character drawing, collision, game logic, and input.

We'll skip the menu and endgame components since they're pretty simple, and instead focus on the game component.

game.d
class Game {
    private static int windowHeight;
    private static int windowWidth;
    
    private static Window *window;        
    private GameState *gameState;
    private static Player player;
    
    private Event event;
    private Direction playerDirection;     
    
    this(ref Window window) {
        this.window = &window;
        this.gameState = gameState;
        
        Size windowSize = this.window.getSize();
        windowWidth = windowSize.width;
        windowHeight = windowSize.height;
        
        player = Player();
        player.create(windowWidth, windowHeight);
        
        playerDirection = Direction.STOP;
    }
    
    public void run(ref GameState gameState) {
        this.gameState = &gameState;
        while(*this.gameState == GameState.PLAYING) {
            this.loop();
        }
    }    
    
    private void loop() {
        window.clear();
        
        InputHandler.handleGameInput(event, *gameState, *window, player, playerDirection);
        
        player.draw(*window);
        
        window.display();
    }
}

There's a bit going on here, so let's break it down.

game.d - constructor
    this(ref Window window) {
        this.window = &window;
        this.gameState = gameState;
        
        Size windowSize = this.window.getSize();
        windowWidth = windowSize.width;
        windowHeight = windowSize.height;
        
        player = Player();
        player.create(windowWidth, windowHeight);
        
        playerDirection = Direction.STOP;
    }

Our constructor takes in a Dgame window object which is used to refresh the screen, and determine the window bounds. We also create our player object and initialize it using its create method. Finally we default our current player's direction to STOP. The playerDirection will be used to determine a vector for our player. This will allow us to "smooth" out the player's movement so that the player doesn't abruptly stop when the user take his or her finger off the arrow keys.

game.d - loop()
    public void run(ref GameState gameState) {
        this.gameState = &gameState;
        while(*this.gameState == GameState.PLAYING) {
            this.loop();
        }
    }    
    
    private void loop() {
        window.clear();
        
        InputHandler.handleGameInput(event, *gameState, *window, player, playerDirection);
        
        player.draw(*window);
        
        window.display();
    }

Here we have our game loop which is determined by the current gameState. GameState is changed depending on the win / lose conditions of the game, as well as by the input received from the user. For example, hitting escape will change the GameState to QUIT, which will then result in program termination.

In the game loop we first clear our window. Next we handle any input, which in our case involves checking if the left / right arrow key is being pressed. Next we draw the player by passing in a reference to the window object. Finally we display the contents of the window with the updated player position.

Let's take a look at our player object.

player.d
struct Player {
    public static const float RUN_VELOCITY_INC = 0.75;
    public static const float RUN_VELOCITY_DEC = 2.5;
    public static const float MAX_VELOCITY = 20.0;
    public static const int MAX_LIVES = 3;
    
    public static Sprite sprite;
    public int lives;
    
    private static int windowWidth;
    private static int windowHeight;
    
    private static Texture texture;
    
    private Direction lastDirection;
    
    private int spriteY;    
    private float currentVelocity = 0;
    
    public void create(int windowWidth, int windowHeight) {
        this.windowWidth = windowWidth;
        this.windowHeight = windowHeight;
        
        Surface surface = Surface("res/sprite/player.png");
        texture = Texture(surface);
        sprite = new Sprite(texture);
        
        lastDirection = Direction.STOP;    
        
        spriteY = windowHeight - 100;
        
        sprite.setPosition(windowWidth / 2, spriteY);
        
        lives = MAX_LIVES;
    }
    
    public void draw(ref Window window) {
        window.draw(sprite);
    }
    
    public void run(Direction dir) {                
        if (dir != Direction.STOP) {
            lastDirection = dir;
        }
        
        if (dir == Direction.STOP  && currentVelocity > 0) {
            currentVelocity -= RUN_VELOCITY_DEC;        
                    
            if (currentVelocity < 0) {
                currentVelocity = 0;
            } else {
                handleStopping();
            }
        } else {
            currentVelocity += RUN_VELOCITY_INC;
        }
        
        if (currentVelocity > MAX_VELOCITY) {
            currentVelocity = MAX_VELOCITY;
        }
        
        if (dir == Direction.LEFT) {
            sprite.move(-currentVelocity, 0);
        } else if (dir == Direction.RIGHT) {
            sprite.move(currentVelocity, 0);
        }
        
        moveInsideWindow();
    }
    
    public void reset() {
        lives = MAX_LIVES;
        
        lastDirection = Direction.STOP;    
        
        currentVelocity = 0;
    }
    
    private void handleStopping() {
        if (lastDirection == Direction.LEFT) {
            sprite.move(-currentVelocity, 0);
        } else if (lastDirection == Direction.RIGHT) {
            sprite.move(currentVelocity, 0);
        }
        
        moveInsideWindow();
    }
    
    private void moveInsideWindow() {
        if (sprite.x() < -32) {
            sprite.setPosition(windowWidth, spriteY);
        } else if (sprite.x() > windowWidth) {
            sprite.setPosition(-32, spriteY);
        }
    }
}

The player object contains a couple key methods - run(Direction dir) and handleStopping(). These two methods "smooth" out the player's movement which give's a nice feel of momentum when moving the player around.

player.d - run(), handleStopping()
    public void run(Direction dir) {                
        // lastDirection should never be set to STOP
        if (dir != Direction.STOP) {
            lastDirection = dir;
        }
        
        if (dir == Direction.STOP  && currentVelocity > 0) {
            currentVelocity -= RUN_VELOCITY_DEC;        
                    
            if (currentVelocity < 0) {
                currentVelocity = 0;
            } else {
                handleStopping();
            }
        } else {
            currentVelocity += RUN_VELOCITY_INC;
        }
        
        if (currentVelocity > MAX_VELOCITY) {
            currentVelocity = MAX_VELOCITY;
        }
        
        if (dir == Direction.LEFT) {
            sprite.move(-currentVelocity, 0);
        } else if (dir == Direction.RIGHT) {
            sprite.move(currentVelocity, 0);
        }
        
        moveInsideWindow();
    }
    
    private void handleStopping() {
        if (lastDirection == Direction.LEFT) {
            sprite.move(-currentVelocity, 0);
        } else if (lastDirection == Direction.RIGHT) {
            sprite.move(currentVelocity, 0);
        }
        
        moveInsideWindow();
    }
    
    private void moveInsideWindow() {
        if (sprite.x() < -32) {
            sprite.setPosition(windowWidth, spriteY);
        } else if (sprite.x() > windowWidth) {
            sprite.setPosition(-32, spriteY);
        }
    }

The player's run method takes in a direction, in our case either LEFT or RIGHT. As long as that direction is sustained, (the user is pressing that arrow key) the player's velocity will incerase in that direction until a maximum velocity is reached. Once the user depresses the arrow key, a direction of STOP will be passed into the run method. At this point the velocity of the player will begin to decrease based on the lastDirection variable. Finally, we check to make sure that the player is still within the boundaries of the window.

inputhandler.d
class InputHandler {    
    public static void handleMenuInput(ref Event event, ref GameState gameState, ref Window window) {
        while(window.poll(&event)) {
            switch (event.type) {
                case Event.Type.Quit:
                    gameState = GameState.QUIT;
                break;
                
                case Event.Type.KeyDown:    
                    if (event.keyboard.key == Keyboard.Key.Esc) {
                        gameState = GameState.QUIT;
                    } else if (event.keyboard.key == Keyboard.Key.Return) {
                        gameState = GameState.PLAYING;
                    }
                break;
                
                default: break;
            }            
        }
    }
    
    public static void handleGameInput(ref Event event, ref GameState gameState, ref Window window, ref Player player, ref Direction playerDirection) {
        while(window.poll(&event)) {
            switch (event.type) {
                case Event.Type.Quit:
                    gameState = GameState.QUIT;
                break;
                
                case Event.Type.KeyDown:    
                    if (event.keyboard.key == Keyboard.Key.Esc) {
                        gameState = GameState.QUIT;
                    } else if (event.keyboard.key == Keyboard.Key.Right) {
                        playerDirection = Direction.RIGHT;
                    } else if (event.keyboard.key == Keyboard.Key.Left) {
                        playerDirection = Direction.LEFT;
                    } else {
                        playerDirection = Direction.STOP;
                    }
                break;
                
                default: 
                    playerDirection = Direction.STOP;
                break;
            }            
        }
        
        player.run(playerDirection);    
    }
    
    public static void handleEndGameInput(ref Event event, ref GameState gameState, ref Window window) {
        while(window.poll(&event)) {
            switch (event.type) {
                case Event.Type.Quit:
                    gameState = GameState.QUIT;
                break;
                
                case Event.Type.KeyDown:    
                    if (event.keyboard.key == Keyboard.Key.Esc) {
                        gameState = GameState.QUIT;
                    } else if (event.keyboard.key == Keyboard.Key.Return) {
                        gameState = GameState.MENU;
                    }
                break;
                
                default: break;
            }            
        }
    }
}

The last of our logic happens in the InputHandler class. We poll the Dgame framework for an event, and then act on that event depending on the key that was pressed. In our case we're either changing the gameState or the player direction. I've decided to implement these as static methods, but feel free to create a struct or non-static class implementation.

Developing our RainDrop Game

Now that we have our simple game engine up and running, let's develop a game on top of it. After months of planning we finally have our requirements and design document in place. Our player must avoid a series of rain drops that fall from the sky. If the player gets wet, he or she loses a life. For each rain drop that the player avoids the score will increment. This will be a simple game, but it can take advantage of the start menu, game, and endgame components of our engine.

First, let's introduce our raindrop class. Create a new file, "raindrop.d" under source/engine/object. In this file we'll use the following imports.

raindrop.d - imports
import std.stdio;
import std.random;

import Dgame.Graphic;

Next, we'll add our RainDrop struct, with some shell methods.

randdrop.d - object
struct RainDrop {
    public void create(int windowWidth, int windowHeight) {

    }

    public void fall(ref int score) {

    }

    public void reset() {

    }

    public bool collidesWith(ref Sprite sprite) {

    }

    public void draw(ref Window window) {

    }
}

To begin, lets add some object level variables. We'll need a few constants to define our max velocity, our velocity increase, and our score increment value. These will act as the gravity and terminal velocity for our falling rain drops. We'll also need our Texture and Surface objects to draw our sprite. Finally, we'll add some non-static properties that will tell the current velocity, visibility, and Y-position.

raindrop.d - properties
struct RainDrop {
    private static const float FALL_VELOCITY_INC = 0.025;
    private static const float MAX_VELOCITY = 8.0;
    private static const int SCORE_INC = 2;

    private static int windowWidth;
    private static int windowHeight;

    private static Texture texture;
    private Sprite sprite;

    private bool visible;
    private int spriteY;
    private float currentVelocity;

    // ...

Let's implement our create method. In this method, we'll need to create the sprite, load the texture, and determine a starting position for the object. Here we use the std.random methods to find a random integer between 0 and the window width which serves as the starting position of our raindrop.

raindrop.d - create()
    public void create(int windowWidth, int windowHeight) {
        this.windowWidth = windowWidth;
        this.windowHeight = windowHeight;
        
        Surface surface = Surface("res/sprite/raindrop.png");
        texture = Texture(surface);
        sprite = new Sprite(texture);
        
        spriteY = uniform(20, 220);        
        auto randomX = uniform(0, windowWidth);
        
        // Set the initial starting position
        sprite.setPosition(randomX, spriteY);
        visible = false;
    }

Next we have our fall method which simulates gravitational forces on our randrop. This method is very similar to the run method our player object implements. However, since we're only dealing with velocity in one direction (down) the raindrop's method can be simplified. You'll also notice that we're incrementing our score here. Once a raindrop goes below the height of our window we add some points to the score and set the raindrop's visibility to false.

raindrop.d - fall()
    public void fall(ref int score) {
        if (visible) {
            currentVelocity += FALL_VELOCITY_INC;
            
            if (currentVelocity > MAX_VELOCITY) {
                currentVelocity = MAX_VELOCITY;
            }
            
            sprite.move(0, currentVelocity);
            
            if (sprite.y() > windowHeight) {
                visible = false;
                currentVelocity = 0;
                
                score += SCORE_INC;
            }
        }
    }

We also implement a reset method on our raindrop. This allows us to recreate raindrops that have already fallen the full length of the window.

raindrop.d - reset()
    public void reset() {
        auto randomX = uniform(0, windowWidth);
        spriteY = uniform(20, 220);
        
        currentVelocity = 0;
        sprite.setPosition(randomX, spriteY);
        visible = true;
    }

We need to determine if our raindrops are hitting our player. I've decided to implement the collision detection on the raindrop object, but you could also implement a more generic method on the game object, or a similar method on the player object. Dgame provides an intersects method which we can use to determine if two objects are overlapping.

raindrop.d - collidesWith()
    public bool collidesWith(ref Sprite sprite) {
        bool colliding = false;
        
        if (this.sprite.getClipRect().intersects(sprite.getClipRect())) {
            colliding = true;
        }
        
        return colliding;
    }

Finally we implement our draw method. This is the same as the player's draw method except it checks to make sure a raindrop is visible first.

raindrop.d - draw()
    public void draw(ref Window window) {
        if (visible) {
            window.draw(sprite);
        }
    }

We've now fully implemented our raindrop object. Now let's make it rain! We need to make a few revisions to our game object which will include the logic required to draw raindrops and make them fall. First let's add a new constant defining the maximum number of raindrops on the srceen at once, and also update our imports.

game.d - class variables
import std.stdio;
import std.container.array;
import std.random;

import Dgame.Window;
import Dgame.Graphic;
import Dgame.Graphic.Color;
import Dgame.System.Keyboard;
import Dgame.Math;
import Dgame.Graphic.Text;
import Dgame.System.Font;

import gamestate;
import player;
import raindrop;
import direction;
import inputhandler

class Game {
    private static const int MAX_RAINDROPS = 25;

    // ...

Let's add a container for our 25 raindrops and initialize it.

game.d - class varaibles
class Game {

    // ...

    private static RainDrop[MAX_RAINDROPS] rainDrops;

    // ...

    this(ref Window window) {

        // ...

        for(int i = 0; i < MAX_RAINDROPS; i++) {
            rainDrops[i] = RainDrop();
            rainDrops[i].create(windowWidth, windowHeight);
        }

    }

If you run the game, we now have... the exact same scene as before we added the raindrops. Looks like we forgot to draw them on the screen. Let's add a draw method to our game object and call it in the loop.

game.d - loop(), draw()
    private void loop() {
        window.clear();
        
        InputHandler.handleGameInput(event, *gameState, *window, player, playerDirection);
        
        draw();
        
        window.display();
    }

    
    private void draw() {
        player.draw(*window);
        
        for(int i = 0; i < MAX_RAINDROPS; i++) {
            rainDrops[i].draw(*window);
        }
    }

While we're at it, let's add a method to move our raindrops. Otherwise they will just pop in at the top of the screen and stay there.

game.d - moveRaindrops()
    private void loop() {
        window.clear();
        
        InputHandler.handleGameInput(event, *gameState, *window, player, playerDirection);
        
        moveRainDrops();

        draw();
        
        window.display();
    }

    private void moveRainDrops() {
        for(int i = 0; i < MAX_RAINDROPS; i++) {
            rainDrops[i].fall(score);
        }
        
        auto randonRainDrop = uniform(0, MAX_RAINDROPS);
        if (rainDrops[randonRainDrop].visible == false) {
            // 70% chance of being visible
            auto visibleChance = dice(30, 70);
            if (visibleChance == 1) {
                rainDrops[randonRainDrop].reset();
            }
        }
    }

The first part of this method simply loops through all the raindrops and calls the fall() method. The second part of the method deals with respawning raindrops that have become invisible due to falling past the boundary of the screen. First a random raindrop is selected from the list of 25. Next, if the raindrop is actually invisible (a visible raindrop could have been selected), the dice roll gives the raindrop of 70% of reappearing. This ensures that raindrops reset at a more randomized interval.

Run the game and you'll notice that our character doesn't mind at all when he or she gets wet. We need to add our collision detection to determine if our character is getting soaked.

game.d - loop(), checkCollisions()
    private void loop() {
        window.clear();
        
        InputHandler.handleGameInput(event, *gameState, *window, player, playerDirection);
        
        moveRainDrops();

        checkCollisions();

        draw();
        
        window.display();
    }

    private void checkCollisions() {
        for(int i = 0; i < MAX_RAINDROPS; i++) {
            if (rainDrops[i].visible) {
                if (rainDrops[i].collidesWith(player.sprite)) {
                    rainDrops[i].visible = false;
                    player.lives--;
                    
                    if (player.lives == 0) {
                        *gameState = GameState.LOST;
                    }
                }
            }
        }
    }

The collision detection is very primitive. We loop through every raindrop and check to see whether or not the player object is being intersected. If a raindrop does hit the player, we deduct a life and check to make sure our player hasn't run out of lives.

We're almost done with our little RainDrop game. All we need to do is add some text to show our score, and the number of lives we have left. First let's define a new Font object in our class variables, and also two Text objects to display our score and lives. We'll also go ahead and create a variables to store our score.

game.d - class variables
class Game {

    // ...

    private static Font arial;
    private static Text scoreText;
    private static Text livesText;

    private int score;

    //...

    this(ref Window window) {

        // ...

        score = 0;
        
        arial = Font("arial.ttf", 22);
        scoreText = new Text(arial);
        livesText = new Text(arial);
    }

All that's left is to draw the text objects to the screen.

game.d - draw()
    private void draw() {
        
        // ...
                
        scoreText.format("Score: %s", score);
        scoreText.setPosition(10, 10);
        window.draw(scoreText);
        
        livesText.format("Lives: %s", player.lives);
        livesText.setPosition(10, windowHeight - 30);
        window.draw(livesText);
    }

This concludes the tutorial. Feel free to make use of the game template we've created. If you would like to take a look at our final product, download the source off of Gitgub.

Permalink
comments powered by Disqus