views:

69

answers:

0

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?