Alternative to enumerators in AI

187 Views Asked by At

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.

2

There are 2 best solutions below

1
On

Every creature has an AI with a heartbeat method that is called every few ms/s,

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.

4
On

You could try turning to the .NET framework for help by using events in your server and having the individual AIs subscribe to them. This works if the Server is maintaining the heartbeat.

Server

The server advertises the events that the AIs can subscribe to. In the heartbeat method you would call the OnIdle and OnAggro methods to raise the Idle and Aggro events.

public class GameServer
{
    // You can change the type of these if you need to pass arguments to the handlers.
    public event EventHandler Idle;
    public event EventHandler Aggro;

    void OnIdle()
    {
        EventHandler RaiseIdleEvent = Idle;
        if (null != RaiseIdleEvent)
        {
            // Change the EventArgs.Empty to an appropriate value to pass arguments to the handlers
            RaiseIdleEvent(this, EventArgs.Empty);
        }
    }

    void OnAggro()
    {
        EventHandler RaiseAggroEvent = Aggro;
        if (null != RaiseAggroEvent)
        {
            // Change the EventArgs.Empty to an appropriate value to pass arguments to the handlers
            RaiseAggroEvent(this, EventArgs.Empty);
        }
    }
}

Generic CreatureAI

All of your developers will implement their creature AIs based on this class. The constructor takes a GameServer reference parameter to allow the events to be hooked. This is a simplified example where the reference is not saved. In practice you would save the reference and allow the AI implementors to subscribe and unsubsrcibe from the events depending on what state their AI is in. For example subscribe to the Aggro event only when a player tries to steal your chicken's eggs.

public abstract class CreatureAI
{
    // For the specific derived class AI to implement
    protected abstract void IdleEventHandler(object theServer, EventArgs args);
    protected abstract void AggroEventHandler(object theServer, EventArgs args);

    // Prevent default construction
    private CreatureAI() { }

    // The derived classes should call this
    protected CreatureAI(GameServer theServer)
    {
        // Subscribe to the Idle AND Aggro events.
        // You probably won't want to do this, but it shows how.
        theServer.Idle += this.IdleEventHandler;
        theServer.Aggro += this.AggroEventHandler;
    }

    // You might put in methods to subscribe to the event handlers to prevent a 
    //single instance of a creature from being subscribe to more than one event at once.
}

The AIs themselves

These derive from the generic CreatureAI base class and implement the creture-specific event handlers.

public class ChickenAI : CreatureAI
{
    public ChickenAI(GameServer theServer) :
        base(theServer)
    {
        // Do ChickenAI construction
    }

    protected override void IdleEventHandler(object theServer, EventArgs args)
    {
        // Do ChickenAI Idle actions
    }

    protected override void AggroEventHandler(object theServer, EventArgs args)
    {
        // Do ChickenAI Aggro actions
    }
}