Wednesday, February 28, 2018

architecture - Handling AI with ECS in a turn based roguelike


I am trying to implement the Entity Component System pattern to use in a roguelike game. Right now, I have 3 systems. Input, AI, and Action. The input system basically is just a system used by the player to get input from the keyboard. The AI system is for the NPCs. The action system processes anything with an Action component, which is used to describe either a move, an attack, an item usage, basically anything that advances the game time.


However, I am having trouble with implementing the correct behaviors. It is clear that the player should have a turn, getting his input. If the input is invalid, he repeats his turn, again and again until he makes a turn that passes time. Then the enemies should get their move from the AI system, and initiate it. So what the behavior should be like is this:


while(!input.passesTime())
input.getInput()
draw()
action.do(Player)
for all Entities with AI
AI.think(Entity)
action.do(Entity)

draw()

Notice how everything is intertwined and doesn't really make sense as a single (or multiple) system. How can I design this better? Note especially that I need to update the screen everytime, even with invalid inputs since they may write something out (e.g. "Nothing interesting happens").


After some thinking it seems clear that the renderer should also be a system, but this doesn't really clear up the requirements.



Answer



Note: I'm playing fast and loose with the pseudocode here, so let me know if anything is unclear.


Ideally, the player shouldn't be special - just another set of components. The main function of entities is to group components. You might think of it this way: components get updates, not entities.


From the good old Evolve Your Hierarchy article:


enter image description here


Updates flow down from the Component Manager, and entity association flows across.



So, you'd start from something like this:


while(running)
turn = timer.Now // or something, time management is another problem altogether
foreach ComponentType in ComponentManager ordered by ComponentType.UpdatePriority
foreach Component in ComponentType
Component.Update(time)
foreach Component in ComponentManager.AllComponents ordered by Component.DrawPriority
if Component is Drawable
Component.Draw(time)


Note the complete lack of references to entities in this loop! The update and draw methods are really the only two special cases you need, and we only tolerate draw being separate as an optimization in the face of system limitations (it's generally better to try and group draw calls, and draw doesn't need to be called every pass) - it's not architecturally ideal. If each component has a reference to its parent entity, it can talk to the other components on its parent entity and there's no need to pass the entity as an argument to its update method. There's no need to have special methods for each component either, since that could just be wrapped into a uniform set of method declarations:


Input.Update(time)
getInput()

AI.Update(time)
think()

Rule of thumb: if you can wrap it, you can combine it into a generic method shared by similar classes.


So, your class hierarchy should look something like


class Component { Update(), parentEntity }

class Drawable : Component { Draw() }
class Entity { Components }
class AI : Component { Update() { /* think stuff */ } }
class Action : Component { Update() { /* do stuff */ } }
class Input : Component { Update() { /* get input and pass it on to other components */ } }
// etc.

Since this is a turn based game, you'll probably want a game keeper component like


class GameKeeper : Component
{

Update(time)
{
if isOdd(time) currentTurn = Player
else currentTurn = Cpu
}
// or use events
TurnFinished(entity)
{
if entity == PlayerEntity
currentTurn = Cpu

else currentTurn = Player
}
}

Then


class AI
{
Update(time)
{
gamekeeper = FindComponent(GameKeeper)

if gamekeeper.currentTurn != Cpu
return
// otherwise think stuff
action = parentEntity.GetComponent(Action)
action.Do("Some action")
}
}
class Action
{
Do(message)

{
// Get ready to do whatever is in the message
}
Update(time)
{
gamekeeper = FindComponent(GameKeeper)
if gamekeeper.currentTurn != Cpu
return
// actually do all the things
}

}

No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...