1

我正在开发一个游戏。游戏中的每个实体都是一个GameObject. 每个GameObject都由GameObjectControllerGameObjectModel和组成GameObjectView。(或其继承人。)

对于 NPC,GameObjectController分为:

IThinkNPC:读取当前状态并决定要做什么

IActNPC:根据需要做的事情更新状态

ISenseNPC:读取当前状态以回答世界查询(例如“我在阴影中吗?”)

我的问题:ISenseNPC界面可以吗?

public interface ISenseNPC
    {
        // ...

        /// <summary>
        /// True if `dest` is a safe point to which to retreat.
        /// </summary>
        /// <param name="dest"></param>
        /// <param name="angleToThreat"></param>
        /// <param name="range"></param>
        /// <returns></returns>
        bool IsSafeToRetreat(Vector2 dest, float angleToThreat, float range);

        /// <summary>
        /// Finds a new location to which to retreat.
        /// </summary>
        /// <param name="angleToThreat"></param>
        /// <returns></returns>
        Vector2 newRetreatDest(float angleToThreat);

        /// <summary>
        /// Returns the closest LightSource that illuminates the NPC.
        /// Null if the NPC is not illuminated.
        /// </summary>
        /// <returns></returns>
        ILightSource ClosestIlluminatingLight();

        /// <summary>
        /// True if the NPC is sufficiently far away from target.
        /// Assumes that target is the only entity it could ever run from.
        /// </summary>
        /// <returns></returns>
        bool IsSafeFromTarget();
    }

这些方法都不接受任何参数。相反,实现应该保持对相关的引用GameObjectController并阅读它。

但是,我现在正在尝试为此编写单元测试。显然,有必要使用模拟,因为我不能直接传递参数。我这样做的方式感觉非常脆弱 - 如果出现另一个以不同方式使用世界查询实用程序的实现怎么办?真的,我不是在测试接口,而是在测试实现。较差的。

我首先使用这种模式的原因是为了保持IThinkNPC实现代码的简洁:

    public BehaviorState RetreatTransition(BehaviorState currentBehavior)
    {
        if (sense.IsCollidingWithTarget())
        {
            NPCUtils.TraceTransitionIfNeeded(ToString(), BehaviorState.ATTACK.ToString(), "is colliding with target");
            return BehaviorState.ATTACK;
        }

        if (sense.IsSafeFromTarget() && sense.ClosestIlluminatingLight() == null)
        {
            return BehaviorState.WANDER;
        }

        if (sense.ClosestIlluminatingLight() != null && sense.SeesTarget())
        {
            NPCUtils.TraceTransitionIfNeeded(ToString(), BehaviorState.ATTACK.ToString(), "collides with target");
            return BehaviorState.CHASE;
        }
        return currentBehavior;
    }

然而,也许清洁不值得。

因此,如果ISenseNPC每次都获取所需的所有参数,我可以将其设为静态。这有什么问题吗?

4

1 回答 1

2

不。不不不。你在你的 AI 中创建了大量隐藏的(而不是隐藏的)依赖项。首先,在这里使用 MVC 并不是一个很好的模式,因为真的没有你需要关心的“视图”,只有动作。另外,这里的“模型”实际上是当时 AI 所知道的世界状态,它与 AI 本身完全不同,尽管这可以被认为是游戏世界的“视图”对象位置和属性的快照(我已经这样做了,非常有效)。

然而,核心问题是您的retreatTransition 代码与动作和状态高度耦合。如果你不得不做出改变会发生什么?如果你需要 200 种相似的不同类型的 AI,你会如何维护它?答案是你不能,那会是一团糟。您在这里有效地创建了一个状态机,并且状态机不能很好地扩展。此外,您无法在不编辑代码的情况下从您的机器中添加/更改/删除状态。

相反,我建议考虑迁移到不同的架构。您在这里的 TDD 方法很棒,但是您需要退后一步,查看不同的 AI 架构并了解核心概念,然后再做出选择。我将首先查看 Jeff Orkin 的优秀文章“3 states and a plan”,该文章是关于 FEAR 的基于目标的架构 (http://web.media.mit.edu/~jorkin/goap.html)。我之前已经实现过它,它非常有效且愚蠢 - 易于设计和维护。它的核心设计也将很好地促进 TDD(实际上 BDD 是一个更好的选择)。

另一件事:您的 ISenseNPC 看起来与世界状态紧密耦合。你的 AI 的感知(它可以从世界观察到的东西)应该是完全独立的,所以这告诉我你应该有一个 WorldModel 类或传递给 ISenseNPC 对象的东西,然后检查 WorldModel 的相关通过其感知获取信息(将感知视为 AI 感知世界的一种方式,例如传感器、视野半径、声纳等),然后您甚至可以创建单独的感知并将它们添加到您的 ISenseNPC 中,这将解耦世界状态,人工智能感知世界的方式,然后是人工智能对世界本身的理解。从那里,你的人工智能可以决定它应该做什么。

您正在建模一个简单的反射代理,它只是一组响应给定感知序列的规则,这对于简单的 AI 来说很好。它基本上是一个美化的状态机,但是您可以在 Think 对象中创建感知到行为的映射,该映射可以单独维护,并且更改该映射或扩展它不需要更改代码(工作中的单一责任原则)。此外,您可以创建一个游戏编辑器,该编辑器可以枚举所有感知、决策和动作,并将它们链接到任何给定的 AI 中,这将有助于您维护您的 AI,而无需进入游戏甚至重建代码(可能) . 我认为您会发现这比您在这里尝试做的更加灵活和可维护。为这个特定的东西放弃 MVC,

如果您对此有任何其他问题,请告诉我,我在为游戏实现基于目标的架构以及其他一些事情方面有一些经验,我很乐意为您提供帮助。

于 2011-04-11T23:16:30.933 回答