This page describes how to setup advanced AI to create more interesting enemies (or friends).



Introduction

The asset comes with some basic AI scripts, that may be fine for standard enemies, but if you want to create more complex behaviours, the Corgi Engine provides an advanced AI system that will help you create all sorts of interesting patterns and attitudes. For a brief overview of what you can do with it, you can have a look at the RetroAI demo scene, which will showcase a tiny portion of the things you can create with this system.

Core concepts

The Advanced AI system is based on a few core classes you’ll find in the MMTools/AI/ folder :

  • AIState : A State is a combination of one or more actions, and one or more transitions. An example of a state could be “patrolling until an enemy gets in range”.
  • AIAction : Actions are behaviours and describe what your character is doing. Examples include patrolling, shooting, jumping, etc. The engine comes with a lot of predefined actions, and it’s very easy to create your own.
  • AIDecision : Decisions are components that will be evaluated by transitions, every frame, and will return true or false. Examples include time spent in a state, distance to a target, or object detection within an area.
  • AITransition : Transitions are a combination of one or more decisions and destination states whether or not these transitions are true or false. An example of a transition could be “if an enemy gets in range, transition to the Shooting state”.
  • AIBrain : the AI brain is responsible from going from one state to the other based on the defined transitions. It’s basically just a collection of states, and it’s where you’ll link all the actions, decisions, states and transitions together.
Jekyll
An example of an Advanced AI powered character's inspector, complete with its brain, two actions and two decisions.

All of these will be put on top of an already setup character (check out the dedicated doc for more on this) and can potentially leverage all existing abilities.

Creating an advanced AI character

We’ll recreate one of the example characters available in the RetroAI demo scene, the yellow RetroSwordsman. Creating the base character is no different from creating any character, so we won’t go over that again here. Once that base character is ready, you’ll want to add an AIBrain to it.

What we want from this character is the following behaviour : patrol until an enemy gets close enough, and if that’s the case use our weapon. Then after a certain time get back to patrolling (and repeat).

To get there, we’ll use some of the ready made actions and decisions available in the engine. We’ll add the following components : AIActionPatrol, AIActionShoot, AIDecisionTargetRadius, and AIDecisionTimeInState. Each of these comes with an inspector where we’ll need to set a few things :

  • AIActionPatrol : we’ll want to make sure our character changes direction on walls, so we’ll check that. The rest can remain as it is.
  • AIActionShoot : our character is equipped with a melee weapon that doesn’t require aiming, but we’ll want it to face its target, so we’ll check that.
  • AIDecisionTargetRadius : we’ll want our character to attack if the player gets within 4 units. We’ll set the radius to 4, leave the origin offset to zero (the center of our character will be used to determine the area), and we’ll select the Player target layer as we want this enemy to only attack the player.
  • AIDecisionTimeInState : we want our character to resume patrolling 2 seconds after attacking, so we’ll put “2” in After Time.
Jekyll
Our character's inspector.

Our character now has all the actions and decisions we need to define its behaviour. All that’s left to do is plug all that into its brain. As stated before, our character will have 2 states : patrolling and attacking. So in its AIBrain component, we’ll simply add two states by pressing the bottom right “+” sign twice. We’ll name our first state “Patrolling” and our second state “Attacking”.

Jekyll
Naming our states.

Now in the Patrolling state we’ll only have one action, and one transition, so we’ll add an Action and a Decision by pressing their respective “+” signs once each. We’ll now drag our character’s AIActionPatrol component into the Actions’ array slot, and we’ll drag the AIDecisionTargetRadius into the Transitions’ first Decision slot. We want to switch to attacking when the player is in range, so we’ll put “Attacking” in our transition’s True State field.

We’ll proceed in the same way for our second state : we’ll add an action and a transition, then drag our AIActionShoot component into our Shooting state’s Action slot, and our AIDecisionTimeInState into our state’s transition’s decision slot. We want to go back to patrolling after attacking, so we’ll put “Patrolling” into our True State field. And that’s it, press play and your character is ready!

Jekyll
Our brain all set up!

To make sure our brain is properly setup, we can just “read” it. From top to bottom, we have a first Patrolling state, in which our character will patrol (AIActionPatrol) until an enemy is in range (AIDecisionTargetRadius), and if that’s the case it’ll go to Attacking, which is a state in which our character will attack (AIActionShoot - shoot works for all weapons, including melee), and it’ll come out of this state if its AIDecisionTimeInState is true, in which case it’ll go back to patrolling.

Congratulations, you’ve completed what is probably the “hardest” thing to do with the Corgi Engine, and you now know how to create all sorts of character behaviours. You can of course have more than one action in a state (running and jumping for example), more than one decision in a transition, and more than one transition, so you can really come up with complex stuff.

Actions

The engine comes with all of these predefined actions, ready to use in your own characters :

  • AIActionChangeWeapon : used to force your character to switch to another weapon. Just drag a weapon prefab into its NewWeapon slot and you’re good to go.
  • AIActionDoNothing : as the name implies, does nothing. Just waits there.
  • AIActionFlyTowardsTarget : you’ll need a CharacterFly ability for this one. Makes the character fly up to the specified MinimumDistance in the direction of the target. That’s how the RetroGhosts move.
  • AIActionFlyPatrol : allows a flying character to patrol through a set of node.
  • AIActionJump : performs the defined number of jumps. Look below for a breakdown of how this class works.
  • AIActionMoveTowardsTarget : directs the CharacterHorizontalMovement ability to move in the direction of the target.
  • AIActionPatrol : patrols until it (optionally) hits a wall or a hole.
  • AIActionPatrolWithinBounds : same thing but you can also define left and right bounds that the character can’t exceed.
  • AIActionShoot : shoots using the currently equipped weapon. If your weapon is in auto mode, will shoot until you exit the state, and will only shoot once in SemiAuto mode. You can optionnally have the character face (left/right) the target, and aim at it (if the weapon has a WeaponAim component).

Just like everything else in the engine, you’re encouraged to create your own. Let’s look at an Action’s code to see how it works :

public class AIActionJump : AIAction
{
    public int NumberOfJumps = 1;
    protected CharacterJump _characterJump;

    protected int _numberOfJumps = 0;

    protected override void Initialization()
    {
        _characterJump = this.gameObject.GetComponent<CharacterJump>();
    }

    public override void PerformAction()
    {
        Jump();
    }

    protected virtual void Jump()
    {
        if (_numberOfJumps < NumberOfJumps)
        {
            _characterJump.JumpStart();
            _numberOfJumps++;
        }            
    }

    public override void OnEnterState()
    {
        base.OnEnterState();
        _numberOfJumps = 0;
    }
}

As you can see this class overrides a few methods :

  • Initialization, in which we’ll do whatever we need to do to initialize our Action, in this case we store the CharacterJump ability for future use.
  • PerformAction : this will be called everytime our character is in the state this Action is in. In this case we’ll just call our Jump method, that in turns calls the CharacterJump ability’s JumpStart method if our conditions are met.
  • OnEnterState : everytime we’ll go back to the state that contains this Action, we’ll want to reset our current number of jumps.

Decisions

  • AIDecisionDetectTargetLine : will return true if any object on its TargetLayer layermask enters its line of sight. It will also set the Brain’s Target to that object. You can choose to have it in ray mode, in which case its line of sight will be an actual line (a raycast), or have it be wider (in which case it’ll use a spherecast). You can also specify an offset for the ray’s origin, and an obstacle layer mask that will block it.
  • AIDecisionTargetRadius : will return true if an object on its TargetLayer layermask is within its specified radius, false otherwise. It will also set the Brain’s Target to that object.
  • AIDecisionDistanceToTarget : will return true if the current Brain’s Target is within the specified range, false otherwise.
  • AIDecisionGrounded : will return true if the character is grounded, false otherwise.
  • AIDecisionHealth : will return true if the specified Health conditions are met. You can have it be lower, strictly lower, equal, higher or strictly higher than the specified value.
  • AIDecisionHit : returns true if the Character got hit this frame, or after the specified number of hits has been reached.
  • AIDecisionNextFrame : will return true when entering the state this Decision is on.
  • AIDecisionTargetFacingAI : will return true if the Brain’s current target is facing this character. Yes, it’s quite specific to Ghosts. But hey now you can use it too!
  • AIDecisionTimeInState : will return true after the specified duration (in seconds) has passed since the Brain has been in the state this decision is in. Use it to do something X seconds after having done something else.
  • AIDecisionTimeSinceStart : will return true after the specified duration (in seconds) has passed since the level was loaded.

And just like Actions, it’s very easy to create your own Decisions.