Friday, September 16, 2011

roguelike tutorial 09: wandering monsters

We need some monsters to wander around our caves. How about some fast moving bats?

First let's add some default movement behavior to the CreatureAi.

public void onEnter(int x, int y, int z, Tile tile){
    if (tile.isGround()){
         creature.x = x;
         creature.y = y;
         creature.z = z;
    } else {
         creature.doAction("bump into a wall");
    }
}

Let's also give it a method of moving randomly. This common behavior can be called by any subclass.

public void wander(){
    int mx = (int)(Math.random() * 3) - 1;
    int my = (int)(Math.random() * 3) - 1;
    creature.moveBy(mx, my, 0);
}

Now the ai for our bats.

package rltut;

public class BatAi extends CreatureAi {

    public BatAi(Creature creature) {
        super(creature);
    }

    public void onUpdate(){
        wander();
        wander();
    }
}

We could set up a system for dealing with different monster speeds but this is simple enough: bats move twice for every one of your moves. Easy to implement, easy to understand.

Now we add bats to our CreatureFactory. I picked some low hp and attack so they could nibble on you a bit but shouldn't be too much of a problem.

public Creature newBat(int depth){
    Creature bat = new Creature(world, 'b', AsciiPanel.yellow, 15, 5, 0);
    world.addAtEmptyLocation(bat, depth);
    new BatAi(bat);
    return bat;
}

And in our PlayScreen we need to update createCreatures. How about 20 bats per level?

for (int i = 0; i < 20; i++){
    creatureFactory.newBat(z);
}

And now that our player is in some danger of being killed, let's add a check after the world get's updated.

if (player.hp() < 1)
    return new LoseScreen();

Try it out. You should see bats being flying about and being batty.


If you run this you'll soon notice that the bats drop dead from attacking each other or even attacking themselves. Suicide bats are funny but they quickly go extinct.

Let's add a check to the first line of the moveBy method in the Creature class. It should bail out early if we're not actually moving. This will take care of creatures killing themselves when all they want to do is stand in one place.

if (mx==0 && my==0 && mz==0)
    return;

Creatures should also be able to see what other creatures are in the world so the CreatureAi can know what's going on.

public Creature creature(int wx, int wy, int wz) {
    return world.creature(wx, wy, wz);
}

We can now improve the CreatureAi wander method to make sure creatures don't fight other's like them.

public void wander(){
    int mx = (int)(Math.random() * 3) - 1;
    int my = (int)(Math.random() * 3) - 1;
    
    Creature other = creature.creature(creature.x + mx, creature.y + my, creature.z);
    
    if (other != null && other.glyph() == creature.glyph())
        return;
    else
        creature.moveBy(mx, my, 0);
}

You could also make it keep trying until mx != 0 && my != 0, that way it would never stand in the same spot. You may want to make sure it doesn't try to move into a wall or make it able to go up or down stairs.

And there you go. You now have some deep and bat-filled caves to explore.



Using the glyph in messages is lame; Creatures should have a name.

private String name;
    public String name() { return name; }

Using constructor injection, update the creatureFactory to pass in the appropriate name. Finally, update any messages that used the creature's glyph to now use the creature's name.

Much better.

download the code

8 comments:

  1. I'm wondering how far you can go with CreatureAI: have you consider some other methods, like behaviours trees?

    ReplyDelete
  2. @Marte: I've tried a couple different things for AI but hadn't read about behavior trees until I read your comment. I've found that a couple if statements has been good enough for my simple AIs and state machines have been good enough for my most complex AIs but this looks interesting. Thanks for showing me something new Marte!

    ReplyDelete
  3. First and foremost - thank you for creating this wonderful tutorial!

    I've been following this tutorial closely after a few unsuccessful roguelike games on my own.
    The problem that I've now encountered has nothing to do with the actual code, but every 1 in 2 times or so when I press enter at the StartScreen() the application freezes (as in no errors and undetectable in debug mode). Could you elaborate any further as of why this happens? I'm thinking it might be something regarding AsciiPanel together with SE 1.7? Anyway, thank you for everything you've written and consider me a fanboy of your blog!

    ReplyDelete
    Replies
    1. Hmmm. I'm not sure why that would happen or how to debug an intermittent bug like that. Are you clearing and redrawing the screen when you enter the PlayScreen?

      Delete
    2. Yes, the times when the game starts without any problem it is working as intended. After trying to get a thread dump from the application I've started to think that this is a JVM error. Every application or Java feature I've used so far to obtain a thread dump has crashed/frozen when I try to evaluate my frozen game.

      I'll run your original source code and see if the same thing happens.

      Delete
    3. After some tinkering I figured it out. Sometimes the cave generation algorithm that you wrote earlier in the tutorial generates a dungeon with less floor tiles than there are creatures being generated. The addAtEmptyLocation() method doesn't take this in consideration, resulting in an infinite loop when searching for empty space.

      I was thinking about creating some dungeon/forest generation algorithms on my own anyway, so I'll just go ahead and do that.

      Thank you for the help though, looking forward to new, exciting posts!

      Delete
  4. Thank you the very informative blog series! I've been taking your blog and adapting the code into c# and using Unity 3D as the graphic and user input layer.

    ReplyDelete