Unity Learn home
View Tutorial Content
Steps

Pluggable AI With Scriptable Objects

Tutorial
Intermediate
1 Hour35 Mins
(73)
Summary
In this recorded live session, we create a finite state machine-based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
Select your Unity version
Last updated: December 09, 2021
5.x
Language
English

1.Intro and Session Goals

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action
using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

2.Finite State AI with the Delegate Pattern

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

3.Action and State Base Classes

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

4.Patrol Action

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

5.Decisions and Looking

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

6.Transitions Between States

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

7.Chasing Until Target Is Dead

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

8.Attack Action

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

9.Scan Decision

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

10.Questions and Answers

In this session we will look at creating a finite state machine based AI system which can be configured in Unity’s inspector using ScriptableObjects for states, actions and transitions between those states.
This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.
Type caption for embed (optional)

Action

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Action : ScriptableObject { public abstract void Act (StateController controller); }

State

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/State")] public class State : ScriptableObject { public Action[] actions; public Transition[] transitions; public Color sceneGizmoColor = Color.grey; public void UpdateState(StateController controller) { DoActions (controller); CheckTransitions (controller); } private void DoActions(StateController controller) { for (int i = 0; i < actions.Length; i++) { actions [i].Act (controller); } } private void CheckTransitions(StateController controller) { for (int i = 0; i < transitions.Length; i++) { bool decisionSucceeded = transitions [i].decision.Decide (controller); if (decisionSucceeded) { controller.TransitionToState (transitions [i].trueState); } else { controller.TransitionToState (transitions [i].falseState); } } } }

StateController

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Complete; public class StateController : MonoBehaviour { public State currentState; public EnemyStats enemyStats; public Transform eyes; public State remainState; [HideInInspector] public NavMeshAgent navMeshAgent; [HideInInspector] public Complete.TankShooting tankShooting; [HideInInspector] public List<Transform> wayPointList; [HideInInspector] public int nextWayPoint; [HideInInspector] public Transform chaseTarget; [HideInInspector] public float stateTimeElapsed; private bool aiActive; void Awake () { tankShooting = GetComponent<Complete.TankShooting> (); navMeshAgent = GetComponent<NavMeshAgent> (); } public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager) { wayPointList = wayPointsFromTankManager; aiActive = aiActivationFromTankManager; if (aiActive) { navMeshAgent.enabled = true; } else { navMeshAgent.enabled = false; } } void Update() { if (!aiActive) return; currentState.UpdateState (this); } void OnDrawGizmos() { if (currentState != null && eyes != null) { Gizmos.color = currentState.sceneGizmoColor; Gizmos.DrawWireSphere (eyes.position, enemyStats.lookSphereCastRadius); } } public void TransitionToState(State nextState) { if (nextState != remainState) { currentState = nextState; OnExitState (); } } public bool CheckIfCountDownElapsed(float duration) { stateTimeElapsed += Time.deltaTime; return (stateTimeElapsed >= duration); } private void OnExitState() { stateTimeElapsed = 0; } }

PatrolAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Patrol")] public class PatrolAction : Action { public override void Act(StateController controller) { Patrol (controller); } private void Patrol(StateController controller) { controller.navMeshAgent.destination = controller.wayPointList [controller.nextWayPoint].position; controller.navMeshAgent.Resume (); if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending) { controller.nextWayPoint = (controller.nextWayPoint + 1) % controller.wayPointList.Count; } } }

Decision

using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Decision : ScriptableObject { public abstract bool Decide (StateController controller); }

LookDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Look")] public class LookDecision : Decision { public override bool Decide(StateController controller) { bool targetVisible = Look(controller); return targetVisible; } private bool Look(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.lookRange, Color.green); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.lookRange) && hit.collider.CompareTag ("Player")) { controller.chaseTarget = hit.transform; return true; } else { return false; } } }

Transition

using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Transition { public Decision decision; public State trueState; public State falseState; }

ChaseAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Chase")] public class ChaseAction : Action { public override void Act (StateController controller) { Chase (controller); } private void Chase(StateController controller) { controller.navMeshAgent.destination = controller.chaseTarget.position; controller.navMeshAgent.Resume (); } }

ActiveStateDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/ActiveState")] public class ActiveStateDecision : Decision { public override bool Decide (StateController controller) { bool chaseTargetIsActive = controller.chaseTarget.gameObject.activeSelf; return chaseTargetIsActive; } }

AttackAction

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Actions/Attack")] public class AttackAction : Action { public override void Act (StateController controller) { Attack (controller); } private void Attack(StateController controller) { RaycastHit hit; Debug.DrawRay (controller.eyes.position, controller.eyes.forward.normalized * controller.enemyStats.attackRange, Color.red); if (Physics.SphereCast (controller.eyes.position, controller.enemyStats.lookSphereCastRadius, controller.eyes.forward, out hit, controller.enemyStats.attackRange) && hit.collider.CompareTag ("Player")) { if (controller.CheckIfCountDownElapsed (controller.enemyStats.attackRate)) { controller.tankShooting.Fire (controller.enemyStats.attackForce, controller.enemyStats.attackRate); } } } }

ScanDecision

using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu (menuName = "PluggableAI/Decisions/Scan")] public class ScanDecision : Decision { public override bool Decide (StateController controller) { bool noEnemyInSight = Scan (controller); return noEnemyInSight; } private bool Scan(StateController controller) { controller.navMeshAgent.Stop (); controller.transform.Rotate (0, controller.enemyStats.searchingTurnSpeed * Time.deltaTime, 0); return controller.CheckIfCountDownElapsed (controller.enemyStats.searchDuration); } }

Pluggable AI With Scriptable Objects
Pluggable AI With Scriptable Objects
General Tutorial Discussion
0
5
1. Intro and Session Goals
1
3
2. Finite State AI with the Delegate Pattern
0
1
3. Action and State Base Classes
0
0
4. Patrol Action
0
0
5. Decisions and Looking
0
0
6. Transitions Between States
1
1
7. Chasing Until Target Is Dead
0
0
8. Attack Action
0
0
9. Scan Decision
0
0
10. Questions and Answers
0
0