Torque 2D/GenreTutorials/PlatformerEnemies
From TDN
[edit] IntroductionIn this tutorial, we are going to concentrate on getting enemies to think somewhat intelligently. Specifically, we will be giving them the ability to patrol an area or chase after the player. Because we have already covered animation in the previous tutorial, and it is not integral to the implementation of AI, I am going to teach the AI concepts, but leave the animation of the enemies up to you. Adding the animations to the enemies is done almost identically to the player depending on your specific requirements. |
|
[edit] Creating EnemiesOur first step will be to create an enemy. This is done very similarly to creating the player. We need to create the actual image, set up collision and physics properties, and give the enemy some attributes. Once again, I am going to use the GarageGames logo as a placeholder image that you can replace with animations of your own. All of the enemy related code is going to go in a new file called enemy.cs. Create this now and add the exec line to initializeT2D.cs:
exec("./enemy.cs");
Now we need to set up a couple of other things in gameData.cs. Add a new collision group for the enemies after the player’s collision group at the top of the file, like so: $enemyGroup = 2; And a layer to place the enemies on: $enemyLayer = 2; At the end of the createLevel function, add this:
createEnemy("50 0", 30, 16, 11, 35, 100, 150, -30, 50);
Here we are calling the as of now undefined function that will create an enemy. The various parameters will be explained when we define it.
case $enemyGroup:
switch (%dstObj.getGraphGroup())
{
case $platformGroup:
collideEnemyPlatform(%srcObj, %dstObj, %normal);
}
To avoid confusion, the whole function should look like this:
function t2dSceneObject::onCollision(%srcObj, %dstObj, %srcRef,
%dstRef, %time, %normal, %contactCount, %contacts)
{
switch (%srcObj.getGraphGroup())
{
case $playerGroup:
switch (%dstObj.getGraphGroup())
{
case $platformGroup:
collidePlayerPlatform(%normal);
}
case $enemyGroup:
switch (%dstObj.getGraphGroup())
{
case $platformGroup:
collideEnemyPlatform(%srcObj, %dstObj, %normal);
}
}
}
Anytime a collision occurs in which a member of $enemyGroup collides with a member of $platformGroup, collideEnemyPlatform will be called. We are passing the function the enemy that is doing the colliding (%srcObj), the platform it collided with (%dstObj), and the collision normal (%normal). We will define this function shortly.
for (%i = 0; %i < EnemyGroup.getCount(); %i++)
{
updateEnemy(EnemyGroup.getObject(%i));
}
All this does is loop through all of the enemies and update each one with the updateEnemy function. I will go into more detail on this when we create EnemyGroup.
new t2dImageMapDatablock(enemyImageMap)
{
imageMode = "full";
imageName = "~/data/images/gglogo";
};
This should be familiar by now. We created an image map by the name of enemyImageMap and assigned it the gglogo texture. Now the good stuff.
new SimGroup(EnemyGroup);
function createEnemy(%pos, %speed, %airSpeed, %jumpHeight,
%maxRunSurfaceAngle, %chase, %stopChase, %patrolMin, %patrolMax)
{
%enemy = new t2dStaticSprite()
{
scenegraph = t2dscene;
};
%enemy.setImageMap(enemyImageMap);
%enemy.setPosition(%pos);
%enemy.setConstantForce(0 SPC $gravity, true);
%enemy.setCollisionActive(true, true);
%enemy.setCollisionPhysics(true, false);
%enemy.setCollisionCallback(true);
%enemy.setCollisionResponse(CLAMP);
%enemy.setCollisionMaxIterations(2);
%enemy.setCollisionPolyPrimitive(8);
%enemy.setLayer($enemyLayer);
%enemy.setGraphGroup($enemyGroup);
%enemy.setCollisionMasks(BIT($platformGroup), BIT($platformLayer));
%enemy.runSpeed = %speed;
%enemy.airSpeed = %airSpeed;
%enemy.jumpHeight = %jumpHeight;
%enemy.maxRunSurfaceAngle = %maxRunSurfaceAngle;
%enemy.chaseDistance = %chase;
%enemy.stopChaseDistance = %stopChase;
%enemy.patrolMin = %patrolMin;
%enemy.patrolMax = %patrolMax;
%enemy.move = 1;
EnemyGroup.add(%enemy);
}
First, we create a SimGroup called EnemyGroup. A SimGroup is basically a container that you can put objects in to store them in an organized fashion. This SimGroup will hold all of the enemies we create. By storing them like this, we can easily get references to our enemies if we ever need them. For example, in the onUpdateScene function we looped through all of the enemies in EnemyGroup so that we could update them. SimGroups and there less restrictive counterpart, SimSets, are very useful constructs. There are other tutorials that cover them in detail, so I won’t belabor it here, but I would recommend getting a good understanding of them as they can be extremely useful in certain situations.
function collideEnemyPlatform(%enemy, %platform, %normal)
{
%enemy.runSurface = isRunSurface(%normal, %enemy.maxRunSurfaceAngle);
}
All we do here is determine if the enemy is on a surface that it is allowed to move on. This is exactly the same as it was for the player. Finally, we need to update the enemies. Here is the updateEnemy function:
function updateEnemy(%enemy)
{
doAI(%enemy);
if (%enemy.runSurface > 0)
{
%enemy.setLinearVelocityX(%enemy.move * %enemy.runSpeed);
}
else if (%enemy.runSurface < 0)
{
%speed = %enemy.getLinearVelocityX();
if (%enemy.move)
{
if (((%speed * %enemy.move) <= 0) || (mAbs(%speed) < %enemy.airSpeed))
%enemy.setLinearVelocityX(%enemy.move * %enemy.airSpeed);
}
}
if (%enemy.jump)
%enemy.setLinearVelocityY(-10 * %enemy.jumpHeight);
%enemy.runSurface = false;
}
This is also very similar to that of the player. The first line calls a function we will define that will determine the actions of the enemy. The enemies are capable of the same actions as our player, running and jumping, so here it will decide which of these it wants to do. The enemy’s moves are handled identically to the movement of the player. %enemy.move stores the direction the enemy wants to move exactly as $moveLeft and $moveRight do for the player. %enemy.jump stores whether or not the enemy wants to jump. If it does, we give it an upward velocity based on its jumpHeight.
|
|
[edit] PatrollingThe first type of AI we are going to implement is patrolling. This is a very simple process, and probably not complex enough to meet any real AI demands. With this, you will end up with something like the enemies from the NES Mario games. The enemies will walk back and forth with no knowledge of the player.
The basic implementation of this is ridiculously simple. Here is the complete doAI function, called from updateEnemy, which will make the enemy patrol an area. Add it to enemy.cs:
function doAI(%enemy)
{
if (%enemy.getPositionX() < %enemy.patrolMin)
%enemy.move = 1;
else if (%enemy.getPositionX() > %enemy.patrolMax)
%enemy.move = -1;
If you remember, %enemy.move stores the direction the enemy wants to move. If it is positive the enemy will move right, and negative the enemy will move left. So, if the enemy goes too far to the left (past the value in %enemy.patrolMin), it will start moving right, and vice versa. |
|
[edit] Seeing the LevelThis works just fine in some cases, but you may want your enemy to patrol across gaps or up a ledge. To do this, we need to provide the enemies with a way to see what is around them in the level. Obviously, the enemy cannot actually see, and it is much more complex than we need to simulate actual sight. Instead, we are going to have the level tell the enemy about the surrounding area. Conveniently, tile maps have functionality we can use to accomplish this.
if (%enemy.runSurface)
{
%pos = %enemy.getPositionX() SPC getWord(%enemy.getAreaMax(), 1);
%tile = %platform.pickTile(%pos);
%enemy.groundDetails = %platform.getTileCustomData(%tile);
}
First we make sure we are on a runSurface, then we get the tile at the position below the player, and use this tile to grab the custom data from the tile layer and store it in %enemy.groundDetails.
%enemy.groundDetails = ""; And now we need to use these new ground details. Add this to the beginning of the doAI function:
if ((%enemy.groundDetails $= "jumpLeft") && (%enemy.move < 0))
%enemy.jump = 1;
else if ((%enemy.groundDetails $= "jumpRight") && (%enemy.move > 0))
%enemy.jump = 1;
else
%enemy.jump = 0;
If the enemy is moving to the left and is being told to jump to the left by the level, then we tell it to jump. If the enemy is moving right and being told to jump right, we also tell it to jump.
|
|
[edit] ChasingA real chasing algorithm is going to involve some complicated path finding routines and is really overkill for what we need. There may be some situations where you want extensive path finding, but that is past the scope of this tutorial and should really be done in the engine code anyway.
$aiPatrolState = 0; $aiChaseState = 1; $aiFindUpState = 2; $aiFindDownState = 3; I will explain these states later. We need to set an initial state for the AI. In the createEnemy function, add this: %enemy.aiState = $aiPatrolState; And all that’s left is the doAI function. Change it to this:
function doAI(%enemy)
{
%yDistance = $player.getPositionY() - %enemy.getPositionY();
%distance = t2dVectorLength(t2dVectorSub(%enemy.getPosition(), $player.getPosition()));
// Determine state
switch (%enemy.aiState)
{
case $aiPatrolState:
if (mAbs(%distance) <= %enemy.chaseDistance)
%enemy.aiState = $aiChaseState;
case $aiChaseState:
if (mAbs(%distance) > %enemy.stopChaseDistance)
%enemy.aiState = $aiPatrolState;
else if (%yDistance > $player.getHeight())
%enemy.aiState = $aiFindDownState;
else if (%yDistance < -$player.getHeight())
%enemy.aiState = $aiFindUpState;
case $aiFindUpState:
if (%yDistance > -$player.getHeight())
%enemy.aiState = $aiChaseState;
case $aiFindDownState:
if (%yDistance < $player.getHeight())
%enemy.aiState = $aiChaseState;
}
if ((%enemy.groundDetails $= "jumpLeft") && (%enemy.move < 0))
%enemy.jump = 1;
else if ((%enemy.groundDetails $= "jumpRight") && (%enemy.move > 0))
%enemy.jump = 1;
else
%enemy.jump = 0;
switch (%enemy.aiState)
{
case $aiPatrolState:
if (%enemy.getPositionX() < %enemy.patrolMin)
%enemy.move = 1;
else if (%enemy.getPositionX() > %enemy.patrolMax)
%enemy.move = -1;
case $aiChaseState:
if ($player.getPositionX() < %enemy.getPositionX())
%enemy.move = -1;
else
%enemy.move = 1;
case $aiFindUpState:
if (%enemy.groundDetails $= "upLeft")
{
%enemy.move = -1;
%enemy.jump = 1;
}
else if (%enemy.groundDetails $= "upRight")
{
%enemy.move = 1;
%enemy.jump = 1;
}
else if ((%enemy.groundDetails $= "edgeLeft") && (%enemy.move < 0))
{
%enemy.move = 1;
}
else if ((%enemy.groundDetails $= "edgeRight") && (%enemy.move > 0))
{
%enemy.move = -1;
}
case $aiFindDownState:
if ((%enemy.groundDetails $= "edgeLeft") && (%enemy.move < 0))
{
%enemy.move = 1;
}
else if ((%enemy.groundDetails $= "edgeRight") && (%enemy.move > 0))
{
%enemy.move = -1;
}
}
}
The first two lines grab the vertical distance the enemy is from the player and the total distance the enemy is from the player. The switch statement is the implementation of the state machine. If the enemy is in the patrol state and is within chaseDistance of the player, it goes to the chase state. If it’s in the chase state and is farther away from the player than stopChaseDistance, it goes back to patrolling. If the player is above or below the enemy, it goes into the find up or find down state respectively. When in the find up or down state, the enemy will search for a way to get higher or lower in the level. If the enemy is in the up or down state, it goes back to the chase state if it is at about the same height as the player.
|
|
With this custom data, you can now see in the code that when in the find up or find down state, the enemy will change direction when it gets to an edge tile, and when in the find up state, the enemy will jump when it hits an “up†tile.
$camera.setTarget(EnemyGroup.getObject(0)); That will set the camera to follow the first enemy you created.
|




