Sleight - Artificial Intelligence

The enemies in Sleight were designed to feel fair to fight and avoid overwhelming the player, while also simplifying the creation and modification of the AI's behaviours and choices. For this, each enemy is equipped with an AIController component, which decides what behaviours should be performed and when to transition between them. These AIControllers have access to a singleton AIManager which is in charge of providing 'tokens' to AIControllers, providing them access to specialised behaviours, and can be limited to prevent overwhelming enemy attacks.


AIController

Each enemy has an AIController component attached that contains the references and data exclusive to this AI, such as movement speed, cooldowns and conditions for requesting tokens, the current target, and references to the behaviour trees to be used, both for general actions and when successfully granted a token. The AIController updates its current state and controls transitioning between these states.

Below is the method the AIController uses to find and change to new states:

protected void GotoNextState(bool finished)	 
{
	// If controller has an AITree
	if (aiTree)
	{
		AIState nextState = null;
        
		// Try to get next state from current state
		if (CurrentState)
			nextState = CurrentState.GetNextState(this, finished);
	
		// If finished previous state, and got no state from previous state
		if (finished && !nextState)
		{
			// Return token if it has one
			if (HasToken)
				ReturnToken();
	
			// Get next state from AITree
			nextState = aiTree.GetState(this);
		}
	
		// If found nextState, and is either not the same as current state,
		// or state is marked as repeatable
		if (nextState && (nextState != CurrentState || nextState.repeatable))
		{
			// Change State to new state
			ChangeState(nextState);
		}
    }
}

AIManager

The AIManager is a singleton component that exists in each scene with enemies, informing them when they are allowed to attack, ensuring players are not overwhelmed. AIControllers request tokens from the AIManager which informs them if one is currently available. If so the AIController enters its inputted token state, which usually results in the enemy attacking the player. If the AIManager has no tokens it adds the enemy to a list of requests, which it uses to provide tokens when more become available.

When AIControllers finish performing their token actions they inform the manager which adds it back to the available tokens, after a short duration. After a token becomes available the AIManager sorts its list of current requests based on a priority, calculated from the time since their last token, whether the player is currently looking towards them, and a personal modifier. This combination ensures each enemy receives opportunities to attack, but also limits the number of attacks from behind the player, allowing them to focus on the more immediate threats. To a player, these systems are unclear but create a more enjoyable and fair experience.


States, Behaviours and Transitions

The actual states, actions and conditions of the AI system are created using Unity's Scriptable Objects. This allows them to be created as assets in the project's file system simplifying the modification of their data, as well as allowing them to be easily passed to the AIControllers and other states.

The AIState is the main class for storing and managing the actions to be performed by the AI. Each state contains references to its actions as well as transitions to other states. The AIController component attached to each enemy updates each state, performing the appropriate actions and manages switching between these states.

Each state contains references to actions and behaviours, which contain the logic for controlling the AIControllers. Some examples of actions created for the enemies in Sleight include chasing, fleeing, attacking and strafing. Every action derives from a base abstract AIAction class, with each overriding the abstract Act method. Behaviours are developed the same way, but with virtual Enter, Exit and Update methods. States contain lists of the actions to be performed when the state is entered, exited, and updated, as well as behaviours, which perform their own enter, exit and update functions. Additionally, these actions can use the power system developed for Sleight's card spells, to cast their own abilities, to easily allow for interesting attacks and actions.

The code for one action, 'Charge', is displayed below:

public override void Act(AIController controller)
{
	NavMeshPath path = new NavMeshPath();
    
	// If controller has a target
	if (controller.target.Value != null)
	{
		// Get the x,z direction from the AIController to their target
		Vector3 targetPosition = controller.transform.position;
		Vector3 direction = controller.target.Value.position - targetPosition;
	 	direction.y = 0;
		direction.Normalize();

		// Find a position a set distance behind the target to charge to
		Vector3 endPos = targetPosition + direction * chargeOvershoot;
		NavMeshHit hit = new NavMeshHit();
        
	 	// NavMeshAgent raycast to find any objects hit on this path
		if (controller.Agent.Raycast(endPos, out hit))
	 		endPos = hit.position;
	
		// Calculate and set path
		if (controller.Agent.isOnNavMesh &&
			!controller.Agent.isOnOffMeshLink &&
			controller.Agent.CalculatePath(endPos, path))
		{
			controller.Agent.SetPath(path);
		}
	}
}

Lastly, conditions are also written similarly to actions, with an abstract Decide method, overridden by each derived class. These conditions, are passed into transitions on states and on conditional behaviour tree nodes to determine when it can access a new state.