using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using Random = UnityEngine.Random; namespace MoreMountains.Tools { /// /// the AI brain is responsible from going from one state to the other based on the defined transitions. It's basically just a collection of states, and it's where you'll link all the actions, decisions, states and transitions together. /// [AddComponentMenu("More Mountains/Tools/AI/AIBrain")] public class AIBrain : MonoBehaviour { [Header("Debug")] /// the owner of that AI Brain, usually the associated character [MMReadOnly] public GameObject Owner; /// the collection of states public List States; /// this brain's current state public AIState CurrentState { get; protected set; } /// the time we've spent in the current state [MMReadOnly] public float TimeInThisState; /// the current target [MMReadOnly] public Transform Target; /// the last known world position of the target [MMReadOnly] public Vector3 _lastKnownTargetPosition = Vector3.zero; [Header("State")] /// whether or not this brain is active public bool BrainActive = true; public bool ResetBrainOnStart = true; public bool ResetBrainOnEnable = false; [Header("Frequencies")] /// the frequency (in seconds) at which to perform actions (lower values : higher frequency, high values : lower frequency but better performance) public float ActionsFrequency = 0f; /// the frequency (in seconds) at which to evaluate decisions public float DecisionFrequency = 0f; /// whether or not to randomize the action and decision frequencies public bool RandomizeFrequencies = false; /// the min and max values between which to randomize the action frequency [MMVector("min","max")] public Vector2 RandomActionFrequency = new Vector2(0.5f, 1f); /// the min and max values between which to randomize the decision frequency [MMVector("min","max")] public Vector2 RandomDecisionFrequency = new Vector2(0.5f, 1f); protected AIDecision[] _decisions; protected AIAction[] _actions; protected float _lastActionsUpdate = 0f; protected float _lastDecisionsUpdate = 0f; protected AIState _initialState; public virtual AIAction[] GetAttachedActions() { AIAction[] actions = this.gameObject.GetComponentsInChildren(); return actions; } public virtual AIDecision[] GetAttachedDecisions() { AIDecision[] decisions = this.gameObject.GetComponentsInChildren(); return decisions; } protected virtual void OnEnable() { if (ResetBrainOnEnable) { ResetBrain(); } } /// /// On awake we set our brain for all states /// protected virtual void Awake() { foreach (AIState state in States) { state.SetBrain(this); } _decisions = GetAttachedDecisions(); _actions = GetAttachedActions(); if (RandomizeFrequencies) { ActionsFrequency = Random.Range(RandomActionFrequency.x, RandomActionFrequency.y); DecisionFrequency = Random.Range(RandomDecisionFrequency.x, RandomDecisionFrequency.y); } } /// /// On Start we set our first state /// protected virtual void Start() { if (ResetBrainOnStart) { ResetBrain(); } } /// /// Every frame we update our current state /// protected virtual void Update() { if (!BrainActive || (CurrentState == null) || (Time.timeScale == 0f)) { return; } if (Time.time - _lastActionsUpdate > ActionsFrequency) { CurrentState.PerformActions(); _lastActionsUpdate = Time.time; } if (!BrainActive) { return; } if (Time.time - _lastDecisionsUpdate > DecisionFrequency) { CurrentState.EvaluateTransitions(); _lastDecisionsUpdate = Time.time; } TimeInThisState += Time.deltaTime; StoreLastKnownPosition(); } /// /// Transitions to the specified state, trigger exit and enter states events /// /// public virtual void TransitionToState(string newStateName) { if (CurrentState == null) { CurrentState = FindState(newStateName); if (CurrentState != null) { CurrentState.EnterState(); } return; } if (newStateName != CurrentState.StateName) { CurrentState.ExitState(); OnExitState(); CurrentState = FindState(newStateName); if (CurrentState != null) { CurrentState.EnterState(); } } } /// /// When exiting a state we reset our time counter /// protected virtual void OnExitState() { TimeInThisState = 0f; } /// /// Initializes all decisions /// protected virtual void InitializeDecisions() { if (_decisions == null) { _decisions = GetAttachedDecisions(); } foreach(AIDecision decision in _decisions) { decision.Initialization(); } } /// /// Initializes all actions /// protected virtual void InitializeActions() { if (_actions == null) { _actions = GetAttachedActions(); } foreach(AIAction action in _actions) { action.Initialization(); } } /// /// Returns a state based on the specified state name /// /// /// protected AIState FindState(string stateName) { foreach (AIState state in States) { if (state.StateName == stateName) { return state; } } if (stateName != "") { Debug.LogError("You're trying to transition to state '" + stateName + "' in " + this.gameObject.name + "'s AI Brain, but no state of this name exists. Make sure your states are named properly, and that your transitions states match existing states."); } return null; } /// /// Stores the last known position of the target /// protected virtual void StoreLastKnownPosition() { if (Target != null) { _lastKnownTargetPosition = Target.transform.position; } } /// /// Resets the brain, forcing it to enter its first state /// public virtual void ResetBrain() { InitializeDecisions(); InitializeActions(); BrainActive = true; this.enabled = true; if (CurrentState != null) { CurrentState.ExitState(); OnExitState(); } if (States.Count > 0) { CurrentState = States[0]; CurrentState?.EnterState(); } } } }