Friday, September 23, 2011

roguelike tutorial 11: hunger and food

Now that we've got monsters to kill and the ability to pick up and use things, how about we add some corpses and the ability to eat them?

We first need to update our Item class to support some nutritional value.
private int foodValue;
public int foodValue() { return foodValue; }
public void modifyFoodValue(int amount) { foodValue += amount; }

And update our creature to leave corpses.

public void modifyHp(int amount) {
    hp += amount;
    if (hp < 1) {
        doAction("die");
        leaveCorpse();
        world.remove(this);
    }
}

private void leaveCorpse(){
    Item corpse = new Item('%', color, name + " corpse");
    corpse.modifyFoodValue(maxHp * 3);
    world.addAtEmptySpace(corpse, x, y, z);
}

Update creatures to also have hunger.

private int maxFood;
public int maxFood() { return maxFood; }

private int food;
public int food() { return food; }

public void modifyFood(int amount) {
    food += amount;
    if (food > maxFood) {
        food = maxFood;
    } else if (food < 1 && glyph == '@') {
        modifyHp(-1000);
    }
}

Do you see the terrible hack there? We only want the player to be able to die of starvation since it would be boring if every monster dropped dead of starvation and if they need to eat they'd have to go around killing each other. We could have an entire ecosystem of bats farming fungus, that would introduce some neat gameplay options, but that's quite a bit more complicated than I'd like to do right now. Anyway dying only if you look like a @ is still an ugly hack — a hack so ugly our children's children will feel the shame. Let's fix it right now:

public void modifyFood(int amount) {
    food += amount;
    if (food > maxFood) {
        food = maxFood;
    } else if (food < 1 && isPlayer()) {
        modifyHp(-1000);
    }
}

public boolean isPlayer(){
    return glyph == '@';
}

The hack is still there but it's isolated for now. Later if we have other creatures with an @ glyph or if the player can assume other forms, we can update this one isolated place. One thing I've learned from real life software is that although ugly hacks are inevitable, you can always isolate them so the callers don't need to deal with it.

But enough preaching, our Creatures also need a method to eat.

public void eat(Item item){
    modifyFood(item.foodValue());
    inventory.remove(item);
}

Don't forget that creatures should start with decent belly full. Add this to the creature constructor:

this.maxFood = 1000;
this.food = maxFood / 3 * 2;

Now add an EatScreen so we can eat something in our inventory.

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class EatScreen extends InventoryBasedScreen {

    public EatScreen(Creature player) {
        super(player);
    }

    protected String getVerb() {
        return "eat";
    }

    protected boolean isAcceptable(Item item) {
        return item.foodValue() != 0;
    }

    protected Screen use(Item item) {
        player.eat(item);
        return null;
    }
}

Wow, that was easy. InventoryBasedScreen is paying off already.


For the PlayScreen we need to map the 'e' key to the EatScreen.
case KeyEvent.VK_E: subscreen = new EatScreen(player); break;
We should also let the player know how hungry he is. Change the stats in displayOutput to this:
String stats = String.format(" %3d/%3d hp %8s", player.hp(), player.maxHp(), hunger());
And add a helper method. You can use whatever text and amounts you want.
private String hunger(){
    if (player.food() < player.maxFood() * 0.1)
        return "Starving";
    else if (player.food() < player.maxFood() * 0.2)
        return "Hungry";
    else if (player.food() > player.maxFood() * 0.9)
        return "Stuffed";
    else if (player.food() > player.maxFood() * 0.8)
        return "Full";
    else
        return "";
}

Of course none of this will do anything if we don't use up the food we've eaten. Go ahead and add a call to modifyFood in the relevant creature methods. Here's a couple examples:
public void dig(int wx, int wy, int wz) {
    modifyFood(-10);
    world.dig(wx, wy, wz);
    doAction("dig");
}
public void update(){
    modifyFood(-1);
    ai.onUpdate();
}

Go ahead and use the food values you want. You should play around with it for a while to decide what feels right. Maybe you want starvation to be a serious problem and hunting bats is the only way to stay alive or maybe you want starvation to hardly ever happen. Maybe heroes start with an inventory full of supplies or maybe they start with an empty and growling belly — as the designer it's up to you.

While looking at the modifyFood method I noticed we don't prevent hp from going higher than maxHp. Even though we don't have a way to do that yet you should add a check for that.


If we eat more than the maxFood shouldn't our stomach stretch and increase our maxFood? Or maybe the user should explode from overeating? Here's my implementation:
public void modifyFood(int amount) {
    food += amount;

    if (food > maxFood) {
        maxFood = maxFood + food / 2;
        food = maxFood;
        notify("You can't believe your stomach can hold that much!");
        modifyHp(-1);
    } else if (food < 1 && isPlayer()) {
        modifyHp(-1000);
    }
}

It's a subtle effect but it gives the player a decision to make when full and carrying a lot of food and under the right circumstances overeating may become a useful strategy.

Now go ahead and add some food to your world. Bread, meat, apples, whatever.

download the code

5 comments:

  1. Hi againt.
    I didn't like food/hunger mechanics into rouges but.. this words:


    "It's a subtle effect but it gives the player a decision to make when full and carrying a lot of food and under the right circumstances overeating may become a useful strategy."

    Make me think! Good point, might consider it!

    ReplyDelete
  2. I followed this tutorial perfectly, and made very few personal changes. In the stat display section in displayOutput, I call the hunger(); method, yet it doesn't display anything. I did a shortcut and it just displays how much food the player has left. Do you have any idea what might be happening? There's no errors or anything, and I see no problems in the code.

    ReplyDelete
    Replies
    1. Are you sure it's not just that you start at 2/3 of your maxFood and at that value it doesn't display anything, and you didn't play enough to reach a fraction of your maxFood where it would display anything?

      Delete
  3. One thing you technically set yourself up for with only allowing players to eat is nicely set up to instead have it reference a config file / settings file that the player can adapt, like controller settings for movement you can have a setting for player glyph. Then isPlayer could simply check if the passed in glyph was the same as the chosen player glyph in the config files where the settings file is a globally accessable Settings Class. That way the user could reconfigure the movement keys etc to suit their play style and the settings can be referenced in many places. e.g when drawing the world the screen would draw player.glyph and get the glyph from config as before etc.

    ReplyDelete
  4. Its been pretty essential for the students to go through with all those possible instances and tutorial programs which would help them to proceed further for the better regarded prospects.

    ReplyDelete