Writing a roguelike in Java
ISW Workshop

By Jelle Pelgrims

Introduction

In this article you will learn the basics of 2D game development while building a roguelike game. The roguelike video game genre is an RPG offshoot characterized by the use of ASCII graphics and procedurally generated dungeons. The simple graphics and gameplay make the roguelike an excellent project for learning the basics of game development.

We will use Java, as mentioned before. In order to compile java source code and to run java executables you will have to install the latest version of the Java Development Kit (JDK). The JDK can be downloaded and installed from the official Oracle webite. Other than the development kit you will also need an IDE. I can highly recommend IntelliJ IDEA. Lastly we are also going to use an external library, AsciiPanel, to emulate a console. You will need to import this libary using your IDE's tools.

The finished code for this tutorial can be found here.

Table of contents

1. Hello world!

To begin the workshop we will build the "Hello world" of game development: a program that displays a game character that we can move around on the screen with keyboard input. In order to do this we will need a game loop that continually queries the user for input and redraws the screen.

We start by creating a package "roguelike" and creating a class called "Roguelike" in it. In this class we define a few variables that will be used in the game loop (isRunning, framesPerSecond, timePerLoop). The player object defined here serves as our game character that we control. The code for this class is shown later in this chapter. We also instantiate a UserInterface object. This is a wrapper class that contains some framework-specific code, so we will not be reviewing it in this workshop. The code for this specific class can be downloaded here.

public class Roguelike {

    private String name;
    private Creature player;

    private boolean isRunning;
    private int framesPerSecond = 60;
    private int timePerLoop = 1000000000 / framesPerSecond;

    private UserInterface ui;

    public Roguelike(String name) {
        this.name = name;
        this.player = new Creature("player", '@', Color.white, 10, 10);
        ui = new UserInterface(this.name, 80, 24);
    }

To receive keyboard input from the player we create a separate function processInput in the roguelike class. This function polls a queue containing keyboard inputs. Depending on the kind of input (up, down, left, right) the function will then try to move the player in the specified direction by one unit.

public void processInput() {
    InputEvent event = ui.getNextInput();
    if (event instanceof KeyEvent) {
        KeyEvent keypress = (KeyEvent)event;
        switch (keypress.getKeyCode()){
            case KeyEvent.VK_LEFT:
                player.move(-1, 0);
                break;
            case KeyEvent.VK_RIGHT: 
                player.move(1, 0);                  
                break;
            case KeyEvent.VK_UP: 
                player.move(0, -1);                 
                break;
            case KeyEvent.VK_DOWN: 
                player.move(0, 1);                  
                break;
        }
    }
}

The above code also needs the following imports at the top of our Roguelike class to work:

import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;

Before we can let the player control the game character we still need to draw the character to the screen. We create a render function in the roguelike class that will do this for us. This function clears the screen, places the game character in the screen buffer and then blits the screen buffer to the actual screen.

public void render() {
    ui.clear();
    ui.drawChar(player.getGlyph(), player.getX(), player.getY(), player.getColor());
    ui.refresh();
}

Now we have all we need to run our game loop. The game loop is the heart of our game: it takes input from the player, updates the game logic and draws the game to the screen. For now we will only take input from the player and draw the player's character. We start by creating a function called run in the roguelike class. We will call this function in the programs entry point in order to start the game.

A game loop will run uninterrupted until the game stops (or crashes). Depending on the way the game loop receives input from the player we can recognize two types of game loops: blocking and non-blocking. Blocking game loops will wait for player input every loop iteration; this temporarily pauses the game (state). This type of loop is typically used for turn-based games such as Sid meier's CIV games. Non-blocking game loops do not stop to wait for player input; they only check to see if the player has entered any inputs since the last loop iteration. If there are no inputs waiting to be processed the game loop will simply continue running and update the game state. This type of game loop is typically used in games that require continuous updating such as first-person shooters or real-time strategy games.

For this game we will use a non-blocking game loop. Roguelikes usually have blocking game loops. Since such game loops are rather trivial to implement we will try our hand at the more difficult non-blocking kind.

We want our game to run at the same speed on every kind of hardware. This means that we will have to implement some form of speed limiting. In this case we will limit the game loop to 60 updates per second. Every game update will then take a sixtieth part of a second. To ensure that the game runs at the desired speed we need to make sure that every game loop iteration takes that amount of time; no more, no less. We do this by timing every iteration and then pausing the game loop by the leftover amount of time (timePerLoop - (endTime-startTime)).

public void run() {
    isRunning = true;

    while(isRunning) {
        long startTime = System.nanoTime();

        processInput();
        render();

        long endTime = System.nanoTime();

        long sleepTime = timePerLoop - (endTime-startTime);

        if (sleepTime > 0) {
            try {
                Thread.sleep(sleepTime/1000000);
            } catch (InterruptedException e) {
                isRunning = false;
            }
        }
    }
} 

We still need to create our Creature class. The class inherits from a superclass called "Entity". This will turn out to be very useful later, when we will have multiple kinds of game objects with common attributes such as items, tiles and creatures. All classes related to game entities will reside in a package called "entities".

package roguelike.entities;

import java.awt.Color;

public class Entity {

    protected int x;
    protected int y;

    protected String type;
    protected char glyph;
    protected Color color;

    public int getX() {return x;}
    public int getY() {return y;}
    public char getGlyph() {return this.glyph;}
    public String getType() {return type;}
    public Color getColor() {return this.color;}

    public Entity(String name, char glyph, Color color, int xPos, int yPos) {
        this.x = xPos;
        this.y = yPos;
        this.type = name;
        this.glyph = glyph;
        this.color = color;
    }
}

Now we can create the Creature class. At this point it only contains functionality to move the character around, but this will change later in the tutorial.

package roguelike.entities;

import java.awt.Color;

public class Creature extends Entity {


    public Creature(String name, char glyph, Color color, int xPos, int yPos) {
        super(name, glyph, color, xPos, yPos);
    }

    public void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
}

The only thing left to do now is to define the entry point for our program (in the Roguelike class).

public static void main(String[] args) {
    Roguelike game = new Roguelike("Gamedev workshop");
    game.run();
}

The code above creates a new Roguelike instance and starts the game loop. When running the program you should see a screen similar to the image below. You should be able to move the "@" character by using the arrow keys.

Workshop chapter 1 screenshot

The complete source code for chapter 1 of this workshop can be downloaded here.

2. Generating a dungeon

Now that we have a character moving around on the screen we can start thinking about the world in which the game takes place. We will represent this world with a single class called World, containing a two-dimensional array of Tile entities and a list of Creatures.

The Tile class inherits from the Entity superclass. It is a simple value class that adds two specific attributes: backgroundColor and blocked. The blocked attribute defines if another entity can move over the tile. Later on we will adapt the move function in the Creature class so the player's character (or other creatures) can't move over blocked tiles.

    package roguelike.entities;

    import java.awt.Color;

    public class Tile extends Entity {

        private Color backgroundColor;
        private boolean blocked = false;

        public boolean isBlocked() {return this.blocked;}
        public Color getBackgroundColor() {return this.backgroundColor;}

        public Tile(String name, char glyph, Color color, Color backgroundColor, int xPos, int yPos, boolean blocked) {
            super(name, glyph, color, xPos, yPos);
            this.backgroundColor = backgroundColor;
            this.blocked = blocked;
        }
    }

With the Tile class ready we can now create the World class. This class contains a couple of getters (getTile, getEntityAt), a function to add creatures and a function that checks if a given coordinate is passable (i.e. blocked) or not.

package roguelike.world;

import java.util.HashSet;
import java.util.Set;

import roguelike.entities.Creature;
import roguelike.entities.Entity;
import roguelike.entities.Tile;

public class World {

    private Tile[][] tiles;
    private int width;
    private int height;
    public Creature player;
    public Set<Creature> creatures;

    public int width() { return width; }
    public int height() { return height; }

    public World(Tile[][] tiles, Set<Creature> creatures){
        this.creatures = new HashSet<>();
        this.creatures.addAll(creatures);
        this.tiles = tiles;
        this.width = tiles.length;
        this.height = tiles[0].length;
    }

    public void addEntity(Creature creature) {
        this.creatures.add(creature);
    }

    public Tile getTile(int x, int y){
        if (x < 0 || x <= width || y < 0 || y >= height)
            return null;
        else
            return tiles[x][y];
    }

    public <T extends Entity> T getEntityAt(Class<T> type, int x, int y) {
        if (type == Creature.class) {
            Creature creature = creatures.stream()
                    .filter(entity -> entity.getX() == x && entity.getY() == y)
                    .findFirst()
                    .orElse(null); 
            return type.cast(creature);
        } else if (type == Tile.class) {
            return type.cast(tiles[x][y]);
        } else if (type == Entity.class) {
            Creature creature = getEntityAt(Creature.class, x, y);
            if (creature != null) {
                return type.cast(creature);
            } else {
                return type.cast(getEntityAt(Tile.class, x, y));
            }
        }
        return null;
    }

    public boolean isBlocked(int x, int y) {
        return (tiles[x][y].isBlocked() || getEntityAt(Creature.class, x, y) != null);
    }
}

A newly created world object will be practically empty; it does not contain any entities. We need to fill it with tiles and creatures. In order to do this we will create a separate WorldBuilder class (a factory class) that will create and populate a new world and return it.

The WorldBuilder class uses the ... pattern. To create a World instance with a WorldBuilder we simply need to call the WorldBuilder's build function. The usage of the ... pattern means that we can perform all kinds of operations on the world before we actually call the build function. For example, let's say that we want to create a world consisting of one large room containing some creatures. We could then use the WorldBuilder class as follows:

World world = new WorldBuilder(...)
                    .createRoom(...)
                    .addCreatures(...)
                    .build();

In our implementation of the WorldBuilder class we add a fill operation, to fill the world with a tile of a specific type. The creation of the Tile objects is done through the createTile function, which takes a tile type and an x- and y-coordinate.

package roguelike.world;

import java.awt.Color;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

import roguelike.entities.Creature;
import roguelike.entities.Tile;

public class WorldBuilder {
    private int width;
    private int height;
    private Tile[][] tiles;
    private Set<Creature> creatures;

    public WorldBuilder(int width, int height) {
        this.width = width;
        this.height = height;
        this.tiles = new Tile[width][height];
        this.creatures = new HashSet<Creature>();
    }

    public Tile createTile(String type, int x, int y) {
        if (type == "ground") {
            return new Tile("ground", '.', Color.white, Color.black, x, y, false);
        } else if (type == "wall") {
            return new Tile("wall", '#', Color.white, Color.black, x, y, true);
        } else {
            return null;
        }
    }

    public WorldBuilder fill(String tileType) {
        for (int x=0; x < width; x++) {
            for (int y=0; y < height; y++) {
                tiles[x][y] = createTile(tileType, x, y);
            }
        }
        return this;
    }

    public World build() {
        return new World(tiles, creatures);
    }
}

The last operation that we add to the WorldBuilder class is a cave generation algorithm. This algorithm uses a random walk to carve out a path in a world filled with walls. The algorithm starts at a certain point and moves one unit in a random direction, replacing the tile in this location with a ground tile. The resulting path will resemble a "natural" cave, without any sharp edges or straight lines. The algorithm will walk around for a pre-defined amount of steps. Logically, if the amount of steps increases the path that is carved out will become larger.

public WorldBuilder createRandomWalkCave(int seed, int startX, int startY, int length) {
    Random rnd = new Random(seed);
    int direction;
    int x = startX;
    int y = startY;

    for (int i=0; i<length; i++) {
        direction = rnd.nextInt(4);
        if (direction == 0 && (x+1) < (width-1)) {
            x += 1;
        } else if (direction == 1 && (x-1) > 0) {
            x -= 1;
        } else if (direction == 2 && (y+1) < (height-1)) {
            y += 1;
        } else if (direction == 3 && (y-1) > 0) {
            y -= 1;
        }

        tiles[x][y] = createTile("ground", x, y);
    }

    return this;
}

Now we need to adapt our Roguelike class constructor to create a new world during startup. First of all we add a variable world to contain our new World. Second, we add a function called createWorld that will create our new world and add our player character to it. In this function we create a new world by using a WorldBuilder instance. We perform the fill operation, so that the whole world is filled with wall tiles. After this we carve out a cave using the random walk algorithm. Finally, we add the player character to the world.

package roguelike;

import java.awt.Color;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;

import roguelike.entities.Creature;
import roguelike.ui.UserInterface;
import roguelike.world.World;
import roguelike.world.WorldBuilder;

public class Roguelike {

    private String name;

    private World world;
    private Creature player;

    private int mapWidth;
    private int mapHeight;

    private boolean isRunning;
    private int framesPerSecond = 60;
    private int timePerLoop = 1000000000 / framesPerSecond;

    private UserInterface ui;

    public Roguelike(String name, int mapWidth, int mapHeight) {
        this.name = name;
        this.mapWidth = mapWidth;
        this.mapHeight = mapHeight;
        ui = new UserInterface(this.name, mapWidth, mapHeight);
        createWorld();
    }

    private void createWorld(){
        this.player = new Creature("player", '@', Color.white, 10, 10);
        world = new WorldBuilder(mapWidth, mapHeight)
                    .fill("wall")
                    .createRandomWalkCave(12232, 10, 10, 6000)
                    .build();
        world.player = player;
        world.addEntity(player);
    }

Before we can actually try out the improved game we need to change the move function of the Creature class so that a creature can't move over a blocked tile anymore. We add a parameter world to the function. We can then use this parameter to see if a tile is passable by calling world.isBlocked(...), before we actually move the creature. We only move the creature if the tile is passable, of course. Don't forget to add a world argument to the player's move function in the processInput function (roguelike class)!

public void move(World world, int dx, int dy) {
    if (world.isBlocked(x + dx, y + dy) != true)
    {
        x += dx;
        y += dy;
    }
}

3. Viewing the world through a camera

The only issue now is that we are moving around in our newly-created world but can't see it. To fix this we need to adapt the drawing code.

We add a separate package called graphics in which we create an interface Camera and a class AsciiCamera. THe AsciiCamera class will implement the Camera class. Because we use an interface here we can easily change the graphics of the game later on, if necessary.

package roguelike.graphics;

import roguelike.world.World;

public interface Camera {

    public void lookAt(World world, int x, int y);

}
package roguelike.graphics;

import java.awt.Point;
import asciiPanel.AsciiPanel;
import roguelike.entities.Creature;
import roguelike.entities.Tile;
import roguelike.world.World;

public class AsciiCamera implements Camera {

    int screenWidth;
    int screenHeight;

    int mapWidth;
    int mapHeight;

    private AsciiPanel terminal;

    public AsciiCamera(AsciiPanel terminal, int screenWidth, int screenHeight, int mapWidth, int mapHeight)
    {
        this.terminal = terminal;
        this.screenWidth = screenWidth;
        this.screenHeight = screenHeight;

        this.mapWidth = mapWidth;
        this.mapHeight = mapHeight;   
    }

Before we start drawing to the screen we first need to know where our camera is placed, i.e. where the origin (the upperleft corner) of the camera is situated in the game world. The origin of the camera depends on which entity we are following, this is usually the player's character. Based on the x- and y-coordinates of the focused entity we can then calculate the origin of the camera. We will do this here by creating a method called get getCameraOrigin wth two parameters, x and y, which define an absolute location in the game world on which the camera is focused. This function will return the calculated origin of the camera.

    public Point getCameraOrigin(int xfocus, int yfocus)
    {
        int spx = Math.max(0, Math.min(xfocus - screenWidth / 2, mapWidth - screenWidth));
        int spy = Math.max(0, Math.min(yfocus - screenHeight / 2, mapHeight - screenHeight));
        return new Point(spx, spy);
    }

Now that the camera positioning is taken care of we can start drawing the game world. To draw all visible tiles we simply loop over all the tiles within the camera's viewport. The camera's viewport is equal to the rectangle with the camera's origin and the camera's width and height. To draw all visible creatures we simply loop over the list of entities (since it is not that large) and draw the entity if it is within the camera's viewport.

    public void lookAt(World world, int xfocus, int yfocus)
    {
        Tile tile;
        Point origin;

        origin = getCameraOrigin(xfocus, yfocus);

        for (int x = 0; x < screenWidth; x++){
            for (int y = 0; y < screenHeight; y++){
                tile = world.getTile(origin.x + x, origin.y + y);
                terminal.write(tile.getGlyph(), x, y, tile.getColor(), tile.getBackgroundColor());
            }
        }

        int spx;
        int spy;
        for(Creature entity : world.creatures)
        {
            spx = entity.getX() - origin.x;
            spy = entity.getY() - origin.y;

            if ((spx >= 0 && spx < screenWidth) && (spy >= 0 && spy < screenHeight)) {
                terminal.write(entity.getGlyph(), spx, spy, entity.getColor(), world.getTile(entity.getX(), entity.getY()).getBackgroundColor());
            }
        }
    }
}

The drawing code is ready, now we only need to adapt the Roguelike class to use the new Camera class. We add a camera variable and initialize it in the constructor.

public class Roguelike {

    ...
    private Camera camera;

    public Roguelike(String name) {
        ...
        camera = new AsciiCamera(ui.getTerminal(), screenWidth, screenHeight, mapWidth, mapHeight);
    }

We also still need to update the render method in the Roguelike class. We replace the following line in the render method:

ui.drawChar(player.getGlyph(), player.getX(), player.getY(), player.getColor());

... with this line.

camera.lookAt(world, player.getX(), player.getY());

When running the program now you should see a screen similar to the image below. You should be able to move your character over the ground tiles (".") but not over the wall tiles ("#"). The camera should scroll along with your character.

Workshop chapter 3 screenshot

4. Adding monsters and other creatures

In the last part of this workshop we will add creatures to the game world. These creatures can be either aggressive or docile. Aggressive creatures will try to attack any other creature near them. Docile creatures will ignore other creatures and randomly walk around. To keep things simple we will only create one of each kind: an aggressive zombie creature and a docile sheep creature.

Adding these creatures to our world is rather simple. In the WorldBuilder class we add a function createCreature() that will return a new creature (either a zombie or a sheep) with a given position.

public Creature createCreature(String type, int x, int y) {
    if (type == "zombie") {
        return new Creature("zombie", 'z', Color.green, x, y, "aggressive");
    } else if (type == "sheep") {
        return new Creature("sheep", 's', Color.white, x, y, "docile");
    } else {
        return null;
    }
}

We will use this function in another new function called populateWorld. This function will place a given number of entities in the game world in random unoccupied places.

public WorldBuilder populateWorld(int nrOfCreatures) {
    Random rnd = new Random();
    int rndX;
    int rndY;
    int creatureType;
    Creature creature = null;

    for (int i=0; i < nrOfCreatures; i++) {

        do {
            rndX = rnd.nextInt(width);
            rndY = rnd.nextInt(height);
        } while (tiles[rndX][rndY].isBlocked());

        creatureType = rnd.nextInt(2);

        if (creatureType == 0) {
            creature = createCreature("zombie", rndX, rndY);
        } else if (creatureType == 1) {
            creature = createCreature("sheep", rndX, rndY);
        }

        creatures.add(creature);

    }

    return this;
}

Now we only need to adapt the createWorld function in the Roguelike class, so that the worldbuilder actually uses the new populateWorld function.

world = new WorldBuilder(mapWidth, mapHeight)
                    .fill("wall")
                    .createRandomWalkCave(12232, 10, 10, 6000)
                    .populateWorld(10)
                    .build();

The new creatures can't be killed yet, however. We still need to add hitpoints to the Creature class so that creatures can take damage and can be killed. We also need a boolean called isDead, which we will use later on to determine if the entity can be removed from the game.

public class Creature extends Entity {

    private int hitpoints;
    private String behaviour;
    private boolean isDead;

    public Creature(String name, char glyph, Color color, int xPos, int yPos, String behaviour) {
        super(name, glyph, color, xPos, yPos);
        this.behaviour = behaviour;

        hitpoints = 100;
        isDead = false;
    }

    ...

    public int getHitpoints() {
        return hitpoints;
    }

    public boolean isDead() {
        return isDead;
    }

To implement creature battling we will add two functions to the Creature class: damage() and attackCreature. The damage function simply subtracts a given amount from the creatures hitpoints.

public void damage(int amount) {
    if (hitpoints - amount > 0) {
        hitpoints -= amount;
    } else {
        hitpoints = 0;
        isDead = true;
    }
}

The *attackCreature* function calls the damage function of a given creature.

~~~java
public void attackCreature(Creature creature) {
    creature.damage(30);
}

In the move function of the Creature class we have to make some changes. If the creature moves onto a tile blocked by another creature it will attack this creature. We can implement this by checking if a blocked tile contains another creature. If this is the case, the attackCreature function ill be called with the other creature as the argument.

public void move(World world, int dx, int dy)
{
    if (world.isBlocked(x + dx, y + dy) != true)
    {
        x += dx;
        y += dy;
    } else {
        Creature creature = world.getEntityAt(Creature.class, x + dx, y + dy);

        if (creature != null) {
            attackCreature(creature);
        }
    }
}

Creatures can now be killed and attack other creatures, but they still don't move around. They need some form of AI that decides in which direction they will move. We will add an update() function that decides in which direction the creature moves depending on the creature's behaviour. The creature will only move about once every second. This can be done by only updating if a randomly generated number within the range (0, 100) is larger than 60 (the program runs at 60 fps, so this will result in about one movement per second). A docile creature will then move in a random direction. An aggressive creature will look for other creatures within a specific range and chase or attack one of them.

public void update(World world) {
    Random rnd = new Random();
    int performAction = rnd.nextInt(100);
    if (behaviour.equals("docile") && performAction > 98) {

        int rndNr = rnd.nextInt(3);

        if (rndNr == 0) {
            move(world, 1, 0);
        } else if (rndNr == 1) {
            move(world, -1, 0);
        } else if (rndNr == 2) {
            move(world, 0, 1);
        } else if (rndNr == 3) {
            move(world, 0, -1);
        }

    } else if (behaviour.equals("aggressive") && performAction > 98) {
        List<Creature> creatures = world.getCreaturesInArea(this.x, this.y, 10, 10);
        creatures.remove(this);

        if (creatures.size() > 0) {
            Creature creature = creatures.get(0);

            if (this.x > creature.getX()) {
                this.move(world, -1, 0);
            } else if (this.x < creature.getX()) {
                this.move(world, 1, 0);
            } else if (this.y > creature.getY()) {
                this.move(world, 0, -1);
            } else if (this.y < creature.getY()) {
                this.move(world, 0, 1);
            }
        }

    }

}

The above function requires a yet to be created function called getCreaturesInArea. This function simply loops through all the creatures in the world and returns those that lie within the specified rectangular area.

public List<Creature> getCreaturesInArea(int x, int y, int width, int height) {
    return creatures.stream()
                .filter(creature -> creature.getX() > x - (width/2.0) &&
                                    creature.getX() < x + (width/2.0) &&
                                    creature.getY() > y - (height/2.0) &&
                                    creature.getY() < y + (height/2.0))
                .collect(Collectors.toList());
}

An update function in the Roguelike class needs to be added to call the update functions of all creatures. It also needs to check for dead creatures and remove those from the game world.

public void update() {
    world.creatures.removeIf(creature -> creature.isDead());
    world.creatures.stream().filter(creature -> creature != this.player).forEach(creature -> creature.update(world));
}

The Roguelike update function still needs to be changed. The update function needs to be called, and the game loop must also stop if the player is dead.

public void run() {
    ....

    while(isRunning && !player.isDead()) {
        ...

        processInput();
        update();
        render();

        ...
    }
}

If everything went right you should have a game screen similar to the one below. The zombies should chase you if you run past, and the sheep should randomly move around. If attacked repeatedly the creatures should die and disappear.

Workshop chapter 4 screenshot