3

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.

SashaZd
  • 3,315
  • 1
  • 26
  • 48
Mars
  • 912
  • 1
  • 10
  • 21
  • Can you use `event`s instead, and have each AI implement an event handler? Then all you do is raise the event (Idle, Aggro, etc) and each AI would respond as scripted. The .NET framework would handle the looping through all of the subscribed AIs' event handlers. – Evil Dog Pie Jan 15 '15 at 12:51
  • Doesn't work because the actions that the scripters define aren't executed instantly =/ For example, an AI might be supposed to walk 100 units, then say something, wait a second, and then attack, all in response to a single event. And since it's so many AIs I don't want to give each AI its own Thread/Task. – Mars Jan 15 '15 at 15:31

2 Answers2

0

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.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
  • How would that change the way the scripts are written? o.o You still have to specify what it's supposed to do after all. The loops are around the calls to the methods returning IEnumerators, so there's a state to return to on the next tick. – Mars Jan 14 '15 at 21:43
0

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
    }
}
Evil Dog Pie
  • 2,300
  • 2
  • 23
  • 46
  • Somehow I have the feeling you guys miss my problem^^" It's not how the "handlers", but how the _actions_ are called, that might take a while to execute. That's why I currently have ticks and enumerators, I go into the current action again and again until it's finished. I'm just wondering if that's possible without using a loop around a call to an action (enumerator) or an await in front of it (if I were to use async). – Mars Jan 15 '15 at 15:35
  • Maybe my answer isn't clear. A .NET `event` is a framework mechanism and an `event` in your class does not have to correspond with a game event. So, your AI might respond to the `Aggro` event by taking one step towards the foe, next call it might peck the foe, or the foe may have run away and it would unsubscribe from further `Aggro` events. – Evil Dog Pie Jan 15 '15 at 16:12
  • But how would you code that behavior? With a state? That's not user friendly =/ I'm looking for a way to implement stateless behavior/actions that require no additional code, a thing that might actually be impossible without more complicated pre-compilation. – Mars Jan 16 '15 at 18:11
  • @Mars In the same way that you expect them to code the 'enumerator' behaviour, except that instead of coding it in the `MoveNext` method, they implement it in the event handler. I suspect that you're right and I've missed the problem. Maybe if you could clarify the architecture, interfaces and responsibilities of the server and AIs? – Evil Dog Pie Jan 19 '15 at 11:51