游戏 AI:实现 Sense-Think-Act 组件的模式?
我正在开发一款游戏。游戏中的每个实体都是一个 GameObject。每个 GameObject
由 GameObjectController
、GameObjectModel
和 GameObjectView
组成。 (或其继承者。)
对于 NPC,GameObjectController
分为:
IThinkNPC
:读取当前状态并决定做什么
IAtNPC
:根据需要完成的事情更新状态
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
每次都获取所需的所有参数,我可以将其设为静态。这有什么问题吗?
I'm developing a game. Each entity in the game is a GameObject
. Each GameObject
is composed of a GameObjectController
, GameObjectModel
, and GameObjectView
. (Or inheritants thereof.)
For NPCs, the GameObjectController
is split into:
IThinkNPC
: reads current state and makes a decision about what to do
IActNPC
: updates state based on what needs to be done
ISenseNPC
: reads current state to answer world queries (eg "am I being in the shadows?")
My question: Is this ok for the ISenseNPC
interface?
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();
}
None of the methods take any parameters. Instead, the implementation is expected to maintain a reference to the relevant GameObjectController
and read that.
However, I'm now trying to write unit tests for this. Obviously, it's necessary to use mocking, since I can't pass arguments directly. The way I'm doing it feels really brittle - what if another implementation comes along that uses the world query utilities in a different way? Really, I'm not testing the interface, I'm testing the implementation. Poor.
The reason I used this pattern in the first place was to keep IThinkNPC
implementation code clean:
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;
}
Perhaps the cleanliness isn't worth it, however.
So, if ISenseNPC
takes all the params it needs every time, I could make it static. Is there any problem with that?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
不。不不不。你正在你的人工智能中创建大量隐藏的(而不是隐藏的)依赖项。首先,MVC 并不是一个真正适合在这里使用的模式,因为实际上没有您需要关心的“视图”,只有操作。另外,这里的“模型”实际上是 AI 当时已知的世界状态,它与 AI 本身完全分开,尽管这可以被认为是游戏世界的“视图”对象位置和属性的快照(我就是这样做的,非常有效)。
然而,核心问题是您的retreatTransition 代码与操作和状态高度耦合。如果你必须做出改变,会发生什么?如果您需要 200 种不同类型且都相似的 AI,您将如何维护它?答案是你不能,那会很混乱。您在这里有效地创建了一个状态机,但状态机无法很好地扩展。此外,如果不编辑代码,您就无法从计算机中添加/更改/删除状态。
我建议的是,考虑迁移到不同的架构。这里的 TDD 方法很棒,但是在做出选择之前,您需要退后一步,查看不同的 AI 架构并了解核心概念。我首先要阅读 Jeff Orkin 的优秀文章“3 个状态和一个计划”,该文章是关于 FEAR 基于目标的架构 (http://web.media.mit.edu/~jorkin/goap.html)。我之前已经实现过这个方法,它非常有效,而且很愚蠢——易于设计和维护。它的核心设计也将很好地促进TDD(实际上BDD是一个更好的选择)。
另一件事:你的 ISenseNPC 看起来与世界状态紧密耦合。你的人工智能的感知(它可以从世界中观察到的东西)应该是完全独立的,所以这对我来说,你应该有一个类 WorldModel 或传递给 ISenseNPC 对象的东西,然后检查 WorldModel 的相关信息通过其感知获取信息(将感知视为人工智能感知世界的一种方式,例如传感器、视觉半径、声纳等),然后您甚至可以创建单独的感知并将它们添加到您的 ISenseNPC 中,这将解耦世界状态、人工智能感知世界的方式,以及人工智能对世界本身的理解。从那里,你的人工智能可以决定它应该做什么。
您正在建模一个简单的反射代理,它只是一组响应给定感知序列的规则,这对于简单的人工智能来说很好。它基本上是一个美化的状态机,但是您可以在 Think 对象中创建感知到行为的映射,该映射可以单独维护,并且更改该映射或扩展它不需要更改代码(工作中的单一职责原则)。此外,您可以创建一个游戏编辑器,它可以枚举所有感知、决策和操作,并将它们针对任何给定的 AI 连接在一起,这将有助于您维护 AI,而无需进入游戏,甚至无需重建代码(可能) 。我想您会发现这比您在这里尝试做的更加灵活和可维护。对于这个特定的事情,放弃 MVC,MVC 非常适合图形,在较小程度上适合物理,但它确实不太适合 AI,因为 AI 并没有真正的“视图”。
如果您对此还有任何其他问题,请告诉我,我在为游戏实现基于目标的架构以及其他一些事情方面拥有一些经验,我很乐意为您提供帮助。
NO. No no no. You're creating a ridiculous number of hidden (and not hidden) dependencies in your AI. First, MVC is not really a good pattern to use here, since there really is no "view" that you need to care about, there are only actions. Also, your "model" here is really the state of the world as known to the AI at the time, which is entirely separate from the AI itself, although this could be considered to be a "view" of the game world in terms of a snapshot of your object's positions and attributes (I've done it this way, was highly effective).
The core problem, however, is that your retreatTransition code is highly coupled to actions and state. What would happen if you had to make a change? What if you needed 200 different types of AI that are all similar, how would you maintain that? The answer is that you couldn't, it would be a mess. You're effectively creating a state machine here and state machines don't scale well. Also, you couldn't add/change/remove a state from your machine without editing code.
What I would recommend is instead, consider moving to a different architecture. Your TDD approach here is great, however you need to take a step back and look at different AI architectures and understand the core concepts before you make a choice. I would start by looking at Jeff Orkin's excellent article "3 states and a plan" which is about the goal-based architecture of F.E.A.R. (http://web.media.mit.edu/~jorkin/goap.html). I've implemented this before and it was highly effective and stupid-easy to design and maintain. Its core design would also facilitate TDD (actually BDD is a better choice) quite well.
Another thing: Your ISenseNPC looks like its tightly coupled to world state. The percepts of your AI (things that it can observe from the world) should be completely separate, so this says to me that you should have a class WorldModel or something that is passed in to the ISenseNPC object, which then inspects the WorldModel for relevant information via its percepts (think of a percept as a way that the AI can perceive the world, something like a sensor, vision radius, sonar, etc) and then you can even create individual percepts and add them to your ISenseNPC, which would decouple the world state, the way an AI perceives that world, and then the AI's understanding of the world itself. From there, your AI can make decisions on what it should do.
You're modeling a simple reflex agent, which is just a set of rules that respond to a given percept sequence, which is fine for simple AI. It's basically a glorified state machine, however you can then create a mapping of percepts to behaviors in your Think object, which could be maintained separately and changing that mapping or extending it would not require changing code (Single Responsibility Principle at work). Furthermore, you could create a game editor that can enumerate all percepts, decisions, and actions and link them together for any given AI, which would facilitate you maintaining your AI's without having to even go into the game or even rebuild the code (potentially). I think you'll find this to be far more flexible and maintainable than what you're trying to do here. Ditch that MVC for this particular thing, MVC is highly suited to graphics and to a lesser extent physics, but it really doesn't fit AI very well since AI doesn't really have a "view."
Please let me know if you have any other questions about this, I've had some experience in implementing a goal-based architecture for a game as well as some other things and I'd be happy to help you out.