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
hi again :D
ReplyDeleteI'm not sure to understand better way to handle creatures. I mean: draw on terminal tiles and then draw Player. But when we'll have more creatures (evil bats, for examples), we need to keep a list of creatures and draw it on top of tiles, right ?
I'm getting errors to do with the player.x and player.y variables... "int cannot be dereferenced"
DeleteHelp me I really have no idea what is going wrong.
Also "can't find symbol player"
Delete@Marte
ReplyDeleteYes, once we have a list of creatures we will draw the tiles then draw each creature where it is supposed to be.
Could you please better explain the part about updating the playscreen? I tried and got heaps of errors.
ReplyDeleteI'm almost a year late, but I just now saw your comment. Basically, instead of having centerX and centerY variables on the PlayScreen instance, we want to use the x and y variables of the Player instance. We're replacing local primitives with a local object.
DeleteHi Shade
DeleteTo whoever reads this: Just delete the two variables CenterX / Y at the beginning and the IDE tells you where you need to put in player.getX()/setX()
(yes I made getters and setter).
Hi Trystan, I've got through to the end of this, but my moveBy method doesn't work :/
ReplyDeleteGame starts up, makes the world, makes a player and randomly places them. But you can't move around. I think it's got something to do with the new PlayerAI(creature) line, I have no idea what that does.
Any ideas?
Oh nevermind, found the bug. Its was a PEBKAC.
DeleteMight be bad form but I put the Creature factory / player declaration up above the constructor.
ReplyDeleteIt resolved all the errors except for some that I needed to replace with player.getX/Y
Hope that helps a few lost dev's
Trystan, if you have a working solution other than what I've posted, please update the page.
Thank you.
An update on my method;
DeleteI have the declaration above the constructor with the assignments withing the constructor.
player.x, player.y works as well, and since he's made them public anyway since they are going to be used a lot it works well with the layout.
ReplyDeleteWe can usually watch all those possible values and meaning out here which are indeed considered to be of utmost importance and needs and surely these would either proved to be much better for them to move around.
ReplyDeleteHi I love your tutorial and I'm using it as a guideline to create my own fantasy rpg and I was wondering if itll be possible to assign the player a sprite like a picture. thx again for this tutorial and any feedback will be appreciated
ReplyDeleteHI there, you could assign another graphics file with colors if you want. But since the Template is black/white and you need to have it in that way, it would be difficult to to it with that.
DeleteThe best way would be, to either rewrite the AsciiPanel itself or to use another graphics library.