Game AI: Pattern for implementing Sense-Think-Act components?
- by Rosarch
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?