/home/jeky

Guide to write a 2D game engine (2)

2017-12-09

Last time we were talking about how animation works. Basically all you need to know are how to paint and how to update. However, if you look at the demo of last article, you would find that the balls won't be removed from the list even when they fly out of game canvas. This causes a memory leak issue.

So the first changes in this article is to have a method named toRemove() in Animation interface.

public interface Animation {

    void update(Dimension canvasDimension);

    boolean toRemove();

    void draw(Graphics2D g2d);
}

If an animation object think itself can be removed, it will set toRemove() return true and the game context will remove it from animation list.

public class Game {

    //...

    @Override
    public void run() {
        while (start) {
            // update all objects
            animations.forEach(a -> a.update(canvas.getSize()));
            // collect all the objects that are no longer needed
            List<Animation> toRemoveList = animations.stream()
                                                     .filter(Animation::toRemove)
                                                     .collect(Collectors.toList());
            animations.removeAll(toRemoveList);
            // render all objects
            canvas.repaint();
            try {
                Thread.sleep(timeDelta);
            } catch (InterruptedException e) {
                start = false;
            }
        }
    }
}

If you are not familiar with Java 8 Stream API, it is time to learn it. It will collect all the objects of which toRemove() method return true. If you are familiar with Python, it equals to:

toRemoveList = [a for a in animations if a.toRemove()]

This time we will talk about how to control the animations. We will use Sprite as the term to describe the controllable objects as in this game engine we will focusing on sprite animation (There will be an article talking about animating sprites).

In Java Swing API, when we press/release a key, it will generate a KeyEvent and dispatch it to focused JComponent (in our case it is GameCanvas). In our architecture, we need to dispatch this key event to all the animations.

So first we will create the class of Sprite:

public abstract class Sprite extends AbstractAnimationObject {

    protected Map<Integer, SpriteAction> pressedActions;
    protected Map<Integer, SpriteAction> releasedActions;

    public Sprite() {
        pressedActions = new HashMap<>();
        releasedActions = new HashMap<>();
    }

    public <T extends Sprite> void registerKeyPressedEvent(int keyCode, SpriteAction<T> action) {
        pressedActions.put(keyCode, action);
    }

    public <T extends Sprite> void registerKeyReleasedEvent(int keyCode, SpriteAction<T> action) {
        releasedActions.put(keyCode, action);
    }

    public void onKeyPressed(int keyCode) {
        if (pressedActions.containsKey(keyCode)) {
            pressedActions.get(keyCode).run(this);
        }
    }

    public void onKeyReleased(int keyCode) {
        if (releasedActions.containsKey(keyCode)) {
            releasedActions.get(keyCode).run(this);
        }
    }
}

The abstract animation object is a class implementing Animation interface and having some location information inside (coordination and size). Registering key event methods are used to bind a key and an action (or a method reference). And when key events come, on key pressed/released methods will be invoked. Sprite will try to find the binded method for this key and if it can find it, this method will be invoked.

And then write the code to dispatch the events:


public class Game implements Runnable {

    // ...
    private JFrame gameWindow;
    private List<Animation> animations;
    private List<Sprite> sprites;

    public Game(int fps) {
        // ...
        gameWindow.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                sprites.forEach(s -> s.onKeyPressed(e.getKeyCode()));
            }

            @Override
            public void keyReleased(KeyEvent e) {
                sprites.forEach(s -> s.onKeyReleased(e.getKeyCode()));
            }
        });
    }

    public void addSprite(Sprite sprite) {
        animations.add(sprite);
        sprites.add(sprite);
    }

    //...
}

We first added a new list containing all the sprites. So each time when we add a sprite into this game, it will be added into animation list as well as a sprite is also an animation. And we added a key listener to game window so that when a key is pressed/released, this key listener will dispatch it to all sprites (using Sprite.onKeyPressed() and Sprite.onKeyReleased() method).

Now we can write a new demo for our new features. I wrote a ME class for a controllable rectangle. First created a class extending Sprite that will draw a blue rectangle on game canvas:

public class Me extends Sprite {

    public Me() {
        width = SIZE;
        height = SIZE;
    }

    @Override
    public void update(Dimension canvasDimension) {
        // TODO
    }

    @Override
    public void draw(Graphics2D g2d) {
        g2d.setColor(Color.blue);
        g2d.fillRect(x, y, width, height);
    }
}

In order to control this blue rectangle, we need to write some methods to let it move and bind some keys to these methods. Note that when moving sprites, the most important thing is not to directly update the location of it. Since the key events are dispatched immediately and game canvas is only updated when game thread updates it, if you update the location of sprite directly, the sprite will be jumping instead of walking.

To get rid of update the location so quick, you need to set the status of this sprite when key events come. For example, if up key is pressed, set the moving direction of sprite to north (up). You can set this moving direction to north for many times without making your sprite jump to north. And when down key is pressed or up key is released, set the moving direction to null which will stop your sprite from moving.

public class Me extends Sprite {

    private int directionH;
    private int directionV;
    private static final int SPEED = 3;
    private static final int SIZE = 10;

    public Me() {
        width = SIZE;
        height = SIZE;
        directionH = 0;
        directionV = 0;
    }

    private void moveUp() {
        directionV = Math.max(directionV - 1, -1);
    }

    private void moveDown() {
        directionV = Math.min(directionV + 1, 1);
    }

    private void moveLeft() {
        directionH = Math.max(directionH - 1, -1);
    }

    private void moveRight() {
        directionH = Math.min(directionH + 1, 1);
    }

    @Override
    public void update(Dimension canvasDimension) {
        x += directionH * SPEED;
        y += directionV * SPEED;

        // make sure we are in canvas
        if (x < 0) {
            x = 0;
        }
        if (y < 0) {
            y = 0;
        }
        if (x + width > canvasDimension.getWidth()) {
            x = (int) (canvasDimension.getWidth() - width);
        }
        if (y + height > canvasDimension.getHeight()) {
            y = (int) (canvasDimension.getHeight() - height);
        }
    }
}

I added the code to ensure we won't be out of canvas in update() method as well. Now this controllable rectangle is almost there. All we need to do is to bind keys to methods.

public class Me extends Sprite {

    // ...    

    public Me() {
        // ...

        // register keys
        registerKeyPressedEvent(KeyEvent.VK_UP, Me::moveUp);
        registerKeyPressedEvent(KeyEvent.VK_DOWN, Me::moveDown);
        registerKeyPressedEvent(KeyEvent.VK_LEFT, Me::moveLeft);
        registerKeyPressedEvent(KeyEvent.VK_RIGHT, Me::moveRight);

        registerKeyReleasedEvent(KeyEvent.VK_UP, Me::moveDown);
        registerKeyReleasedEvent(KeyEvent.VK_DOWN, Me::moveUp);
        registerKeyReleasedEvent(KeyEvent.VK_LEFT, Me::moveRight);
        registerKeyReleasedEvent(KeyEvent.VK_RIGHT, Me::moveLeft);
    }

    //...
}

All the changes and this demo can be found in this commit. If you run the demo, you can see this (In fact it moves really smoothly. Frame rate of this gif is really low so you will see it is jumping. Try to run it by yourself to see the real one):

Moving Blue Rectangle