Friday, September 2, 2011

roguelike tutorial 05: stationary monsters

Time for some monsters! I want to keep things simple so our first monster will be a stationary fungus. Sounds too boring? Well then I'll throw in a little something to make things interesting.

If our world's going to have a bunch of creatures then we should start there. Add a list for them to the World class and initialize it in the World constructor.

private List<creature> creatures;

We need a way to get the creature at a specific location.

public Creature creature(int x, int y){
    for (Creature c : creatures){
        if (c.x == x && c.y == y)
            return c;
    }
    return null;
}

And we'll update the addAtEmptyLocation method to make sure we don't add a creature where one already is. This will also be the place to add new creatures to our list.

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,y) != null);
  
    creature.x = x;
    creature.y = y;
    creatures.add(creature);
}

If we want multiple creatures then we have to change how we display the world. Currently we just display the player. Get rid of that line and let's change the displayTiles method to show any creatures in the section of the world we're interested in.

private void displayTiles(AsciiPanel terminal, int left, int top) {
    for (int x = 0; x < screenWidth; x++){
        for (int y = 0; y < screenHeight; y++){
            int wx = x + left;
            int wy = y + top;

            Creature creature = world.creature(wx, wy);
            if (creature != null)
                terminal.write(creature.glyph(), creature.x - left, creature.y - top, creature.color());
            else
                terminal.write(world.glyph(wx, wy), x, y, world.color(wx, wy));
        }
    }
}
This is actually a very inefficient way to do this. It would be far better to draw all the tiles and then, for each creature, draw it if it is in the viewable region of left to left+screenWidth and top to top+screenHeight. That way we loop through screenWidth * screenHeight tiles + the number of creatures. The way I wrote we loop through screenWidth * screenHeight * the number of creatures. That's much worse. I don't know why I didn't realize this when I first wrote this since I've always drawn the creatures after the tiles. Consider this an example of one way to not do it.

You should be able to run it. It will look the same as before but now we can throw in some more creatures and they'll show up too. Our fungus first needs it's own ai:

package rltut;

public class FungusAi extends CreatureAi {
 
    public FungusAi(Creature creature) {
        super(creature);
    }
}

And add it to our CreatureFactory so we can make them.

public Creature newFungus(){
    Creature fungus = new Creature(world, 'f', AsciiPanel.green);
    world.addAtEmptyLocation(fungus);
    new FungusAi(fungus);
    return fungus;
}

Now modify the PlayScreen to populate the world with fungus.

public PlayScreen(){
    screenWidth = 80;
    screenHeight = 21;
    createWorld();
  
    CreatureFactory creatureFactory = new CreatureFactory(world);
    createCreatures(creatureFactory);
}

private void createCreatures(CreatureFactory creatureFactory){
    player = creatureFactory.newPlayer();
  
    for (int i = 0; i < 8; i++){
        creatureFactory.newFungus();
    }
}

If you run it now you should see some green f's standing about.


We've got some monsters but to way to slay them. Let's work on that now; starting with the Creature class.

public void moveBy(int mx, int my){
    Creature other = world.creature(x+mx, y+my);
  
    if (other == null)
        ai.onEnter(x+mx, y+my, world.tile(x+mx, y+my));
    else
        attack(other);
}

public void attack(Creature other){
    world.remove(other);
}

For now an attack will be an instant kill - just tell the world to get rid of the creature. Later on we can add hitpoints and whatnot.

Here's how the world class can remove a killed creature:

public void remove(Creature other) {
    creatures.remove(other);
}

There you go. Stationary monsters waiting to be slain.



And now time to throw in a little something to make things interesting. What if the fungi were able to reproduce and spread? I really like games where each creature has something special instead of all being the same things with slightly different stats so this could be interesting. We first need to let each creature know when it's time to update itself and whatever else it wants to do for it's turn. First and add a method to the World class that lets each creature know it's time to take a turn.

public void update(){
    for (Creature creature : creatures){
        creature.update();
    }
}

Your IDE is probably letting you know that the Creature class doesn't have an update method. Let's add one that delegates to the ai.

public void update(){   
    ai.onUpdate();  
}

Now your IDE is probably letting you know that the CreatureAi class doesn't have an onUpdate method so go ahead and add an empty one. We want the fungi to spread to a nearby open space every once in a while as part of it's behavior during updating. Here's what I came up with:

package rltut;

public class FungusAi extends CreatureAi {
    private CreatureFactory factory;
    private int spreadcount;
 
    public FungusAi(Creature creature, CreatureFactory factory) {
        super(creature);
        this.factory = factory;
    }

    public void onUpdate(){
        if (spreadcount < 5 && Math.random() < 0.02)
            spread();
    }
 
    private void spread(){
        int x = creature.x + (int)(Math.random() * 11) - 5;
        int y = creature.y + (int)(Math.random() * 11) - 5;
  
        if (!creature.canEnter(x, y))
            return;
  
        Creature child = factory.newFungus();
        child.x = x;
        child.y = y;
        spreadcount++;
    }
}

You can play around with how far it spreads, how often it spreads, and how many times it can spread. Don't forget to modify the newFungus method to pass itself into the FungusAi constructor. The last thing you need to do is tell the world to let everyone take a turn by calling world.update() in the PlayScreen after handling user input. The user's input makes the player move and each creature can move after that. Since we're relying on Java's event handling it would be very cumbersome to make our code pause and wait for user input during the player's onUpdate like with many other roguelikes. Just one tiny detail left. If you run this then you will probably see some exceptions happen because we're adding new creatures to our list while looping through the same list — which you can't do. There are different ways of dealing with this but the easiest I know of is to just make a copy of the list and loop through that instead.

public void update(){
    List<creature> toUpdate = new ArrayList<creature>(creatures);
    for (Creature creature : toUpdate){
        creature.update();
    }
}

download the code
So now you've got some underground caves with a hero and some fungi. You can attack and they can spread. We're a bit closer to being an actual game. Next up, actual combat.

36 comments:

  1. I like the spread mechanics, good idea!! :D

    but fungus doesn't attack player, at least for now, right ?

    About CreatureFactory, can I suggest to use a class with static methods that build creatures? So you don't need to use it as parameter for every ai you write :D

    ReplyDelete
  2. @Marte
    I used to use static methods just like you suggest but I found that once they rely on variables then you need static variables and some way to set or reset them. Static variables are also global and global variables can cause huge headaches. So it's definitely possible to use static methods and variables, but I try to error on the side of not using them. If I learn the hard way that it would have been easier, then I'll start using them but until then, I don't mind passing a factory instance. But that's just my preference, you should try different ways and see what works best for you.

    ReplyDelete
  3. Interesting. I had some lag, but I fixed the code you said was a bad idea, and updated to the latest version of asciipanel. The first step helped a fair bit, the second cleared up the problem ( until I set the spawn rate to 0.2 ). I found a decent spawn rate for the fungus was more like 0.001. Interesting idea. Thank you, sir.

    ReplyDelete
  4. Re: static factory

    I would also be inclined to use a static factory. All the variables in CreatureFactory could easily be moved to the world, since the factory really shouldn't care how many it has created and may be used by multiple worlds (if each dungeon/town is a different world).

    But since this is a tutorial, keeping things basic is much better than trying to get the optimum architecture. As you develop your game, play around with things and see how it works.

    ReplyDelete
  5. @sdp0et, are you suggesting the World class have methods like newPlayer, newFungus, etc?

    ReplyDelete
  6. No, no. If there is 1 world' as in this example, the Factory either has a static reference to it, or the world is passed in to the factory methods (would make sense only if each level/locale/city/etc in the game was its own "world).

    In this simple example the only variable is the world. A Static factory could either hold that instance, or, since there is only one world, world could have a static wrapper
    public static World.getInstance()
    that returns the single instance of World that exists. Then world does not need to be passed around all the time. This is a case where the essential global variable is ok, because everything in the world exists IN it and therefor would have access to interact with it. As you said, when you know the rules, you know when to break them. For someone who is not as experienced or confident, best to keep it simple.

    I have no fault with what's in the example, I think it's great. I was just offering Marte a suggestion since he wanted to do it.

    ReplyDelete
  7. @sdp0et, that is a sensible plan and I've done things like that before. For this project I had an explicit goal of not using global variables and it made some things easier and some things uglier. I definitely agree with what you said: play around with things and see how it works.

    Thanks for the feedback by the way!

    ReplyDelete
  8. Hello, and I love your tutorials.

    I'm having a bit of trouble though. So far, I've had very little difficulty understanding things in this tutorial. But I reached this one and off the bat got tons of errors. I've never worked with Lists before. I have the right import (java.util.List) and I've checked if I've typed it right and even copypasted it just to make sure. I get the following error on the List:

    The type List is not generic; it cannot be parameterized with arguments

    The only fix is to remove the type arguments. Now whenever I call creatures, it tells me it cannot be resolved. Will these issues be fixed later on, as I'm afraid going on may cause more issues, or did I miss something?

    ReplyDelete
  9. Edit: Sorry, I accidentally used the wrong import, should have been java.util.List (as I said in my previous post.) But in the actual program it as awt. My next issue was with the part. It had no clue what to do with that, so I simply capitalized the c in creature.

    ReplyDelete
  10. Nevermind, that didn't work either.

    How do I initialize the list? It's on the tutorial, but you never say how to do it. I've looked it up, but it doesn't work. Eclipse doesn't give me errors, but when I run it and hit enter I get a huge list of errors, and I have no clue what they mean.

    ReplyDelete
    Replies
    1. To initialize the creature list as an ArrayList (a class that implements the List interface):

      this.creatures = new ArrayList<Creature>();

      Delete
  11. Thanks, I figured that out (I should have told you.) I'm actually all the way up to 13 with very few issues. Also, you should capitalize creature on the first snippet in this, as it caused a bit of confusion with me (of course I'm using this as a bit of a refresher as it's been about 6 months since I've used java and my memory is a little clouded.

    Once again, great tutorial! About 8 months ago I tried making an RL, and that failed (I tried not using libraries but still outputting on the terminal. Every time the player did something, I would put enough space to make it seem as if it cleared.) I found this actually on the roguebasin forums. Unlike most tutorials I've read, this one actually works.

    One more thing, displayTiles class, you said underneath that the code would lag a lot? I did not notice a change in lag at all. Is this just me, or did you fix it? I'm not sure I see what the issue could be, but I'm not much good at solving these kinds of things.

    ReplyDelete
    Replies
    1. Alright, I finished all your tutorials and began turning this into a possibly fun game. It's a bit like your Hyrule game but you go through a randomly generated forest trying to hunt down a dark elf. I posted on another tutorial my problem, but I think I figured it out that it has to do with the displayTiles method. The issue I'm finding is the game lags to an unplayable point after about 550 monsters, and I'd prefer to have thousands of monsters(of course it will be random.) Now you said beneath displayTiles that that script will loop map width * map height * monster count times, and it should instead loop map width * map height + monster count times. Can you point me in the right direction to revising the script to work that way?

      Also, sorry for commenting so much. Hope I'm not bugging you!

      Delete
    2. Instead of checking if there's a creature at each point and then drawing it or the ground, draw all the ground tiles and then loop through the creatures and draw the ones that should be on the screen. That way you loop through the creatures only once instead of once for each space you want to draw.

      Delete
  12. Alright, thanks! I'll try that tomorrow. Thanks for answering!

    ReplyDelete
    Replies
    1. I finished the tutorial by the way, and I remember the creature class got modified so it returned the creatures glyph, that way we just looped through all the spaces and set the spaces glyph instead of looping through both... Is there any changes that should be made to that to improve speed? I don't see anything looking at it right now, but I'm probably not even half the programmer you are :). I'm talking about the update that was made in tutorial 8 (the one with FOV and stuff.)

      Delete
    2. I actually figured it out. I modified the update script in the creatures to run only if the player can see them, and if the player couldn't they now just wander. This increased speed to a point where I quadrupled the size of the map and the amount of enemies and it was lagging about 10% what it did before. Just a tip for anyone who runs into a any lag issues.

      Delete
    3. In what way did you modify it?

      Delete
  13. could you show me an example of the code you said you SHOULD have wrote. please :)

    ReplyDelete
  14. I've worked through the tut to this point and I'm wondering, it seems that you let the window handle the main loop through the update window repaint/paint method. Is there any way to check how often the window gets updated? Until this point, is our tutrl real-time and not turn-based? I find this method of avoiding a main loop a bit confusing.

    ReplyDelete
    Replies
    1. From what I understand of Java - and most modern GUI frameworks - you don't have access to the main loop. The framework controls that and you supply the callbacks and overrides. If you need to know how often it gets updated or repainted then you can track that yourself. The game is still turn based but painting the window happens whenever it's needed: after resizing, after another window is no longer obscuring it, or when we update the game state and tell it to repaint. Having a main loop would make some things easier, but I don't know how to get a window based app to work that way and it goes against how everything else in the framework works.

      TL;DR: no main loop - just callbacks and overrides.

      Delete
    2. I did some thinking on this. Using a SwingWorker, to create a second thread in the background to handle the main loop, would do the trick. The callbacks from the window send the input to the main loop and the window get the things it has to draw from the second thread. This would make the code even more loosely coupled, which should/would be a good thing in software engineering, atleast I think so.

      Delete
    3. I tried something like that when I first started playing with roguelikes in java. I gave up on it because I had problems with the input, output, and main loop being on separate threads. Or maybe they weren't on separate threads - I don't remember exactly. Java multithreading isn't my strong suit so hopefully you can make it work. I'm not sure how that would be loosely coupled though. I would still want separate classes to deal with interpreting input, maintaining the current state, and displaying the correct output.

      Delete
  15. Out of curiosity, maybe this is a "feature", but it looks a bit like a bug. The way the spreadcount mechanic is handled, since the spreadcount is attached to the FungusAi, which gets its own instance each time a new fungus is created, there is no upper limit to how many fungus there could be? Sure there is a maximum of 5 per fungus, but since we keep creating new fungus...

    ReplyDelete
    Replies
    1. That actually is a design choice. Caves that overflow with fungi is oddly funny to me.

      Delete
  16. Awesome tutorial!
    But I've got one problem - my fungus don't appear. I've no idea why :(

    ReplyDelete
  17. The way I figured I should replace the inefficient bit of the loop was

    for(Creature c : world.creatureList) {
    if((c.x > left && c.x < left + screenWidth) && (c.y > top && c.y < top + screenHeight)) {
    terminal.write(c.glyph(),c.x,c.y,c.color());
    }
    }

    after making World#creatureList public

    ReplyDelete
    Replies
    1. Hmmm... should the line that actually writes it to the terminal look like this?

      terminal.write(c.glyph(), c.x - left, c.y - top, c.color());

      Otherwise it draws the player on a spot on the screen further away from what it actually is in the game's reality. Only problem is, it doesn't draw the player when they are hard up against the world border. Wonder why that could be... any ideas?

      Delete
    2. Derp, the check also needs a >= clause on the lower bounds for each dimension. If anyone else is stuck on this here is the code I have used, and is now working:

      private void displayTiles(AsciiPanel terminal, int left, int top) {
      //display all the tiles, then loop through creatures and display them if in range
      for(int x = 0; x < screenWidth ; x++) {
      for(int y = 0; y < screenHeight; y++) {
      int wx = x + left;
      int wy = y + top;
      terminal.write(world.glyph(wx, wy), x, y, world.color(wx, wy));
      }
      } ///// Up until this point we have just displayed the world tiles, now we will loop through creatures, check if in bounds, and display

      for(Creature c : world.creatures) {
      if((c.x >= left && c.x < left + screenWidth) && (c.y >= top && c.y < top + screenHeight)) {
      terminal.write(c.glyph(), c.x - left, c.y - top, c.color());
      }
      }
      }

      Delete
    3. I have the same code like you. Works nice and perfect.

      Delete
  18. Where does canEnter come from?
    I have a different class structure so I'll check my alt classes.
    If not I'll have to make my own canEnter boolean method :P

    ReplyDelete
  19. Hi, you can uses this code

    public boolean canEnter(int wx, int wy) {
    return world.tile(wx, wy).isGround() && world.creature(wx, wy) == null;
    }

    put it into your Creature.java Class File

    ReplyDelete
  20. This comment has been removed by the author.

    ReplyDelete
  21. New Comment
    To to avoid the copy you could use the "CopyOnWriteArrayList" Class for the creatures-array.

    ReplyDelete
  22. I am in the step before adding the fungus. When I run the main application it throws the nullpointer exception:

    at rltut.World.creature(World.java:58)
    at rltut.World.addAtEmptyLocation(World.java:50)
    at rltut.CreatureFactory.newPlayer(CreatureFactory.java:14)
    at rltut.screens.PlayScreen.(PlayScreen.java:50)

    How can I fix this? Thanks in advance for your response!

    ReplyDelete