Wikis / Unreal Wiki / Legacy:Basic AI Scripting Tutorial

This is a tutorial on UScripting a custom AI.

This page is under construction.

Overview

This tutorial will detail re-construction of the NaliCow NPC for UT2004. It was originally available in UT as a Cow, but this tutorial will revise its AI design as if it were new. The stock meshes and sounds that are available will allow us to concentrate purely on the AI that drives its behavior. We will start will a comprehensive behavior design before coding the elements and constructing the custom classes that will give this NPC life and then implement it in the game environment.

The NaliCow and BabyCow from Unreal Tournament.

Prerequisites

Although creating only a simple AI for this NPC, this will be an advanced tutorial that assumes a solid understanding of UnrealScript. This tutorial will assume you know how to Create A Subclass, Set up package folders, Launch a local netgame and Add EditPackage. In terms of Making Mods, this tutorial will assume that you know about Replication, Coding Guidelines and Compiling with UCC.

Even if you aren't confident in your understanding of these many topics, this tutorial will still serve as an overview discussion of designing a simple Artificial Intelligence construct.

Behavior Design

We will begin by defining a design for the NaliCow's behavior.

AI Design Plan

The behavior design is the roadmap for our AI scripting, giving us a clear set of coding elements to complete before it's AI takes shape.

Although there are many different methods of designing AI (including sophisticated methods such as neural networks and artificial evolution), with a simple creature like this, our tutorial will follow the most basic and standard method used by all stock Unreal AI; conditional decision-making. This is the method that takes stimuli as a constant condition or trigger to either do this or that; also known as "if this, then do that", or "if-then" decision-making. A common example of this kind of AI is the AI Script in a ScriptedSequence, but even Bots use this method, just in an extremely complex form.

We have some features of our NPC already defined for us; it's model, textures, animation and sounds. Normally, a custom character would be started by defining it's purpose or goal and then design those features around it. But for this behavior design, we will work "backwards"; starting with those features and finding the purpose and behavior design that fits it.

With that in mind, we will first look at what we have to work with.

Provided Animation and Sounds

There are plenty of stock animations provided in UT2004 for the NaliCow even though there is no Monster subclass for it. With these animation sequences, we can easily construct an AI design flow that will convey a creature of convincing realism and range of ability. The following is a list of the NaliCow animations found the Mesh Browser. The number of animation frames in the sequence are shown in brackets. Loop indicates the last frame matches a common start pose of all other Loop animations. SingleFrame is just one frame, a pose. Sequence indicates an animation sequence that is meant to only play through once and does not end on the common starting pose of the Loop animations.

  • root [20] : Loop. Bends down to graze.
  • Breath [6] : Loop. Breathes. Prone.
  • Chew [7] : Loop. Chews.
  • Poop [20] : Loop. Lifts tail and poops.
  • Shake [18] : Loop. Shakes head, as if shaking off water or dust.
  • Swish [20] : Loop. Swishes it's tail from side to side and stamps foot.
  • Walk [15] : Loop. A walk cycle.
  • Run [10] : Loop. A run cycle.
  • Landed [1] : Single frame. Indicates recovery from a fall.
  • TakeHit1 [1] : Single frame. Indicates recovery from damage.
  • TakeHit2 [1] : Single frame. Indicates recovery from more damage.
  • BigHit [1] : Single frame. Indicates recovery from a lot of damage. (both feet in the air)
  • Dead [23] : Sequence. Indicates a slow death, slumping to the side.
  • Dead2 [13] : Sequence. Death animation, starting from TakeHit2, slumping forward.
  • Dead3 [23] : Sequence. Severe death, starting from BigHit, showing a complete back flip.

These stock sounds are available in the Sound Browser, under SkaarjPack_rc, that relate to the NaliCow:

  • ambCow : Breath of a NaliCow.
  • cMoo1c : NaliCow call. (sounds suspiciously like a Doom creature's death cry)
  • cMoo2c : Another call.
  • DeathC1c : "Urgent" call.
  • DeathC2c : Another "urgent" call.
  • injurC1c : Pained call.
  • injurC2c : A call that sounds a little like, "Ow".
  • munch1p : A biting sound (normally used for the SkaarjPupae).
  • shakeC : Ears flapping as the NaliCow shakes.
  • swishC : Swishing sound of the NaliCow tail.
  • Thump : Generic hard landing sound.
  • WalkC : NaliCow footstep.

AI Design Flow

With these pieces, we can begin to analyze the potential behavior and assemble the design in such a way that we understand how one piece leads to the next.

  • Some of these pieces indicate behavior that's initiated by the NaliCow; such as Grazing, Swishing, Pooping. Others are behaviors that are in reaction to external stimuli; such as TakeHit or Death.
  • We know we will want the NaliCow to navigate from place to place. Some decision making will involve where to go and whether to walk or run. Running away from danger will serve a self-preservation purpose while walking toward a goal, such as another NaliCow, may serve to support the idea these are real creatures (an immersion purpose) through a percieved flocking behavior.
  • Obvious connections can be made between the animation sequences and the sounds that correspond to them. We will want to have these sounds play whenever those animations are played. Other connections can be made between some of these pieces, where one will likely lead to another; such as Grazing leading to Chewing, a TakeHit leading to either Death or Run, etc.
  • One piece of this behavior, Pooping, appears to need an extra effect; the NaliCowPoop itself. It will have to be an extra actor class with it's own code.

We can see a prone state, where the Breath animation and ambCow sound would be looped. This appears to be a good place to start. We will be able to branch out from this prone state to any other looping animation sequence and corresponding sound, building up the behavior design flow, piece by piece.

The following chart attempts to describe all the possible behaviors, their animation, their sound and what behaviors might preceed and follow them.

NaliCow Behaviors

1. Prone 2. Graze 3. Chew 4. Poop 5. Shake 6. Swish 7. Walk 8. Run 9. Land 10. TakeHit 11. Dying
Animation Breath root Chew Poop Shake Swish Walk Run Landed TakeHit1, 2 or BigHit Dead, 2, 3
Sound ambCow munch1p munch1p cMoo1c, 2c shakeC swishC WalkC WalkC Thump injurC1c, 2c DeathC1c, 2c
Lead From 1-7 1-7 2 1-7 1-7 1-7 1-10 7-10 7 or 8** 1-9*** 10
Lead To 1-7 3 1-7 1-7* 1-7 1-7 1-8 7-8 1, 5, 7 7, 8, 11 None
* – A NaliCowPoop actor should be spawned during this behavior.
** – This behavior will activate immeditately upon landing from a fall, without waiting for animation loops to end..
*** – This behavior will activate immeditately upon taking damage, without waiting for animation loops to end.
† – The NaliCow carcass will DeRes and disappear a short time after this behavior.

Coding Elements

With the AI Design Flow sketched out, we will be able to code the AI behaviors appropriately. But first, we will have to complete the code that will handle the basic tasks these behaviors will have to perform. These tasks include animation, sound, movement, damage, death, etc.

Animation & Sound

As shown in the above chart, animation sequences and sounds are linked together. A couple exceptions to that rule include animations that can play one of a few available sounds and actions that play one animation, but more than one sound, such as Walk or Run, where two footsteps should be heard. We will use the animation sequence as a handle for the action and have that dictate the possible sounds.

// DisplayBehavior() takes a BehaviorIndex value and sets the appropriate animation and sound.
// Degrees of behavior will be taken into account via an optional arguement.
function DisplayBehavior( int BehaviorIndex, optional int Degree )
{
    switch( BehaviorIndex ) {
        case(1) :
            Acting( 'Breath', sound'SkaarjPack_rc.ambCow', true ); break;
        case(2) : 
            Acting( 'root', sound'SkaarjPack_rc.munch1p', true ); break;
        case(3) : 
            Acting( 'Chew', sound'SkaarjPack_rc.munch1p', true ); break;
        case(4) : 
            if ( FRand() > 0.5 )
                Acting( 'Poop', sound'SkaarjPack_rc.cMoo1c', false );
            else
                Acting( 'Poop', sound'SkaarjPack_rc.cMoo2c', false );
            break;
        case(5) : 
            Acting( 'Shake', sound'SkaarjPack_rc.shakeC', true ); break;
        case(6) : 
            Acting( 'Swish', sound'SkaarjPack_rc.swishC', true ); break;
        case(7) : 
            Acting( 'Walk', sound'SkaarjPack_rc.WalkC', true ); break;
        case(8) : 
            Acting( 'Run', sound'SkaarjPack_rc.WalkC', true ); break;
        case(9) : 
            Acting( 'Landed', sound'SkaarjPack_rc.Thump', false ); break;
        case(10) : 
            if ( Degree < 2 )
                Acting( 'TakeHit', sound'SkaarjPack_rc.cMoo1c', false );
            else if ( Degree == 2 )
                Acting( 'TakeHit2', sound'SkaarjPack_rc.injurC1c', false );
            else if ( Degree == 3 )
                Acting( 'BigHit', sound'SkaarjPack_rc.injurC2c', false );
            break;
        case(11) : 
            if ( Degree < 2 )
                Acting( 'Dead', sound'SkaarjPack_rc.cMoo2c', false );
            else if ( Degree == 2 )
                Acting( 'Dead2', sound'SkaarjPack_rc.DeathC1c', false );
            else if ( Degree == 3 )
                Acting( 'Dead3', sound'SkaarjPack_rc.DeathC2c', false );
            break;
        default : 
            Acting( 'Breath', sound'SkaarjPack_rc.ambCow', true ); break;
     }
}
 
// Acting() loops the given animation sequence name and plays the given sound.
// For Replication purposes, animation is normally looped.  Sequences may be interupted each AnimEnd().
function Acting( name Anim, sound Soundname, bool bLoop )
{
    if ( bLoop )
        LoopAnim( Anim, 1.0, 0.1 );
    else
        PlayAnim( Anim, 1.0, 0.1 );
    SimAnim.AnimSequence = Anim;
    SimAnim.AnimRate = 1.0;
    if ( bLoop )
        SimAnim.bAnimLoop = true;
    else
        SimAnim.bAnimLoop = false;
	SimAnim.TweenRate = 0.1;
    PlaySound( Soundname, SLOT_Misc );
}

Navigation

This is our first decision-making code. We will need our AI to decide where to go and what to look at. Controllers use MoveTarget and ViewTarget by default. They are the Destination and Focus for the controller. We will simply alter those properties based on desires of self-preservation.

First, if we have reached our destination, we want to be able to select another one reasonably. Next, if another Actor has hurt our NPC, we want our AI to avoid it. So we will do checks to make sure the selected destination or focus is not the Actor that hurt our NPC. We will also avoid any Actor who Bumps or Touches our NPC, as if it had hurt it. If the NPC is Bumped, Touched or Triggered, we will have our NPC Walk away, or if already Walking, perhaps begin to Run. Finally, we want to know if our NPC is stuck, trying to reach a destination but not able to progress from it's current Location. We will do a simple check to ensure it makes progress when it Walks or Runs, and if not, we will select another destination.

var		Actor		HurtingActor;	// An Actor to get away from.
var		vector		OldLocation;	// Used to determine if stuck.
 
// PostBeginPlay() is originally defined in Actor().
// We will use it to make sure an AIController is spawned for our NaliCow.
event PostBeginPlay()
{
	Super.PostBeginPlay();
 
	if ( ( ControllerClass != None ) && ( Controller == None ) )
		Controller = spawn( ControllerClass );
	if ( Controller != None )
		Controller.Possess( self );
}
 
// ReachedDestination() is originally defined in Pawn.
// MoveTarget and other relevant properties are defined in Controller.
function bool ReachedDestination( Actor OldTarget )
{
    local   Actor   NewTarget, A;
    local   NavigationPoint    NP;
 
    forEach VisibleActors( class'Actor', A )
        if ( A != OldTarget && A != HurtingActor )
            NewTarget = A;
 
    if ( A == None )
        forEach RadiusActors( class'NavigationPoint', NP, 1024 )
            if ( FRand() > 0.5 && NP != OldTarget && FastTrace( NP.Location ) )
            {
                A = NP;
                NewTarget = A;
            }
 
    if ( A == None )
        forEach AllActors( class'Actor', A )
            if ( FRand() > 0.5 && A != HurtingActor && A != OldTarget && A != self && !A.IsA('Controller') )
                NewTarget = A;
 
    if ( NewTarget != None )
    {
        Controller.MoveTarget = NewTarget;
        Controller.Destination = NewTarget.Location;
        Controller.FocalPoint = NewTarget.Location;
        Controller.Focus = NewTarget;
    }
    else
    {
        if ( HurtingActor != None )
        {
            Controller.Destination = ( Location - HurtingActor.Location );
            Controller.FocalPoint = Controller.Destination;
        }
        else
        {
            Controller.Destination = vector( Rotation ) * ( 512 * FRand() + 512 );
            Controller.FocalPoint = Controller.Destination;
        }
    }
}
 
// Trigger() is originally defined in Actor.
function Trigger( Actor Other, Pawn EventInstigator )
{
    if ( self.IsInState('Run') )
        return;
	if ( self.IsInState('Walk') )
	{
		GotoState('Run');
		return;
	}
	GotoState('Walk');
}
 
// Touch() is originally defined in Actor.
function Touch( Actor Other )
{
    HurtingActor = Other;
    if ( self.IsInState('Run') )
        return;
	GotoState('Walk');
}
 
// Bump() is originally defined in Actor.
function Bump( Actor Other )
{
    HurtingActor = Other;
    if ( self.IsInState('Run') && ( FRand() < 0.9 ) )  // Chance to calm.
        return;
	if ( self.IsInState('Walk') && ( FRand() > 0.9 ) )  // Change due to startle.
	{
		GotoState('Run');
		return;
	}
	GotoState('Walk');
}

And then somewhere in during movement, we will perform checks to determine if the character has reached it's destination or if it's gotten stuck while traveling.

Movement

We need our NPC NaliCow to travel to destinations. It's default Physics will be PHYS_Walking. So unless we find that the character should be falling, we're just going to concern ourselves with ground movement. Our checks determining if the NPCs destination is reached or if the NPC is stuck will also happen during movement. We also want the ability to halt all movement by defining a zero SpeedRatio, so that our character does not slide around while showing an animation with it's feet firmly planted. At this point, we will also introduce another property bStartled that will indicate the NaliCow has been recently hurt or otherwise made to react. We will take advantage of the fact that movement will be checked often and use it to make a few decisions about bStartle, either to react to it by Running or to calm back down. bStartled will be set to true during damage.

var		bool		bStartled;		// Recently startled.
 
// MoveCheck() deals with periodic checks for bStartled, DamageTaken and falling.
// Then moves the character in direction of Rotation and at speed of SpeedRatio.
function MoveCheck( float SpeedRatio )
{
	DamageTaken = 0;  // Reset most recent damage memory.
 
	if ( Base == None )  // Should be falling.
	{
		if ( Physics != PHYS_Falling )
			SetPhysics( PHYS_Falling );
		return;
	}
 
	if ( Controller.MoveTarget == None )
		ReachedDestination( self );
 
    if ( VSize( Controller.MoveTarget.Location - Location ) < 64 )
   	    ReachedDestination( Controller.MoveTarget );
 
    if ( VSize( OldLocation - Location ) < 48 )
   	    ReachedDestination( Controller.MoveTarget );
 
    OldLocation = Location;
 
    RotationRate.Yaw = SpeedRatio * 20000;
   	Velocity = ( SpeedRatio * GroundSpeed ) * vector( Rotation );
    Acceleration = Velocity;
 
	if ( FRand() > 0.95 )  // Chance to calm down.
		bStartled = false;
	if ( bStartled && !self.IsInState('Run') )
		GotoState('Run');  // React to startle.	
}

During Walk and Run behaviors, we will want to update the character's movement, based on any changes to Rotation. The following code uses a Tick() function to constantly update the movement. We only need to do this during certain behaviors, so we will only use this code within a state designated for either Walk or Run. Normally, SetTimer() and Timer() functions set to activate every tenth of a second or so are used in place of Tick(), but this code is not particularly expensive.

    function Tick( float DeltaTime )
    {
        if ( Physics == PHYS_Walking && DamageTaken == 0 )
        {
            Velocity = ( 0.5 * GroundSpeed ) * vector( Rotation );
            Acceleration = Velocity;
        }
    }

Damage

When our NPC gets hurt, we want to display specific behavior and effects. The effects needed here are blood puffs that emit from the character at the appropriate angle to which it was hurt. The behavior will be similar to those in Navigation, as the Instigator of the damage will automatically become the NPCs HurtingActor, the Actor to avoid. Any damage will cause our NPC to react by Running, unless it recieves enough damage to bring the Health down to zero. In that case, our NPC will switch to a Dying state. A new property, DamageTaken, is introduced to keep track of the most recent damage inflicted on our NaliCow. This will be used to determine the Degree to specify for our TakeHit and Dying behavior displays and it will serve to remind the NaliCow to Run away from danger.

var		int			DamageTaken;	// Most recent damage amount.
 
// TakeDamage() is originally defined in Actor.
function TakeDamage(int Damage, Pawn InstigatedBy, Vector HitLocation, Vector Momentum, class<DamageType> DamageType)
{
    HurtingActor = InstigatedBy;
	DamageTaken = Damage;
	bStartled = true;
 
    Super.TakeDamage( Damage, InstigatedBy, HitLocation, Momentum, DamageType );
 
    if ( DamageTaken > 3 )
        spawn( class'STR_green_blood_puff',,, HitLocation, Rotator( Momentum ) );
 
    if ( Controller != None && ( Controller.MoveTarget == HurtingActor || Controller.Focus == HurtingActor ) )
		ReachedDestination( HurtingActor );  // Do not approach HurtingActor.
 
    if ( Health > 0 )
        GotoState('TakeHit');
    else if ( !self.IsInState('Dying') )
        GotoState('Dying');
}

Death

When our NPC dies, it will be switched to a Dying state. Switching to a unique state will allow us to Ignore various functions, like Bump or Touch, which would cause further behavior. At the beginning of this state, our NPC should stop all animation. After displaying the appropriate Dying behavior, it should stay still for a short time before removing itself from the gameworld for the sake of garbage collection. The typical garbage collection method in Unreal involves a simple check for any player currently looking at the Actor, and if no player is, delete it. If a player happens to be watching our character at this time, we will DeRez it's Mesh, a typical function where the Skin is changed to a gently flashing texture before the Actor is deleted. It visually lets players know this Actor is about to be removed. On Clients, this function will be called by a TornOff() event, which signals the Client has been given control of this actor. We will use bTearOff to signal to Clients that this NaliCow is dead and should be removed. If not explicitly removed via Destroy(), this NPC will be automatically destroyed after a set time defined with Lifespan.

The following code shows the Dying behavior state.

state Dying
{
    Ignores Trigger, Touch, Bump, Landed, Died;
 
    function Timer()
    {
        SetPhysics( PHYS_None );
        SetCollision( false, false );
        Lifespan = 3.0;
        if ( PlayerCanSeeMe() )
            DeRez();
        else
            Destroy();
    }
 
    event AnimEnd( int Channel )
    {
        SetTimer( 4.0, false );
    }
 
    Begin:
        StopAnimating();
        DisplayBehavior( 11, Min( ( DamageTaken / 20 ), 3 ) );
        MoveCheck( 0.0 );
        if ( Level.NetMode == NM_DedicatedServer )
        {
            SetPhysics( PHYS_None );
            SetCollision( false, false );
            LifeSpan = 3.0;
            bTearOff = true;
        }
}

Our DeRez function is made available on Clients and can be called from a TornOff() event.

// TornOff() is originally defined in Actor.
event TornOff()
{
    if ( PlayerCanSeeMe() )
        DeRez();
    else
        Destroy();
}
 
// DeRez() will signal removal of the dead NaliCow.
simulated function DeRez()
{
    if ( Level.NetMode == NM_DedicatedServer )
        return;
 
    Skins.length = 0;
    Skins[0] = Material'DeRez.Shaders.DeRezFinalBody';  
}

Sometimes Pawns are asked to die automatically. The following function will catch any "automatic death" event, such as being crushed by a Mover or Telefragged.

// Died() is originally defined in Pawn.
function Died( Controller Killer, class<DamageType> DamageType, vector HitLocation )
{
	TakeDamage( Health, None, Location, vect(0,0,-1), class'Crushed' );
}

Effects

Besides the blood puffs needed to indicate Damage, the only effect this NPC needs is it's poop. When it performs its Poop behavior, we will want a separate Actor, a Projectile, to spawn at the appropriate place and drop to the ground.

We will use this line during the Poop behavior to spawn the NaliCowPoop in the appropriate location and orientation; towards the back of the character and pointing down.

	spawn( class'NaliCowPoop',, 'Poop', ( Location + ( vector(Rotation) * -24 ) ), rotator( vect(0,0,-1) ) );

Class Construction

Our NPC mostly uses functions and methods already found in the Pawn class. Because this character is not meant to respawn, pickup Adrenaline, carry Inventory, etc., a simple Pawn subclass can be used without a custom Controller. The AI can be coded into the custom Pawn class, NaliCow, and we can use the default AIController.

For the effects, the NaliCowPoop, we will use make a custom subclass from the Projectile class, BioGlob.

NaliCow

With our code elements defined, we can simply begin to piece them together within a Pawn subclass called NaliCow. Along the way, we will make decisions about any further behaviors needed, such as a simple Turn behavior, where the Walk animation is played but the character only spins in place, or a redefinition of the Landed() event to make sure the character reacts appropriately if bStartled. We will also make decisions about the frequency and chance that any given behavior be initiated from any other. Most of the time, a simple random chance filtered through a switch statement will suffice.

A state will be defined for each behavior, allowing us to completely control the character, including it's movement and further behavior.

Putting together this NPC character requires the use of properties and methods, some will be custom, but most of which have been defined in the Pawn, Actor or Controller classes.

Pawn Default Properties

(Advanced) float Lifespan 
Pawn default is 0. We keep it there by default, but will be modifying it in game.
(Collision) int CollisionHeight 
Pawn default is 74. Our default will be 34.
(Collision) SurfaceType 
Pawn default is EST_Default. Our default will be EST_Flesh.
(Display) Mesh Mesh 
Our default is VertMesh'SkaarjPack_rc.NaliCow'.
(Movement) Physics 
Pawn default is PHYS_None. Our default will be PHYS_Walking.
(Movement) rotator RotationRate 
Pawn default RotationRate.Yaw is 20000. We keep it there by default, but will be modifying it in game.
(Movement) vector Velocity, Acceleration 
These two properties are used during Movement.
(None) Controller Controller 
By default Pawns automatically spawn a Controller of Pawn.ControllerClass during PreBeginPlay(). This will be our handle to the Controller.
(None) class<Controller> ControllerClass 
Pawn default value is AIController. We keep it there by default.
(None) int GroundSpeed 
Pawn default value is 440. Our default value will be 100.
(None) int Health 
Pawn default value is 100. We keep it there by default, but will be modifying it in game.
(None) bool bReplicateAnimations 
Pawn default is false. Our default value will be true. For use with SimAnim struct.
(None) SimAnims 
This struct is used for Animation Replication. Includes AnimSequence, bAnimLoop, etc.
(None) bool bTearOff 
This property is used to make the Client the owner of the actor, making it "TornOff" and no longer replicated to new Clients. We will use this during Death only.
(Sound) bool bFullVolume 
Pawn default is false. Our default value will be true.
(Sound) int SoundRadius 
Pawn default is 160. Our default value will be 512.

Pawn Default Methods

bool PlayAnim( name Sequence, optional float Rate, optional float TweenTime, optional int Channel ) 
This function is originally defined in Actor. It will allow an Anim Sequence to play.
bool LoopAnim( name Sequence, optional float Rate, optional float TweenTime, optional int Channel ) 
This function is originally defined in Actor. It will allow an Anim Sequence to loop.
StopAnimating() 
This function is originally defined in Actor.
PlaySound( Sound Sound, optional ESoundSlot Slot, optional float Volume, optional bool bNoOverride, optional float Radius, optional float Pitch, optional bool Attenuate ) 
This function is originally defined in Actor. It will allow a Sound to play.
SetPhysics( EPhysics newPhysics ) 
This function is originally defined in Actor. We will use it when in a Dying state.
SetCollision( optional bool NewColActors, optional bool NewBlockActors, optional bool NewBlockPlayers ) 
This function is originally defined in Actor. We will use it when in a Dying state.
bool PlayerCanSeeMe() 
This function is originally defined in Actor. It returns true when a player has a line of sight to this Actor. We will use it when in a Dying state.
AnimEnd( int Channel ) 
This event is originally defined in Actor. It is called when the current animation sequence has played through. This will allow us to switch from action to action at an appropriate time.
Landed( vector HitNormal ) 
This event is originally defined in Actor. It is called when the Actor has landed on a Base after falling.
TornOff() 
This event is a signal that this actor is no longer being replicated to new Clients and is now set to Role == ROLE_Authority on the Clients. We will use this for removal of a dead NaliCow on Clients.

AIController Properties

(None) Actor MoveTarget 
Originally defined in Controller, this is the Actor to move toward.
(None) vector Destination 
Originally defined in Controller, this is the vector (Location) to move toward. We will use this to set desired location.
(None) vector FocalPoint 
Originally defined in Controller, this is the vector to look toward. We will use this to set desired rotation.
(None) Actor Focus
Originally defined in Controller, this is the Actor to look toward.

Final Source Code

The following is the AI Source Code for a NaliCow NPC, using all the available animations and sounds to convey believable behavior.

//=============================================================================
// NaliCow.
// NaliCow Non-Player Character for UT2004.
// by SuperApe -- Dec 2005
// ( Based on Mesh animations and sounds from Epic Games )
//=============================================================================
class NaliCow extends Pawn
    placeable;
 
var     Actor       HurtingActor;   // An Actor to get away from.
var     vector      OldLocation;    // Used to determine if stuck.
var		int			DamageTaken;	// Most recent damage amount.
var		bool		bStartled;		// Recently startled.
 
// PostBeginPlay() is originally defined in Actor().
// We will use it to make sure an AIController is spawned for our NaliCow.
event PostBeginPlay()
{
	Super.PostBeginPlay();
 
	if ( ( ControllerClass != None ) && ( Controller == None ) )
		Controller = spawn( ControllerClass );
	if ( Controller != None )
		Controller.Possess( self );
}
 
// DisplayBehavior() takes a BehaviorIndex value and sets the appropriate animation and sound.
// Degrees of behavior will be taken into account via an optional arguement.
function DisplayBehavior( int BehaviorIndex, optional int Degree )
{
    switch( BehaviorIndex ) {
        case(1) :
            Acting( 'Breath', sound'SkaarjPack_rc.ambCow', true ); break;
        case(2) : 
            Acting( 'root', sound'SkaarjPack_rc.munch1p', true ); break;
        case(3) : 
            Acting( 'Chew', sound'SkaarjPack_rc.munch1p', true ); break;
        case(4) : 
            if ( FRand() > 0.5 )
                Acting( 'Poop', sound'SkaarjPack_rc.cMoo1c', false );
            else
                Acting( 'Poop', sound'SkaarjPack_rc.cMoo2c', false );
            break;
        case(5) : 
            Acting( 'Shake', sound'SkaarjPack_rc.shakeC', true ); break;
        case(6) : 
            Acting( 'Swish', sound'SkaarjPack_rc.swishC', true ); break;
        case(7) : 
            Acting( 'Walk', sound'SkaarjPack_rc.WalkC', true ); break;
        case(8) : 
            Acting( 'Run', sound'SkaarjPack_rc.WalkC', true ); break;
        case(9) : 
            Acting( 'Landed', sound'SkaarjPack_rc.Thump', false ); break;
        case(10) : 
            if ( Degree < 2 )
                Acting( 'TakeHit', sound'SkaarjPack_rc.cMoo1c', false );
            else if ( Degree == 2 )
                Acting( 'TakeHit2', sound'SkaarjPack_rc.injurC1c', false );
            else if ( Degree == 3 )
                Acting( 'BigHit', sound'SkaarjPack_rc.injurC2c', false );
            break;
        case(11) : 
            if ( Degree < 2 )
                Acting( 'Dead', sound'SkaarjPack_rc.cMoo2c', false );
            else if ( Degree == 2 )
                Acting( 'Dead2', sound'SkaarjPack_rc.DeathC1c', false );
            else if ( Degree == 3 )
                Acting( 'Dead3', sound'SkaarjPack_rc.DeathC2c', false );
            break;
        default : 
            Acting( 'Breath', sound'SkaarjPack_rc.ambCow', true ); break;
     }
}
 
// Acting() loops the given animation sequence name and plays the given sound.
// For Replication purposes, animation is normally looped.  Sequences may be interupted each AnimEnd().
function Acting( name Anim, sound Soundname, bool bLoop )
{
    if ( bLoop )
        LoopAnim( Anim, 1.0, 0.1 );
    else
		PlayAnim( Anim, 1.0, 0.1 );
    SimAnim.AnimSequence = Anim;
    SimAnim.AnimRate = 1.0;
    if ( bLoop )
        SimAnim.bAnimLoop = true;
    else
        SimAnim.bAnimLoop = false;
    SimAnim.TweenRate = 0.1;
    PlaySound( Soundname, SLOT_Misc );
}
 
// MoveCheck() deals with periodic checks for bStartled, DamageTaken and falling.
// Then moves the character in direction of Rotation and at speed of SpeedRatio.
function MoveCheck( float SpeedRatio )
{
	DamageTaken = 0;  // Reset most recent damage memory.
 
	if ( Base == None )  // Should be falling.
	{
		if ( Physics != PHYS_Falling )
			SetPhysics( PHYS_Falling );
		return;
	}
 
	if ( Controller.MoveTarget == None )
		ReachedDestination( self );
 
    if ( VSize( Controller.MoveTarget.Location - Location ) < 64 )
   	    ReachedDestination( Controller.MoveTarget );
 
    if ( VSize( OldLocation - Location ) < 48 )
   	    ReachedDestination( Controller.MoveTarget );
 
    OldLocation = Location;
 
    RotationRate.Yaw = SpeedRatio * 20000;
   	Velocity = ( SpeedRatio * GroundSpeed ) * vector( Rotation );
    Acceleration = Velocity;
 
	if ( FRand() > 0.95 )  // Chance to calm down.
		bStartled = false;
	if ( bStartled && !self.IsInState('Run') )
		GotoState('Run');  // React to startle.	
}
 
// ReachedDestination() is originally defined in Pawn.
// MoveTarget and other relevant properties are defined in Controller.
function bool ReachedDestination( Actor OldTarget )
{
    local   Actor   NewTarget, A;
    local   NavigationPoint    NP;
 
    forEach VisibleActors( class'Actor', A )
        if ( A != OldTarget && A != HurtingActor )
            NewTarget = A;
 
    if ( A == None )
        forEach RadiusActors( class'NavigationPoint', NP, 1024 )
            if ( FRand() > 0.5 && NP != OldTarget && FastTrace( NP.Location ) )
            {
                A = NP;
                NewTarget = A;
            }
 
    if ( A == None )
        forEach AllActors( class'Actor', A )
            if ( FRand() > 0.5 && A != HurtingActor && A != OldTarget && A != self && !A.IsA('Controller') )
                NewTarget = A;
 
    if ( NewTarget != None )
    {
        Controller.MoveTarget = NewTarget;
        Controller.Destination = NewTarget.Location;
        Controller.FocalPoint = NewTarget.Location;
        Controller.Focus = NewTarget;
    }
    else
    {
        if ( HurtingActor != None )
        {
            Controller.Destination = ( Location - HurtingActor.Location );
            Controller.FocalPoint = Controller.Destination;
        }
        else
        {
            Controller.Destination = vector( Rotation ) * ( 512 * FRand() + 512 );
            Controller.FocalPoint = Controller.Destination;
        }
    }
}
 
// Trigger() is originally defined in Actor.
function Trigger( Actor Other, Pawn EventInstigator )
{
    if ( self.IsInState('Run') )
        return;
	if ( self.IsInState('Walk') )
	{
		GotoState('Run');
		return;
	}
	GotoState('Walk');
}
 
// Touch() is originally defined in Actor.
function Touch( Actor Other )
{
    HurtingActor = Other;
    if ( self.IsInState('Run') )
        return;
	GotoState('Walk');
}
 
// Bump() is originally defined in Actor.
function Bump( Actor Other )
{
    HurtingActor = Other;
    if ( self.IsInState('Run') && ( FRand() < 0.9 ) )  // Chance to calm.
        return;
	if ( self.IsInState('Walk') && ( FRand() > 0.9 ) )  // Change due to startle.
	{
		GotoState('Run');
		return;
	}
	GotoState('Walk');
}
 
// Landed() is originally defined in Actor.
event Landed( vector HitNormal )
{
	Super.Landed( HitNormal );
 
	if ( Velocity.Z > -200 )  // Slow enough fall to forgive landing.
		return;
 
	if ( DamageTaken == 0 )
		GotoState('Land');
	else
		GotoState('TakeHit');
}
 
// TakeDamage() is originally defined in Actor.
function TakeDamage(int Damage, Pawn InstigatedBy, Vector HitLocation, Vector Momentum, class<DamageType> DamageType)
{
    HurtingActor = InstigatedBy;
	DamageTaken = Damage;
	bStartled = true;
 
    Super.TakeDamage( Damage, InstigatedBy, HitLocation, Momentum, DamageType );
 
	if ( DamageTaken > 3 )
	    spawn( class'STR_green_blood_puff',,, HitLocation, Rotator( Momentum ) );
 
    if ( Controller != None && ( Controller.MoveTarget == HurtingActor || Controller.Focus == HurtingActor ) )
		ReachedDestination( HurtingActor );  // Do not approach HurtingActor.
 
    if ( Health > 0 )
        GotoState('TakeHit');
    else if ( !self.IsInState('Dying') )
        GotoState('Dying');
}
 
// Died() is originally defined in Pawn.
function Died( Controller Killer, class<DamageType> DamageType, vector HitLocation )
{
	TakeDamage( Health, None, Location, vect(0,0,-1), class'Crushed' );
}
 
// TornOff() is originally defined in Actor.
event TornOff()
{
    if ( PlayerCanSeeMe() )
        DeRez();
    else
        Destroy();
}
 
// DeRez() will signal removal of the dead NaliCow.
simulated function DeRez()
{
    if ( Level.NetMode == NM_DedicatedServer )
        return;
 
    Skins.length = 0;
    Skins[0] = Material'DeRez.Shaders.DeRezFinalBody';  
}
 
auto state() Prone
{
    event AnimEnd( int Channel )
    {
        local   int     X;
 
        X = FRand() * 20;
        switch ( X ) {
            case(0) : GotoState('Turn');        break;
            case(1) : GotoState('Graze');       break;
            case(2) : GotoState('Poop');        break;
            case(3) : GotoState('Shake');       break;
            case(4) : GotoState('Swish');       break;
            case(5) : GotoState('Walk');        break;
            default : DisplayBehavior( 1 );     break;
        }
    }
 
    Begin:
        DisplayBehavior( 1 );
		MoveCheck( 0.0 );
        if ( HurtingActor != None )
            if ( FRand() > 0.95 )  // Chance to forgive HurtingActor.
                HurtingActor = None;
}
 
state() Turn
{
    event AnimEnd( int Channel )
    {
        RotationRate.Yaw = 0;
        GotoState('Prone');
    }
 
    Begin:
		MoveCheck( 0.0 );
        if ( DesiredRotation != Rotation )
        {
            RotationRate.Yaw = 10000;
            DisplayBehavior( 7 );
        }
        else
            GotoState('Prone');
}
 
state() Graze
{
    event AnimEnd( int Channel )
    {
        GotoState('Chew');
    }
 
    Begin:
        DisplayBehavior( 2 );
		MoveCheck( 0.0 );
}
 
state() Chew
{
    event AnimEnd( int Channel )
    {
        local   int     X;
 
        X = FRand() * 15;
        switch ( X ) {
            case(0) : GotoState('Prone');       break;
            case(1) : GotoState('Graze');       break;
            case(2) : GotoState('Poop');        break;
            case(3) : GotoState('Shake');       break;
            case(4) : GotoState('Swish');       break;
            case(5) : GotoState('Walk');        break;
            default : DisplayBehavior( 3 );     break;
        }
    }
 
    Begin:
        DisplayBehavior( 3 );
		MoveCheck( 0.0 );
}
 
state Poop
{
    function Timer()
    {
        spawn( class'NaliCowPoop',, 'Poop', ( Location + ( vector(Rotation) * -24 ) ), rotator( vect(0,0,-1) ) );
    }
 
    event AnimEnd( int Channel )
    {
        local   int     X;
 
        X = FRand() * 20;
        switch ( X ) {
            case(0) : GotoState('Graze');       break;
            case(1) : GotoState('Shake');       break;
            case(2) : GotoState('Swish');       break;
            case(3) : GotoState('Walk');        break;
            default : GotoState('Prone');       break;
        }
    }
 
    Begin:
        DisplayBehavior( 4 );
		MoveCheck( 0.0 );
        SetTimer( 1.0, false );
}
 
state() Shake
{
    event AnimEnd( int Channel )
    {
        local   int     X;
 
        X = FRand() * 20;
        switch ( X ) {
            case(0) : GotoState('Graze');       break;
            case(1) : GotoState('Poop');        break;
 
            case(2) : DisplayBehavior( 5 );     break;
            case(3) : GotoState('Swish');       break;
            case(4) : GotoState('Walk');        break;
            default : GotoState('Prone');       break;
        }
    }
 
    Begin:
        DisplayBehavior( 5 );
		MoveCheck( 0.0 );
}
 
state Swish
{
    event AnimEnd( int Channel )
    {
        local   int     X;
 
        X = FRand() * 20;
        switch ( X ) {
            case(0) : GotoState('Graze');       break;
            case(1) : GotoState('Poop');        break;
            case(2) : GotoState('Shake');       break;
            case(3) : DisplayBehavior( 6 );     break;
            case(4) : GotoState('Walk');        break;
            default : GotoState('Prone');       break;
        }
    }
 
    Begin:
        DisplayBehavior( 6 );
		MoveCheck( 0.0 );
}
 
state Walk
{
    function Tick( float DeltaTime )
    {
        if ( Physics == PHYS_Walking && DamageTaken == 0 )
        {
            Velocity = ( 0.5 * GroundSpeed ) * vector( Rotation );
            Acceleration = Velocity;
        }
    }
 
    event AnimEnd( int Channel )
    {
        local   int     X;
 
        X = FRand() * 20;
        switch ( X ) {
            case(0) : GotoState('Prone');							break;
            case(1) : GotoState('Turn');							break;
            case(1) : GotoState('Graze');							break;
            case(2) : GotoState('Poop');							break;
            case(3) : GotoState('Shake');							break;
            case(4) : GotoState('Swish');							break;
            default : DisplayBehavior( 7 ); MoveCheck( 0.5 );		break;
        }
    }
 
    Begin:
        DisplayBehavior( 7 );
		MoveCheck( 0.5 );
}
 
state Run
{
    function Tick( float DeltaTime )
    {
        if ( Physics == PHYS_Walking && DamageTaken == 0 )
        {
            Velocity = ( GroundSpeed ) * vector( Rotation );
            Acceleration = Velocity;
        }
    }
 
    event AnimEnd( int Channel )
    {
        local   int     X;
 
        X = FRand() * 10;
        switch ( X ) {
            case(0) : GotoState('Walk');							break;
            default : DisplayBehavior( 8 ); MoveCheck( 1.0 );		break;
        }
    }
 
    Begin:
        DisplayBehavior( 8 );
		MoveCheck( 1.0 );
}
 
state Land
{
    function Timer()
    {
        local   int     X;
 
		if ( bStartled )  // Always run if startled.
		{
			GotoState('Run');
			return;
		}
 
        X = FRand() * 10;
        switch ( X ) {
            case(0) : GotoState('Prone');   break;
            case(1) : GotoState('Turn');    break;
            case(2) : GotoState('Shake');   break;
            default : GotoState('Walk');    break;
        }
    }
 
    event AnimEnd( int Channel )
    {
        SetTimer( 0.2, false );
    }
 
    Begin:
        DisplayBehavior( 9 );
		MoveCheck( 0.0 );
}
 
state TakeHit
{
    function Timer()
    {
        GotoState('Run');
    }
 
    event AnimEnd( int Channel )
    {
        if ( Health < 1 )
            GotoState('Dying');
        else
            SetTimer( 0.2, false );
    }
 
    Begin:
        DisplayBehavior( 10, Min( ( DamageTaken / 15 ), 3 ) );
        MoveCheck( 0.0 );
}
 
state Dying
{
    Ignores Trigger, Touch, Bump, Landed, Died;
 
    function Timer()
    {
        SetPhysics( PHYS_None );
        SetCollision( false, false );
        Lifespan = 3.0;
        if ( PlayerCanSeeMe() )
            DeRez();
        else
            Destroy();
    }
 
    event AnimEnd( int Channel )
    {
        SetTimer( 4.0, false );
    }
 
    Begin:
        StopAnimating();
        DisplayBehavior( 11, Min( ( DamageTaken / 20 ), 3 ) );
        MoveCheck( 0.0 );
        if ( Level.NetMode == NM_DedicatedServer )
        {
            SetPhysics( PHYS_None );
            SetCollision( false, false );
            LifeSpan = 3.0;
            bTearOff = true;
        }
}
 
defaultProperties 
{
    CollisionHeight=34
    SurfaceType=EST_Flesh
    Mesh=VertMesh'SkaarjPack_rc.NaliCow'
    Physics=PHYS_Walking
    GroundSpeed=100
    bReplicateAnimations=true
    bFullVolume=true
    SoundRadius=512
}

NaliCowPoop

An obvious Projectile to use for this NaliCowPoop Actor is the BioRifle's Projectile, BioGlob. It uses a Mesh that automatically drips, jiggles and splats like our desired NaliCowPoop should. We will need to make a custom subclass of BioGlob, change its Texture, Lighting and alter some of it's default code to prevent it from causing Damage.

BioGlob Default Properties

(Display) Skins 
It's default Skins are set for BioRifle ammo. We will set it to Texture'XEffectMat.goop.SlimeSkin'.
(Movement) vector Velocity 
The BioGlob starts with a lot of velocity by default, we will set it to vect(0,0,0) during PostBeginPlay().
(Lighting) int AmbientGlow 
BioGlobs have an AmbientGlow by default. We will set this to zero.
(Lighting) bool bUnlit 
BioGlobs are unlit by default. We will set this to false.
(Lighting) bool bDynamicLight 
BioGlobs glow by default. We will set this to false.
(Lighting) LightType 
We will set this to LT_None.
float RestTime 
This property is originally defined in BioGlob. It determines how long the BioGlob sits before it calls BlowUp(). We will set this value to 10.0.
int BaseDamage 
This property is originally defined in BioGlob. We will make sure this doesn't cause damage by setting it to 0.

BioGlob Default Methods

BlowUp( vector HitLocation ) 
This function is originally defined in BioGlob. We will modify it to not cause damage and instead switch to a Shriveling state, which shrinks the BioGlob and "pops" it.

Final Source Code

//=============================================================================
// NaliCowPoop.
// The effects result of the NaliCow Poop behavior.
// by SuperApe -- Dec 2005
//=============================================================================
class NaliCowPoop extends BioGlob;
 
function PostBeginPlay()
{
	Super.PostBeginPlay();
 
	Velocity = vect(0,0,0);
}
 
// BlowUp() is originally defined in BioGlob.
function BlowUp( vector HitLocation )
{
	GotoState('Shriveling');
}
 
defaultProperties
{
	Skins(0)=Texture'XEffectMat.goop.SlimeSkin'
	AmbientGlow=0
	bUnlit=false
	bDynamicLight=false
	LightType=LT_None
	RestTime=10.0
	BaseDamage=0
}

Testing and Implementation

The new NPC character should be tested thoroughly to ensure it is behaving reliably both as a character and as a game object.

Compiling

If you're familiar with Compiling With UCC, then you can skip this section.

The steps for compiling our NPC code are as follows:

  1. Find your UT2004 base directory. This is usually C:/UT2004/. We will refer to this as .../UT2004/ from this point on.
  2. Create a subdirectory there for our NaliCow code, .../UT2004/NaliCow/, and create a subdirectory within that for our Classes, .../UT2004/NaliCow/Classes
  3. Within that Classes subdirectory, create a simple (text) file with a .uc extention called, "NaliCow.uc", and another called, "NaliCowPoop.uc".
  4. Copy the above final source code for the NaliCow and paste it into the .uc file. Do the same for the NaliCowPoop code.
  5. Find your .../UT2004/System/UT2004.ini file and edit it with a text editor. Find the EditPackages= block and add EditPackages=NaliCow to the end of the block. This will make sure the compiling application knows which subdirectory to look for things to compile.
  6. In Windows, use StartMenu -> Run... to launch .../UT2004/System/ucc make.
  7. If there were no errors, you will now have a .../UT2004/System/NaliCow.u package file. (Note that to re-compile a package, you must remove it from the .../UT2004/System/ subdirectory, or else the compiling application will skip it.)
  8. If there were errors, they are in the ucc log file, .../UT2004/System/ucc.txt, and will tell you which line number the error came from.

Implementation

Now, we will bring our NPC into the game environment to test and debug.

  1. Within Ued, open a map suitable for testing our NPC.
  2. In the Actor Browser, go to File -> Open... and select our NaliCow.u package file.
  3. Add an Actor -> Pawn -> NaliCow to the map.
  4. Configure the desired properties, if any.
  5. Hit the Playtest ("Joystick") button on the Toolbar.

Now you have the ability to test it's behaviors under a variety of situations. This is a good idea to do with any new AI construct. Be vigilant and thorough in your testing to find all the possible bugs.

Some helpful Console Commands to use during testing are:

summon NaliCow.NaliCow 
This spawns a default NaliCow, placed directly in front of your view.
addbots <int NumberOfBots> 
This adds bots to the map.
killall <class KillClass> 
This destroys all objects of the given KillClass.
allweapons 
Gives all weapons and default ammo.
map <name MapName> 
Starts a new match within the MapName provided. Note that map autoplay will re-start the current map within Ued's memory.

Replication

If you know how to Launch a Local Netgame, you may skip this section.

Testing Replication is important to ensure that what you have made offline will work just as well online.

  1. Save a map with at least a few NaliCows in it, different settings, etc. Save it to the .../UT2004/Maps/ subdirectory with a valid Map File Prefix.
  2. Launch UT2004 and Host a game. Pick your custom map and click on Dedicated. The menu window will disappear and a server log window will open as the game launches in the background.
  3. Launch UT2004 again. Bring down the console and type, map 127.0.0.1, to join the local server you just started.
  4. Once there for the first time, bring up the menu by hitting Esc and save this local server as one of your "Favs" for easier Joining in the future.

Now, you're seeing what a slow Dial-Up client player sees. There will be some lag due to your machine handling both the Server and Client at the same time. However, this is important to see what imformation is being Replicated properly and what isn't.

For pinpointing the problems, it's useful to use Logs to be called either on the DedicatedServer or not. See also Netcode Idioms. The Logs are found at .../UT2004/System/Server.txt for the Server and .../UT2004/System/UT2004.txt for the Client.

Further Development

Beyond this basic NaliCow NPC, more can be done to improve behavior, realism, gameplay or ease of use.

DrawScale Adjustments

Based on the DrawScale property, several adjustments can be made during PostBeginPlay() to make sure the NaliCow behaves, moves and sounds properly. The result is a simple one-property adjustment that can be made to make a small, fast baby cow or a big, slow older cow. We will use a separate function that can be called from PostBeginPlay(), AdjustForSize(). Before the adjustments are made, we can make sure the DrawScale is within a reasonable size range.

// PostBeginPlay() is originally defined in Actor().
// We will use it to make sure an AIController is spawned for our NaliCow.
event PostBeginPlay()
{
	Super.PostBeginPlay();
 
	if ( ( ControllerClass != None ) && ( Controller == None ) )
		Controller = spawn( ControllerClass );
	if ( Controller != None )
		Controller.Possess( self );
 
	AdjustForSize();
}
 
// AdjustForSize() will make various adjustments according to DrawScale.
function AdjustForSize()
{
	if ( DrawScale > 3.0 || DrawScale < 0.5 )
		SetDrawScale( FClamp( DrawScale, 3.0, 0.5 ) );
	SetCollisionSize( 34 * DrawScale, 34 * DrawScale );
	Mass = 100 * DrawScale;
	Health = 100 * DrawScale;
	SoundRadius = 512 * DrawScale;
	SoundPitch = 128 / DrawScale;
	if ( DrawScale > 1.5 )
		bCanBeBaseForPawns = true;
}

Other functions and elements will also have to be changed based on DrawScale. Changes to the AnimationRate, RotationRate, SoundVolume, SoundPitch, NaliCowPoop DrawScale and RestTime, etc. can all be made to reflect the size of the NaliCow. For example, the Acting() function can be altered to look like this:

// Acting() loops the given animation sequence name and plays the given sound.
// For Replication purposes, animation is normally looped.  Sequences may be interupted each AnimEnd().
function Acting( name Anim, sound Soundname, bool bLoop )
{
    if ( bLoop )
        LoopAnim( Anim, ( 1.0 / DrawScale ), ( 1.0 / DrawScale )/10 );
    else
		PlayAnim( Anim, ( 1.0 / DrawScale ), ( 1.0 / DrawScale )/10 );
    SimAnim.AnimSequence = Anim;
    SimAnim.AnimRate = ( 1.0 / DrawScale );
    if ( bLoop )
        SimAnim.bAnimLoop = true;
    else
        SimAnim.bAnimLoop = false;
	SimAnim.TweenRate = ( 1.0 / DrawScale )/10;
    PlaySound( Soundname, SLOT_Misc, ( DrawScale * 2 ),,, ( SoundPitch / 128 ) );
}

Cow and Baby Relationships

The original Cow and BabyCow UT creatures had properties to define their relationships. These can be reproduced with our NaliCow to perform the same behaviors.

bHasBaby and BabyCow

In UT, the property bHasBaby was meant to automatically spawn a BabyCow (a smaller version) for this Cow. BabyCows were not meant to be explicitly placed in maps. A simple modification to PostBeginPlay() should allow us to use our new DrawScale adjustments to do this easily.

var()		bool		bHasBaby;		// This NaliCow will spawn a smaller "Baby" at map start.
var  		NaliCow		Mama;			// This NaliCow is a "Baby" and should stay close to "Mama".
 
event PostBeginPlay()
{
	local	NaliCow		Baby;
 
	Super.PostBeginPlay();
 
	if ( ( ControllerClass != None ) && ( Controller == None ) )
		Controller = spawn( ControllerClass );
	if ( Controller != None )
		Controller.Possess( self );
 
	AdjustForSize();
 
	if ( bHasBaby )
		while ( Baby == None )
		{
			Baby = spawn( class'NaliCow', self,, Location + ( VRand() * ( 64 + CollisionRadius ) ), Rotation );
			if ( Baby != None )
			{
				Baby.Mama = self;
				Baby.SetDrawScale( DrawScale / 2 );
				Baby.AdjustForSize();
			}
		}
}

Also, we should encourage the BabyCow to stay close to its "Mama". We will do this with a modification to ReachedDestination(). The following code will be added to ReachedDestination() just before NewTarget is assigned to the Controller attributes.

	if ( Mama != None )
		NewTarget = Mama;

bStayClose and WanderRadius

bStayClose modified the behavior of the Cow so that it's Movement destinations were never more than WanderRadius UU away from it's starting Location. Note: Although there is a home property defined in Controller, it is defined as a NavigationPoint and won't serve this purpose.

var()		bool		bStayClose;		// This NaliCow will only select MoveTargets within WanderRadius.
var()		int 		WanderRadius;	// The radius from Home that MoveTargets will be selected.
var  		vector		Home;			// The Location this NaliCow started at.

And the following code will be added to the end of ReachedDestination().

	if ( bStayClose && WanderRadius > 0 && 
		( VSize( NewTarget.Location - Home ) > WanderRadius || 
			VSize( Controller.Destination - Home ) > WanderRadius ) )
	{
		Controller.Destination = Home + ( VRand() * ( FRand() * WanderRadius ) );
		Controller.FocalPoint = Controller.Destination;
	}

Ridable NaliCows

An extra gameplay feature could be to have players be able to ride and loosely steer larger NaliCows. You may have noticed the bCanBeBaseForPawns property set during AdjustForSize() in the section above. This will allow larger cows to support and carry Pawns. Once a player has managed to get on top of one, a simple UsedBy() function can enable them to press Use and become a special Pawn to the NaliCow.

var		Pawn		Rider;			// PlayerPawn riding this cow.
 
// UsedBy() is originally defined in Actor.
function UsedBy( Pawn P )
{
	if ( !bCanBeBaseForPawns )
		return;
 
	if ( Rider == None )
	{
		Rider = P;
		if ( PlayerController( Rider.Controller ) != None )
			PlayerController( Rider.Controller ).bBehindView = true;
	}
 
	if ( P == Rider )
	{
		Controller.Destination = Location + ( vector( rot(0,1,0) * Rider.Controller.Rotation.Yaw ) * 1024 );
		Controller.FocalPoint = Controller.Destination;
		if ( !self.IsInState('Run') )
			GotoState('Run');
		else
			PlaySound( sound'SkaarjPack_rc.injurC1c', SLOT_Misc, ( DrawScale * 2 ),,, ( SoundPitch / 128 ) );
	}
}

Once the player has become a Rider to the NaliCow, the NaliCow's Controller's Destinations are subject to the whims of the Rider.Controller. The Rider.Controller also is automatically set to bBehindView to give a visual cue that the change has taken place. By pressing the Use key, the NaliCow is urged to keep Running. When the NaliCow reaches it's Destinations, we'll keep the player in control by adding a little code to the top of ReachedDestination().

    if ( Rider != None )
    {
        if ( VSize( Rider.Location - Location ) > ( CollisionHeight + Rider.CollisionHeight ) )
        {
            if ( PlayerController( Rider.Controller ) != None )
                PlayerController( Rider.Controller ).bBehindView = false;
            Rider = None;  // Rider has left.
        }
        else
        {
            Controller.Destination = Location + ( vector( rot(0,1,0) * Rider.Controller.Rotation.Yaw ) * 1024 );
            Controller.FocalPoint = Controller.Destination;
            return true;
        }
    }

To stop being a Rider, the player can simply move off the NaliCow. The bBehindView setting will return after the NaliCow has reached it's next Destination.

External Links

Other links to basic AI scripting tutorials should be listed here:

Mattias Worch worked on the Unreal 2 team and has written a few useful tutorials on AI scripting. The tutorials on this page directly relating to AI scripting are "Set up Dialog and AI Scripting for a Level," and "Create a Basic AI Script." Please note that the copyright on these tutorials allows us to link to them, but not to "wikify" the tutorials and place them here. If you would like to have a tutorial on the Wiki, I'm sure that everyone would appreciate if someone were to read over these tutorials and experiment with AI scripting until they had a complete enough understanding to place an original tutorial here.

Related Topics

Discussion

Tarquin: Link to documentation wiki pages, if they exist, or start writing them! That's a good way to start.

Foxpaw: I actually don't have any experience with AI scripting myself, I was just using the random page function and thought this page could use a bit of refactoring.

SuperApe: The above link is dead. Yes, we need links or some tuts on the wiki itself.

SuperApe: I will most likely take over this page and revive it. Linking to NPC Support and others. My thought is to take the Mesh for the NaliCow and re-construct it's AI coding as an NPC for UT2004. (I never had UT, so this will be a from scratch effort that I look forward to) Tutorial outlined. Working...

SuperApe: This is a tutorial on UScripting a custom AI. Comments welcome.

Solid Snake: Hmmm, I don't really like some of the code in here as I think it could be a lot better.

SuperApe: I'll do my best to take that as constructive criticism, but it would really help if you could elaborate. I guess, This page is under construction.

SuperApe: After spending some time with Monsters, I see a much easier way to do this. I will redo this tutorial, taking advantage of the all Pawn functions, instead of "re-inventing the wheel", like I do above. This will make sounds and animation play better. The code will be much smaller. However, I will need to build a custom Controller, as MonsterController does not have a "pacifist" setting. ;) Other than that, it should be much more generalized and applicable to other people's projects. Working...

SuperApe: Something like this, plus a new MonsterController to favor Wandering, avoid Hunting, Attacking, StakeOut, etc.

//=============================================================================
// NaliCow.
// Recreation of UT NaliCow.
// by SuperApe -- Jan 2006
//=============================================================================
class NaliCow extends Monster
	placeable;
 
var()	bool        bHasBaby;       // Spawns a smaller "Baby" at map start.
var()	bool        bStayClose;     // Only select MoveTargets within WanderRadius.
var()	int         WanderRadius;   // The radius from Home that MoveTargets will be selected.
 
var		name		DeathAnim[4];  // Death animations.
var		name		InjureAnim[4];  // Injury animations.
 
var		name		IdleAnim[6];  // Idle animations.
var		sound		IdleSounds[6];  // Idle sounds.
 
function PostBeginPlay()
 
{
	Super.PostBeginPlay();
 
	// FIXME: Make attitude ignore, friendly or fear players only.
}
 
// This stand-alone creature should avoid being placed in a roster.  Skip UnrealPawn's PostNetBeginPlay().
simulated function PostBeginNetPlay()
{
	// Do Pawn's PostNetBeginPlay()
	local playercontroller P;
 
	if ( Level.bDropDetail || (Level.DetailMode == DM_Low) )
		MaxLights = Min(4,MaxLights);
	if ( Role == ROLE_Authority )
		return;
	if ( (Controller != None) && (Controller.Pawn == None) )
	{
		Controller.Pawn = self;
		if ( (PlayerController(Controller) != None)
			&& (PlayerController(Controller).ViewTarget == Controller) )
			PlayerController(Controller).SetViewTarget(self);
	}
 
	if ( Role == ROLE_AutonomousProxy )
		bUpdateEyeHeight = true;
 
	if ( (PlayerReplicationInfo != None)
		&& (PlayerReplicationInfo.Owner == None) )
	{
		PlayerReplicationInfo.SetOwner(Controller);
		if ( left(PlayerReplicationInfo.PlayerName, 5) ~= "PRESS" )
		{
			P = Level.GetLocalPlayerController();
			if ( (P.PlayerReplicationInfo != None) && !(left(PlayerReplicationInfo.PlayerName, 5) ~= "PRESS") )
				bScriptPostRender = true;
		}
	}
	PlayWaiting();
 
	// Do xPawn's PostNetBeginPlay()
	MultiJumpRemaining = MaxMultiJump;
	bCanDoubleJump = CanMultiJump();
}
 
function Step()
{
	PlaySound(sound'SkaarjPack_rc.WalkC', SLOT_Interact);
}
 
simulated function AnimEnd(int Channel)
{
	local 	name	Anim, New;
	local 	float	frame, rate;
	local	int		n;
	local	BioGlob	B;
 
    if( Level.TimeSeconds - LastPainSound < MinTimeBetweenPainSounds )
        return;  // Allow pain to play through.
 
	if ( Channel == 0 )
	{
		GetAnimParams(0, Anim,frame,rate);
		if ( Anim == 'Swish' || Anim == 'Shake' || Anim == 'Poop' )
			n = int( FRand() * 6 );		
		if ( ( Anim != 'Breath' || Anim != 'Chew' ) && Anim == IdleAnim[n] )
			n = int( FRand() * 6 );
 
		if ( Anim == 'Poop' )
		{
			B = spawn( class'BioGlob', self,, Location - ( vector( Rotation ) * 24 ) );
			B.Velocity = vect(0,0,0);
			B.bDynamicLight = false;
			B.AmbientGlow = 0;
			B.RestTime *= 2.0;
			B.BaseDamage = 0;
			B.Damage = 0;
			B.Skins[0] = Texture'XEffectMat.goop.SlimeSkin';
			if ( FRand() > 0.5 )
				IdleSounds[3] = sound'SkaarjPack_rc.cMoo1c';
			else
				IdleSounds[3] = sound'SkaarjPack_rc.cMoo2c';
		}
		if ( Anim == 'root' )
			n = 2;
		if ( FRand() > 0.8 )
			n = 0;
 
		IdleWeaponAnim = IdleAnim[n];
		PlayAnim( IdleAnim[n] );
		PlaySound( IdleSounds[n] );
	}
	Super.AnimEnd(Channel);
}
 
function PlayTakeHit(vector HitLocation, int Damage, class<DamageType> DamageType)
{
	local	int		i;
 
    if( Level.TimeSeconds - LastPainSound < MinTimeBetweenPainSounds )
        return;
 
	i = Clamp( ( Damage / 15 ), 0, 3 );
	PlayAnim( InjureAnim[i] );
 
    LastPainSound = Level.TimeSeconds;
	if ( i == 1 )
		PlaySound(sound'SkaarjPack_rc.cMoo1c', SLOT_Pain, 2*TransientSoundVolume,, 200);
	else if ( i == 2 )
		PlaySound(sound'SkaarjPack_rc.injurC1c', SLOT_Pain, 2*TransientSoundVolume,, 200);
	else
		PlaySound(sound'SkaarjPack_rc.injurC2c', SLOT_Pain, 2*TransientSoundVolume,, 200);
}
 
simulated function PlayDying(class<DamageType> DamageType, vector HitLoc)
{
	AmbientSound = None;
    bCanTeleport = false; 
    bReplicateMovement = false;
    bTearOff = true;
    bPlayedDeath = true;
 
	HitDamageType = DamageType; // these are replicated to other clients
    TakeHitLocation = HitLoc;
	LifeSpan = RagdollLifeSpan;
 
    GotoState('Dying');
 
	Velocity += TearOffMomentum;
    BaseEyeHeight = Default.BaseEyeHeight;
    SetPhysics(PHYS_Falling);
 
	PlayAnim(DeathAnim[Rand(4)],1.2,0.05);		
}
 
defaultproperties
{
     DeathAnim(0)="Dead"
     DeathAnim(1)="Dead2"
     DeathAnim(2)="Dead3"
     DeathAnim(3)="Dead3"
     InjureAnim(0)="TakeHit"
     InjureAnim(1)="TakeHit2"
     InjureAnim(2)="BigHit"
     InjureAnim(3)="BigHit"
     IdleAnim(0)="Breath"
     IdleAnim(1)="root"
     IdleAnim(2)="Chew"
     IdleAnim(3)="Poop"
     IdleAnim(4)="Shake"
     IdleAnim(5)="Swish"
     IdleSounds(0)=Sound'SkaarjPack_rc.Cow.ambCow'
     IdleSounds(1)=Sound'SkaarjPack_rc.Pupae.munch1p'
     IdleSounds(2)=Sound'SkaarjPack_rc.Pupae.munch1p'
     IdleSounds(3)=Sound'SkaarjPack_rc.Cow.cMoo2c'
     IdleSounds(4)=Sound'SkaarjPack_rc.Cow.shakeC'
     IdleSounds(5)=Sound'SkaarjPack_rc.Cow.swishC'
     StartingAnim="Breath"
     bMeleeFighter=False
     bCanDodge=False
     HitSound(0)=Sound'SkaarjPack_rc.Cow.injurC1c'
     HitSound(1)=Sound'SkaarjPack_rc.Cow.injurC2c'
     HitSound(2)=Sound'SkaarjPack_rc.Cow.injurC1c'
     HitSound(3)=Sound'SkaarjPack_rc.Cow.injurC2c'
     DeathSound(0)=Sound'SkaarjPack_rc.Cow.cMoo2c'
     DeathSound(1)=Sound'SkaarjPack_rc.Cow.DeathC1c'
     DeathSound(2)=Sound'SkaarjPack_rc.Cow.DeathC2c'
     DeathSound(3)=Sound'SkaarjPack_rc.Cow.DeathC2c'
     bCanDodgeDoubleJump=False
     WallDodgeAnims(0)="Landed"
     WallDodgeAnims(1)="Landed"
     WallDodgeAnims(2)="Landed"
     WallDodgeAnims(3)="Landed"
     IdleHeavyAnim="Breath"
     IdleRifleAnim="Breath"
     bCanJump=False
     bCanClimbLadders=False
     bCanStrafe=False
     bCanDoubleJump=False
     bCanUse=False
     MeleeRange=80.000000
     GroundSpeed=100.000000
     WaterSpeed=75.000000
     JumpZ=75.000000
     ControllerClass=Class'MonsterController'
     MovementAnims(0)="Run"
     MovementAnims(1)="Run"
     MovementAnims(2)="Run"
     MovementAnims(3)="Run"
     TurnLeftAnim="Walk"
     TurnRightAnim="Walk"
     SwimAnims(0)="Run"
     SwimAnims(1)="Run"
     SwimAnims(2)="Run"
     SwimAnims(3)="Run"
     WalkAnims(0)="Walk"
     WalkAnims(1)="Walk"
     WalkAnims(2)="Walk"
     WalkAnims(3)="Walk"
     AirAnims(0)="Landed"
     AirAnims(1)="Landed"
     AirAnims(2)="Landed"
     AirAnims(3)="Landed"
     TakeoffAnims(0)="Landed"
     TakeoffAnims(1)="Landed"
     TakeoffAnims(2)="Landed"
     TakeoffAnims(3)="Landed"
     LandAnims(0)="Landed"
     LandAnims(1)="Landed"
     LandAnims(2)="Landed"
     LandAnims(3)="Landed"
     DoubleJumpAnims(0)="Landed"
     DoubleJumpAnims(1)="Landed"
     DoubleJumpAnims(2)="Landed"
     DoubleJumpAnims(3)="Landed"
     DodgeAnims(0)="Landed"
     DodgeAnims(1)="Landed"
     DodgeAnims(2)="Landed"
     DodgeAnims(3)="Landed"
     AirStillAnim="Landed"
     TakeoffStillAnim="Landed"
     IdleCrouchAnim="Breath"
     IdleSwimAnim="Walk"
     IdleWeaponAnim="Breath"
     IdleRestAnim="Breath"
     Mesh=VertMesh'SkaarjPack_rc.NaliCow'
     PrePivot=(Z=0.000000)
     CollisionRadius=34.000000
     CollisionHeight=34.000000
}

Page Categories

Page Information

2022-11-18T09:38:39.521639Z 2006-02-04T03:20:46Z SuperApe * https://wiki.beyondunreal.com/Legacy:Basic AI Scripting Tutorial Attribution-NonCommercial-ShareAlike 3.0