/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.StateSystem { using Opsive.Shared.Events; using Opsive.Shared.Game; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; /// /// Handles the activation and deactivation of states. /// public class StateManager : MonoBehaviour { [Tooltip("Should the OnStateChange event be sent when the state changes active status?")] [SerializeField] protected bool m_SendStateChangeEvent; public bool SendStateChangeEvent { get { return m_SendStateChangeEvent; } set { m_SendStateChangeEvent = value; } } private static StateManager s_Instance; private static StateManager Instance { get { if (!s_Initialized) { s_Instance = new GameObject("State Manager").AddComponent(); s_Initialized = true; } return s_Instance; } } private static bool s_Initialized; private Dictionary> m_ObjectNameStateMap = new Dictionary>(); private Dictionary>> m_GameObjectNameStateList = new Dictionary>>(); private Dictionary> m_LinkedGameObjectList = new Dictionary>(); private Dictionary m_StateArrayMap = new Dictionary(); private Dictionary> m_ActiveCharacterStates = new Dictionary>(); private Dictionary> m_DisableStateTimerMap; /// /// The object has been enabled. /// private void OnEnable() { // The object may have been enabled outside of the scene unloading. if (s_Instance == null) { s_Instance = this; s_Initialized = true; SceneManager.sceneUnloaded -= SceneUnloaded; } } /// /// Initializes the states belonging to the owner on the GameObject. /// /// The GameObject to enable or disable all of the states on. /// The object that state belongs to. /// A list of all of the states which the owner contains. public static void Initialize(GameObject gameObject, IStateOwner owner, State[] states) { Instance.InitializeInternal(gameObject, owner, states); } /// /// Internal method which initializes the states belonging to the owner on the GameObject. /// /// The GameObject to enable or disable all of the states on. /// The object that state belongs to. /// A list of all of the states which the owner contains. private void InitializeInternal(GameObject gameObject, IStateOwner owner, State[] states) { // The last state will always be reserved for the default state. if (states[states.Length - 1] == null) { states[states.Length - 1] = new State("Default", true); } states[states.Length - 1].Preset = DefaultPreset.CreateDefaultPreset(); Dictionary nameStateMap; if (!m_ObjectNameStateMap.TryGetValue(owner, out nameStateMap)) { nameStateMap = new Dictionary(); m_ObjectNameStateMap.Add(owner, nameStateMap); } // Populate the maps for quick lookup based on owner and GameObject. GameObject characterGameObject = null; var characterLocomotion = gameObject.GetCachedParentComponent(); if (characterLocomotion != null) { characterGameObject = characterLocomotion.gameObject; } else { var cameraController = gameObject.GetCachedParentComponent(); if (cameraController != null) { characterGameObject = cameraController.Character; } } for (int i = 0; i < states.Length; ++i) { if (states[i].Preset == null) { Debug.LogError(string.Format("Error: The state {0} on {1} does not have a preset. Ensure each non-default state contains a preset.", states[i].Name, owner), owner as Object); } nameStateMap.Add(states[i].Name, states[i]); Dictionary> nameStateList; if (!m_GameObjectNameStateList.TryGetValue(gameObject, out nameStateList)) { nameStateList = new Dictionary>(); m_GameObjectNameStateList.Add(gameObject, nameStateList); } // Child GameObjects should listen for states set on the parent. This for example allows an item to react to a state change even if that state change // is set on the character. The character GameObject does not need to be made aware of the Default state. if (i != states.Length - 1) { if (characterGameObject != null && gameObject != characterGameObject) { Dictionary> characterNameStateList; if (!m_GameObjectNameStateList.TryGetValue(characterGameObject, out characterNameStateList)) { characterNameStateList = new Dictionary>(); m_GameObjectNameStateList.Add(characterGameObject, characterNameStateList); } List characterStateList; if (!characterNameStateList.TryGetValue(states[i].Name, out characterStateList)) { characterStateList = new List(); characterNameStateList.Add(states[i].Name, characterStateList); } characterStateList.Add(states[i]); } } List stateList; if (!nameStateList.TryGetValue(states[i].Name, out stateList)) { stateList = new List(); nameStateList.Add(states[i].Name, stateList); } stateList.Add(states[i]); m_StateArrayMap.Add(states[i], states); } // Initialize the state after the map has been created. for (int i = 0; i < states.Length; ++i) { states[i].Initialize(owner, nameStateMap); } // The default state is always last. states[states.Length - 1].Active = true; // Remember the active character states so if a GameObject is initialized after a state has already been activated that newly initialized GameObject // can start the correct states. As an example an item could be picked up after the character is already aiming. That item should go directly // into the aim state instead of requring the character to aim again. if (characterGameObject != null) { if (characterGameObject == gameObject) { // If the current GameObject is the character then the active states should be tracked. if (!m_ActiveCharacterStates.ContainsKey(gameObject)) { m_ActiveCharacterStates.Add(gameObject, new HashSet()); } } else { // If the current GameObject is not the character then the active character states should be applied to the child object. HashSet activeStates; if (m_ActiveCharacterStates.TryGetValue(characterGameObject, out activeStates)) { if (activeStates.Count > 0) { foreach (var stateName in activeStates) { SetState(gameObject, stateName, true); } } } } } } /// /// Links the original GameObject to the linked GameObject. When GameObjects are linked the state will be updated for each GameObject even when only the /// original GameObject is set. /// /// The original GameObject to link. /// The GameObject that should be linked to the original GameObject. /// Should the GameObjects be linked. If fales the GameObjects will be unlinked. public static void LinkGameObjects(GameObject original, GameObject linkedGameObject, bool link) { Instance.LinkGameObjectsInternal(original, linkedGameObject, link); } /// /// Internal method which links the original GameObject to the linked GameObject. When GameObjects are linked the state will be updated for each /// GameObject even when only the original GameObject is set. /// /// The original GameObject to link. /// The GameObject that should be linked to the original GameObject. /// Should the GameObjects be linked. If fales the GameObjects will be unlinked. private void LinkGameObjectsInternal(GameObject original, GameObject linkedGameObject, bool link) { List linkedGameObjectList; if (!m_LinkedGameObjectList.TryGetValue(original, out linkedGameObjectList) && link) { linkedGameObjectList = new List(); m_LinkedGameObjectList.Add(original, linkedGameObjectList); } if (linkedGameObjectList != null) { if (link) { linkedGameObjectList.Add(linkedGameObject); // If the current GameObject is not the character then the active character states should be applied to the child object. HashSet activeStates; if (m_ActiveCharacterStates.TryGetValue(original, out activeStates)) { if (activeStates.Count > 0) { foreach (var stateName in activeStates) { SetState(linkedGameObject, stateName, true); } } } } else { linkedGameObjectList.Remove(linkedGameObject); } } } /// /// Activates or deactivates the specified state. /// /// The object that state belongs to. /// A list of all of the states which the owner contains. /// The name of the state to change the active status of. /// Should the state be activated? public static void SetState(object owner, State[] states, string stateName, bool active) { Instance.SetStateInternal(owner, states, stateName, active); } /// /// Internal method which activates or deactivates the specified state. /// /// The object that state belongs to. /// A list of all of the states which the owner contains. /// The name of the state to change the active status of. /// Should the state be activated? private void SetStateInternal(object owner, State[] states, string stateName, bool active) { // Lookup the state by owner. Dictionary nameStateMap; if (!m_ObjectNameStateMap.TryGetValue(owner, out nameStateMap)) { Debug.LogWarning("Warning: Unable to find the name state map on object " + owner); return; } // Lookup the state by name. State state; if (!nameStateMap.TryGetValue(stateName, out state)) { Debug.LogWarning("Warning: Unable to find the state with name " + stateName); return; } // The state has been found, activate or deactivate the states. if (state.Active != active) { ActivateStateInternal(state, active, states); } } /// /// Activates or deactivates all of the states on the specified GameObject with the specified name. /// /// The GameObject to enable or disable all of the states on. /// The name of the state to change the active status of. /// Should the state be activated? public static void SetState(GameObject gameObject, string stateName, bool active) { Instance.SetStateInternal(gameObject, stateName, active); } /// /// Internal method which activates or deactivates all of the states on the specified GameObject with the specified name. /// /// The GameObject to enable or disable all of the states on. /// The name of the state to change the active status of. /// Should the state be activated? private void SetStateInternal(GameObject gameObject, string stateName, bool active) { // Remember the active character status. var characterLocomotion = gameObject.GetCachedComponent(); if (characterLocomotion != null) { HashSet activeStates; if (m_ActiveCharacterStates.TryGetValue(gameObject, out activeStates)) { // If the state name appears within the set then the state is active. if (active) { activeStates.Add(stateName); } else { activeStates.Remove(stateName); } } } // Lookup the states by GameObject. Dictionary> nameStateList; if (!m_GameObjectNameStateList.TryGetValue(gameObject, out nameStateList)) { SetLinkStateInternal(gameObject, stateName, active); return; } // Lookup the states by name. List stateList; if (!nameStateList.TryGetValue(stateName, out stateList)) { SetLinkStateInternal(gameObject, stateName, active); return; } // An event can be sent when the active status changes. This is useful for multiplayer in that it allows the networking implementation // to send the state changes across the network. if (m_SendStateChangeEvent) { EventHandler.ExecuteEvent("OnStateChange", gameObject, stateName, active); } // The states have been found, activate or deactivate the states. for (int i = 0; i < stateList.Count; ++i) { if (stateList[i].Active != active) { // The state array must exist to be able to apply the changes. State[] states; if (!m_StateArrayMap.TryGetValue(stateList[i], out states)) { Debug.LogWarning("Warning: Unable to find the state array with state name " + stateName); return; } // Notify the owner that the states will change. stateList[i].Owner.StateWillChange(); ActivateStateInternal(stateList[i], active, states); // Notify the owner that the state has changed. stateList[i].Owner.StateChange(); } } SetLinkStateInternal(gameObject, stateName, active); } /// /// Internal method which activates or deactivates all of the states on the GameObjects linked from the GameObject with the specified name. /// /// The GameObject to enable or disable all of the states on. /// The name of the state to change the active status of. /// Should the state be activated? private void SetLinkStateInternal(GameObject gameObject, string stateName, bool active) { List linkedGameObjects; if (m_LinkedGameObjectList.TryGetValue(gameObject, out linkedGameObjects)) { for (int i = 0; i < linkedGameObjects.Count; ++i) { SetStateInternal(linkedGameObjects[i], stateName, active); } } } /// /// Activates or deactivates the specified state. In most cases SetState should be used instead of ActivateState. /// /// The state to activate or deactivate. /// Should the state be activated? /// The array of states that the state belongs to. public static void ActivateState(State state, bool active, State[] states) { Instance.ActivateStateInternal(state, active, states); } /// /// Internal method which activates or deactivates the specified state. In most cases SetState should be used instead of ActivateState. /// /// The state to activate or deactivate. /// Should the state be activated? /// The array of states that the state belongs to. private void ActivateStateInternal(State state, bool active, State[] states) { // Return early if there no work needs to be done. if (state.Active == active) { return; } // Set the active state. state.Active = active; // Apply the changes. CombineStates(state, active, states); } /// /// Loops through the states and applies the value. The states are looped in the order specified within the inspector from top to bottom. /// /// The state that was activated or deactivated. /// Was the activated? /// The array of states that the state belongs to. private void CombineStates(State state, bool active, State[] states) { if (active) { // Apply the default value of the blocked states before looping through all of the states. This will ensure the default value // is set for that property if no other states set the property value. for (int i = states.Length - 2; i > -1; --i) { if (states[i].Active && states[i].IsBlocked()) { states[states.Length - 1].ApplyValues(states[i].Preset.Delegates); } } } else { // Restore the default values if the state is no longer active. states[states.Length - 1].ApplyValues(state.Preset.Delegates); } // Loop backwards so the higher priority states are applied first. Do not apply the default state because it was applied above. for (int i = states.Length - 2; i > -1; --i) { // Don't apply the state if the state isn't active. if (!states[i].Active) { continue; } // Do not apply the state if it is currently blocked by another state. if (states[i].IsBlocked()) { continue; } states[i].ApplyValues(); } } /// /// Activates the state and then deactivates the state after the specified amount of time. /// /// The Gameobject to set the state on. /// The name of the state to activate and then deactivate. /// The amount of time that should elapse before the state is disabled. public static void DeactivateStateTimer(GameObject gameObject, string stateName, float time) { Instance.DeactivateStateTimerInternal(gameObject, stateName, time); } /// /// Internal method which activates the state and then deactivates the state after the specified amount of time. /// /// The Gameobject to set the state on. /// The name of the state to activate and then deactivate. /// The amount of time that should elapse before the state is disabled. private void DeactivateStateTimerInternal(GameObject gameObject, string stateName, float time) { if (m_DisableStateTimerMap == null) { m_DisableStateTimerMap = new Dictionary>(); } Dictionary stateNameEventMap; if (m_DisableStateTimerMap.TryGetValue(gameObject, out stateNameEventMap)) { ScheduledEventBase disableEvent; if (stateNameEventMap.TryGetValue(stateName, out disableEvent)) { // The state name exists. This means that the timer is currently active and should first been cancelled. Scheduler.Cancel(disableEvent); disableEvent = Scheduler.Schedule(time, DeactivateState, gameObject, stateName); } else { // The state name hasn't been added yet. Add it to the map. disableEvent = Scheduler.Schedule(time, DeactivateState, gameObject, stateName); stateNameEventMap.Add(stateName, disableEvent); } } else { // Neither the GameObject nor the state has been activated. Create the maps. stateNameEventMap = new Dictionary(); var disableEvent = Scheduler.Schedule(time, DeactivateState, gameObject, stateName); stateNameEventMap.Add(stateName, disableEvent); m_DisableStateTimerMap.Add(gameObject, stateNameEventMap); } } /// /// Deactives the specified state and removes it form the timer map. /// /// The GameObject to set the state on. /// The name of the state to set. private void DeactivateState(GameObject gameObject, string stateName) { SetState(gameObject, stateName, false); Dictionary stateNameEventMap; if (m_DisableStateTimerMap.TryGetValue(gameObject, out stateNameEventMap)) { stateNameEventMap.Remove(stateName); } } /// /// Reset the initialized variable when the scene is no longer loaded. /// /// The scene that was unloaded. private void SceneUnloaded(Scene scene) { s_Initialized = false; s_Instance = null; SceneManager.sceneUnloaded -= SceneUnloaded; } /// /// The object has been disabled. /// private void OnDisable() { SceneManager.sceneUnloaded += SceneUnloaded; } #if UNITY_2019_3_OR_NEWER /// /// Reset the static variables for domain reloading. /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void DomainReset() { s_Initialized = false; s_Instance = null; } #endif } }