munificent / game-programming-patterns

Source repo for the book
http://gameprogrammingpatterns.com/
Other
4.14k stars 498 forks source link

Comments on the "State" Chapter #283

Open DanielGibson opened 9 years ago

DanielGibson commented 9 years ago

State: why not just make the states (non-static!) members of the Heroine class?
Then Coop is no problem because each Heroine has her own states and you still don't have the allocation overhead and memory fragmentation.
If you wanna reset some state (e.g. chargeTime_ = 0;), you can either do that in FooState::handleInput() after heroine.state_ = heroine.barState_; or in the state's exit() method.

A Problem with the Concurrent State Machines not explicitly mentioned: Even if you somehow have two state machines for state and equipment, you will still want to set different sprites with/without gun for each state.. which probably creates kinda ugly code.

I'd use the following to make it less ugly which also has some other benefits:
One can combine Enums with State Objects - just put the states in an array, so the index matches the corresponding enum value (actually, this "pattern" is not only useful for states).
Now you don't mess around with pointers anymore, but just with an state enum value.

class Heroine
{
  friend class HeroineState;
  enum State
  {
    STATE_STANDING = 0,
    STATE_JUMPING,
    STATE_DUCKING,
    STATE_DIVING,
    // ...
    _STATE_NUMSTATES
  }
public:
  virtual void handleInput(Input input)
  {
    states[stateIndex]->(*this, input);
  }

  virtual void update()
  {
    states[stateIndex]->(*this);
  }

  // Other methods...
private:
  State stateIndex = STATE_STANDING;
  HeroineState* states[_STATE_NUMSTATES];
};

StandingState::handleInput(Heroine& heroine, Input input)
{
  if (input == PRESS_DOWN)
  {
    // Other code...
    heroine.stateIndex = STATE_DUCKING;
    // TODO: use a HeroineState::setState(Heroine&, State) method so only HeroineState
    // needs to be a friend of Heroine to change the private Heroine::stateIndex
  }

  // Stay in this state by doing nothing
}

Now if we wanna introduce a second set of states (e.g. for equipment), it could get its own enum Equipment, an equipmentIndex and equipments[_EQUIPMENT_NUMSTATES].
For the sprites however, (assuming we really need one for each combination of states) we can have a multidimensional array for each combination of states: Sprite stateSprites[_STATE_NUMSTATES][_EQUIPMENT_NUMSTATES]; and the sprite to use is just stateSprites[stateIndex][equipmentIndex].
Or maybe it's really just one big image that has _STATE_NUMSTATES colums and _EQUIPMENT_NUMSTATES rows of sprites and we get the right sprite by calculating an offset from those two indexes.

This keeps the states at n+m complexity (+ a few hacks for "no gun while diving" and similar), while allowing n*m different animations - without too much pain.

Another advantage is, that serialization is easier:

I found this "pattern" (array with enum index) helpful in other cases as well.. for example if you have several class fields of the same type (or at leasts one with a common supertype) and in several methods you do basically the same thing with them (call the same method on each one, like update()), but in some you specifically want to handle only one of them. Then iterating over them is nicer then calling each of them separately and accessing them with myArray[NAME] is about as readable as myName.

BTW: The case of state_ just being a function pointer reminds me of the think function pointers of entities in the Quake engines :-)

PS: Great book so far, this is the first time I could stand reading about design pattern for longer than 5 minutes :-)

scantics commented 9 years ago

I would like to add that putting static instances of the derived classes as members of the base class is impossible, because there's no way to have defined them before the base class' definition is finished.