Creating And Using ScriptedActions
ScriptedActions, and their related ScriptedSequences, are the premier method for getting UT2003 AI to "do stuff". Back in UT, when coders wanted to introduce new behavior into bots or ScriptedPawns, they would subclass whatever AI they were deriving from, and add code there.
In UT2003, a similar action would be to subclass Controller. However, with the addition of ScriptedSequences, there is no longer the need to do this. Developers can just make a new ScriptedAction, and tell pawns to perform this action.
There are two reasons for this article. First is a very simple discussion about how to create subclasses of ScriptedAction. The second is on how to actually use ScriptedActions, from the perspective of the coder.
How Actions Work
Within classes derived from ScriptedController (This includes Bot?s) there is a mechanism to perform actions. Basically, it's a state: state Scripting
What happens is that it's given a ScriptedSequence, in its variable SequenceScript. This ScriptedSequence contains an array of [ScriptedAction?]s. Then, the controller runs through the actions one by one down the line until the last action. After that, if this Controller was artificially created, it destroys itself!
What this means is that: you can give "unpossessed" Pawns (with no controller) orders. They will temporarily don the ScriptedController hat and do the actions. Afterwards, it reverts to the original controller or no controller.
Since the state Scripting
code is pretty self-explanatory, I won't run through that. Suffice to know, it's there and does your dirty work.
Creating ScriptedActions
Anatomy of a ScriptedAction
class ACTION_ChangeName extends ScriptedAction; var(Action) string NewName; var int Count;
All published variables (var()) will be editable by the level designer at design time - in that nifty object properties box.
Basic Functions
function bool InitActionFor(ScriptedController C) { C.PlayerReplicationInfo.PlayerName = NewName; return true; }
InitActionFor is the meat of the Action. This is where the action will usually happen. The passed variable C is, of course, the Controller of the Pawn that you are about to manipulate. From this, you can access its C.Pawn variable if you're interested.
function string GetActionString() { return ActionString; }
GetActionString is, I believe, for logging purposes. It tells the log or whoever wants to know, what this Pawn is doing.
Flow Control
function ProceedToNextAction(ScriptedController C) { C.ActionNum = Max(0,ActionNumber); } function ProceedToSectionEnd(ScriptedController C);
ProceedToNextAction is called after your action has been executed. Therefore, if you want to (evilly) disrupt the flow, you can change this one here. A Controller's ActionNum property tells you which # in the ActionSequence it is currently performing
ProceedToSectionEnd is pre-coded. Call it, and you will be moved to the end of the section.
function bool StartsSection(); function bool EndsSection();
StartsSection signifies if the following Action is actually a section start. "Section" Actions are such like ACTION_IFCONDITION? - they mark off a block of other sections.
EndsSection signifies if the Action is a section end.
function ScriptedSequence GetScript(ScriptedSequence S) { }
GetScript - This is used if a different ScriptedSequence is desired. The passed variable S is the current ScriptedSequence, and you return a new one if you want the Controller to perform a different ScriptedSequence. If you return NONE, then the Controller stops scripting.
Latent Functions
A latent function is one that doesn't happen right away. It has latency. In other words, when you give the action, it doesn't (shazzam) happen right away. For example, if I tell my dog to be in the bathroom, it won't be there right away. If I tell it to jump, it will jump right away. That's the long and short of it.
Latent actions will subclass LatentScriptedAction?. If you are writing latent actions, you must subclass this too!
function bool MoveToGoal(); function Actor GetMoveTargetFor(ScriptedController C); function bool TurnToGoal(); function float GetDistance(); function bool CompleteOnAnim(int Num); function bool CompleteWhenTriggered(); function bool CompleteWhenTimer(); function bool WaitForPlayer(); function bool TickedAction(); function bool StillTicking(ScriptedController C, float DeltaTime);
Latent Actions
GetMoveTargetFor is where you can pass the MoveTarget, or "goal" as it is referred to. The actor you return is the target.
MoveToGoal tells the pawn to move to the goal specified (above). Return of true means that it will move to the goal.
TurnToGoal tells the pawn to turn towards the goal if you return true. If the Pawn is moving toward the goal, it will automatically turn.
Latent Pauses
GetDistance tells the pawn to wait until a Player gets within this value (you return this value) of the Controller's Pawn
CompleteOnAnim - if you return TRUE, then the sequence will complete when the animation finishes (the Num is the animation channel). CompleteOnAnim is called in the Pawn's "AnimEnd" function within its State Scripting.
CompleteWhenTriggered - TRUE means that the sequence will complete when the controller is triggered
CompleteWhenTimer - TRUE means that the sequence will complete when a player's Timer() is called (that is, you use the SetTimer command
TickedAction tells the Controller whether to process the StillTicking function.
StillTicking acts like the Tick(float DeltaTime) for us. If you return TRUE, then this function (StillTicking) will continue to be called every Tick. A return of FALSE signifies this action is done. It stopps recieving ticks and the action sequence moves on to the next action.
Misc Functions
function SetCurrentAnimationFor(ScriptedController C); function bool PawnPlayBaseAnim(ScriptedController C, bool bFirstPlay);
These two are hard-coded into ScriptedController - they are special functions. They allow you to modify whatever animation the Pawn is playing
Default Properties
var localized string ActionString; var bool bValidForTrigger;
ActionString, as talked about above, is a very short description of what the action does
bValidForTrigger signals whether this action is appropriate for use with a ScriptedTrigger (discussed briefly below)
Summary
Hopefully, with these explanations you are well on your way to writing new Actions and extending the capabilities of the AI.
Using ScriptedActions and ScriptedSequences
Writing Scripted Sequences
Typically, ScriptedActions aren't used by themselves, but used in a chain of actions called a ScriptedSequence. Since it's just an array, I won't go into the details of this. It'll suffice to give you an example.
class OPNavPoint extends UnrealScriptedSequence placeable; function FreeScript() { Destroy(); // When this sequence is completed, destroy it. } defaultproperties { EnemyAcquisitionScriptProbability=+1.0 bRoamingScript = false Priority = 200 bCollideWhenPlacing=false bStatic=false bNoDelete=false bFreeLance = false Begin Object Class=Action_MOVETOPOINT Name=OPACTIONMoveToLocation End Object Begin Object Class=Action_CROUCH Name=OPACTIONCrouch End Object Begin Object Class=Action_WAITFORTIMER Name=OPACTIONWaitShort PauseTime = 3.0 End Object Begin Object Class=Action_MOVETOPLAYER Name=OPACTIONMoveToPlayer End Object Begin Object Class=Action_SETALERTNESS Name=OPACTIONMakeAlert Alertness = ALERTNESS_LeaveScriptForCombat End Object Begin Object Class=Action_RUN Name=OPACTIONRun End Object Actions(0)=ScriptedAction'OPACTIONRun' Actions(1)=ScriptedAction'OPACTIONMoveToLocation' Actions(2)=ScriptedAction'OPACTIONCrouch' }
First, you see, you have to create the Action objects. You can set their variables if you wish. Then, you simply add new ScriptedActions to the array. This is much like adding buttons to a menu, if you're familiar with that. I've defined more objects than actions, for future references in classes that are subclasses of OPNavPoint. This way, I don't have to re-define everything.
What this OPNavPoint does is this. It makes the pawn start running to where the OPNavPoint is located, and then makes it crouch. Simple, and fun!
Using Scripted Sequences
To use the above script, I would have to put it in the position I want my pawn to go. Here is an example function, called within a subclass of PlayerController, that creats this order, and tells the pawn to execute it.
function DoMoveOut(Bot b, Vector MyLocation) { local OPNavPoint opnp; MyLocation = Somewhere; // something like this opnp = Spawn(class'OPNavPoint',self,,MyLocation); // self is the commanding officer, in this case OPBot(b).ReceiveDestination(self,opnp); }
What's ReceiveDestination? It's a small function within OPBot, my subclass of Bot? that tells pawns what to do.
function RecieveDestination(Controller c, UnrealScriptedSequence s) { if(s == none) // no script, no action return; s.CurrentUser = self; // Me, I'm the current user of the script. StopCurrentScript(); // Stop doing whatever i'm doing. GoalScript = s; // Assing the Script to the "GoalScript" property SetNewScript(s); // Tell myself that I have new orders SendMessage(c.PlayerReplicationInfo, 'ACK', 0, 5, 'TEAM'); // Tell the person who sent me the order that I heard them Level.Game.Broadcast(self, "Order Recieved.", 'TeamSay'); // Same if(c.IsA('PlayerController')) MyPlayerController = PlayerController(c); // The person who sent me the order is MyPlayerController (for certain Action's, and debugging) GoToState('Scripting'); // Now, go script! }
There's a bit of extra code up there, but you can get the general picture. As you can see, it's not to hard to give the bot orders!
Broken?
Finally, there's one more thing I must mention. Occasionally, scripts "break". There could be many reasons for this, but basically, it stops working. This is quite annoying for after a script is broken, Pawn behavior is very erratic and strange. The game might crash. I leave this fragment to you:
// Broken scripted sequence - for debugging State Broken { Begin: MyPlayerController.Pawn.ClientMessage(Pawn$" Scripted Sequence BROKEN "$SequenceScript$" ACTION "$CurrentAction); WanderOrCamp(true); }
This will tell you if your pawn's script broke, and make sure that the pawn doesn't actually go haywire. It tells the pawn to wander around. Thus, you know which action is causing the break-age, and without a VERY annoying crash.
One last thing to think about
Finally, some things will compete with you when giving orders to pawns. That would be TeamAI and SquadAI. For example, if I told a pawn to move past a ball in a regular bombing run game, it would go for the ball, ignore all subsequent orders of mine, and score on the opposing team.
The easy way to solve this problem would be to subclass these two AI classes and modify their code. It's just a consideration for you.
ScriptedTrigger
ScriptedTrigger and ScriptedTriggerController? are two more classes that help you out. ScriptedTriggerController is able to perform scripted events without a Pawn. In other words, if you have certain actions like ACTION_SpawnActor? that do not need a pawn to perform, you would use a ScriptedTrigger (which incidentally subclasses ScriptedSequence) to perform these. You'll notice, if you examine UT's source code that actions that require a Pawn to perform are generally bValidForTrigger=false and cannot be used with ScriptedTriggers.
And, if you look at ScriptedTrigger, it spawns a ScriptedTriggerController, which needs and has no Pawn attached. The funny thing is, I think ScriptedTrigger is a misnomer, as this class has nothing to do with Triggers. You can of course add an action like the Action_WaitForEvent? that causes your ScriptedTrigger to activate upon being triggered. However, it doesn't seem to be directly linked to triggers.
Conclusion
Okay! Hopefully, with this, we are on the way to cool new things. Some possibilities for using this code:
- New AI abilities (like finding cover)
- A method for ordering bots around (You! Go Here!)
- Movies - dynamic ingame movies dictated by code and not pre-made by level designers
Comments
Soldat: todo: talk about the default properties. I forgot to do that. also, I have example code if anyone's interested, I coded bot orders and such.
Soldat: okay, done all that. added scripted trigger stuff too, which i did not notice earlier.
Tarquin: Nice article! I actually feel I understand this whole scripted stuff!