//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; namespace BansheeEngine { /** @addtogroup Animation * @{ */ /// /// Determines how an animation clip behaves when it reaches the end. /// public enum AnimWrapMode // Note: Must match C++ enum AnimWrapMode { /// /// Loop around to the beginning/end when the last/first frame is reached. /// Loop, /// /// Clamp to end/beginning, keeping the last/first frame active. /// Clamp } /// /// Represents an animation clip used in 1D blending. Each clip has a position on the number line. /// public class BlendClipInfo { public AnimationClip clip; public float position; } /// /// Defines a 1D blend where two animation clips are blended between each other using linear interpolation. /// public class Blend1DInfo { public BlendClipInfo[] clips; } /// /// Defines a 2D blend where two animation clips are blended between each other using bilinear interpolation. /// public class Blend2DInfo { public AnimationClip topLeftClip; public AnimationClip topRightClip; public AnimationClip botLeftClip; public AnimationClip botRightClip; } /// /// Contains information about a currently playing animation clip. /// [StructLayout(LayoutKind.Sequential), SerializeObject] public struct AnimationClipState // Note: Must match C++ struct AnimationClipState { /// /// Layer the clip is playing on. Multiple clips can be played simulatenously on different layers. /// public int layer; /// /// Current time the animation is playing from. /// public float time; /// /// Speed at which the animation is playing. /// public float speed; /// /// Determines how much of an influence does the clip have on the final pose. /// public float weight; /// /// Determines what happens to other animation clips when a new clip starts playing. /// public AnimWrapMode wrapMode; /// /// Initializes the state with default values. /// public void InitDefault() { speed = 1.0f; weight = 1.0f; wrapMode = AnimWrapMode.Loop; } } /// /// Handles animation playback. Takes one or multiple animation clips as input and evaluates them every animation update /// tick depending on set properties.The evaluated data is used by the core thread for skeletal animation, by the sim /// thread for updating attached scene objects and bones (if skeleton is attached), or the data is made available for /// manual queries in the case of generic animation. /// public class Animation : Component { private NativeAnimation _native; [SerializeField] private SerializableData serializableData = new SerializableData(); private FloatCurvePropertyInfo[] floatProperties; /// /// Contains mapping for a suffix used by property paths used for curve identifiers, to their index and type. /// internal static readonly Dictionary PropertySuffixInfos = new Dictionary { {".x", new PropertySuffixInfo(0, true)}, {".y", new PropertySuffixInfo(1, true)}, {".z", new PropertySuffixInfo(2, true)}, {".w", new PropertySuffixInfo(3, true)}, {".r", new PropertySuffixInfo(0, false)}, {".g", new PropertySuffixInfo(1, false)}, {".b", new PropertySuffixInfo(2, false)}, {".a", new PropertySuffixInfo(3, false)} }; /// /// Returns the non-component version of Animation that is wrapped by this component. /// internal NativeAnimation Native { get { return _native; } } /// /// Determines the default clip to play as soon as the component is enabled. If more control over playing clips is /// needed use the , , and /// methods to queue clips for playback manually, and method for modify their states /// individually. /// public AnimationClip DefaultClip { get { return serializableData.defaultClip; } set { serializableData.defaultClip = value; if (value != null && _native != null) { RebuildFloatProperties(value); _native.Play(value); } } } /// /// Determines the wrap mode for all active animations. Wrap mode determines what happens when animation reaches the /// first or last frame. /// /// public AnimWrapMode WrapMode { get { return serializableData.wrapMode; } set { serializableData.wrapMode = value; if (_native != null) _native.WrapMode = value; } } /// /// Determines the speed for all animations. The default value is 1.0f. Use negative values to play-back in reverse. /// public float Speed { get { return serializableData.speed; } set { serializableData.speed = value; if (_native != null) _native.Speed = value; } } /// /// Checks if any animation clips are currently playing. /// public bool IsPlaying { get { if (_native != null) return _native.IsPlaying(); return false; } } /// /// Plays the specified animation clip. /// /// Clip to play. public void Play(AnimationClip clip) { if (_native != null) { RebuildFloatProperties(clip); _native.Play(clip); } } /// /// Plays the specified animation clip on top of the animation currently playing in the main layer. Multiple such /// clips can be playing at once, as long as you ensure each is given its own layer. Each animation can also have a /// weight that determines how much it influences the main animation. /// /// Clip to additively blend. Must contain additive animation curves. /// Determines how much of an effect will the blended animation have on the final output. /// In range [0, 1]. /// Applies the blend over a specified time period, increasing the weight as the time /// passes. Set to zero to blend immediately. In seconds. /// Layer to play the clip in. Multiple additive clips can be playing at once in separate layers /// and each layer has its own weight. public void BlendAdditive(AnimationClip clip, float weight, float fadeLength, int layer) { if (_native != null) _native.BlendAdditive(clip, weight, fadeLength, layer); } /// /// Blends multiple animation clips between each other using linear interpolation. Unlike normal animations these /// animations are not advanced with the progress of time, and is instead expected the user manually changes the /// parameter. /// /// Information about the clips to blend. Clip positions must be sorted from lowest to highest. /// /// Parameter that controls the blending, in range [0, 1]. t = 0 means left animation has full /// influence, t = 1 means right animation has full influence. public void Blend1D(Blend1DInfo info, float t) { if (_native != null) _native.Blend1D(info, t); } /// /// Blend four animation clips between each other using bilinear interpolation. Unlike normal animations these /// animations are not advanced with the progress of time, and is instead expected the user manually changes the /// parameter. /// /// Information about the clips to blend. /// Parameter that controls the blending, in range [(0, 0), (1, 1)]. t = (0, 0) means top left /// animation has full influence, t = (0, 1) means top right animation has full influence, /// t = (1, 0) means bottom left animation has full influence, t = (1, 1) means bottom right /// animation has full influence. /// public void Blend2D(Blend2DInfo info, Vector2 t) { if (_native != null) _native.Blend2D(info, t); } /// /// Fades the specified animation clip in, while fading other playing animation out, over the specified time period. /// /// Clip to fade in. /// Determines the time period over which the fade occurs. In seconds. public void CrossFade(AnimationClip clip, float fadeLength) { if (_native != null) _native.CrossFade(clip, fadeLength); } /// /// Stops playing all animations on the provided layer. /// /// Layer on which to stop animations on. public void Stop(int layer) { if (_native != null) _native.Stop(layer); } /// /// Stops playing all animations. /// public void StopAll() { if (_native != null) _native.StopAll(); } /// /// Retrieves detailed information about a currently playing animation clip. /// /// Clip to retrieve the information for. /// Animation clip state containing the requested information. Only valid if the method returns /// true. /// True if the state was found (animation clip is playing), false otherwise. public bool GetState(AnimationClip clip, out AnimationClipState state) { if (_native != null) return _native.GetState(clip, out state); state = new AnimationClipState(); return false; } /// /// Searches the scene object hierarchy to find a property at the given path. /// /// Root scene object to which the path is relative to. /// Path to the property, where each element of the path is separated with "/". /// /// Path elements prefixed with "!" signify names of child scene objects (first one relative to /// . Name of the root element should not be included in the path. /// /// Path element prefixed with ":" signify names of components. If a path doesn't have a /// component element, it is assumed the field is relative to the scene object itself (only /// "Translation", "Rotation" and "Scale fields are supported in such case). Only one component /// path element per path is allowed. /// /// Path entries with no prefix are considered regular script object fields. Each path must have /// at least one such entry. Last field entry can optionally have a suffix separated from the /// path name with ".". This suffix is not parsed internally, but will be returned as /// . /// /// Path examples: /// :MyComponent/myInt (path to myInt variable on a component attached to this object) /// !childSO/:MyComponent/myInt (path to myInt variable on a child scene object) /// !childSO/Translation (path to the scene object translation) /// :MyComponent/myVector.z (path to the z component of myVector on this object) /// /// Suffix of the last field entry, if it has any. Contains the suffix separator ".". /// If found, property object you can use for setting and getting the value from the property, otherwise /// null. internal static SerializableProperty FindProperty(SceneObject root, string path, out string suffix) { suffix = null; if (string.IsNullOrEmpty(path) || root == null) return null; string trimmedPath = path.Trim('/'); string[] entries = trimmedPath.Split('/'); // Find scene object referenced by the path SceneObject so = root; int pathIdx = 0; for (; pathIdx < entries.Length; pathIdx++) { string entry = entries[pathIdx]; if (string.IsNullOrEmpty(entry)) continue; // Not a scene object, break if (entry[0] != '!') break; string childName = entry.Substring(1, entry.Length - 1); so = so.FindChild(childName); if (so == null) break; } // Child scene object couldn't be found if (so == null) return null; // Path too short, no field entry if (pathIdx >= entries.Length) return null; // Check if path is referencing a component, and if so find it Component component = null; { string entry = entries[pathIdx]; if (entry[0] == ':') { string componentName = entry.Substring(1, entry.Length - 1); Component[] components = so.GetComponents(); component = Array.Find(components, x => x.GetType().Name == componentName); // Cannot find component with specified type if (component == null) return null; } } // Look for a field within a component if (component != null) { pathIdx++; if (pathIdx >= entries.Length) return null; SerializableObject componentObj = new SerializableObject(component); StringBuilder pathBuilder = new StringBuilder(); for (; pathIdx < entries.Length - 1; pathIdx++) pathBuilder.Append(entries[pathIdx] + "/"); // Check last path entry for suffix and remove it int suffixIdx = entries[pathIdx].LastIndexOf("."); if (suffixIdx != -1) { string entryNoSuffix = entries[pathIdx].Substring(0, suffixIdx); suffix = entries[pathIdx].Substring(suffixIdx, entries[pathIdx].Length - suffixIdx); pathBuilder.Append(entryNoSuffix); } else pathBuilder.Append(entries[pathIdx]); return componentObj.FindProperty(pathBuilder.ToString()); } else // Field is one of the builtin ones on the SceneObject itself { if ((pathIdx + 1) < entries.Length) return null; string entry = entries[pathIdx]; if (entry == "Position") { SerializableProperty property = new SerializableProperty( SerializableProperty.FieldType.Vector3, typeof(Vector3), () => so.LocalPosition, (x) => so.LocalPosition = (Vector3) x); return property; } else if (entry == "Rotation") { SerializableProperty property = new SerializableProperty( SerializableProperty.FieldType.Vector3, typeof(Vector3), () => so.LocalRotation.ToEuler(), (x) => so.LocalRotation = Quaternion.FromEuler((Vector3) x)); return property; } else if (entry == "Scale") { SerializableProperty property = new SerializableProperty( SerializableProperty.FieldType.Vector3, typeof(Vector3), () => so.LocalScale, (x) => so.LocalScale = (Vector3) x); return property; } return null; } } /// /// Changes the state of a playing animation clip. If animation clip is not currently playing the state change is /// ignored. /// /// Clip to change the state for. /// New state of the animation (e.g. changing the time for seeking). public void SetState(AnimationClip clip, AnimationClipState state) { if (_native != null) _native.SetState(clip, state); } private void OnUpdate() { // TODO: There could currently be a mismatch between the serialized properties and the active animation clip // - Add PrimaryClip field to NativeAnimation, then compare to the active clip and update if needed // TODO: If primary animation clip isn't playing, don't update float properties // - Add dirty flags to each curve value and don't update unless they changed // Apply values from generic float curves foreach (var entry in floatProperties) { float curveValue; if (_native.GetGenericCurveValue(entry.curveIdx, out curveValue)) entry.setter(curveValue); } } private void OnEnable() { RestoreNative(); } private void OnDisable() { DestroyNative(); } private void OnDestroy() { DestroyNative(); } /// /// Creates the internal representation of the animation and restores the values saved by the component. /// private void RestoreNative() { if (_native != null) _native.Destroy(); _native = new NativeAnimation(); _native.OnEventTriggered += EventTriggered; // Restore saved values after reset _native.WrapMode = serializableData.wrapMode; _native.Speed = serializableData.speed; if (serializableData.defaultClip != null) _native.Play(serializableData.defaultClip); RebuildFloatProperties(serializableData.defaultClip); Renderable renderable = SceneObject.GetComponent(); if (renderable == null) return; NativeRenderable nativeRenderable = renderable.Native; if (nativeRenderable != null) nativeRenderable.Animation = _native; } /// /// Destroys the internal animation representation. /// private void DestroyNative() { Renderable renderableComponent = SceneObject.GetComponent(); if (renderableComponent != null) { NativeRenderable renderable = renderableComponent.Native; if (renderable != null) renderable.Animation = null; } if (_native != null) { _native.Destroy(); _native = null; } } /// /// Builds a list of properties that will be animated using float animation curves. /// /// Clip to retrieve the float animation curves from. private void RebuildFloatProperties(AnimationClip clip) { if (clip == null) { floatProperties = null; return; } AnimationCurves curves = clip.Curves; List newFloatProperties = new List(); for (int i = 0; i < curves.FloatCurves.Length; i++) { string suffix; SerializableProperty property = FindProperty(SceneObject, curves.FloatCurves[i].Name, out suffix); if (property == null) continue; int elementIdx = 0; if (!string.IsNullOrEmpty(suffix)) { PropertySuffixInfo suffixInfo; if (PropertySuffixInfos.TryGetValue(suffix, out suffixInfo)) elementIdx = suffixInfo.elementIdx; } Action setter = null; Type internalType = property.InternalType; switch (property.Type) { case SerializableProperty.FieldType.Vector2: if (internalType == typeof(Vector2)) { setter = f => { Vector2 value = property.GetValue(); value[elementIdx] = f; property.SetValue(value); }; } break; case SerializableProperty.FieldType.Vector3: if (internalType == typeof(Vector3)) { setter = f => { Vector3 value = property.GetValue(); value[elementIdx] = f; property.SetValue(value); }; } break; case SerializableProperty.FieldType.Vector4: if (internalType == typeof(Vector4)) { setter = f => { Vector4 value = property.GetValue(); value[elementIdx] = f; property.SetValue(value); }; } else if (internalType == typeof(Quaternion)) { setter = f => { Quaternion value = property.GetValue(); value[elementIdx] = f; property.SetValue(value); }; } break; case SerializableProperty.FieldType.Color: if (internalType == typeof(Color)) { setter = f => { Color value = property.GetValue(); value[elementIdx] = f; property.SetValue(value); }; } break; case SerializableProperty.FieldType.Bool: setter = f => { bool value = f > 0.0f; property.SetValue(value); }; break; case SerializableProperty.FieldType.Int: setter = f => { int value = (int)f; property.SetValue(value); }; break; case SerializableProperty.FieldType.Float: setter = f => { property.SetValue(f); }; break; } if (setter == null) continue; FloatCurvePropertyInfo propertyInfo = new FloatCurvePropertyInfo(); propertyInfo.curveIdx = i; propertyInfo.setter = setter; newFloatProperties.Add(propertyInfo); } floatProperties = newFloatProperties.ToArray(); } /// /// Called whenever an animation event triggers. /// /// Clip that the event originated from. /// Name of the event. private void EventTriggered(AnimationClip clip, string name) { // TODO - Find a scene object, component and method based on the event name, and call it } /// /// Holds all data the animation component needs to persist through serialization. /// [SerializeObject] private class SerializableData { public AnimationClip defaultClip; public AnimWrapMode wrapMode = AnimWrapMode.Loop; public float speed = 1.0f; } /// /// Contains information about a property animated by a generic animation curve. /// private class FloatCurvePropertyInfo { public int curveIdx; public Action setter; } /// /// Information about a suffix used in a property path. /// internal struct PropertySuffixInfo { public PropertySuffixInfo(int elementIdx, bool isVector) { this.elementIdx = elementIdx; this.isVector = isVector; } public int elementIdx; public bool isVector; } } /** @} */ }