We'll need something to represent our player and eventually monsters. They'll all have an x and y coordinate, a glyph, and a color. Since they will be interacting with the world, they should have a reference to that too.
package rltut; import java.awt.Color; public class Creature { private World world; public int x; public int y; private char glyph; public char glyph() { return glyph; } private Color color; public Color color() { return color; } public Creature(World world, char glyph, Color color){ this.world = world; this.glyph = glyph; this.color = color; } }
I made the x and y coordinate publicly accessible since they'll be used a lot, we don't need to constrain them or do anything when they change, and I'd rather not have to deal with getters and setters. Getters and setters are almost always a better idea than public fields (especially when using C# properties) but part of software engineering is knowing the rules and part is knowing when to break them. If this turns out to be a bad idea and we wish we used getters and setters instead, then it's not a big deal since most IDE's can automatically create getters and setters and rewrite your code to use those (Encapsulate Field or Generate Getters And Setters in Eclipse).
To implement all the different behaviors of all the different creatures, we could use a bunch of flags representing creature traits, or we could use subclassing, but let's use something that's usually more flexible: delegation. Each creature will have a reference to a CreatureAi and the creatures can let their ai decide what to do. Instead of using what's called constructor injection and passing the CreatureAi in the constructor like we do with the world, glyph, and color, which is usually a good way of doing things, we'll use setter injection.
private CreatureAi ai; public void setCreatureAi(CreatureAi ai) { this.ai = ai; }
Since the caves we have so far aren't all connected, the player can only walk around in the open area he starts in. We could change how we build the world to make sure that all open cave floors are connected but there's a much easier way to make sure the player can explore everything; we'll let creatures dig through the walls.
public void dig(int wx, int wy) { world.dig(wx, wy); }
And here's our addition to the World class allowing us to dig into cave walls.
public void dig(int x, int y) { if (tile(x,y).isDiggable()) tiles[x][y] = Tile.FLOOR; }
And the Tile class needs an isDiggable method. This way we don't even have to know what the tile is we can just care about if it can be dug through. If we later add new tiles, no-dig zones, or something else we just need to update this method.
public boolean isDiggable() { return this == Tile.WALL; }
And that's the end of our "stuck in a tiny cave" problem. Any day I solve a tricky problem by adding a few little methods is a good day indeed.
Getting back to our Creature class, creatures will also move around in the world. What happens when they try to enter a new tile is up to the creature's ai.
public void moveBy(int mx, int my){ ai.onEnter(x+mx, y+my, world.tile(x+mx, y+my)); }
I think were done with the Creature class for now so let's start on the CreatureAi. Remember when we created the setter for the creature's ai? We'll use that to wire up the creature and the creature's ai. The ai also needs to do deal with the creature trying to enter a new tile. We're going to have a specific ai for the player so it doesn't matter what you use here since we are just going to override it. Here's what I came up with:
package rltut; public class CreatureAi { protected Creature creature; public CreatureAi(Creature creature){ this.creature = creature; this.creature.setCreatureAi(this); } public void onEnter(int x, int y, Tile tile) { } }
Remember when I said we're going to have a specific ai for the player a minute ago? Well here it is:
package rltut; public class PlayerAi extends CreatureAi { public PlayerAi(Creature creature) { super(creature); } }
We need to override the onEnter method to dig through walls and walk on ground tiles.
public void onEnter(int x, int y, Tile tile){ if (tile.isGround()){ creature.x = x; creature.y = y; } else if (tile.isDiggable()) { creature.dig(x, y); } }
If your world has doors then you can make the player automatically open them by walking into them with code very similar to this.
Instead of checking the tile type directly we just ask if it can be walked on or dug through like with isDiggable. Here's our little addition to the Tile class to support that.
public boolean isGround() { return this != WALL && this != BOUNDS; }
We've got a creature class and classes for creature ai — so far so good. We're going to create a lot of creatures that have the same values, all goblins will have a g glyph etc, and we need to make sure we always wire up the correct ai for each new creature. To centralize and hide all this assembly we'll create a class that's responsible for nothing else: the CreatureFactory. Using a factory means the other code doesn't have to deal with all this assembly each time a new creature is created.
package rltut; import asciiPanel.AsciiPanel; public class CreatureFactory { private World world; public CreatureFactory(World world){ this.world = world; } }
The only creature we're assembling so far is the player so that's the only method we need to add.
public Creature newPlayer(){ Creature player = new Creature(world, '@', AsciiPanel.brightWhite); world.addAtEmptyLocation(player); new PlayerAi(player); return player; }
Since the creature needs to start on some empty space and we don't really care which one, we'll add an addAtEmptyLocation method to the world class and let the world take care of that for us.
public void addAtEmptyLocation(Creature creature){ int x; int y; do { x = (int)(Math.random() * width); y = (int)(Math.random() * height); } while (!tile(x,y).isGround()); creature.x = x; creature.y = y; }
All that's left is updating the PlayScreen to display and control our @ rather than scrolling on it's own. Our player is very similar to the current scrolling stuff so it won't be difficult to swap it out.
Remove the centerX and centerY variables and use a variable named player instead. Remove the scrollBy method and replace calls to scrollBy with calls to player.moveBy. (find and replace is your friend for things like this). Also replace references to centerX and centerY with player.x and player.y.
Now that we have a real live hero, we can display that rather than the 'X'.
terminal.write(player.glyph(), player.x - left, player.y - top, player.color());
If you missed any of these steps then the compiler or IDE should let you know. Just remember: error messages are your friend and will help you find what needs to be done.
Now you need to make a CreatureFactory and use it to create the player's creature. This can be done as the last step of the PlayScreen constructor.
CreatureFactory creatureFactory = new CreatureFactory(world); player = creatureFactory.newPlayer();
Just about everything here was adding new classes or a few little methods. The only code we had to really muck around with was a fairly straightforward swap out of the temporary scrolling stuff. That may have seemed like a lot of work just to replace the X on the screen with an @, but we did a lot more than that and are in a really good position for moving on and adding other monsters.
download the code