Sunday 8 May 2011

Creating a custom actor part 4/4

Now that we know how to add various Actor components to a custom actor class, we are going to add functionality to the HealthFountain class making it behave in the following way:

  • When the player is away from it, the actor is idle and emits a number of particles based on the amount of health points it currently holds.

  • When the player enters it, the actor starts regenerating player’s health every second.

  • Each time player’s health is increased, the actor’s amount of health points is decreased. Each time the actor’s health points amount is decreased, fewer particles are emitted. Each time this regeneration process takes place a sound indicating player’s health increase is played to provide feedback.

  • When the player reaches their maximum health or steps away from the actor, the regeneration process is ended and the actor goes back to being idle.

  • When the actor’s health points amount reaches 0 the actor becomes empty and can no longer regenerate player’s health. This will happen unless the level designer decides to make a specific instance of this class a never ending source of health points, so we need to provide the level designer with some form of control to do that.


This fairly simple behaviour can be easily set up in UnrealScript using States, Functions and Events. If you are new to these programming concepts or if you want to see how they are used in UnrealScript I recommend reading this and this UDN documents.

Before we move to discussing states specific to our actor we need to add three more variables to the existing class.


var int MaxHealthPoints;
var() bool bCanGoEmpty;
var SoundCue HealingSound;


The integer variable MaxHealthPoints will store the maximum amount of health points. The Boolean variable will be used to determine if the HealthFountain can run out of health points. As you can see the var() makes it exposed in the editor so that it can be changed for a specific instance of the class if the level designer wishes so. The SoundCue variable will allow us to play a sound indicating healing process. Let’s initialise two of these variables in the DefaultProperties block:


HealingSound=SoundCue'A_Pickups.Health.Cue.A_Pickups_Health_Small_Cue_Modulated'

bCanGoEmpty=true


So by default each HealthFountain will be set to run out of health points and it will play a sound of a UT3 Health Vial pickup when player’s health will increase. We will initialise the MaxHealthPoints later in a different way, outside of DefaultProperties block.

Using state programming allows us to break the overall functionality into logical pieces. So for example in case of our actor we want the actor to behave differently when it is idle, when it is healing the player and when it is empty. We will therefore need three states:

  1. Idle

  2. Healing

  3. Empty


By separating the behaviour into these three states, we can create functions that will only exist within a specific state. For example the healing function should only exist and be executed if the actor is in Healing state. What is more you can have the same function exist in all the states, but actually do something completely different depending on the state the actor is in. Finally you can have states ignore functions and events altogether (there is even an ignores keyword for that). In case of our HealthFountain we will see that no code will be executed when it goes into Empty state.

So let’s add states to our class. Between the instance variables and DefaultProperties add the following code:


auto state Idle
{
}

state Healing
{
}

state Empty
{
}


This is the basic state definition and the keyword auto means that this will be the default state the actor is in when added to the level. Now that we have the states in place, we need to react to things happening to our actor. We can do that by using events. Think of events as special kind of functions happening in the level when the game starts and when various actors interact with each other. In UnrealScript the keyword event has the same meaning as keyword function, but functions with the event keyword can also be called from native code. I’ll be using the keyword event for the actual events that happen in the level during play and the keyword function to declare our own functions. For the purpose of our HealthFountain we will need the following four events:

  • event PostBeginPlay() – this is called when the level is loaded and immediately after play begins.

  • event Touch(Actor Other, PrimitiveComponent OtherComp, vector HitLocation, vector HitNormal) – this is called when an actory is colliding with another actor

  • event UnTouch(Actor Other) – this is called when the touching of two actors ends


Add the PostBeginPlay event code after the declaration of instance variables:


event PostBeginPlay()
{
Super.PostBeginPlay();

MaxHealthPoints = HealthPoints;

ParticleEffect.SetFloatParameter('SpawnRateParameter', Float(HealthPoints) / Float(MaxHealthPoints));
}


As you can figure out from above code, when the level starts and the PostBeginPlay is called, we will call the PostBeginPlay of the parent class, and then we will initialise MaxHealthPoints with the value from HealthPoints. Finally we pass a value into the SpawnRateParameter of the ParticleSystemComponent we created earlier, a float value that comes from dividing current HealthPoint by MaxHealthPoints. It will be 1.0 when the level starts and HealthFountain is still full of health points.

Next let’s look at what happens to the actor when it is in the Idle state. Being in Idle state for our actor means that it waits for collision with the player, and once this happens, it then goes into the Healing state. Instead of regularly checking if a collision with player has occurred we simply use the Touch event in the Idle state definition:


auto state Idle
{
event Touch(Actor Other, PrimitiveComponent OtherComp, vector HitLocation, vector HitNormal)
{
if (UTPawn(Other) != None && UTPawn(Other).Controller.bIsPlayer && UTPawn(Other).Health < UTPawn(Other).SuperHealthMax)
{
GoToState('Healing');
}
}
}


This code will check if the colliding actor is a player (UTPawn and UTPawn.Controller.bIsPlayer). We wouldn’t want the HealthFountain to go into HealingState when colliding with a projectile actor would we? We also need to check if the player’s health is lower than the maximum health (SuperHealthMax for UTPawn which is 199 points). If that is true, then the actor will then be moved into the Healing state by calling the function:


GoToState('Healing');


So now that the actor is in Healing state we finally need to code the function responsible for restoring player’s health. Add the following function to the Healing state:


function Heal()
{
local UTPawn P;

if (HealthPoints > 0)
{
foreach self.TouchingActors(class'UTPawn', P)
{
//heal the pawn only when it's health is lower than it's max
if (P.Health < P.SuperHealthMax)
{
P.Health = Min(P.Health + HealingAmount, P.SuperHealthMax);

PlaySound(HealingSound);

if (bCanGoEmpty)
{
HealthPoints = Max(HealthPoints - HealingAmount, 0);

ParticleEffect.SetFloatParameter('SpawnRateParameter', Float(HealthPoints) / Float(MaxHealthPoints));
}
}
else
{
GoToState('Idle');
}
}
}
else if (HealthPoints == 0)
{
GoToState('Empty');
}
}


In the Heal function we first create a local UTPawn variable called P. We will need this to get access the UTPawn object colliding with our actor (that UTPawn is of course the player). Remember that all local variables need to be declared at the start of the function, otherwise you will get an error when compiling the scripts.

We then check if the HealthFountain has any more health points left (HealthPoints > 0). If no, we need to move it into the Empty state. If yes, we need to find all actors currently colliding with the HealthFountain. We do this by using foreach iteration. What would look like this in C# for example:


foreach (UTPawn P in this.TouchingActors)
{
}


Is written like this in UnrealScript:


foreach self.TouchingActors(class'UTPawn', P)
{
}


This allows us to iterate through all UTPawns currently touching our actor. Once we get the UTPawn touching our actor we need to check if that UTPawn’s Health is not at maxium. If it is, we need to stop regeneration process and move our actor back to Idle state. Otherwise, we heal the UTPawn by adding HealingAmount to its Health (making sure it does not go above the SuperHealthMax). And we play the HealingSound SoundCue. Last thing we need to make sure is that the HealthFountain gets its health points decreased and the SpawnRateParameter is updated in the particle system. We do that of course if we decide that our actor can go empty. That covers the Heal function for our actor.

We also need to add an UnTouch event, which will transition our actor back to Idle state in case the player will step out the HealthFountain radius.


event UnTouch(Actor Other)
{
if (UTPawn(Other) != None && UTPawn(Other).Controller.bIsPlayer)
{
GoToState('Idle');
}
}


Again we need to make sure that it is the player who is the instigator of this UnTouch event. Otherwise the Healing state could end when the player fires a weapon at our actor, which is undesirable.

One last thing left to add to the Healing state is the actual state code. After all we need to somehow call the Heal function when the Healing state begins. State code together with functions makes up the state. Stated code however is not located in any function, it just sits within the state itself and usually starts with the Begin label like this:


Begin:

//call the heal function first time this state is started
Heal();

//call the Heal function every second
SetTimer(1.0, true, 'Heal');


So when Healing state begins, the code after the Begin label will be executed. Heal function will be called once and then a timer will be set to call the Heal function in a loop ever second the actor is in the Healing state.

For the Empty state, we leave it blank as we don’t want any functionality in that state. When in Empty state our actor should act as if it is switched off.

This makes the HealthFountain functionality complete and as originally outlined in part 1. Once again let’s look at the video to get an idea how the actor works when placed in the level.




Below is the entire HealthFountain.uc class code:


class HealthFountain extends Actor
ClassGroup(Custom)
placeable;

/** Health points stored by this instance */
var() int HealthPoints;

var int MaxHealthPoints;

/** How much health points are regenerated */
var() int HealingAmount;

/** Can the health points run out? */
var() bool bCanGoEmpty;

var ParticleSystemComponent ParticleEffect;

var SoundCue HealingSound;


event PostBeginPlay()
{
Super.PostBeginPlay();

MaxHealthPoints = HealthPoints;

ParticleEffect.SetFloatParameter('SpawnRateParameter', Float(HealthPoints) / Float(MaxHealthPoints));
}

auto state Idle
{
event Touch(Actor Other, PrimitiveComponent OtherComp, vector HitLocation, vector HitNormal)
{
if (UTPawn(Other) != None && UTPawn(Other).Controller.bIsPlayer && UTPawn(Other).Health < UTPawn(Other).SuperHealthMax)
{
GoToState('Healing');
}
}
}

state Healing
{
function Heal()
{
local UTPawn P;

if (HealthPoints > 0)
{
foreach self.TouchingActors(class'UTPawn', P)
{
//heal the pawn only when it's health is lower than it's max
if (P.Health < P.SuperHealthMax)
{
P.Health = Min(P.Health + HealingAmount, P.SuperHealthMax);

PlaySound(HealingSound);

if (bCanGoEmpty)
{
HealthPoints = Max(HealthPoints - HealingAmount, 0);

ParticleEffect.SetFloatParameter('SpawnRateParameter', Float(HealthPoints) / Float(MaxHealthPoints));
}
}
else
{
GoToState('Idle');
}
}
}
else if (HealthPoints == 0)
{
GoToState('Empty');
}
}

event UnTouch(Actor Other)
{
if (UTPawn(Other) != None && UTPawn(Other).Controller.bIsPlayer)
{
GoToState('Idle');
}
}

Begin:

//call the heal function first time this state is started
Heal();

//call the Heal function every second
SetTimer(1.0, true, 'Heal');
}

state Empty
{
}


DefaultProperties
{
Begin Object Class=DynamicLightEnvironmentComponent Name=HealthFountainLightEnvironment
bEnabled=true
bDynamic=false
bCastShadows=false
End Object
Components.Add(HealthFountainLightEnvironment)

Begin Object Class=CylinderComponent Name=CollisionCylinder
CollisionRadius=32.0
CollisionHeight=50.000000
CollideActors=true
End Object
Components.Add(CollisionCylinder)

CollisionComponent=CollisionCylinder
bCollideActors=true

Begin Object Class=StaticMeshComponent Name=BaseMesh
StaticMesh=StaticMesh'Pickups.Base_Powerup.Mesh.S_Pickups_Base_Powerup01'
CollideActors=false
Translation=(X=0.0, Y=0.0, Z=-50.0)
CastShadow=false
bCastDynamicShadow=false
bAcceptsLights=true
bForceDirectLightMap=true
LightingChannels=(BSP=true,Dynamic=false,Static=true,CompositeDynamic=true)
LightEnvironment=HealthFountainLightEnvironment
End Object
Components.Add(BaseMesh)

Begin Object Class=ParticleSystemComponent Name=ParticleSystemComponent0
Template=ParticleSystem'HealthFountain.Effects.P_HF_Whirl'
bAutoActivate=true
Translation=(X=0.0, Y=0.0, Z=-35.0)
End Object
ParticleEffect=ParticleSystemComponent0
Components.Add(ParticleSystemComponent0)

Begin Object Class=AudioComponent Name=AmbientSound
SoundCue=SoundCue'A_Gameplay.JumpPad.JumpPad_Ambient01Cue'
bAutoPlay=true
bUseOwnerLocation=true
bShouldRemainActiveIfDropped=true
bStopWhenOwnerDestroyed=true
End Object
Components.Add(AmbientSound)

HealingSound=SoundCue'A_Pickups.Health.Cue.A_Pickups_Health_Small_Cue_Modulated'

HealthPoints=100

HealingAmount=5

bCanGoEmpty=true
}

No comments:

Post a Comment