Thursday, April 22, 2010

Rogue Survivor Design approach

(You should know about OOP and the design issues behind designing a roguelike game to read this article)

 OOP for Roguelikes
 When designing a game architecture with an OOP (Object Oriented Programming) approach, one naturaly tend to put the data and the logic associated with a game entity in the same place, namely a class.
So you create classes like Item, Actor, Map and the likes and make a one-to-one relation between a game entity and an instance of a class (object). 
One particular Sword entity in the game will likely be an instance of ItemMeleeWeapon, which a hiearchy like ItemMeleeWeapon is ItemWeapon is Item.

Entities Interactions : Assigning responsabilities
Works fine. But then what about entities interaction? Say the player actor wants to open the big door.
Who does it? There are many differents ways and paths to choose. The most common are:
  1. The Actor does it: playerActor.OpenDoor(theBigDoor), which in turns calls theBigDoor.SetState(OPEN) etc...
  2. The Map does it: map.ActorOpenDoor(thePlayer, theBigDoor)...
  3. The Action does it: openDoorAction.Execute()...
Thats works fine too. But to be consistent in your design you have to garantee all actions will follow the same path and logic, otherwise you will end up with anarchic code.

Shared responsabilities = Spaghetti Logic = Bugs and Refactoring Hell
The danger -read: the source of bugs or game logic inconsistencies- resides when one entity (active) initiate an action on another entity (passive) which could trigger a response somewhere else (eg: the door electrocutes the actor) which in turn could trigger another response (eg: the actor roll for electrocution resistance) etc.. and you end up with spaghetti logic. In general this ends up provoking bugs, because somewhere in one of your derived classes you made an assumption about the caller or some property that is broken by the logic chain triggered by the action initiator...
People who take this standard OOP approach tend to search for the "perfect" architecture, refactor their code a lot and most of the time aim for a roguelike engine rather than a game. It works but with great pain.

Game Data separated from Game Logic : Multiple Data, One Controller
In Rogue Survivor I choose since the start to go with a Data-Driven approach with a single Controller.
All the game entities are represented by Data classes. Data classes are "stupid" and only know to maintain themselves. Data classes use inheritance only when requiring additional fields and properties. Eg: ItemWeapon derives from Item because it needs additionnal data to represent a weapon.

All the game logic in centralized in one place, the Game. The RogueGame class has absolute control on everything, is the game master and holds all the game logic.

Actions are Data too, whose job is only to call the RogueGame appropriate methods when checking for legality or executing. Remember, as Data classes they have no game logic in them at all. They are like a dumbed down version of the Command design pattern.

Types of entities (eg: the skeleton actor type, the crowbar item type) are instances of Data classes too, called Models (eg: skeleton actor type is an instance of ActorModel).

AIs are special cases because I consider them as abstract game logic, since the AI logic in itself does nothing in the game world, it just computes stuff and produce an Action command.

The Holy Grail?
Certainly not. In other pet game projects I favor other architectures such as the standard OOP mentionned above.
For Rogue Survivor it just works and is very easy to maintain. All architectures have limitations and flaws, you just have to accept them.

End of post.

5 comments:

  1. I'm curious, which of the three entity interactions that you mentioned do you use in your game? It seems to me that the first interaction, where the "Actor" class initiates the behavior, is the most natural. But perhaps some other method is more effective within the framework of your game?

    ReplyDelete
  2. Neither of the 3, although it uses action objects.

    1. The Game ask an ActorController for an ActorAction.
    2. ActorActions are just command wrappers that delegate the legality checks and the resolutions to the Game itself (they contain no game logic by themselves).
    3. All the game logic itself (actual legality check and actual action resolution) is in Game.

    Consider the Game as the Game Master. All other game entities can just ask him or tell him stuff, they never trigger changes in the game world by themselves.

    Eg: the actor do not tell the door to open, its controller (ai or human) can just return a query (action) to open the door when asked to by the game, then the game resolves it.

    It works very well if you use a data-driven approach for entities.

    ReplyDelete
  3. What role does an ActorController play? Is it something like "UI (open door)->Player (ActorController)->OpenDoor (ActorAction)->Game ?

    ReplyDelete
  4. Depends on who controls the Actor :
    - for the player, it just let the UI do its stuff and by pass the ActorAction : UI -> Game.
    - for the NPCs, the controler is an AI (eg: CivilianAI for civilians) than returns ActorAction : AI -> ActorAction -> Game.

    ReplyDelete
  5. Forgot to add that the player cannot do illegal actions, the UI check for that by asking the game, like an ActorAction would do. You don't want ActorAction for players, because you want to do additional stuff like sending UI messages etc...

    ReplyDelete