Game Development Community

Improving AI Targeting

by Nathan Kent · in Torque Game Engine · 05/31/2008 (1:40 pm) · 9 replies

This is now a resource, you can see it here.

I am attempting to change some of the AIPlayer code, inorder to have the AIPlayers look their target (if it is an AIPlayer, or a regular Player) in they eye, instead of down towards their feet. (EDIT-> The code for this is below) Instead of possibly blundering the AICode, I attempted to make a "isInLOS" funtion. The system works, except for one thing: if the target is a Player, or AIPlayer, it will return false if the eye is on the far-side of the target.

ConsoleMethod(Player, isInLOS, bool, 3, 3, "(target) Checks to see if the target is in the LOS for the player.  If the target is a Player or AIPlayer, then the raycast is to their eye transform, otherwise it's to the result of getPosition.")
{
	ShapeBase* target1;
	Player* target2;
	AIPlayer* target3;

	Point3F targetLoc;
	Point3F startLoc;

	MatrixF eyeXFM;

	if(!Sim::findObject(argv[2], target1)) {
		if(!Sim::findObject(argv[2], target2)) {
			if(!Sim::findObject(argv[2], target3)) {
				Con::errorf("Player::isInLOS -> Target must be a child of shapebase!");
				return false;
			}
			else {
				target3->getEyeTransform(&eyeXFM);
				targetLoc = eyeXFM.getPosition();
				target1 = target3;
			}
		}
		else {
			target2->getEyeTransform(&eyeXFM);
			targetLoc = eyeXFM.getPosition();
			target1 = target2;
		}
	}
	else {
		targetLoc = target1->getPosition();
	}

	object->getEyeTransform(&eyeXFM);
	startLoc = eyeXFM.getPosition();

	RayInfo dummy;
	if (object->getContainer()->castRay( startLoc, targetLoc,
            InteriorObjectType | StaticShapeObjectType | StaticObjectType |
            TerrainObjectType, &dummy)) {
         return false;
    }
    else
       return true;
}


What I would like to do, is check to see if there are any collisions, and if there are, is it the target. I can't seem to get that to work though. I've tried thowing in a target1 = dummy.object, but it doesn't work. Any help would be appreciated!

#1
06/02/2008 (3:59 pm)
Okay, got it working. If anyone is interested, here's my C++ code for sight that ajusts the raycast position based on the type of object.
ConsoleMethod(Player, isInLOS, bool, 3, 3, "(target) Checks to see if the target is in the LOS for the player.  If the target is a Player or AIPlayer, then the raycast is to their eye transform, otherwise it's to the result of getPosition.")
{
	ShapeBase* target1;
	Player* target2;
	AIPlayer* target3;

	Point3F targetLoc;
	Point3F startLoc;

	MatrixF eyeXFM;
	const char* id;

	if(!Sim::findObject(argv[2], target1)) {
		if(!Sim::findObject(argv[2], target2)) {
			if(!Sim::findObject(argv[2], target3)) {
				Con::errorf("Player::isInLOS -> Target must be a child of shapebase!");
				return false;
			}
			else {
				target3->getEyeTransform(&eyeXFM);
				targetLoc = eyeXFM.getPosition();
				id = target3->scriptThis();
			}
		}
		else {
			target2->getEyeTransform(&eyeXFM);
			targetLoc = eyeXFM.getPosition();
			id = target2->scriptThis();
		}
	}
	else {
		targetLoc = target1->getPosition();
		id = target1->scriptThis();
	}

	object->getEyeTransform(&eyeXFM);
	startLoc = eyeXFM.getPosition();

	RayInfo dummy;
	bool intersect = object->getContainer()->castRay( startLoc, targetLoc, InteriorObjectType | StaticShapeObjectType | StaticObjectType | TerrainObjectType, &dummy);
	if (intersect && dStrcmp(dummy.object->scriptThis(),id) != 0) {
         return false;
    }
	else {
       return true;
	}
}

If anyone is interested, I'll also post the improved targeting method for the AIPlayer once I get it finished. I'm probably going to change the setTarget function so that you can specify where you want the AIPlayer to target: position, legs, chest, or head.
#2
06/04/2008 (10:50 am)
Ok, I have most the changes done, and here's a list of them:

- If the target is a player, default aim location is their eyes
- Allowed the AIPlayer to target specific nodes in the object
- Improved the targetEnterLOS and targetExitLOS checks
- Set, clear, and get AimOffset functions (TS and C++)
- Set, clear, and get AimNode functions (TS and C++)

For now, I think all I really need to do for it to be "done", is test it with projectiles.
#3
06/04/2008 (3:27 pm)
Looking nice. Since the code is posted on the forums I'm guessing you won't mind if some of us use this? :p
#4
06/04/2008 (5:21 pm)
Sure, I'll post the code changes for the AI as soon as I can.
#5
06/04/2008 (6:48 pm)
Well done, I'd tried something similar in script - but C++ is so much better.
#6
06/05/2008 (4:54 am)
Ok, here's my code. It's coming from a highly modified Torque, so I can't guarenty that it will work (it

should though). Also, this hasn't been tested all that much, and I have no clue how accurate it is with

projectile weapons (my game consists of magic and melee weapons), or with targets other than Players and AIPlayers.

In AIPlayer.cc

Change AIPlayer::AIPlayer() to this:
AIPlayer::AIPlayer()
{
   mMoveDestination.set( 0.0f, 0.0f, 0.0f );
   mMoveSpeed = 1.0f;
   mMoveTolerance = 0.25f;
   mMoveSlowdown = true;
   mMoveState = ModeStop;
   mStuckCount = 0;
   mStuck = false;

   mAimObject = 0;
   mAimNode = 0;
   mAimLocationSet = false;
   mTargetInLOS = false;
   mAimOffset = Point3F(0.0f, 0.0f, 0.0f);

   mTypeMask |= AIObjectType;
}

Now, after AIPlayer::setAimObject, put this:
// Aim node setting
void AIPlayer::setAimNode( S32 node )
{
	mAimNode = node;
}

// Aim offset setting
void AIPlayer::setAimOffset( Point3F offset )
{
	mAimOffset = offset;
}

In AIPlayer::getAIMove replace
if (mAimObject)
         mAimLocation = mAimObject->getPosition() + mAimOffset;
with
if (mAimObject) {
		ShapeBase *aimShape;
		Player *aimPlayer;
		if(Sim::findObject(mAimObject->getId(), aimShape)) {
			if(mAimNode != 0) {
				MatrixF boneXFM;
				S32 nodeID;
				nodeID = aimShape->getShape()->findNode(mAimNode);
				boneXFM = 

aimShape->getShapeInstance()->mNodeTransforms[nodeID];

				mAimLocation = aimShape->getPosition() + 

boneXFM.getPosition() + mAimOffset;
			}
			else {
				if(Sim::findObject(mAimObject->getId(), aimPlayer)) {
					MatrixF eyeXFM;
					aimPlayer->getEyeTransform(&eyeXFM);
					mAimLocation = eyeXFM.getPosition() + mAimOffset;
				}
				else
					mAimObject->getPosition() + mAimOffset;
			}
		}
		else
			mAimLocation = mAimObject->getPosition() + mAimOffset;
	  }

This next part is for the onMoveStuck callback, you don't need this for any other part of my code, but

I'll post it incase anyone wants it. I'm not sure if this works better in stock Torque than the stock

callback system does, but it does in my highly modified Torque. So, still under

AIPlayer::getAIMove, as the very last thing in the if (mMoveState == ModeMove), add

this:
if(mFabs(xDiff) >= mMoveTolerance && mFabs(yDiff) >= mMoveTolerance && path.size() && 

dStrcmp(getDamageStateName(), "Enabled") != 0) {
		// We should check to see if we are stuck...
		 F32 xLocDiff = mLastLocation.x - location.x;
		 F32 yLocDiff = mLastLocation.y - location.y;
		 F32 sensitivity = 0.001;
         if (mFabs(xLocDiff) < sensitivity && mFabs(yLocDiff) < sensitivity) {
			 if(mStuckCount > 2) {
				 mMoveState = ModeStop;
				 throwCallback("onMoveStuck");
				 mStuck = true;
				 mStuckCount = 0;
			 }
			 else {
				 mStuckCount++;
			 }
		 }
		 else {
			 mStuck = false;
			 mLastLocation = location;
			 mStuckCount = 0;
		 }
	  }
	  else {
		  mStuckCount = 0;
		  mLastLocation = location;
	   }

Now, this next code will improve the onTarget(Enter/Exit)LOS callbacks. You could use this for the

target bones, or, with a lot of improvement, check a variety of bones are out of sight, giving a more

accurate LOS callback. Change the if (mAimObject) check, that will be right under the stuck

check (above) to this:
if (mAimObject) {
      MatrixF eyeMat;
      getEyeTransform(&eyeMat);
      eyeMat.getColumn(3,&location);
      Point3F targetLoc;
	  ShapeBase *aimShape;
	  if(Sim::findObject(mAimObject->getId(), aimShape) && mAimNode != 0) {
		  MatrixF boneXFM;
		  S32 nodeID;
		  nodeID = aimShape->getShape()->findNode(mAimNode);
		  boneXFM = aimShape->getShapeInstance()->mNodeTransforms[nodeID];

          targetLoc = aimShape->getPosition() + boneXFM.getPosition() + mAimOffset;
	  }
	  else {
		  targetLoc = mAimObject->getBoxCenter();
	  }

	  const char* targetID = mAimObject->getIdString();


      // This ray ignores non-static shapes. Cast Ray returns true
      // if it hit something.
      RayInfo dummy;
      bool intersect = getContainer()->castRay( location, targetLoc, InteriorObjectType | 

StaticShapeObjectType | StaticObjectType | TerrainObjectType, &dummy);
      if (intersect && dStrcmp(dummy.object->getIdString(),targetID) != 0) {

         if (mTargetInLOS) {
            throwCallback( "onTargetExitLOS" );
            mTargetInLOS = false;
         }
      }
      else
         if (!mTargetInLOS) {
            throwCallback( "onTargetEnterLOS" );
            mTargetInLOS = true;
         }
   }

Now, we're at the ConsoleMethods. Change the AIPlayer.setAimObject method to this:
ConsoleMethod( AIPlayer, setAimObject, void, 3, 5, "( GameBase obj, [Point3F offset, char boneId] )"
              "Sets the bot's target object. Optionally set an offset from target location. If the object is a 

child of Player, you can use bone to determine where the aim location is.")
{
   Point3F off( 0.0f, 0.0f, 0.0f );

   // Find the target
   GameBase *targetObject;
   Player *targetPlayer;

   if( Sim::findObject( argv[2], targetPlayer ) )
   {
	   S32 node = 0;

	   if (argc >= 4)
         dSscanf( argv[3], "%g %g %g", &off.x, &off.y, &off.z );
	   if (argc == 5) {
		 node = object->getShape()->findNode(argv[4]);
	   }

	  object->setAimNode(node);
      object->setAimObject( targetPlayer, off );
   }
   else if( Sim::findObject( argv[2], targetObject ) )
   {
      if (argc >= 4)
         dSscanf( argv[3], "%g %g %g", &off.x, &off.y, &off.z );

      object->setAimObject( targetObject, off );
   }
   else
      object->setAimObject( 0, off );
}

Now, somewhere in the file, you need to add these ConsoleMethods:
ConsoleMethod( AIPlayer, setAimNode, void, 3, 3, "(char NodeName)"
			  "Sets the target node to NodeName.")
{
	S32 node = 0;
	node = object->getShape()->findNode(argv[2]);

	object->setAimNode(node);
}

ConsoleMethod( AIPlayer, getAimNode, S32, 2, 2, "()"
              "Gets the ID of the node the AIPlayer is targeting.")
{
   return object->getAimNode();
}

ConsoleMethod( AIPlayer, clearAimNode, void, 2, 2, "()"
              "Clears the node that the AIPlayer is targeting.  If the target is a player, it will set it to the 

eye node, otherwise, it is set to the position of the target.")
{
	object->clearAimNode();
}

ConsoleMethod( AIPlayer, getNodeID, S32, 3, 3, "(nodeName) Returns the S32 id of the bone in the 

AIPlayer.")
{
	return object->getShape()->findNode(argv[2]);
}

ConsoleMethod( AIPlayer, getAimOffset, const char*, 2, 2, "()"
              "Gets the current aim offset for the AIPlayer.")
{
	Point3F aimOff = object->getAimOffset();

   char *returnBuffer = Con::getReturnBuffer( 256 );
   dSprintf( returnBuffer, 256, "%g %g %g", aimOff.x, aimOff.y, aimOff.z );

   return returnBuffer;
}

ConsoleMethod( AIPlayer, setAimOffset, void, 3, 3, "(Point3F offset)"
              "Sets the AIPlayer's aim offset.")
{
	Point3F off;

	dSscanf( argv[2], "%g %g %g", &off.x, &off.y, &off.z );

	object->setAimOffset(off);
}

ConsoleMethod( AIPlayer, clearAimOffset, void, 2, 2, "()"
              "Clears the aim offset.")
{
	object->clearAimOffset();
}

bool AIPlayer::isStuck()
{
	return mStuck;
}

ConsoleMethod( AIPlayer, isStuck, bool, 2, 2, "()"
              "Returns if the AIPlayer is stuck or not")
{
   return object->isStuck();
}

cont
#7
06/05/2008 (4:55 am)
cont


Now, in AIPlayer.h, make these changes.

Under Private: in class AIPlayer : public Player, add these:
int mStuckCount;				// For stuck check
	  bool mStuck;				// For stuck check
	  S32 mAimNode;				// The node/bone to look at

Under Public add all these:
void setAimNode( S32 node );
		void clearAimNode() { mAimNode = 0; }
		S32 getAimNode() const { return mAimNode; }
		void setAimOffset( Point3F offset );
		void clearAimOffset() { mAimOffset = Point3F(0.0f, 0.0f, 0.0f); }
		Point3F getAimOffset() const { return mAimOffset; }

---

That's it! The methods are used like this:
AIPlayer.setAimObject(Object, [offset, node]); // Node is the string name of the bone
AIPlayer.getAimObject();
AIPlayer.clearAimObject();
//
AIPlayer.setAimNode(node); // Node is the string name of the bone
AIPlayer.getAimNode(); // Don't know how to get it to return the string name, so it returns the S32 ID
AIPlayer.getNodeID(node); // To be used with getAimNode
AIPlayer.clearAimNode(); // Set's the aim node back to default (The Eye node for Players/AIPlayers, Position for everything else)
//
AIPlayer.setAimOffset(offset); // Point3F offset
AIPlayer.getAimOffset();
AIPlayer.clearAimOffset();

If you really want to get a good look at how the target location is changing in game, run the starter.fps demo, scale your player by four or five times, and have the AIPlayer alternate between the eye node, and the ski nodes.

Edit-> I still have some work I'd like to do on it, making it more sensitive, turning the head instead of the body (in certain situations), etc

Edit2 -> Found one problem: Sometime while shooting (narrowed it down to onFire, I think that it's getMuzzlePoint), the engine crashes. I'm not sure that this is the new aiming code, and I don't think that it should effect the engine like this, because I'm only changing how it gets the target location, not how the engine handles it. I'll work on it and see what I can come up with.
#8
07/07/2008 (9:07 am)
I've made a few improvements to this in the last month, including checking several nodes in the LOS checks. Right now, an AIPlayer will check to see if the any of the arms, thighs, feet, chest, or head are visible, before saying that it can't see the target. Other than that, it's been minor code clean up.

I'm thinking about making this a resource, as soon as I can figure out why it crashes when the AI shoot.
#9
07/07/2008 (5:45 pm)
Wow, quadruple post =P

I figured out the problem, and have submitted this as a resource. It's not approved yet, but you can see it here.

Edit->Approved now!