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:
- Components
Use components to represent capabilities
- Type objects
Define your own type where each instance represents a type
- Command objects
When in doubt, try turning an operation into an object (like a closure)
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
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
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;
}
more functional capability over domain composition over inheritance e.g. monsters have special moves and those are capability objects
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;
}
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
}
}
- 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
Look at the
Applications of command objects: allows you to make an undo function you just maintain a list of commands
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