Allar's Blog

Allar's Blog

Allar's Dev Diary #16: Day 7, Tower Defense Side Project

Day 7 (11/24/2010)

12. Getting Towers to Track Their Targets

Today I decided to switch gears back into actual tower development, as that is the real nature of the game isn't it? In any case, we need a way for our enemies to be registered as enemies for our towers. We don't want our towers to be attacking every actor that is within its range, it should only track and attack enemy targets. To do this, I've decided to make another Interface that way I can make any class an enemy instead of having all my enemies derive from a single class. Right now there is no actual use of the interface than to register a class as an enemy, but I've decided to throw in a function that could be used to allow for classes implementing this interface to decide that a particular instance of it is not an enemy. Today I haven't created that checks this or requires this, but I think it might come handy in the future.


Lets get to the interface:

[csharp]/*************************
CRInterface_TowerEnemy

Creation date: 24/11/2010 12:42  
Copyright (c) 2010, Allar  

If a class implements this interface, it is targetable by towers  

*************************/

interface CRInterface_TowerEnemy;

//Override for classes that could exist as an enemy and non-enemy
simulated function bool IsTowerEnemy() { return true; }[/csharp]

Pretty straight forward. Its an interface, it has a function, thats it. I defined the function here so that I can just copy paste it anywhere to fulfill the needs of implementing an interface. Now we need to have a class implement this! Right now our only enemies are crowd actors so lets go ahead and create a crowd actor enemy. Implementing the interface is quite easy and we've done it before, so I don't need to go over how that works again, but there is something else we need to add to our CrowdAgents. Currently the crowd actors do not have any form of collision with them at all and do not trigger any type of touch/encroach event against any other actor and for the tower system I have in mind I want to use touch events for tower triggers instead of checking every tick for all actors within a radius. Now the touch events for our tower are going to be a bit difficult to implement, however to facilitate this we need to make sure our crowd agents can register touch events. For an object to register a touch event, it must have bCollideActors set to true and it also must have a collision component. Because this collision component is only going to be used for triggering, I will use a fairly standard CylinderComponent for my collision, and a small one at that. This way crowd actors won't create a lot of physics checks if they penetrate each other, but we might need to make this for bigger actors so that their position is more accurately defined for better tower aiming. The last thing we need to be aware of is that crowd actors are set to tick during TGDuringAsyncWork which makes physics events a bit tricky to control as these actors in this group are updated as physics is happening, not before. There is a lot of technical info about tick groups that I will not get into mainly because I don't know enough to be confident I have correct knowledge about the subject. What I do know is, collision components on crowd actors that are not set to TGPreAsyncWork causes a huge list of log errors and that is something I'd prefer not to have. With that said, here is the small little GameCrowdAgent class that I wrote that will allow our crowd actors to support touch events and whatnot.

[csharp]/*************************
CRGameCrowdAgent

Creation date: 24/11/2010 13:54  
Copyright (c) 2010, Allar  

Crowd Agent that is capable of triggering touch events  

*************************/

class CRGameCrowdAgent extends UTGameCrowdAgent
implements (CRInterface_TowerEnemy)
showcategories(Collision);

simulated function bool IsTowerEnemy() { return true; }

simulated function PostBeginPlay()
{
super.PostBeginPlay();
SetTickGroup(TG_PreAsyncWork);
}

defaultproperties
{
Begin Object Class=CylinderComponent Name=CollisionCylinder
CollisionRadius=+0015.000000
CollisionHeight=+0015.000000
BlockNonZeroExtent=true
BlockZeroExtent=true
BlockActors=false
CollideActors=true
End Object
CollisionComponent=CollisionCylinder
Components.Add(CollisionCylinder)

bCollideActors=true  
bCollideWorld=false  
bBlockActors=false  

}[/csharp]

Cool. Be sure to create an archetype of this new crowd agent class and replace the one that we made earlier in our list of agent archetype, otherwise we will still be using the old crowd actors that don't support trigger events.

Now we must create something that will get triggered by the crowd actors to alert the towers of their presence. I tried a few ways of implementing this in the tower class itself but it led to various collision issues in that I could either have the trigger cylinder set off trigger events but then my towers would lose blocking collision or the other way around. I couldn't find an easy method to create a touch-only component of an actor that must also perform full collision as well, so what I ended up doing was creating another actor class called CRTowerToucher that will feed back touch events to the towers they belong to. A tower can then create a CRTowerToucher and assign itself as the toucher's owner and the toucher will automatically call targeting events in the tower. This effectively compartmentalizes the enemy detection logic from the tower's attacking logic and makes for cleaner, more object oriented code. With this separation, we can create custom touchers that apply to any tower instead of specific towers needing to have their enemy touching logic rewritten.

The CRTower_Toucher should be able to store the enemy the toucher is currently 'watching' or 'active', a reference to its owning tower, and a CylinderComponent to use as its touch radius. On the Toucher's touch event, it should first see if our touched actor is a tower enemy and simply bail out if it isn't as we don't want to perform lots of unneeded calculations in an event that will be called very often. If it is a tower enemy, we should go ahead and check to see if we an active enemy already and take care of two situations: our touched actor is the same as our active enemy or a different enemy has touched our toucher. In the first situation, we should just ignore the touch completely. In the second situation, we need to first check to see that if our current enemy is within range. If our current enemy is within range, we should do nothing and keep tracking our current enemy, but if the current enemy is out of range we should go ahead and start tracking the enemy that just touched us. With this alone, our toucher will be able to acquire targets and then switch targets when the last target is out of range. What it won't do is stop targeting when an enemy leaves its range and no new enemies touch it again.

The CRTower_Toucher should also be able to stop targeting when an enemy leaves its radius without waiting for a new enemy to touch it. When an enemy leaves its range, and it is our current enemy and not some other enemy that we aren't tracking, we should first check to see if there are other enemies already within our radius that we should track. Preferably we should track an enemy that isn't on the verge of leaving the tower, but more towards the middle or outer reaches of our range, as otherwise it will only target enemies just as they are leaving the toucher's range and the tower will only have a chance to shoot enemies for a very short time. Also, it doesn't look as cool or dramatic as a tower switching targets if it only has to adjust a tiny bit. If there are no enemies within the toucher when the current enemy leaves, that means there is no one to track and we should simply just store none. With this added functionality, our toucher can now track an enemy as it enters, and when it leaves it can either pick an enemy within itself or target no one until a new enemy enters range.

Every time the CRTower_Toucher switches enemies, it should also notify the tower owner of its new enemy. This code listing below allows for all of this to happen:

[csharp]/*************************
CRTower_Toucher

Creation date: 24/11/2010 12:36  
Copyright (c) 2010, Allar  

Forwards touch events to the owning tower  
This way our towers don't have to tick every frame and  
target acquisition only calculates on touch/untouch events.  

*************************/

class CRTower_Toucher extends Actor;

var CRInterface_TowerEnemy ActiveEnemy;
var CRTowerBase OwnerTower;

var CylinderComponent TouchCylinder;

event Touch(Actor Other, PrimitiveComponent OtherComp, vector HitLocation, vector HitNormal)
{
local Actor TestActor;

if (CRInterface_TowerEnemy(Other) == none)  
   return;  

//A new enemy has entered while we already have an active enemy  
//check to see if active enemy is outside our toucher  
if (ActiveEnemy != none && ActiveEnemy != CRInterface_TowerEnemy(Other))  
{  
    //We can't search for CRInterface_TowerEnemy because its an interface  
    //and not a class of Actor, however TouchingActors runs pretty fast  

    foreach TouchingActors(class'Actor',TestActor)  
    {  
        //Our active enemy is still within the toucher, so its still active  
        if (TestActor == ActiveEnemy)  
           return;  
    }  
    //If we get here, our old active enemy is no longer active  
    //so we can go ahead and assign our new enemy  
    ActiveEnemy = CRInterface_TowerEnemy(Other);  
    OwnerTower.TargetEnemy(ActiveEnemy);  
}  
else if (ActiveEnemy == none && CRInterface_TowerEnemy(Other) != none)  
{  
    ActiveEnemy = CRInterface_TowerEnemy(Other);  
    OwnerTower.TargetEnemy(ActiveEnemy);  
}  

}

event UnTouch(Actor Other)
{
local int i;
if (Other ActiveEnemy)
{
//We can't search for CRInterface_TowerEnemy because its an interface
//and not a class of Actor, however TouchingActors runs pretty fast

    for (i = (Touching.length-1); i >= 0; --i)  
    {  
        if (Touching[i] != Other && CRInterface_TowerEnemy(Touching[i]) != none )  
        {  
            ActiveEnemy = CRInterface_TowerEnemy(Touching[i]);  
            OwnerTower.TargetEnemy(ActiveEnemy);  
            return;  
        }  
    }  
    //We found no tower enemies touching us  
    ActiveEnemy = none;  
    OwnerTower.TargetEnemy(none);  
}  

}

defaultproperties
{

bCollideActors=true  

Begin Object Class=CylinderComponent Name=CollisionCylinder  
    CollisionRadius=+0512.000000  
    CollisionHeight=+0060.000000  
    CollideActors=true  
End Object  
TouchCylinder=CollisionCylinder  
CollisionComponent=CollisionCylinder  
Components.Add(CollisionCylinder)  

}[/csharp]

At this point, I decided that having our towers as Pawns was actually quite overkill. The Pawn class has a lot of cool things with it but it also has a large amount of things that will just cause bloat, excess memory use, and lots of irrelevant replication to deal with. Because of this, I decided to refactor CRTowerBase into a direct subclass of Actor. There are a few things that are missing that we need in the base Actor class that Pawn provides, but theres nothing that we can't make for ourselves, such as a SkeletalMeshComponent. Also, we lose functionality of TickSpecial so we must set up our own timer to use when we want to create a temporary ticking solution and a timer interval of 0.16 seconds (60frames per second) works just as well. We need to create a new SkeletalMeshComponent in our default properties block to assign to our new actor's mesh component too, and we can just copy this from our original default properties box. Next we need a way to store and create a CRTowerToucher for our tower to use to detect our crowd agents. This is done fairly simple, we just create a variable called TowerToucher that stores our CRTowerToucher and then we spawn one in our PostBeginPlay. Now when we create a subclass of our tower actor that actually shoots, we should set up a way to only allow certain towers to spawn touchers to save performance, as an unbuilt tower shouldn't have to respond to crowd agents running around it. We are temporarily doing this for now to see if our design system works however. Once its spawned, we must associate the tower with the toucher so we just assign the OwnerTower variable of the toucher with 'self'. We must also create a TargetEnemy function that will take in a CRInterface_TowerEnemy and then store it. The reason why we use a simple function like this instead of directly assigning it in the toucher is that our sub-classed towers might need to perform special behavior when it acquires a new target, so we offer a way to override this behavior in an object oriented manner instead of a per-class basis. Given any practical opportunity to take advantage object oriented programming, you should pounce on it and take a hold of it as this will lead to cleaner and more elegant code that is simpler to work with in the future. I've also included some debug drawing in my new TimerTick function (which replaced TickSpecial) to allow me to visually see which target is tracking temporarily.

[csharp]/*************************
CRTowerBase

Creation date: 20/11/2010 17:29  
Copyright (c) 2010, Allar  

This class will serve as an unconstructed tower  

*************************/

class CRTowerBase extends Actor HideCategories(Camera,Attachment,AI,Debug,Mobile,Physics,Swimming,TeamBeacon,UDKPawn)
implements(CRInterface_Interaction)
placeable;

//Our mesh!
var() SkeletalMeshComponent Mesh;

/** The pawn's light environment */
var DynamicLightEnvironmentComponent LightEnvironment;

//Max distance the player can be to interact with this object
var int InteractRadius;

//Instance of our Tower Material to set 'interactive' look.
var MaterialInstanceConstant TowerMaterial;

//The CRPlayerControllers currently interacting with this object
//Decided to make an array in case of multiplayer support? Iunno.
//Just seems like a good idea at the moment
var array<CRPlayerController> InteractingPCList;

//Actor that gets spawned that relays encroach events
//to the tower so the tower has its own collision intact
//and doesn't have to rely on tick every frame
var() CRTowerToucher TowerToucher;
//The current enemy that is targeted by the tower
var CRInterface
TowerEnemy TargetedEnemy;

//These things should do nothing on damage taking.
event TakeDamage(int Damage, Controller InstigatedBy, vector HitLocation, vector Momentum, class<DamageType> DamageType, optional TraceHitInfo HitInfo, optional Actor DamageCauser)
{
return;
}

simulated function PostBeginPlay()
{
super.PostBeginPlay();
TowerMaterial = Mesh.CreateAndSetMaterialInstanceConstant(0);
TowerToucher = Spawn(class'CRTower_Toucher',self,,,,,true); //skip collision check on spawn
TowerToucher.OwnerTower = self; //associate ourselves with the toucher
}

function EInteractionType GetInteractionType(){ return IT_Tower; }

//Sets our EmissiveStrength in our material instance for the tower to 1 or 0
function ToggleTowerEmissive(bool bEnable)
{
local LinearColor LC;
LC = bEnable ? MakeLinearColor(2,2,2,2) : MakeLinearColor(0,0,0,0);
TowerMaterial.SetVectorParameterValue('EmissiveStrength',LC);
}

//Sets our EmissiveLerp to 1 if we are within InteractRadius, 0 if not
//Inheriting from the master material, 0 lerp is red and 1 lerp is green
simulated function SetEmissiveLerpForPC(CRPlayerController InteractingPC)
{
if (InteractingPC.Pawn none)
return;
if (VSizeSq(InteractingPC.Pawn.Location - Location) < InteractRadius**2)
TowerMaterial.SetScalarParameterValue('EmissiveLerp',1);
else
TowerMaterial.SetScalarParameterValue('EmissiveLerp',0);
}

//Turns on emissive, sets distance color, adds PC to list of
//interacting PCs, and then enables TickSpecial calls
simulated function HintInteraction(CRPlayerController InteractingPC)
{
ToggleTowerEmissive(true);
SetEmissiveLerpForPC(InteractingPC);

if (InteractingPCList.Find(InteractingPC) == -1)  
   InteractingPCList.AddItem(InteractingPC);  

//Update our material if player movies in special tick  
SetTimer(0.016,true,'TimerTick');  

}

//Turns off emissive, removes PC from list, and if no PCs are interacting
//we no longer have a need to call TickSpecial
simulated function UnHintInteraction(CRPlayerController InteractingPC)
{
ToggleTowerEmissive(false);

if (InteractingPCList.Find(InteractingPC) != -1)  
   InteractingPCList.RemoveItem(InteractingPC);  

if (InteractingPCList.Length == 0)  
   ClearTimer('TimerTick');  

}

//This ticks when we should be checking player positions for interaction
event TimerTick( float DeltaTime )
{
local CRPlayerController InteractingPC;

//If for some reason we lost all interactors, lets disable  
//TickSpecial to restrict unneeded tick calls.  
if (InteractingPCList.length == 0)  
{  
    ClearTimer('TimerTick');  
    return;  
}  

foreach InteractingPCList(InteractingPC)  
    SetEmissiveLerpForPC(InteractingPC);  

DrawDebugCylinder(Location-vect(0,0,30),Location+vect(0,0,30),512,12,0,255,255,false);  
DrawDebugBox(Location,vect(512,512,30),255,255,0,false);  
if (TargetedEnemy != none)  
{  
   DrawDebugLine(Location+vect(0,0,64),Actor(TargetedEnemy).Location,0,0,255,false);  
}  

}

//Not sure where I'm going with this yet, but I need it to
//implement our interface.
function Interact(CRPlayerController InteractingPC, byte Mode)
{
}

function TargetEnemy(CRInterface_TowerEnemy NewTarget)
{
TargetedEnemy = NewTarget;
}

defaultproperties
{
bEdShouldSnap=true
InteractRadius = 386.0f

//Tower Icon  
Begin Object class=SpriteComponent Name=Sprite  
    Sprite=Texture2D'Design_Allar.Textures.T_Sprite_TowerIcon_D'  
    HiddenGame=True  
    AlwaysLoadOnClient=False  
    AlwaysLoadOnServer=False  
    Translation=(Z=96)  
End Object  
Components.Add(Sprite);  

Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment  
    bSynthesizeSHLight=TRUE  
    bIsCharacterLightEnvironment=TRUE  
    bUseBooleanEnvironmentShadowing=FALSE  
End Object  
Components.Add(MyLightEnvironment)  
LightEnvironment=MyLightEnvironment  

bCollideActors=true  
bCollideWorld=true  
bBlockActors=true  

//Copied from UTPawn for the most part  
Begin Object Class=SkeletalMeshComponent Name=WPawnSkeletalMeshComponent  
    bCacheAnimSequenceNodes=FALSE  
    AlwaysLoadOnClient=true  
    AlwaysLoadOnServer=true  
    bOwnerNoSee=false  
    CastShadow=true  
    BlockRigidBody=TRUE  
    bUpdateSkelWhenNotRendered=false  
    bIgnoreControllersWhenNotRendered=TRUE  
    bUpdateKinematicBonesFromAnimation=true  
    bCastDynamicShadow=true  
    Translation=(Z=8.0)  
    RBChannel=RBCC_Untitled3  
    RBCollideWithChannels=(Untitled3=true)  
    LightEnvironment=MyLightEnvironment  
    bOverrideAttachmentOwnerVisibility=true  
    bAcceptsDynamicDecals=FALSE  
    AnimTreeTemplate=None  
    bHasPhysicsAssetInstance=true  
    TickGroup=TG_PreAsyncWork  
    MinDistFactorForKinematicUpdate=0.2  
    bChartDistanceFactor=true  
    //bSkipAllUpdateWhenPhysicsAsleep=TRUE  
    RBDominanceGroup=20  
    Scale=1  
    SkeletalMesh=SkeletalMesh'Design_Allar.Mesh.SK_Building_Unconstructed'  
    //PhysicsAsset I made of my tile. Its just one block around the mesh  
    PhysicsAsset=PhysicsAsset'Design_Allar.Physics.SK_Building_Unconstructed_Physics'  
    MotionBlurScale=0.0  
    bAllowAmbientOcclusion=True  
    BlockActors=True  
    BlockNonZeroExtent=True  
    BlockZeroExtent=True  
    CollideActors=True  
    // Nice lighting for hair  
    bUseOnePassLightingOnTranslucency=TRUE  
End Object  
Mesh=WPawnSkeletalMeshComponent  
CollisionComponent=WPawnSkeletalMeshComponent  
Components.Add(WPawnSkeletalMeshComponent)  

}[/csharp]