Behind the Scenes: Creating Artificial Intelligence for Introspect:

Introspect is a first-person exploration game for PC, in which the player explores the mind of a deceased person to determine the cause of death. While the player explores the layers of the patients mind in a representation of their home, they will encounter monsters of the demons that plagued the person throughout their life.

The monsters themselves don’t need to be specific in their AI behaviours, but they do need to do the following:

  • Be flexible enough for me to add behaviours when needed
  • Use a vision-based navigation system that allows for minor stealth behaviours
  • Be simple enough to be understood by me, and have simple behaviours for player to understand the AI for themselves

The AI system I created is able to perform some critical behaviours over a range of behaviour levels, for example:

  • Can see the player through an adjustable and efficient field of view and distance (not relying on Unity’s physics collisions)
  • When no enemy is in sight, the AI will move between a series of pre-set patrol positions
  • When an enemy is in sight, the AI will pause its current behaviour to confirm the presence of the player
  • When the enemy is certain of the player’s presence, it will pursue the player at a higher movement speed, and execute damage functions on the player when they touch
  • If the player breaks line of sight from the AI, it will still move towards their last-known position, before pausing for a moment and returning to its original behaviour
  • The whole system is flexible and simple enough for behaviours and animation controllers to be added after the fact

I’m very proud of how this system works, and I’d like to show you how I made it.

How I’ve Tackled Artificial Intelligence in Introspect:

1481016108525-c2b837b9-30de-4231-95b2-0c5048e24635_Defining the behaviour stack – what should this agent do? In what order? How does it transition?

This is best left to a pen and notepad. When brainstorming the behaviours for this agent, I started by considering games that matched the systems of familiar games. Introspect’s gameplay functions similar to the Amnesia franchise, where enemies will pursue on sight, and attack on-contact. Players can evade and hide from the enemy, who will give up if the player can’t be found.

I drew a flowchart of the behaviour and the transitions:

From this information, we can derive that there will be three critical states, a Patrol, Alert and Attack state, each of which has particular behaviours that the previous doesn’t. In an AIController.cs file, I created an enumerator, since these behaviours are like booleans, in which only one condition is true.

Public enum EnemyState
{
       Patrol, // Moving randomly between patrol nodes
       Alert, // Stopping to check for the player
       Attack // Approach and deal damage to the player if close enough
}

Making Life Easier – the tools which Unity provides for AI Navigation

Luckily, I don’t need to program a pathfinding algorithm for allowing the agent to move through an environment – I can use Unity’s NavMesh system. In any environment, static objects that are horizontal enough can have a NavMesh baked onto them. A NavMesh allows a NavMesh Agent to move on top of it. I’ve gone deeper into AI navigation in a previous blog post, but this combination of technologies work seamlessly with the AI system I need.

By adding a NavMesh Agent component to the primitive object in the scene, and by ensuring that a large enough horizontal area exists for an Agent to move, the system works out of the box. Using SetDestination(Vector3 Destination) the Agent will easily move towards any location, pivoting as it moves. NavMesh Agents are very simple entities that make things substantially faster than messing around with your own solution.

Screen Shot 2016-12-13 at 11.40.47 pm.png

“Do the thing!”- what should the Agent do in every behaviour?

1481020014828-062051b7-3a9c-4d09-ac5f-4f469ebee08e_This part requires a notebook too. The big question that you should know the answer to is what your Agent will do in every state. Each state must have a very different set of behaviours to differentiate the behaviour. Make sure that any common functions are totally separate from the enumerator, and execute no matter the state.

Here, the Patrol function will search for the player, move between Patrol Points at a moderate speed, and wait for each one for x seconds. The Alert state will be short but will determine whether or not the player continues to be visible. In the Attack phase, the enemy will run towards the player, execute a hit function if close enough, and return to being Alert if they cannot be found.

To begin, I set up a Switch statement in Update() to allow each state to run a separate function when a particular case is active:

void Update()
{
    switch (enemyState)
        {
            case EnemyState.Patrol:
                // Return the speed to normal
                if (agent.speed != speed)
                    agent.speed = speed; 
                UpdateVision();
                Patrol();
                break;

            case EnemyState.Alert:
                Alert();
                break;

            case EnemyState.Attack:
                Attack();
                // Double the speed while in this state
                if (agent.speed == speed)
                    agent.speed = speed * 2; 
                break;
        }
}

This setup will ensure that every state will execute its required functions as needed.

Next, we need to outline all of the functions within each state.

Firstly, I set up another enumerator

Navigation State Enumerator

I thought I’d need a secondary enumerator for me to store all of the navigation states so I can organise the different states within the Patrol state, without cramming them all into the EnemyState enumerator.

public enum NavigationState
 {
     Moving, // Heading to a destination
     Waiting, // Pausing at the destination
     Ready, // ready to select another destination
     Busy // Used when the state doesn't require any of the above
 }

Not all of these states are totally necessary, but they do make life easier if I want to check what an agent is doing, and letting its other functions happen in tandem more easily.

Now, to put these states into a Switch statement within Update():

switch(navState) // Used primarily to compartmentalise the functions to avoid spilling into other behaviours
    {
         case NavigationState.Moving:
             break;
         case NavigationState.Waiting:
             Wait(); // To manage waiting between Patrol Nodes
             break;
         case NavigationState.Ready:
             break;
         case NavigationState.Busy:
             break;
     }
 }

These four states are primarily to help to debug, but I can delegate all of the waiting logic into the Wait() function:

Wait():

public void Wait()
 {
     if (enemyState != EnemyState.Alert)
     {
         if (waiting == false)
         {
             // Add to the wait time
             changeNodetime = Time.timeSinceLevelLoad + navPauseTime;

             waiting = true;
         }
         else
         {
             if (changeNodetime < Time.timeSinceLevelLoad)
             {
                 // Let the navigation system begin moving again
                 navState = NavigationState.Ready;
                 waiting = false;
             }
         }
     }
 }

This function automatically checks whether or not a timer has begun, and automatically sets it. While not the cleanest and most ideal method of setting and managing a timer, it is embedded in a single function and doesn’t require values to be set outside itself.

With a proper Wait system set, we can use the four Navigation States to tie together the three EnemyStates

Patrol:

public void Patrol()
   {

     // If the enemy is ready, move to a random patrol node
     if (navState == NavigationState.Ready)
     {
         navState = NavigationState.Moving;

         // Select a random patrol node to navigate to
         GameObject patrolNode = PatrolNodes[Random.Range(0, PatrolNodes.Length)];

         // Move to it
         agent.SetDestination(patrolNode.transform.position);

         Debug.Log("Moving to: " + patrolNode.name + ". State is: " + navState);
     }

     // Has the agent reached the node?
     if (Vector3.Distance(transform.position, agent.destination) < 1f && agent.destination != null)
     {
         // Tell the navigator that it's reached the destination
         navState = NavigationState.Waiting;
     }
 }

The comments here are fairly self-explanatory. This function tells the Agent to select a random PatrolNode to move towards, and stops is when it’s close enough, using the Navigation along the way.

While the Agent is patrolling, it is using a more precise vision system to allow it to detect the player at varying degrees (and allow for a semi-stealth system which I can iterate easily on further).

Using the variable I’ve set above:

// Vision System
 public float visionRange = 100; // How far can the AI see?
 public float visionFOV = 90; // How wide can the AI see?
 public float certainty = 0; // How certain is the AI of the player's presence?

———–

public void UpdateVision()
 {
    // Check the angle from this object to the player to determine how far the 
    // player is from the enemies FOV
    Vector3 playerDirection = player.transform.position - transform.position;
    float angle = Vector3.Angle(playerDirection, transform.forward);

    if (angle < visionFOV / 2 && Vector3.Distance(transform.position, player.transform.position) <= visionRange)
     {
         timeUntilAttack = Time.timeSinceLevelLoad + alertTime;

         enemyState = EnemyState.Alert;
     }
 }

Here, by comparing the angle between the front of the Agent to the position of the player with its field of vision, the agent will determine if the player is within sight or not, and transition to the next state. The AI will set up its “timeUntilAttack” variable before moving into the Alert state, so that when timeUntilAttack is complete, and the player is still in view, the agent will enter Attack.

Alert:

public void Alert()
    {
        // cast a ray at the player
        RaycastHit hit;

        if (Physics.Raycast(transform.position, (player.transform.position - transform.position), out hit))
        {
            // If the player is found
            if (hit.collider.gameObject.tag == "Player")
            {
                certainty = 1;
                giveUp = giveUpTime + Time.timeSinceLevelLoad;

                // Deduct time from the timeUntilAttack
                if (timeUntilAttack <= Time.timeSinceLevelLoad && certainty > 0.75f) // The initial calculation of timeUntilAttack is performed in the DetectObject function
                    enemyState = EnemyState.Attack;
            }
            else
            {
                // If the player is not found
                if (giveUp <= Time.timeSinceLevelLoad)

                    enemyState = EnemyState.Patrol;
                timeUntilAttack = Time.timeSinceLevelLoad + alertTime;
            }
        }    
    }

In this example, the agent will continue to check for the player’s position every time it is refreshed. If the player moves out of sight, the AI will begin counting down before ‘giving up’, and continuing patrolling. If the player can’t break line of sight, the enemy will enter the Attack phase

Attack

public void Attack()
    {
        // cast a ray at the player
        RaycastHit hit;

        if (Physics.Raycast(transform.position, (player.transform.position - transform.position), out hit))
        {
            // Check if the player
            if (hit.collider.gameObject.tag == "Player")
            {
                playerInView = true;

                // Update the destination with the newly established position
                agent.SetDestination(player.transform.position);

                lastKnownPosition = player.transform.position;

                if (Vector3.Distance(transform.position, player.transform.position) < 0.2f)
                {
                    if (nextAttack < Time.timeSinceLevelLoad)
                    {
                       // Hit the player and prepare for the next attack
                       Hit();

                        nextAttack = Time.timeSinceLevelLoad + attackInterval;
                    }
                }
            }
            else
            {
                playerInView = false;

                agent.SetDestination(lastKnownPosition);
            }

            //Perform functions if the player is withinsight of this AI
            if(playerInView == false)
            {
                // check if the enemy reaches the player's lastknownlocation

                if (Vector3.Distance(transform.position, agent.destination) < 1)
                {
                    // Decrease the certainty over time, before reaching the lowest possible threshold
                    certainty -= 0.05f;

                    // Since the player can't be found, count down to returning to patrol
                    if (certainty < 0.25f)
                    {
                        certainty = 0;

                        // if the player isn't found, return to patrolling
                        enemyState = EnemyState.Patrol;
                    }
                }
            }
        }
    }

In the Attack function, the AI will continuously check for the position of the player ad update its destination with the next one. If the agent gets close enough to the player, it will execute a “Hit” function to deal damage to them. If the player can’t be found, and the agent has reached the player’s last known location, then the agent’s certainty value will decrease each AI refresh. When the certainty has reached the lowest threshold, it will return to 0, and the Agent will return to patrolling.

Screen Shot 2016-12-13 at 11.39.02 pm.png

This AI system is work-in-progress but provides all of the features that I need to test Introspect’s gameplay through various stages. There is definitely a lot of optimisation that needs to be done to ensure this system is reliable and watertight. For now, however, this is my first foray into approaching artificial intelligence systems in games.

The full script is available here

 

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s