// Copyright (c) Microsoft Corporation. // Licensed under the MIT License using Microsoft.MixedReality.Toolkit.Experimental.InteractiveElement; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using UnityEditor; using UnityEngine; #if UNITY_EDITOR using UnityEditor.Animations; #endif [assembly: InternalsVisibleTo("Microsoft.MixedReality.Toolkit.SDK.Experimental.Editor.Interactive")] namespace Microsoft.MixedReality.Toolkit.Experimental.StateVisualizer { /// /// The State Visualizer component adds animations to an object based on the states defined in a linked Interactive Element component. /// This component creates animation assets, places them in the MixedRealityToolkit.Generated folder and enables /// simplified animation keyframe setting through adding animatable properties to a target game object. /// To enable animation transitions between states, an Animator Controller asset is created and a default state machine /// is generated with associated parameters and transitions. This state machine can be viewed in Unity's Animator window. /// [RequireComponent(typeof(Animator))] public class StateVisualizer : MonoBehaviour { [SerializeField] [Tooltip("A list of containers that map to the states in the attached Interactive Element component. ")] private List stateContainers = new List(); /// /// A list of containers that map to the states in the attached Interactive Element component. /// public List StateContainers { get => stateContainers; protected set => stateContainers = value; } [Experimental] [SerializeField] [Tooltip("The linked Interactive Element component for this State Visualizer." + " The State Visualizer component depends on the presence of a component " + " that derives from BaseInteractiveElement.")] private BaseInteractiveElement interactiveElement; /// /// The linked Interactive Element component for this State Visualizer. /// The State Visualizer component depends on the presence of a component /// that derives from BaseInteractiveElement. /// public BaseInteractiveElement InteractiveElement { get => interactiveElement; set => interactiveElement = value; } [SerializeField] [Tooltip("The Animator for this State Visualizer component. The State Visualizer component" + " leverages the capabilities of the Unity animation system and requires the presence of " + " an Animator component.")] private Animator animator; /// /// The Animator for this State Visualizer component. The State Visualizer component /// leverages the capabilities of the Unity animation system and requires the presence of /// an Animator component. /// public Animator Animator { get => animator; set => animator = value; } // The states within an Interactive Element public List States => InteractiveElement != null ? InteractiveElement.States : null; // The state manager within the Interactive Element private StateManager stateManager; #if UNITY_EDITOR // The animator state machine public AnimatorStateMachine RootStateMachine; // Editor animation controller public AnimatorController EditorAnimatorController; #endif private void OnValidate() { if (InteractiveElement == null) { if (gameObject.GetComponent() != null) { InteractiveElement = gameObject.GetComponent(); } } if (Animator == null) { Animator = gameObject.GetComponent(); } if (stateContainers.Count == 0) { InitializeStateContainers(); } } private void Start() { // If interactive element is null, then the component has not been initialized via inspector if (InteractiveElement != null) { stateManager = InteractiveElement.StateManager; InitializeStateContainers(); stateManager.OnStateActivated.AddListener((state) => { if (GetStateContainer(state.Name) != null) { Animator.SetTrigger("On" + state.Name); } }); } else { Debug.LogError("The State Visualizer currently must be initialized via inspector as the animation clips" + "for each state are added to an Editor Animation Controller."); } } #if UNITY_EDITOR #region Animator State Methods /// /// Initialize the Animator State Machine by creating new animator states to match the states in Interactive Element. /// /// The animation controller contained in the attached Animator component public void SetUpStateMachine(AnimatorController animatorController) { // Update Animation Clip References RootStateMachine = animatorController.layers[0].stateMachine; EditorAnimatorController = animatorController; foreach (var stateContainer in StateContainers) { AddNewStateToStateMachine(stateContainer.StateName, animatorController); } AssetDatabase.SaveAssets(); } /// /// Add a new state to the animator state machine and generate a new associated animation clip. /// /// The name of the new animation state /// The animation controller contained in the attached Animator component /// The new animator state in the animator state machine private AnimatorState AddNewStateToStateMachine(string stateName, AnimatorController animatorController) { // Create animation state AnimatorState animatorState = AddAnimatorState(RootStateMachine, stateName); // Add associated parameter AddAnimatorParameter(animatorController, "On" + stateName, AnimatorControllerParameterType.Trigger); // Create and attach animation clip AddAnimationClip(animatorState); AddAnyStateTransition(RootStateMachine, animatorState); StateContainer stateContainer = GetStateContainer(stateName); stateContainer.AnimatorStateMachine = RootStateMachine; return animatorState; } private AnimatorState AddAnimatorState(AnimatorStateMachine stateMachine, string animatorStateName) { bool doesStateExist = Array.Exists(stateMachine.states, (animatorState) => animatorState.state.name == animatorStateName); if (!doesStateExist) { return stateMachine.AddState(animatorStateName); } else { Debug.LogError($"The {animatorStateName} state already exisits in the animator state machine"); return null; } } private void AddAnimatorParameter(AnimatorController animatorController, string parameterName, AnimatorControllerParameterType animatorParameterType) { animatorController.AddParameter(parameterName, animatorParameterType); } private void AddAnimationClip(AnimatorState animatorState) { string animationAssetPath = GetAnimationDirectoryPath(); AnimationClip stateAnimationClip = new AnimationClip(); stateAnimationClip.name = gameObject.name + "_" + animatorState.name + "Clip"; string animationClipFileName = stateAnimationClip.name + ".anim"; AssetDatabase.CreateAsset(stateAnimationClip, animationAssetPath + "/" + animationClipFileName); animatorState.motion = stateAnimationClip; StateContainer stateContainer = GetStateContainer(animatorState.name); stateContainer.AnimationClip = stateAnimationClip; } private void AddAnyStateTransition(AnimatorStateMachine animatorStateMachine, AnimatorState animatorState) { // Idle state AnimatorStateTransition transition = animatorStateMachine.AddAnyStateTransition(animatorState); transition.name = "To" + animatorState.name; // Add Trigger Parameter as a condition for the transition transition.AddCondition(AnimatorConditionMode.If, 0, "On" + animatorState.name); } /// /// Remove an animator state from the state machine. Used in the StateVisualizerInspector /// /// The state machine for state removal /// The name of the animator state internal void RemoveAnimatorState(AnimatorStateMachine stateMachine, string animatorStateName) { AnimatorState animatorStateToRemove = GetAnimatorState(animatorStateName); stateMachine.RemoveState(animatorStateToRemove); } /// /// Creates and returns the path to a directory for the animation controller and animation clips assets. /// /// Returns path to the animation controller and animation clip assets private string GetAnimationDirectoryPath() { string animationDirectoryPath = Path.Combine("Assets", "MixedRealityToolkit.Generated", "MRTK_Animations"); // If the animation directory path does not exist, then create a new directory if (!Directory.Exists(animationDirectoryPath)) { Directory.CreateDirectory(animationDirectoryPath); } return animationDirectoryPath; } // Create a new animator controller asset and add it to the MixedRealityToolkit.Generated folder. // Then set up the state machine for the animator controller. internal void InitializeAnimatorControllerAsset() { // Create MRTK_Animation Directory if it does not exist string animationAssetDirectory = GetAnimationDirectoryPath(); string animatorControllerName = gameObject.name + ".controller"; string animationControllerPath = Path.Combine(animationAssetDirectory, animatorControllerName); // Create Animation Controller EditorAnimatorController = AnimatorController.CreateAnimatorControllerAtPath(animationControllerPath); // Set the runtime animation controller gameObject.GetComponent().runtimeAnimatorController = EditorAnimatorController; SetUpStateMachine(EditorAnimatorController); } #endregion #endif #region State Container Methods private void InitializeStateContainers() { if (States != null && StateContainers.Count == 0) { foreach (InteractionState state in States) { AddStateContainer(state.Name); } } } private void UpdateStateContainers(List interactionStates) { if (interactionStates.Count != StateContainers.Count) { if (interactionStates.Count > StateContainers.Count) { foreach (InteractionState state in interactionStates) { // Find the container that matches the state StateContainer container = GetStateContainer(state.Name); if (container == null) { AddStateContainer(state.Name); } } } else if (interactionStates.Count < StateContainers.Count) { foreach (StateContainer stateContainer in StateContainers.ToList()) { // Find the state in interactive element for this container InteractionState interactionState = interactionStates.Find((state) => (state.Name == stateContainer.StateName)); // Do not remove the default state if (interactionState == null) { RemoveStateContainer(stateContainer.StateName); } } } } } private void RemoveStateContainer(string stateName) { StateContainer containerToRemove = StateContainers.Find((container) => container.StateName == stateName); StateContainers.Remove(containerToRemove); } private void AddStateContainer(string stateName) { StateContainer stateContainer = new StateContainer(stateName); StateContainers.Add(stateContainer); } #if UNITY_EDITOR /// /// Update the state containers in the state visualizer to match the states in InteractiveElement. Used in the StateVisualizerInspector. /// internal void UpdateStateContainerStates() { UpdateStateContainers(InteractiveElement.States); List stateContainerNames = new List(); List animatorStateNames = new List(); // Get state container names StateContainers.ForEach((stateContainer) => stateContainerNames.Add(stateContainer.StateName)); // Get animation state names Array.ForEach(RootStateMachine.states, (animatorState) => animatorStateNames.Add(animatorState.state.name)); // Add new animator state in the root state machine if a state container has been added var statesToAdd = stateContainerNames.Except(animatorStateNames); foreach (var state in statesToAdd) { AddNewStateToStateMachine(state, animator.runtimeAnimatorController as AnimatorController); } // Remove animator state in the root state machine if a state container has been removed var statesToRemove = animatorStateNames.Except(stateContainerNames); foreach (var stateAni in statesToRemove) { RemoveAnimatorState(RootStateMachine, stateAni); } } #endif #endregion #region Helper Methods /// /// Get state container given a state name. /// /// The name of the state container /// The state container with given state name public StateContainer GetStateContainer(string stateName) { StateContainer stateContainer = StateContainers.Find((container) => container.StateName == stateName); return stateContainer != null ? stateContainer : null; } /// /// Add an animation target to a state container. An animation target contains a reference to /// the target game object and a list of the animatable properties associated with the target. /// /// The name of the state container /// The target game object to add /// The newly created AnimationTarget for a state container public AnimationTarget AddAnimationTargetToState(string stateName, GameObject target) { StateContainer stateContainer = GetStateContainer(stateName); stateContainer.AnimationTargets.Add(new AnimationTarget() { Target = target }); return stateContainer.AnimationTargets.Last(); } /// /// Add an animatable property to an animation target in a state container. /// /// The name of the state container /// The index of the animation target in the StateContainer's AnimationTarget list /// The name of the AnimatableProperty to add /// The new animatable property added public StateAnimatableProperty AddAnimatableProperty(string stateName, int animationTargetIndex, AnimatableProperty animatableProperty) { return CreateAnimatablePropertyInstance(animationTargetIndex, animatableProperty.ToString(), stateName); } /// /// Get an animatable property by type. /// /// A type that derives from StateAnimatableProperty /// The name of the state container /// The index of the animation target in the StateContainer's AnimationTarget list /// The animatable property with given type T public T GetAnimatableProperty(string stateName, int animationTargetIndex) where T : StateAnimatableProperty { StateContainer stateContainer = GetStateContainer(stateName); AnimationTarget animationTarget = stateContainer.AnimationTargets[animationTargetIndex]; IStateAnimatableProperty animatableProperty = animationTarget.StateAnimatableProperties.Find((animatableProp) => animatableProp is T); return animatableProperty as T; } /// /// Get a list of the shader animatable properties by type. /// /// A type that derives from ShaderStateAnimatableProperty /// The name of the state container /// The index of the animation target in the StateContainer's AnimationTarget list /// A list of the animatable properties in a container with the given type T public List GetShaderAnimatablePropertyList(string stateName, int animationTargetIndex) where T : ShaderStateAnimatableProperty { StateContainer stateContainer = GetStateContainer(stateName); AnimationTarget animationTarget = stateContainer.AnimationTargets[animationTargetIndex]; List shaderPropertyList = new List(); foreach (var animatableProp in animationTarget.StateAnimatableProperties) { if (animatableProp is T) { shaderPropertyList.Add(animatableProp as T); } } return shaderPropertyList; } /// /// Set the keyframes for a given animatable property. /// /// The name of the state container /// The index of the animation target game object /// The name of the animatable property public void SetKeyFrames(string stateName, int animationTargetIndex) { StateContainer stateContainer = GetStateContainer(stateName); stateContainer.SetKeyFrames(animationTargetIndex); } /// /// Remove previously set keyframes. /// /// The name of the state container /// The index of the animation target game object /// The name of the animatable property public void RemoveKeyFrames(string stateName, int animationTargetIndex, string animatablePropertyName) { StateContainer stateContainer = GetStateContainer(stateName); stateContainer.RemoveKeyFrames(animationTargetIndex, animatablePropertyName); } /// /// Set the animation clip for a state. /// /// The name of the state /// The animation clip to set public void SetAnimationClip(string stateName, AnimationClip animationClip) { StateContainer stateContainer = GetStateContainer(stateName); stateContainer.AnimationClip = animationClip; } #if UNITY_EDITOR /// /// Set the AnimationTransitionDuration for a state. /// /// The name of the state /// The duration of the transition in seconds public void SetAnimationTransitionDuration(string stateName, float transitionDurationValue) { StateContainer stateContainer = GetStateContainer(stateName); if (stateContainer.AnimatorStateMachine == null) { stateContainer.AnimatorStateMachine = RootStateMachine; } stateContainer.AnimationTransitionDuration = transitionDurationValue; } /// /// Get an animator state in the animator state machine by state name. /// /// The name of the animator state /// The animator state in the animator state machine public AnimatorState GetAnimatorState(string animatorStateName) { return Array.Find(RootStateMachine.states, (animatorState) => animatorState.state.name == animatorStateName).state; } #endif internal StateAnimatableProperty CreateAnimatablePropertyInstance(int animationTargetIndex, string animatablePropertyName, string stateName) { StateContainer stateContainer = GetStateContainer(stateName); if (stateContainer != null) { return stateContainer.CreateAnimatablePropertyInstance(animationTargetIndex, animatablePropertyName, stateName); } else { Debug.LogError($"Could not find a state container with the name {stateName}"); return null; } } #endregion } }