Tutorial Overview

In this tutorial, we will create an AI that can follow a patrol path and opportunistically attack the player. We will also see how we can control the lifecycle of a bot, including preventing them from being restarted by the game. We will use the UT* branch of classes for this tutorial.

Downloading the map

Patrol System

L9PatrolPath

  • Create the class L9PatrolPath, have it extend PathNode, ensure it is placeable
  • Set its sprite component to Texture2D'MobileResources.HUD.AnalogHat'
  • Give it the Patrol property NextNode of type L9PatrolPath

L9AISpawner

  • Create the class L9AISpawner, have it extend L9PatrolPath, ensure it is placeable
  • Give it the static mesh StaticMesh'NodeBuddies.NodeBuddy_PerchUp'
  • Give it the Spawner property BotsToSpawn of type int, have it default to 3
  • Give it the Spawner property SpawnInterval of type float, have it default to 2
  • Set the property bStatic to false as a default
PostBeginPlay()
  • Override to set the timer to SpawnInterval, have it loop
  • Execute super postbeginplay
L9Bot SpawnBot()
  • Create this function
  • Place the following code in it:
Timer()
  • Implement this function
  • If BotsToSpawn is <= 0, return
  • Call the function SpawnBot. If it does not return None, decrement BotsToSpawn
    local L9Bot NewBot;
    local Pawn NewPawn;
    local rotator StartRotation;

    NewBot = Spawn(class'L9Bot');
    if (NewBot == None)
    {
        `log("Couldn't spawn "$class'L9Bot'$" at "$self);
        return None;
    }

    StartRotation = Rotation;
    StartRotation.Yaw = Rotation.Yaw;

    NewPawn = Spawn(class'UTPawn',,,Location,StartRotation);
    if ( NewPawn == None )
    {
        `log("Couldn't spawn "$class'UTPawn'$" at "$self);
        NewBot.Destroy();
        return None;
    }

    NewPawn.SetAnchor(self);
    NewPawn.LastStartTime = WorldInfo.TimeSeconds;
    NewBot.Possess(NewPawn, false);
    NewBot.Pawn.PlayTeleportEffect(true, true);
    NewBot.ClientSetRotation(NewBot.Pawn.Rotation, TRUE);
    WorldInfo.Game.AddDefaultInventory(NewPawn);
    WorldInfo.Game.SetPlayerDefaults(NewPawn);

    NewBot.EnterPatrolPath(self);

    return NewBot;

L9LinkGun

  • Create the L9LinkGun class, have it extend UTWeap_LinkGun
  • Set the default property WeaponRange to 500

byte BestMode()

  • Override this function to return 0

bool CanAttack(Actor Other)

  • Have this function return true if its super version returns true and the distance between other and instigator is <= our WeaponRange

L9Game

  • Create the class L9Game, have it extend UTGame
  • Set the array property element 0 DefaultInventory to the class L9LinkGun

RestartPlayer(Controller aPlayer)

  • Override this function to test if aPlayer is an L9Bot and is in the state Dead
    • If they are, call destroy on them
    • Otherwise, call our super version

Logout(Controller exiting)

  • Override this function to test if exiting is an L9Bot
    • If it is, simply return
    • Otherwise, call our super version

L9Bot

  • Create the class L9Bot, have it extend UTBot
  • Give it the following properties:
var float AggroDistance;
var float EscapeDistance;
var float AttackDistance;
var float PatrolPointReachedThreshold;
var L9PatrolPath NextPatrolPoint;
var L9AISpawner MySpawner;

var private Actor _intermediate;
var private float _dist;
  • Set the default of PatrolPointReachedThreshold to 50
  • Set the default of AggroDistance to 600
  • Set the default of EscapeDistance to 1500
  • Set the default of AttackDistance to 300

State FollowingPatrolPath

  • Create this state, make it an auto state
  • Create the Begin state label. Place the following code under it:
    • Check if NextPatrolPoint is not None
      • If it's not, check if the next patrol point is directly reachable
        • If it is, move towards it
        • If not, assign an intermediate path into _intermediate
          • Move toward _intermediate
    • Call LatentWhatToDoNext

State HuntingPlayer

  • Create this state
  • Create the Begin state label. Place the following code under it:
    • Check if Enemy is not None and Pawn is not None
      • Calculate the distance between our pawn and the Enemy then place this value in _dist
      • If _dist is greater than the escape distance or a fast trace fails between our pawn's location and the enemy location, set Enemy to None
      • Otherwise if _dist is <= attack distance
        • Have the pawn switch to its best weapon then fire weapon at enemy
      • Otherwise
        • If enemy is directly reachable, move towards the enemy
        • Otherwise, assign an intermediate path into _intermediate
          • Move toward _intermediate
    • Call LatentWhatToDoNext

bool IsPawnTouchingActor(Actor other)

  • Implement this function
  • Have this function return false if pawn is None or if other is None
  • Return true if the distance between our pawn and other is less than the patrol point reached threshold

EnterPatrolPath(L9PatrolPath path)

  • Implement this function
  • Set the next patrol point to path
  • if path is an L9AISpawner, assign it to MySpawner
  • If path is none, go to state ''
  • Otherwise, go to state 'FollowingPatrolPath'

PawnDied(Pawn P)

  • Implement this function
  • If MySpawner is not none, increment its BotsToSpawn variable
  • Call the super version of this function

HuntEnemy(Pawn p)

  • Implement this function
  • If our Pawn is None, return
  • Assign P into Enemy
  • Assign P into Focus
  • Go to state 'HuntingPlayer' at label 'Begin'

NotifyTakeHit(Controller InstigatedBy, Vector HitLocation, int Damage, class<DamageType> damageType, Vector momentum)

  • Implement this function
  • Call the super version of it
  • If InstigatedBy is not None and its Pawn is not None
    • Call HuntEnemy on its pawn

Possess(Pawn aPawn, bool bVehicleTransition)

  • Implement this function
  • Call the suerp version of this function
  • If NextPatrolPoint is not None, go to state 'FollowingPatrolPath' at label 'Begin'

protected event ExecuteWhatToDoNext()

  • Implement this function
  • Create the local variable of type PlayerController PC
  • If our pawn is None, call destroy() and return
  • Call GetALocalPlayerController() and assign the result into pc
  • If pc is not None and pc's Pawn is not none and the distance between our pawn and pc's pawn is <= aggro distance then call HuntEnemy on pc's pawn and return
  • If the next patrol point is not none:
    • Call IsPawnTouchingActor passing it Next Patrol point. If it returns true, assign NextPatrolPoint.NextNode into NextPatrolPoint
  • If next patrol point is not none, go to state 'FollowingPatrolPath' at label 'Begin'

Updating the Map

  • Open your map
  • Place an L9AISpawner in one of the rooms
  • Place another L9AISpawner in another room on the other side of the map
  • Create two loops of L9PatrolPath objects spanning from each spawner
    • Have the loops move through 4 rooms creating a cyclic path
  • Connect the L9PatrolPath objects via their NextNode property
    • Include the L9AISpawner objects in this process
  • Create a path node at every hallway entrance of each room and one additional path node in the center of each empty room
  • Rebuild geometry
  • Rebuild lights (without lightmass)
  • Rebuild paths
  • Save and exit

Completed Tutorial

Download the completed tutorial code here.

  • Place the Lab9 folder in Development/Src
  • Place L9-Map.udk in UDKGame/Content/Maps
  • Before running the game or opening the map in the editor, be sure to compile