using System.Collections.Generic; using UnityEngine; using MalbersAnimations.Events; using UnityEngine.AI; using MalbersAnimations.Scriptables; using UnityEngine.Serialization; using System; using System.Linq; #if UNITY_EDITOR using UnityEditor; #endif namespace MalbersAnimations.Controller.AI { [AddComponentMenu("Malbers/Animal Controller/AI/Animal Brain")] public class MAnimalBrain : MonoBehaviour, IAnimatorListener { /// Reference for the Ai Control Movement public IAIControl AIControl; [Obsolete("Use AIControl Instead")] public IAIControl AIMovement => AIControl; /// Transform used to raycast Rays to interact with the world [RequiredField, Tooltip("Transform used to raycast Rays to interact with the world")] public Transform Eyes; /// Time needed to make a new transition. Necesary to avoid Changing to multiple States in the same frame [Tooltip("Time needed to make a new transition. Necessary to avoid Changing to multiple States in the same frame")] public FloatReference TransitionCoolDown = new FloatReference(0.2f); /// Reference AI State for the animal [CreateScriptableAsset] public MAIState currentState; [Tooltip("Removes all AI Components when the Animal Dies. (Brain, AiControl, Agent)")] [FormerlySerializedAs("RemoveAIOnDeath")] public bool DisableAIOnDeath = true; public bool debug = false; public bool debugAIStates = false; public IntEvent OnTaskStarted = new IntEvent(); public IntEvent OnDecisionSucceeded = new IntEvent(); public IntEvent OnAIStateChanged = new IntEvent(); /// Last Time the Animal make a new transition private float TransitionLastTime; /// Last Time the Animal started a transition public float StateLastTime { get; set; } /// Check if all the Task are done.. public bool AllTasksDone() { foreach (var done in TasksDone) { if (!done) return false; } return true; } /// Check if an Specific Task is Done.. public bool IsTasksDone(int index) { return TasksDone[index % TasksDone.Length]; } /// Tasks Local Vars (1 Int,1 Bool,1 Float) public BrainVars[] TasksVars; /// Saves on the a Task that it has finish is stuff internal bool[] TasksDone; /// Current Decision Results internal bool[] DecisionResult; /// Store if a Task has Started internal bool[] TasksStarted; /// Decision Local Vars to store values on Prepare Decision public BrainVars[] DecisionsVars; internal bool BrainInitialize; #region Properties /// Reference for the Animal public MAnimal Animal { get; private set; } /// Reference for the AnimalStats public Dictionary AnimalStats { get; set; } #region Target References /// Reference for the Current Target the Animal is using public Transform Target { get; set; } //{ // get => target; // set // { // target = value; // } //} //private Transform target; /// Reference for the Target the Animal Component public MAnimal TargetAnimal { get; set; } public Vector3 Position => AIControl.Transform.position; public float AIHeight => Animal.transform.lossyScale.y * AIControl.StoppingDistance; /// True if the Current Target has Stats public bool TargetHasStats { get; private set; } /// Reference for the Target the Stats Component public Dictionary TargetStats { get; set; } #endregion /// Reference for the Last WayPoint the Animal used public IWayPoint LastWayPoint { get; set; } /// Time Elapsed for the Tasks on an AI State public float[] TasksStartTime { get; set; } public float[] TasksUpdateTime { get; set; } /// Time Elapsed for the State Decisions [HideInInspector] public float[] DecisionsTime;// { get; set; } #endregion #region Unity Callbakcs void Awake() { if (Animal == null) Animal = gameObject.FindComponent(); if (AIControl == null) AIControl = gameObject.FindInterface(); var AnimalStatscomponent = Animal.FindComponent(); if (AnimalStatscomponent) AnimalStats = AnimalStatscomponent.stats_D; Animal.isPlayer.Value = false; //If is using a brain... disable that he is the main player ResetVarsOnNewState(); } void OnEnable() { //AIMovement.OnTargetArrived.AddListener(OnTargetArrived); //AIMovement.OnTargetPositionArrived.AddListener(OnPositionArrived); AIControl.TargetSet.AddListener(OnTargetSet); AIControl.OnArrived.AddListener(OnTargetArrived); Animal.OnStateChange.AddListener(OnAnimalStateChange); Animal.OnStanceChange.AddListener(OnAnimalStanceChange); Animal.OnModeStart.AddListener(OnAnimalModeStart); Animal.OnModeEnd.AddListener(OnAnimalModeEnd); Invoke(nameof(StartBrain), 0.1f); //Start AI a Frame later; } void OnDisable() { //AIMovement.OnTargetArrived.RemoveListener(OnTargetArrived); //AIMovement.OnTargetPositionArrived.RemoveListener(OnPositionArrived); AIControl.TargetSet.RemoveListener(OnTargetSet); AIControl.OnArrived.RemoveListener(OnTargetArrived); Animal.OnStateChange.RemoveListener(OnAnimalStateChange); Animal.OnStanceChange.RemoveListener(OnAnimalStanceChange); Animal.OnModeStart.RemoveListener(OnAnimalModeStart); Animal.OnModeEnd.RemoveListener(OnAnimalModeEnd); // AIControl.Stop(); StopAllCoroutines(); if (currentState) { for (int i = 0; i < currentState.tasks.Length; i++) //Exit the Current Tasks currentState.tasks[i]?.ExitAIState(this, i); } BrainInitialize = false; } void Update() { if (BrainInitialize && currentState != null) currentState.Update_State(this); } #endregion public void StartBrain() { if (currentState) { for (int i = 0; i < currentState.tasks.Length; i++) { if (currentState.tasks[i] == null) { Debug.LogError($"The [{currentState.name}] AI State has an Empty Task. AI States can't have empty Tasks. {Animal.name}", currentState); // enabled = false; return; }; } StartNewState(currentState); } else { enabled = false; return; } AIControl.AutoNextTarget = false; LastWayPoint = null; if (AIControl.Target) SetLastWayPoint(AIControl.Target); BrainInitialize = true; } public virtual void TransitionToState(MAIState nextState, bool decisionValue, MAIDecision decision, int Index) { if (MTools.ElapsedTime(TransitionLastTime, TransitionCoolDown)) //This avoid making transition on the same Frame ****IMPORTANT { if (nextState != null && nextState != currentState) //Do not transition to itself! { TransitionLastTime = Time.time; decision.FinishDecision(this, Index); Debuging($"Changed AI State from [{currentState.name}] to" + $" [{nextState.name}]. Decision: [{decision.name}] = [{decisionValue}].", currentState); InvokeDecisionEvent(decisionValue, decision); StartNewState(nextState); } } } protected virtual void Debuging(string Log, UnityEngine.Object val) { if (debug) Debug.Log($"[{Animal.name}] - " + Log,val); } private void InvokeDecisionEvent(bool decisionValue, MAIDecision decision) { if (decision.send == MAIDecision.WSend.SendTrue && decisionValue) { OnDecisionSucceeded.Invoke(decision.DecisionID); } else if (decision.send == MAIDecision.WSend.SendFalse && !decisionValue) { OnDecisionSucceeded.Invoke(decision.DecisionID); } } public virtual void StartNewState(MAIState newState) { if (!enabled) enabled = true; //Make sure the Brain is enabled!!!! IMPORTANT StateLastTime = Time.time; //Store the last time the Animal made a transition if (currentState != null && currentState != newState) { currentState.Finish_Tasks(this); //Finish all the Task on the Current State // currentState.Finish_Decisions(this); //Finish all the Decisions on the Current State } currentState = newState; //Set a new State ResetVarsOnNewState(); OnAIStateChanged.Invoke(currentState.ID); currentState.Start_AIState(this); //Start all Tasks on the new State currentState.Prepare_Decisions(this); //Start all Tasks on the new State Debuging($" Set AI State [{currentState.name}] ", currentState); } /// Prepare all the local variables on the New State before starting new tasks private void ResetVarsOnNewState() { if (currentState) { var tasks = (currentState.transitions != null && currentState.tasks.Length > 0) ? currentState.tasks.Length : 1; var transitions = (currentState.transitions != null && currentState.transitions.Length > 0) ? currentState.transitions.Length : 1; TasksVars = new BrainVars[tasks]; //Local Variables you can use on your tasks TasksUpdateTime = new float[tasks]; //Reset all the Tasks Time elapsed time TasksStartTime = new float[tasks]; //Reset all the Tasks Time elapsed time TasksDone = new bool[tasks]; //Reset if they are Done TasksStarted = new bool[tasks]; //Reset if they tasks are started DecisionsVars = new BrainVars[transitions]; //Local Variables you can use on your Decisions DecisionsTime = new float[transitions]; //Reset all the Decisions Time elapsed time DecisionResult = new bool[transitions]; //Reset if they tasks are started } } public bool IsTaskDone(int TaskIndex) => TasksDone[TaskIndex]; public void TaskDone(int TaskIndex, bool value = true) //If the first task is done then go and do the next one { TasksDone[TaskIndex] = value; if (TaskIndex + 1 < currentState.tasks.Length && currentState.tasks[TaskIndex + 1].WaitForPreviousTask) //Start the next task that needs to wait for the previus one { // Debug.Log($"*Task DONE!!!!: [{name}] [{TaskIndex}]-[{currentState.tasks[TaskIndex].name }]"); currentState.StartWaitforPreviusTask(this, TaskIndex + 1); } } /// Check if the time elapsed of a task using a duration or CountDown time /// Duration of the countDown|CoolDown /// Index of the Task on the AI State Tasks list public bool CheckIfDecisionsCountDownElapsed(float duration, int index) { DecisionsTime[index] += Time.deltaTime; return DecisionsTime[index] >= duration; } /// Set the time on which a task has started on the current AI State public void SetTaskStartTime(int Index) { TasksStartTime[Index] = Time.time; } /// Reset the Time elapsed on a Decision using its index from the Transition List /// Index of the Decision on the AI State Transition List public void ResetDecisionTime(int Index) => DecisionsTime[Index] = 0; public virtual bool OnAnimatorBehaviourMessage(string message, object value) => this.InvokeWithParams(message, value); #region SelfAnimal Event Listeners void OnAnimalStateChange(int state) { currentState?.OnAnimalStateEnter(this, Animal.ActiveState); currentState?.OnAnimalStateExit(this, Animal.LastState); if (state == StateEnum.Death) //meaning this animal has died { for (int i = 0; i < currentState.tasks.Length; i++) //Exit the Current Tasks currentState.tasks[i].ExitAIState(this, i); enabled = false; if (DisableAIOnDeath) { AIControl.SetActive(false); enabled = false; } } } void OnAnimalStanceChange(int stance) => currentState.OnAnimalStanceChange(this, Animal.Stance.ID); void OnAnimalModeStart(int mode, int ability) => currentState.OnAnimalModeStart(this, Animal.ActiveMode); void OnAnimalModeEnd(int mode, int ability) => currentState.OnAnimalModeEnd(this, Animal.ActiveMode); #endregion #region TargetAnimal Event Listeners //void OnTargetAnimalStateChange(int state) //{ // currentState.OnTargetAnimalStateEnter(this, Animal.ActiveState); // currentState.OnTargetAnimalStateExit(this, Animal.LastState); //} private void OnTargetArrived(Transform target) => currentState.OnTargetArrived(this, target); //private void OnPositionArrived(Vector3 position) => currentState.OnPositionArrived(this, position); #endregion /// Stores if the Current Target is an Animal and if it has the Stats component private void OnTargetSet(Transform target) { Target = target; if (target) { TargetAnimal = target.FindComponent();// ?? target.GetComponentInChildren(); TargetStats = null; var TargetStatsC = target.FindComponent();// ?? target.GetComponentInChildren(); TargetHasStats = TargetStatsC != null; if (TargetHasStats) TargetStats = TargetStatsC.stats_D; } } public bool CheckForPreviusTaskDone(int index) { if (index == 0) return true; if (!TasksStarted[index] && IsTaskDone(index - 1)) return true; return false; } public void SetLastWayPoint(Transform target) { var newLastWay = target.gameObject.FindInterface(); if (newLastWay != null) LastWayPoint = target?.gameObject.FindInterface(); //If not is a waypoint save the last one } [SerializeField] private int Editor_Tabs1; #if UNITY_EDITOR void Reset() { // remainInState = MTools.GetInstance("Remain in State"); AIControl = this.FindComponent(); if (AIControl != null) { AIControl.AutoNextTarget = false; AIControl.UpdateDestinationPosition = false; AIControl.LookAtTargetOnArrival = false; if (Animal) Animal.isPlayer.Value = false; //Make sure this animal is not the Main Player } else { Debug.LogWarning("There's No AI Control in this GameObject, Please add one"); } } void OnDrawGizmos() { if (isActiveAndEnabled && currentState && Eyes) { Gizmos.color = currentState.GizmoStateColor; Gizmos.DrawWireSphere(Eyes.position, 0.2f); if (debug) { if (currentState != null) { if (currentState.tasks != null) foreach (var task in currentState.tasks) task?.DrawGizmos(this); if (currentState.transitions != null) foreach (var tran in currentState.transitions) tran?.decision?.DrawGizmos(this); } } if (Application.isPlaying && debugAIStates) { string desicions = ""; var Styl = new GUIStyle(EditorStyles.boldLabel); Styl.normal.textColor = Color.yellow; UnityEditor.Handles.Label(Eyes.position, "State: " + currentState.name + desicions, Styl); } } } #endif } public enum Affected { Self, Target } public enum ExecuteTask { OnStart, OnUpdate, OnExit } [System.Serializable] public struct BrainVars { public int intValue; public float floatValue; public bool boolValue; public Vector3 vector3; public Component[] Components; public GameObject[] gameobjects; public Dictionary ints; public Dictionary floats; public Dictionary bools; public void SetVar(int key, bool value) => bools[key] = value; public void SetVar(int key, int value) => ints[key] = value; public void SetVar(int key, float value) => floats[key] = value; public bool GetBool(int key) => bools[key]; public int GetInt(int key) => ints[key]; public float GetFloat(int key) => floats[key]; public bool TryGetBool(int key, out bool value) => bools.TryGetValue(key, out value); public bool TryGetInt(int key, out int value) => ints.TryGetValue(key, out value); public bool TryGetFloat(int key, out float value) => floats.TryGetValue(key, out value); public void AddVar(int key, bool value) { if (bools == null) bools = new Dictionary(); bools.Add(key, value); } public void AddVar(int key, int value) { if (bools == null) bools = new Dictionary(); ints.Add(key, value); } public void AddVar(int key, float value) { if (bools == null) bools = new Dictionary(); floats.Add(key, value); } public void AddComponents(Component[] components) { if (Components == null || Components.Length == 0) Components = components; else { Components = Components.Concat(components).ToArray(); } } public void AddComponent(Component comp) { if (Components == null || Components.Length == 0) Components = new Component[1] { comp }; else { var ComponentsL = Components.ToList(); ComponentsL.Add(comp); Components = ComponentsL.ToArray(); } } } #if UNITY_EDITOR [CustomEditor(typeof(MAnimalBrain)), CanEditMultipleObjects] public class MAnimalBrainEditor : Editor { SerializedProperty Eyes, debug, TransitionCoolDown, DisableAIOnDeath, Editor_Tabs1, debugAIStates, currentState, OnTaskStarted, OnDecisionSucceded, OnAIStateChanged; protected string[] Tabs1 = new string[] { "AI States" , "Events" ,"Debug"}; MAnimalBrain M; private void OnEnable() { M = (MAnimalBrain)target; Eyes = serializedObject.FindProperty("Eyes"); TransitionCoolDown = serializedObject.FindProperty("TransitionCoolDown"); DisableAIOnDeath = serializedObject.FindProperty("DisableAIOnDeath"); currentState = serializedObject.FindProperty("currentState"); OnTaskStarted = serializedObject.FindProperty("OnTaskStarted"); OnDecisionSucceded = serializedObject.FindProperty("OnDecisionSucceeded"); OnAIStateChanged = serializedObject.FindProperty("OnAIStateChanged"); Editor_Tabs1 = serializedObject.FindProperty("Editor_Tabs1"); debug = serializedObject.FindProperty("debug"); // AISource = serializedObject.FindProperty("AISource"); debugAIStates = serializedObject.FindProperty("debugAIStates"); } public override void OnInspectorGUI() { serializedObject.Update(); MalbersEditor.DrawDescription("Brain Logic for the Animal"); EditorGUILayout.BeginVertical(MTools.StyleGray); { Editor_Tabs1.intValue = GUILayout.Toolbar(Editor_Tabs1.intValue, Tabs1); if (Editor_Tabs1.intValue == 0) DrawGeneral(); else if (Editor_Tabs1.intValue == 1) DrawEvents(); else DrawDebug(); if (Eyes.objectReferenceValue == null) EditorGUILayout.HelpBox("The AI Eyes [Reference] is missing. Please add a transform the AI Eyes parameters", MessageType.Error); } EditorGUILayout.EndVertical(); serializedObject.ApplyModifiedProperties(); } private void DrawDebug() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); { EditorGUILayout.PropertyField(debugAIStates, new GUIContent("Debug On Screen")); if (Application.isPlaying) { EditorGUI.BeginDisabledGroup(true); Repaint(); EditorGUILayout.ObjectField("Brain Target", M.Target, typeof(Transform), false); if (M.enabled && M.BrainInitialize && M.currentState != null) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); { EditorGUILayout.ObjectField("AI State", M.currentState, typeof(MAIState), false); EditorGUILayout.LabelField("Tasks", EditorStyles.boldLabel); for (int i = 0; i < M.currentState.tasks.Length; i++) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.ObjectField(GUIContent.none, M.currentState.tasks[i], typeof(MTask), false, GUILayout.MinWidth(100)); EditorGUILayout.LabelField($" Started: {(M.TasksStarted[i] ? "☑" : "[ ]")}. Done: {(M.TasksDone[i] ? "☑" : "[ ]")}", GUILayout.MinWidth(100)); EditorGUILayout.LabelField($"Start Time: {M.TasksStartTime[i]:F2}", GUILayout.MinWidth(50)); EditorGUILayout.EndHorizontal(); } EditorGUILayout.BeginVertical(EditorStyles.helpBox); { EditorGUILayout.LabelField("Task Variables", EditorStyles.boldLabel); for (int i = 0; i < M.currentState.tasks.Length; i++) { var TasksVars = serializedObject.FindProperty("TasksVars"); if (TasksVars != null && TasksVars.arraySize > i) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(TasksVars.GetArrayElementAtIndex(i), new GUIContent(M.currentState.tasks[i].name), true); EditorGUI.indentLevel--; } } } EditorGUILayout.EndVertical(); } EditorGUILayout.EndVertical(); } EditorGUILayout.Space(); EditorGUILayout.BeginVertical(EditorStyles.helpBox); { EditorGUILayout.LabelField("Decision Variables", EditorStyles.boldLabel); for (int i = 0; i < M.currentState.transitions.Length; i++) { var DecisionsVars = serializedObject.FindProperty("DecisionsVars"); var Des = M.currentState.transitions[i].decision; var waiting = ""; if (Des.WaitForAllTasks && !M.AllTasksDone()) waiting = "[WAIT T*]"; if (Des.waitForTask != -1 && !M.IsTaskDone(Des.waitForTask)) waiting = "[WAIT T]"; EditorGUILayout.ObjectField($"Decision [{i }] {waiting}", Des, typeof(MAIDecision), false, GUILayout.MinWidth(100)); if (DecisionsVars != null && DecisionsVars.arraySize > i) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(DecisionsVars.GetArrayElementAtIndex(i), new GUIContent(M.currentState.transitions[i].decision.name), true); EditorGUI.indentLevel--; } } } EditorGUILayout.EndVertical(); EditorGUI.EndDisabledGroup(); } } EditorGUILayout.EndVertical(); } private void DrawGeneral() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.PropertyField(Eyes); EditorGUILayout.BeginHorizontal(); EditorGUILayout.PropertyField(currentState); MalbersEditor.DrawDebugIcon(debug); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(2); // EditorGUILayout.PropertyField(remainInState); EditorGUILayout.PropertyField(TransitionCoolDown); EditorGUILayout.PropertyField(DisableAIOnDeath); EditorGUILayout.EndVertical(); } private void DrawEvents() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.PropertyField(OnAIStateChanged); EditorGUILayout.PropertyField(OnTaskStarted); EditorGUILayout.PropertyField(OnDecisionSucceded); EditorGUILayout.EndVertical(); } } #endif }