Adding Inhabitants

Datetime:2016-08-22 21:49:16          Topic: Hive           Share

Populating Worlds with Critters

This blog post will explain one solution to integrating entities with simple AI into virtual worlds. These entities should be able to independently decide what action they should be carrying out and flexible enough to respond to changes dynamically. Finite-state machines were chosen in this instance because they meet these requirements and are simple to implement.

A Visual Demo

The following example demonstrates a world with three entity types: plants, critters and hives. The critters will go about collecting mature plants to bring back to their hive, but this work makes them tired so they'll go home to sleep occasionally. The plants are also finite-state machines, they slowly collect nutrients and grow in size until they mature and are ready to be picked. Hives make new critters when enough resources are delivered.

The Basics

The principle idea here is that an entity carries out the actions of its current state, and transitions to new states when the time is right. The definitions of these states shouldn't contain much logic, only check for conditions and invoke actions.

A Minimal Machine

The implementation of this FSM is nothing fancy but it gets the job done. There are many flavors of this pattern but this one is called the Pushdown Automaton , and it's used here specifically to help with the navigation flow. A state will typically either return nothing, meaning it will stay in the current state, or it will return a new state which is pushed into a stack. The first item in this stack is the used as the current state. Sometimes a state may choose to transition to a previous state, even without knowing what it was, and this is where the state history comes into use. The behavior of this type of FSM is useful because multiple states may want to transition into navigation and this pattern will return control once the requested navigation is complete.

class GeoGenFSM {
  constructor (target, initialState) {
    this.target = target;
    this.states = [initialState];
  }

  update () {
    const args = Array.prototype.slice.call(arguments, 0);
    args.unshift(this.target, this);
    const newState = this.states[0].apply(this.states[0], args);
    if (newState) {
      this.unshift(newState);
    }
  }

  unshift (newState) {
    this.states.unshift(newState);
  }

  shift () {
    this.states.shift();
  }
}

This is an example usage of the FSM; there's almost nothing to it, just instantiate it with an initial state and call update() to get it processing.

class Hive {
  constructor () {
    this.fsm = new GeoGenFSM(this, hiveStates.collecting);
  }

  update () {
    this.fsm.update();
  }

  readyToSpawnCritter () { }

  spawnCritter () { }
}

The sections below show examples of the actual state definitions which map to the class methods such as the ones shown above.

Hive States

A very simple case, the hive simply waits for resources to be brought to it and creates new critters when it's ready.

const hiveStates = {
  collecting: (hive) => {
    if (hive.readyToSpawnCritter()) {
      return hiveStates.spawning;
    }
  },

  spawning: (hive) => {
    hive.spawnCritter();
    return hiveStates.collecting;
  }
};

Plant States

A little more complex than the hive, the plant collects nutrients to fuel its growth through 4 maturity stages until it is ripe and ready to be picked.

const plantStates = {
  collecting: (plant) => {
    if (plant.isMature()) {
      return;
    }
    if (plant.readyToGrow()) {
      return plantStates.growing;
    } else {
      plant.collectNutrients();
    }
  },

  growing: (plant) => {
    plant.grow();
    return plantStates.collecting;
  }
};

Critter States

A critter seeks mature plants to harvest and deliver to its hive. Collecting plants consumes energy and a tired critter will go back to the hive for some sleep.

const critterStates = {
  idling: (critter) => {
    if (critter.pickNextJob()) {
      critter.wakeUp();
      return critterStates.working;
    } else if (critter.needsSleep) {
      critter.setDestinationHive();
      return critterStates.sleeping;
    }
  },

  working: (critter) => {
    if (critter.distanceToDestination()) {
      return critterStates.navigating;
    }
    return critterStates.collecting;
  },

  sleeping: (critter) => {
    if (critter.distanceToHive()) {
      return critterStates.navigating;
    } else if (critter.needsSleep()) {
      critter.sleep();
      return;
    }
    return critterStates.idling;
  },

  navigating: (critter, machine) => {
    if (critter.distanceToDestination()) {
      critter.followNav();
      return;
    }
    machine.shift(); // return to the state that requested navigation
  },

  collecting: (critter) => {
    critter.collectPlant();
    critter.setDestinationHive();
    return critterStates.delivering;
  },

  delivering: (critter) => {
    if (critter.distanceToHive()) {
      return critterStates.navigating;
    }
    critter.deliverPlant();
    return critterStates.idling;
  }
};

Conclusion

This approach has the advantage of decoupling the logic and actions of a behavior, the code to decide a critter should be moving isn't tied to the code that moves the critter. Code organization improves also, more logic can easily be added without creating repetitive blocks of code.

Thanks To





About List