Skip to content

Instantly share code, notes, and snippets.

@tnn4
Last active September 22, 2022 20:52
Show Gist options
  • Save tnn4/d07cdaf2f9ccc64bac499e7fd1f7dbd7 to your computer and use it in GitHub Desktop.
Save tnn4/d07cdaf2f9ccc64bac499e7fd1f7dbd7 to your computer and use it in GitHub Desktop.
Bob Nystrom's roguelike tips

Bob Nystrom - Is There More to Game Architecture than ECS?

ECS for Roguelikes?

I actually don't use ECS, not a good fit for any roguelike thats relatively simple, i.e. if not graphically rich, physically rich, turn-based or tile-based ECS isn't doing much good for you

SUMMARY:

  1. Components

Use components to represent capabilities

  1. Type objects

Define your own type where each instance represents a type

  1. Command objects

When in doubt, try turning an operation into an object (like a closure)

ECS for Roguelikes

Systems

some people pull systems out of the component(ECS)

  • some people don't (EC) example:
class AISystem {
    void update(){
        for var component in aiComponents{
            component.update();
        }
    }
}
  • Does not mention entity at all
  • Skips straight to the component
  • Makes the cpu happy

Co mplexity?

Physics

  • not super complicated hehe
if (tile.isWall){
    print("You hit a wall.");
}

Graphics

  • AAA graphics
putchar(x,y, "@")

AI

  • Pathfinding
  • Fear and Fleeing
  • Melee attacks
  • Ranged attacks
  • Spells
  • Breath Attacks
  • Line-of-sight
  • Flow by scent
  • Different intelligence levels
  • There's more richness here

Entities can

walk eat haste
rest throw freeze
melee slash poision
open door spear blind
close door bash dazzle

behavorial richness

ADoM has like 300 monsters

  • roguelikes have a greater breadth of content
  • which presents an organizational challenge

Design patterns for RogueLikes

Command objects

Items

A lot of player capabilities is based on items / equipment

  • Basically means, there's a lot of stuff items can do
  • weapons: melee, range, shield,
  • armor: shield, dodge
  • stuff to eat
  • stuff to activate/reuse
  • stuff with passive effects
class Item(
    int minDamage;
    int maxDamage;
    int armor;
    int dodgeBonus;

    void eatFood(){...}
    void quaff(){...}
    void fireBall(){...}
    void lightningBolt(){...}
    void teleport(){...}
    void meleeAttack(){...}
    void rangedAttakc(){...}
    // More ...
)

No Good, jamming all that stuff into one class is not a great idea

  • just like monopolies are bad you need to do some breaking up

split each capability that an item has into a separate class

class Item {
    Attack melee;
    Attack ranged;
    Defense defense;
    Use use;
}

example capabilities

// attacking
class Attack {
    int minDamage;
    int maxDamage;
    
    void hit() {...}
}
// providing defense
class Defense {
    int armor;
    int dodgeBonus;
    
    void defend() {...}
}
// using the item
abstract class Use {
    void use();
}

class HealUse extends Use{
    void use(){
        hero.health += 20;
    }
}

class FireBallUse extends Use {
    void(){
        // Cast fire ball...
    }
} // More uses...
  • Inheritance works here Aim for wide hierarchies

Items are just combinations of capabilities

  • mix and match
  • now you can define content in data files
var sword = Item(
    melee: Attack(10,20));

var crossbow = Item(
    ranged: Attack(10,20));

var shield = Item(
    melee: Attack(5,8);
    defense: Defense(3,0));

var healPotion = Item(
    quaff: HealUse());

var fireSword = Item(
    melee: Attack(30,40)
    activate: FireBallUse());
    

classic solution - use inheritance make subclasses

  • we've learned inheritance is rigid
  • what if you want to add functionality to a class?
  • I want my sword to shoot fireballs now
  • with inheritance, nope

Solution

  • split each capability into a separate class
class Item{
    Attack melee;
    Attack ranged;
    Defense defense;
    Use use;
}

Idea #1: use components to represent capabilities

more functional capability over domain composition over inheritance e.g. monsters have special moves and those are capability objects


Breeds of Monsters

class Monster {
    int x,y;
    int health;
    String name;
    int maxHealth;
    Attack attack;
    List<Use> moves;
    Set<String> flags;
    Drop loot;
}

Define a separate class that represents a type of monster

  • One instance of breed class for a goblin
  • Every goblin has a breed that points to that
  • Type Object pattern
  • Breed is a class that represents a class, it's a metaclass
  • You give breed a pointer to a parent breed and define your own inheritance semantics
  • items, different types of items, special modifiers on the items
class Breed {
    String name;
    int maxHealth;
    Attack attack;
    List<Use> moves;
    Set<String> flags;
    Drop loot;
}

class Monster{
    Breed breed;
    int health;
    int x, y;
}

Idea #2: Use types that represents types

How to do it: take some verb in the game and turn it into a noun (an object)

you're basically turning a function into a closure

abstract class Action {
    ActionResult perform();
}
  • without it different implementation of player and actor
  • With abstraction layer both can use Action taketurn()
class Monster extends Actor{
    Action takeTurn(){
        // Pathfinding, AI
    }
}

Function Objects, Closures

  • object that represents operation
  • first class functions
  • basically a closure
  • object represents a thing you can invoke
  • in practice, you can use a raw function

function object aka. closure

abstract class Action{
    ActionResult perform()
}

an action is a first class turn, represents a single step an actor can perform

void gameLoop(){
    for (var actor in actors){
        actor.gainEnergy(actor.speed); // gain action points on how fast they move
        if (actor.hasEnoughEnergy){
            var action = actor.takeTurn();
            action.perform()
        }
    }
}

Here's the action class for taking a step:

class WalkAction extends Action{
    Direction dir;

    ActionResult perform(){
        var pos = actor.pos + dir;

        // see if there is an actor there
        var target = game.stage.actorAt(pos)
        if (target != null) return alternate(AttackAction(target));

        // see if it's door
        var tile = game.stage(pos)
        if (tile.isDoor) return alternate(OpenDoor(pos));

        // See if we can walk there
        if (!actor.canOccupy(pos)) return fail("You hit the wall!");

        actor.pos = pos;

        // See if the hero stopped on anything interesting
        if (actor is Hero){
            for (var item in game.stage.itemsAt(pos).toList()){
                log("YOu are standing on $item.");
            }
        }
        return succeed();
    }
}

Notice: this is a lot of code and it's code that's pulled outside Actor

Idea #3: When in doubt, try turning an operation into an object

Look at the

Applications of command objects: allows you to make an undo function you just maintain a list of commands

Questions to ask for adding features

Add a feature?

Is it useful? Do users want it?

Is it affordable? Cost of implementation?

Is is flexible? How many use cases does it cover?

appendix:

a closure - a stateful function, persistent scope function, it holds on to variables even after execution has moved out of scope

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment