359 lines
14 KiB
C#
359 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
using UnityEngine.Playables;
|
|
using UnityEngine.Timeline;
|
|
using UnityObject = UnityEngine.Object;
|
|
|
|
namespace UnityEditor.Timeline
|
|
{
|
|
static class AnimatedParameterUtility
|
|
{
|
|
static readonly Type k_DefaultAnimationType = typeof(TimelineAsset);
|
|
static SerializedObject s_CachedObject;
|
|
|
|
public static ICurvesOwner ToCurvesOwner(IPlayableAsset playableAsset, TimelineAsset timeline)
|
|
{
|
|
if (playableAsset == null)
|
|
return null;
|
|
|
|
var curvesOwner = playableAsset as ICurvesOwner;
|
|
if (curvesOwner == null)
|
|
{
|
|
// If the asset is not directly an ICurvesOwner, it might be the asset for a TimelineClip
|
|
curvesOwner = TimelineRecording.FindClipWithAsset(timeline, playableAsset);
|
|
}
|
|
|
|
return curvesOwner;
|
|
}
|
|
|
|
public static bool TryGetSerializedPlayableAsset(UnityObject asset, out SerializedObject serializedObject)
|
|
{
|
|
serializedObject = null;
|
|
if (asset == null || Attribute.IsDefined(asset.GetType(), typeof(NotKeyableAttribute)) || !HasScriptPlayable(asset))
|
|
return false;
|
|
|
|
serializedObject = GetSerializedPlayableAsset(asset);
|
|
return serializedObject != null;
|
|
}
|
|
|
|
public static SerializedObject GetSerializedPlayableAsset(UnityObject asset)
|
|
{
|
|
if (!(asset is IPlayableAsset))
|
|
return null;
|
|
|
|
var scriptObject = asset as ScriptableObject;
|
|
if (scriptObject == null)
|
|
return null;
|
|
|
|
if (s_CachedObject == null || s_CachedObject.targetObject != asset)
|
|
{
|
|
s_CachedObject = new SerializedObject(scriptObject);
|
|
}
|
|
|
|
return s_CachedObject;
|
|
}
|
|
|
|
public static void UpdateSerializedPlayableAsset(UnityObject asset)
|
|
{
|
|
var so = GetSerializedPlayableAsset(asset);
|
|
if (so != null)
|
|
so.UpdateIfRequiredOrScript();
|
|
}
|
|
|
|
public static bool HasScriptPlayable(UnityObject asset)
|
|
{
|
|
if (asset == null)
|
|
return false;
|
|
|
|
var scriptPlayable = asset as IPlayableBehaviour;
|
|
return scriptPlayable != null || GetScriptPlayableFields(asset as IPlayableAsset).Any();
|
|
}
|
|
|
|
public static FieldInfo[] GetScriptPlayableFields(IPlayableAsset asset)
|
|
{
|
|
if (asset == null)
|
|
return new FieldInfo[0];
|
|
|
|
FieldInfo[] scriptPlayableFields;
|
|
if (!AnimatedParameterCache.TryGetScriptPlayableFields(asset.GetType(), out scriptPlayableFields))
|
|
{
|
|
scriptPlayableFields = GetScriptPlayableFields_Internal(asset);
|
|
AnimatedParameterCache.SetScriptPlayableFields(asset.GetType(), scriptPlayableFields);
|
|
}
|
|
|
|
return scriptPlayableFields;
|
|
}
|
|
|
|
static FieldInfo[] GetScriptPlayableFields_Internal(IPlayableAsset asset)
|
|
{
|
|
return asset.GetType()
|
|
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
|
.Where(
|
|
f => typeof(IPlayableBehaviour).IsAssignableFrom(f.FieldType) && // The field is an IPlayableBehaviour
|
|
(f.IsPublic || f.GetCustomAttributes(typeof(SerializeField), false).Any()) && // The field is either public or marked with [SerializeField]
|
|
!f.GetCustomAttributes(typeof(NotKeyableAttribute), false).Any() && // The field is not marked with [NotKeyable]
|
|
!f.GetCustomAttributes(typeof(HideInInspector), false).Any() && // The field is not marked with [HideInInspector]
|
|
!f.FieldType.GetCustomAttributes(typeof(NotKeyableAttribute), false).Any()) // The field is not of a type marked with [NotKeyable]
|
|
.ToArray();
|
|
}
|
|
|
|
public static bool HasAnyAnimatableParameters(UnityObject asset)
|
|
{
|
|
return GetAllAnimatableParameters(asset).Any();
|
|
}
|
|
|
|
public static IEnumerable<SerializedProperty> GetAllAnimatableParameters(UnityObject asset)
|
|
{
|
|
SerializedObject serializedObject;
|
|
if (!TryGetSerializedPlayableAsset(asset, out serializedObject))
|
|
yield break;
|
|
|
|
var prop = serializedObject.GetIterator();
|
|
|
|
// We need to keep this variable because prop starts invalid
|
|
var outOfBounds = false;
|
|
while (!outOfBounds && prop.NextVisible(true))
|
|
{
|
|
foreach (var property in SelectAnimatableProperty(prop))
|
|
yield return property;
|
|
|
|
// We can become out of bounds by calling SelectAnimatableProperty, if the last iterated property is a color.
|
|
outOfBounds = !prop.isValid;
|
|
}
|
|
}
|
|
|
|
static IEnumerable<SerializedProperty> SelectAnimatableProperty(SerializedProperty prop)
|
|
{
|
|
// We're only interested by animatable leaf parameters
|
|
if (!prop.hasChildren && IsParameterAnimatable(prop))
|
|
yield return prop.Copy();
|
|
|
|
// Color type is not considered "visible" when iterating
|
|
if (prop.propertyType == SerializedPropertyType.Color)
|
|
{
|
|
var end = prop.GetEndProperty();
|
|
|
|
// For some reasons, if the last 2+ serialized properties are of type Color, prop becomes invalid and
|
|
// Next() throws an exception. This is not the case when only the last serialized property is a Color.
|
|
while (!SerializedProperty.EqualContents(prop, end) && prop.isValid && prop.Next(true))
|
|
{
|
|
foreach (var property in SelectAnimatableProperty(prop))
|
|
yield return property;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static bool IsParameterAnimatable(UnityObject asset, string parameterName)
|
|
{
|
|
SerializedObject serializedObject;
|
|
if (!TryGetSerializedPlayableAsset(asset, out serializedObject))
|
|
return false;
|
|
|
|
var prop = serializedObject.FindProperty(parameterName);
|
|
return IsParameterAnimatable(prop);
|
|
}
|
|
|
|
public static bool IsParameterAnimatable(SerializedProperty property)
|
|
{
|
|
if (property == null)
|
|
return false;
|
|
|
|
bool isAnimatable;
|
|
if (!AnimatedParameterCache.TryGetIsPropertyAnimatable(property, out isAnimatable))
|
|
{
|
|
isAnimatable = IsParameterAnimatable_Internal(property);
|
|
AnimatedParameterCache.SetIsPropertyAnimatable(property, isAnimatable);
|
|
}
|
|
|
|
return isAnimatable;
|
|
}
|
|
|
|
static bool IsParameterAnimatable_Internal(SerializedProperty property)
|
|
{
|
|
if (property == null)
|
|
return false;
|
|
|
|
var asset = property.serializedObject.targetObject;
|
|
|
|
// Currently not supported
|
|
if (asset is AnimationTrack)
|
|
return false;
|
|
|
|
if (IsParameterKeyable(property))
|
|
return asset is IPlayableBehaviour || IsParameterAtPathAnimatable(asset, property.propertyPath);
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool IsParameterKeyable(SerializedProperty property)
|
|
{
|
|
return IsTypeAnimatable(property.propertyType) && IsKeyableInHierarchy(property);
|
|
}
|
|
|
|
static bool IsKeyableInHierarchy(SerializedProperty property)
|
|
{
|
|
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
|
var pathSegments = property.propertyPath.Split('.');
|
|
var type = property.serializedObject.targetObject.GetType();
|
|
foreach (var segment in pathSegments)
|
|
{
|
|
if (type.GetCustomAttributes(typeof(NotKeyableAttribute), false).Any())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (type.IsArray)
|
|
{
|
|
if (segment != "Array")
|
|
type = type.GetElementType();
|
|
continue;
|
|
}
|
|
|
|
var fieldInfo = type.GetField(segment, bindingFlags);
|
|
|
|
if (fieldInfo == null ||
|
|
fieldInfo.GetCustomAttributes(typeof(NotKeyableAttribute), false).Any() ||
|
|
fieldInfo.GetCustomAttributes(typeof(HideInInspector), false).Any())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
type = fieldInfo.FieldType;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool IsParameterAtPathAnimatable(UnityObject asset, string path)
|
|
{
|
|
if (asset == null)
|
|
return false;
|
|
|
|
return GetScriptPlayableFields(asset as IPlayableAsset)
|
|
.Any(
|
|
f => path.StartsWith(f.Name, StringComparison.Ordinal) &&
|
|
path.Length > f.Name.Length &&
|
|
path[f.Name.Length] == '.');
|
|
}
|
|
|
|
public static bool IsTypeAnimatable(SerializedPropertyType type)
|
|
{
|
|
// Note: Integer is not currently supported by the animated property system
|
|
switch (type)
|
|
{
|
|
case SerializedPropertyType.Boolean:
|
|
case SerializedPropertyType.Float:
|
|
case SerializedPropertyType.Vector2:
|
|
case SerializedPropertyType.Vector3:
|
|
case SerializedPropertyType.Color:
|
|
case SerializedPropertyType.Quaternion:
|
|
case SerializedPropertyType.Vector4:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static bool IsParameterAnimated(UnityObject asset, AnimationClip animationData, string parameterName)
|
|
{
|
|
if (asset == null || animationData == null)
|
|
return false;
|
|
|
|
var binding = GetCurveBinding(asset, parameterName);
|
|
var bindings = AnimationClipCurveCache.Instance.GetCurveInfo(animationData).bindings;
|
|
return bindings.Any(x => BindingMatchesParameterName(x, binding.propertyName));
|
|
}
|
|
|
|
// Retrieve an animated parameter curve. parameter name is required to include the appropriate field for vectors
|
|
// e.g.: position
|
|
public static AnimationCurve GetAnimatedParameter(UnityObject asset, AnimationClip animationData, string parameterName)
|
|
{
|
|
if (!(asset is ScriptableObject) || animationData == null)
|
|
return null;
|
|
|
|
var binding = GetCurveBinding(asset, parameterName);
|
|
return AnimationUtility.GetEditorCurve(animationData, binding);
|
|
}
|
|
|
|
// get an animatable curve binding for this parameter
|
|
public static EditorCurveBinding GetCurveBinding(UnityObject asset, string parameterName)
|
|
{
|
|
var animationName = GetAnimatedParameterBindingName(asset, parameterName);
|
|
return EditorCurveBinding.FloatCurve(string.Empty, GetValidAnimationType(asset), animationName);
|
|
}
|
|
|
|
public static string GetAnimatedParameterBindingName(UnityObject asset, string parameterName)
|
|
{
|
|
if (asset == null)
|
|
return parameterName;
|
|
|
|
string bindingName;
|
|
if (!AnimatedParameterCache.TryGetBindingName(asset.GetType(), parameterName, out bindingName))
|
|
{
|
|
bindingName = GetAnimatedParameterBindingName_Internal(asset, parameterName);
|
|
AnimatedParameterCache.SetBindingName(asset.GetType(), parameterName, bindingName);
|
|
}
|
|
|
|
return bindingName;
|
|
}
|
|
|
|
static string GetAnimatedParameterBindingName_Internal(UnityObject asset, string parameterName)
|
|
{
|
|
if (asset is IPlayableBehaviour)
|
|
return parameterName;
|
|
|
|
// strip the IScript playable field name
|
|
var fields = GetScriptPlayableFields(asset as IPlayableAsset);
|
|
foreach (var f in fields)
|
|
{
|
|
if (parameterName.StartsWith(f.Name, StringComparison.Ordinal))
|
|
{
|
|
if (parameterName.Length > f.Name.Length && parameterName[f.Name.Length] == '.')
|
|
return parameterName.Substring(f.Name.Length + 1);
|
|
}
|
|
}
|
|
|
|
return parameterName;
|
|
}
|
|
|
|
public static bool BindingMatchesParameterName(EditorCurveBinding binding, string parameterName)
|
|
{
|
|
if (binding.propertyName == parameterName)
|
|
return true;
|
|
|
|
var indexOfDot = binding.propertyName.IndexOf('.');
|
|
return indexOfDot > 0 && parameterName.Length == indexOfDot &&
|
|
binding.propertyName.StartsWith(parameterName, StringComparison.Ordinal);
|
|
}
|
|
|
|
// the animated type must be a non-abstract instantiable object.
|
|
public static Type GetValidAnimationType(UnityObject asset)
|
|
{
|
|
return asset != null ? asset.GetType() : k_DefaultAnimationType;
|
|
}
|
|
|
|
public static FieldInfo GetFieldInfoForProperty(SerializedProperty property)
|
|
{
|
|
FieldInfo fieldInfo;
|
|
|
|
if (!AnimatedParameterCache.TryGetFieldInfoForProperty(property, out fieldInfo))
|
|
{
|
|
Type _;
|
|
fieldInfo = ScriptAttributeUtility.GetFieldInfoFromProperty(property, out _);
|
|
AnimatedParameterCache.SetFieldInfoForProperty(property, fieldInfo);
|
|
}
|
|
|
|
return fieldInfo;
|
|
}
|
|
|
|
public static T GetAttributeForProperty<T>(SerializedProperty property) where T : Attribute
|
|
{
|
|
var fieldInfo = GetFieldInfoForProperty(property);
|
|
return fieldInfo.GetCustomAttributes(typeof(T), false).FirstOrDefault() as T;
|
|
}
|
|
}
|
|
}
|