AI中替代枚举器的方法

3
我正在为一个多人游戏开发服务器,需要控制几千个在世界中奔跑的生物。每个生物都有一个AI和心跳方法,如果有玩家附近,每隔几毫秒/秒就会调用一次,以便它们做出反应。
目前,该AI使用枚举器作为“例程”,例如:
IEnumerable WanderAround(int radius)
{
    // Do something
}

这些方法被称为“状态方法”,在foreach循环中调用,在心跳中产生yield,因此您可以在每个时刻回到相同的位置。
void OnHeartbeat()
{
    // Do checks, maybe select a new state method...
    // Then continue the current sequence
    currentState.MoveNext();
}

当然,这些例程也必须在循环中调用,否则它们不会执行。但由于编写这些AI的不是我,而是那些并非程序员的新手,因此我在服务器启动前预编译这些AI(简单的.cs文件)以进行编译。这给我提供了像这样的AI脚本:
override IEnumerable Idle()
{
    Do(WanderAround(400));
    Do(Wait(3000));
}

override IEnumerable Aggro()
{
    Do(Attack());
    Do(Wait(3000));
}

“Do”被替换为一个foreach循环,该循环迭代调用例程。
我真的很喜欢这个设计,因为人工智能很容易理解,但又非常强大。它不仅仅是简单的状态,也不像行为树那样难以理解和编写。
现在到了我的实际“问题”,我不喜欢“Do”包装器,也不喜欢预编译我的脚本。但我就是想不出其他实现方式,而我想隐藏循环,因为它们过于冗长,而且撰写这些脚本的人的技能水平有限。
foreach(var r in Attack()) yield return r;

我希望有一种方法可以在没有显式循环的情况下调用例程,但这是不可能的,因为我必须从状态方法中进行yield。
我无法使用async/await,因为它不适合我所依赖的tick设计(AI可能非常复杂,而且我真的不知道如何使用async来实现它)。此外,我只会将Do()与await交换,改进并不大。
所以我的问题是:有人能想到一种摆脱循环包装器的方法吗?如果有一种支持这种方式的其他.NET语言可用作脚本(在服务器启动时编译),我也愿意使用。

你能否使用event(事件),并让每个AI实现一个事件处理程序呢?那么你所要做的就是触发事件(Idle,Aggro等),每个AI都会按照脚本进行响应。.NET框架将处理循环遍历所有已订阅AI的事件处理程序。 - Evil Dog Pie
无法工作,因为脚本编写者定义的操作不会立即执行 = / 例如,一个 AI 可能应该走 100 个单位,然后说些什么,等一秒钟,然后攻击,所有这些都是响应于单个事件。而且由于有这么多的 AI,我不想给每个 AI 分配自己的线程/任务。 - Mars
2个回答

0

你可以尝试使用.NET框架来帮助你,通过在服务器中使用事件,并让各个AI订阅它们。如果服务器维护心跳,则此方法有效。

服务器

服务器会广告AIs可以订阅的事件。在心跳方法中,您将调用OnIdleOnAggro方法来引发IdleAggro事件。

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);
        }
    }
}

通用生物AI

所有开发人员都将基于此类实现其生物AI。构造函数接受一个GameServer引用参数,以允许挂钩事件。这是一个简化的示例,其中引用未保存。实际上,您将保存该引用并允许AI实现者根据其AI状态订阅和取消订阅事件。例如,仅在玩家尝试偷取你的鸡蛋时订阅“Aggro”事件。

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.
}
AI本身

它们源于通用的基类,并实现了特定于生物的事件处理程序。

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
    }
}

我有一种感觉,你们可能没有理解我的问题^^" 问题不在于“handlers”,而在于如何调用“_actions_”,这可能需要一段时间才能执行完毕。这就是为什么我目前使用ticks和枚举器,我一遍又一遍地进入当前的动作,直到它完成为止。我只是想知道是否可以在不使用循环调用操作(枚举器)或在其前面使用await(如果我要使用async)的情况下实现这一点。 - Mars
也许我的回答不够清晰。.NET的“事件”是一个框架机制,而你类中的“事件”并不一定与游戏中的事件相对应。因此,你的 AI 可能会响应“Aggro”事件并向敌人迈进一步;在下一次调用时,它可能会啄击敌人;或者,敌人可能已经逃走并且它会取消订阅后续的“Aggro”事件。 - Evil Dog Pie
但是你如何编写这种行为?使用状态吗?那不太用户友好=/ 我正在寻找一种实现无状态行为/操作的方法,不需要额外的代码,这可能实际上是不可能的,除非进行更复杂的预编译。 - Mars
@Mars 就像你期望他们编写“enumerator”行为一样,只是不是在MoveNext方法中编码,而是在事件处理程序中实现它。我怀疑你是对的,我可能忽略了这个问题。也许如果你能澄清服务器和AI的架构、接口和责任,会更好理解。 - Evil Dog Pie

0
每个生物都有一个带有心跳方法的AI,每隔几毫秒/秒调用一次。
为什么不完全像SkyNet那样,让每个生物负责自己的心跳呢?
比如说,为每个生物创建一个计时器(所谓的心脏),具有特定的心跳。当每个计时器跳动时,它会执行设计好的任务,并与游戏检查是否需要关闭、空闲、漫游或其他操作。
通过分散循环,你已经摆脱了循环,只需向订阅者(生物)广播在全局/基本层面上要做什么。这段代码对新手来说是不可访问的,但在概念层面上理解它所做的事情。

这将如何改变脚本编写的方式?你毕竟仍然需要指定它应该做什么。循环围绕着调用返回 IEnumerators 的方法,因此下一次运行时有一个状态可供返回。 - Mars

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接