I'm working on a server for a multi-player game that has to control a few thousand creatures, running around in the world. Every creature has an AI with a heartbeat method that is called every few ms/s, if a player is nearby, so they can react.
Currently the AI uses enumerators as "routines", e.g.
IEnumerable WanderAround(int radius)
{
// Do something
}
which are called from "state methods", which are called in foreachs, yielding in the heartbeat so you get back to the same spot on every tick.
void OnHeartbeat()
{
// Do checks, maybe select a new state method...
// Then continue the current sequence
currentState.MoveNext();
}
Naturally the routines have to be called in a loop as well, because they wouldn't execute otherwise. But since I'm not the one writing those AIs, but newbies who aren't necessarily programmers, I'm pre-compiling the AIs (simple .cs files) before compiling them on server start. This gives me AI scripts that look like this:
override IEnumerable Idle()
{
Do(WanderAround(400));
Do(Wait(3000));
}
override IEnumerable Aggro()
{
Do(Attack());
Do(Wait(3000));
}
with Do
being replaced by a foreach that iterates over the routine call.
I really like this design because the AIs are easy to understand, yet powerful. It's not simple states but it's not a hard to understand/write behavior tree either.
Now to my actual "problem", I don't like the Do
wrapper, I don't like having to pre-compile my scripts. But I just can't think of any other way to implement this without the loops, that I want to hide because of verbosity and the skill level of the people who're gonna write these scripts.
foreach(var r in Attack()) yield return r;
I'd wish there'd be a way to call the routines without an explicit loop, but that's not possible because I have to yield from the state method.
And I can't use async/await because it doesn't fit the tick design that I depend on (the AIs can be quite complex and I honestly don't know how I would implement that using async). Also I'd just trade Do()
against await
, not that much of an improvement.
So my question is: Can anyone think of a way to get rid of that loop wrapper? I'd be open to using other .NET languages that I can use as scripts (compiling them on server start) if there's one that supports this somehow.
Why not go full SkyNet and have each creature responsible for its own heartbeat?
Such as creating each creature with a timer (the heart so to speak with a specific heartbeat). When each timer beats it does what it was designed to do, but also checks with the game as to whether it needs to shut-down, be idle, wander or other items.
By decentralizing the loop, you have gotten rid of the loop and you simply have a broadcast to subscribers (the creatures) on what to do on a global/basic level. That code is not accessible to the newbies, but it is understood what it does on a conceptual level.