我正在开发一款游戏。游戏中的每个实体都是GameObject
。每个GameObject
由GameObjectController
,GameObjectModel
和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
的引用并阅读该内容。
但是,我现在正在尝试为此编写单元测试。显然,有必要使用mocking,因为我无法直接传递参数。我这样做的方式感觉非常脆弱 - 如果另一个实现以不同的方式使用世界查询实用程序会怎样?真的,我没有测试界面,我正在测试实现。差。
我首先使用此模式的原因是保持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
每次都需要它所需的所有参数,我可以将其设为静态。这有什么问题吗?
答案 0 :(得分:2)
NO。不不不。您在AI中创建了大量隐藏(而非隐藏)的依赖项。首先,MVC在这里使用并不是一个好的模式,因为实际上没有你需要关注的“视图”,只有动作。此外,这里的“模型”实际上是当时人工智能已知的世界状态,它完全独立于人工智能本身,尽管这可以被认为是游戏世界的“视角”。你对象的位置和属性的快照(我这样做,非常有效)。
然而,核心问题是你的retreatTransition代码与行为和状态高度耦合。如果你不得不做出改变会发生什么?如果你需要200种不同类型的人工智能,那么你会怎样保持这种状态呢?答案是,你不能,这将是一个烂摊子。你在这里有效地创建了一个状态机,并且状态机不能很好地扩展。此外,如果不编辑代码,则无法在计算机上添加/更改/删除状态。
我建议的是,考虑转移到不同的架构。您的TDD方法非常棒,但是在做出选择之前,您需要退后一步,查看不同的AI架构并了解核心概念。我首先看看Jeff Orkin的优秀文章“3个州和一个计划”,该文章是关于F.E.A.R的基于目标的架构。 (http://web.media.mit.edu/~jorkin/goap.html)。我之前已经实现了它,它非常有效且易于设计和维护。它的核心设计也可以很好地促进TDD(实际上BDD是一个更好的选择)。
另一件事:您的ISenseNPC看起来与世界状态紧密耦合。你的AI(它可以从世界中观察到的东西)的感知应该是完全独立的,所以这告诉我你应该有一个类WorldModel或传递给ISenseNPC对象的东西,然后检查WorldModel的相关性信息通过其感知(将感知视为AI可以感知世界的方式,如传感器,视觉半径,声纳等),然后您甚至可以创建单独的感知并将其添加到您的ISenseNPC,这将解耦世界国家,人工智能感知世界的方式,以及人工智能对世界本身的理解。从那里,你的AI可以决定它应该做什么。
您正在建模一个简单的反射代理,它只是一组响应给定知觉序列的规则,这对于简单的AI来说很好。它基本上是一个美化的状态机,但是你可以在Think对象中创建一个感知到行为的映射,可以单独维护,改变映射或扩展它不需要改变代码(工作中的单一责任原则)。此外,您可以创建一个游戏编辑器,可以枚举所有感知,决策和操作,并将它们链接在一起用于任何给定的AI,这将有助于您维护AI而无需进入游戏甚至重建代码(可能) 。我想你会发现这比你在这里尝试做的更加灵活和可维护。抛弃MVC这个特殊的东西,MVC非常适合图形和较小程度的物理,但它真的不适合AI非常好,因为AI没有真正的“视图”。
如果您对此有任何其他疑问,请告诉我。我在为游戏实施基于目标的架构以及其他一些事情方面有一些经验,我很乐意帮助您。< / p>