290 lines
11 KiB
C#
290 lines
11 KiB
C#
|
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
|
|||
|
|
|||
|
using System;
|
|||
|
|
|||
|
#if UNITY_EDITOR
|
|||
|
using Animancer.Editor;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.Reflection;
|
|||
|
using UnityEditor;
|
|||
|
using UnityEngine;
|
|||
|
using System.Collections;
|
|||
|
#endif
|
|||
|
|
|||
|
namespace Animancer
|
|||
|
{
|
|||
|
/// <summary>[Editor-Conditional]
|
|||
|
/// Specifies a set of acceptable names for <see cref="AnimancerEvent"/>s so they can be displayed using a dropdown
|
|||
|
/// menu instead of a text field.
|
|||
|
/// </summary>
|
|||
|
///
|
|||
|
/// <remarks>
|
|||
|
/// Placing this attribute on a type applies it to all fields in that type.
|
|||
|
/// <para></para>
|
|||
|
/// Note that values selected using the dropdown menu are still stored as strings. Modifying the names in the
|
|||
|
/// script will NOT automatically update any values previously set in the Inspector.
|
|||
|
/// <para></para>
|
|||
|
/// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer/usage#event-names">Event Names</see>
|
|||
|
/// </remarks>
|
|||
|
///
|
|||
|
/// <example><code>
|
|||
|
/// [EventNames(...)]// Apply to all fields in this class.
|
|||
|
/// public class AttackState
|
|||
|
/// {
|
|||
|
/// [SerializeField]
|
|||
|
/// [EventNames(...)]// Apply to only this field.
|
|||
|
/// private ClipTransition _Action;
|
|||
|
/// }
|
|||
|
/// </code>
|
|||
|
/// See the constructors for examples of their usage.
|
|||
|
/// </example>
|
|||
|
///
|
|||
|
/// https://kybernetik.com.au/animancer/api/Animancer/EventNamesAttribute
|
|||
|
///
|
|||
|
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
|
|||
|
[System.Diagnostics.Conditional(Strings.UnityEditor)]
|
|||
|
public sealed class EventNamesAttribute : Attribute
|
|||
|
{
|
|||
|
/************************************************************************************************************************/
|
|||
|
|
|||
|
#if UNITY_EDITOR
|
|||
|
/// <summary>[Editor-Only] The names that can be used for events in the attributed field.</summary>
|
|||
|
public readonly string[] Names;
|
|||
|
#endif
|
|||
|
|
|||
|
/************************************************************************************************************************/
|
|||
|
|
|||
|
/// <summary>Creates a new <see cref="EventNamesAttribute"/> containing the specified `names`.</summary>
|
|||
|
/// <exception cref="ArgumentNullException"/>
|
|||
|
/// <exception cref="ArgumentOutOfRangeException">`names` contains no elements.</exception>
|
|||
|
/// <example><code>
|
|||
|
/// public class AttackState
|
|||
|
/// {
|
|||
|
/// [SerializeField]
|
|||
|
/// [EventNames("Hit Start", "Hit End")]
|
|||
|
/// private ClipTransition _Animation;
|
|||
|
///
|
|||
|
/// private void Awake()
|
|||
|
/// {
|
|||
|
/// _Animation.Events.SetCallback("Hit Start", OnHitStart);
|
|||
|
/// _Animation.Events.SetCallback("Hit End", OnHitEnd);
|
|||
|
/// }
|
|||
|
///
|
|||
|
/// private void OnHitStart() { }
|
|||
|
/// private void OnHitEnd() { }
|
|||
|
/// }
|
|||
|
/// </code></example>
|
|||
|
public EventNamesAttribute(params string[] names)
|
|||
|
{
|
|||
|
#if UNITY_EDITOR
|
|||
|
if (names == null)
|
|||
|
throw new ArgumentNullException(nameof(names));
|
|||
|
else if (names.Length == 0)
|
|||
|
throw new ArgumentOutOfRangeException(nameof(names), "Array must not be empty");
|
|||
|
|
|||
|
Names = AddSpecialItems(names);
|
|||
|
#endif
|
|||
|
}
|
|||
|
|
|||
|
/************************************************************************************************************************/
|
|||
|
|
|||
|
/// <summary>Creates a new <see cref="EventNamesAttribute"/> with <see cref="Names"/> from the `type`.</summary>
|
|||
|
///
|
|||
|
/// <remarks>
|
|||
|
/// If the `type` is an enum, all of its values will be used.
|
|||
|
/// <para></para>
|
|||
|
/// Otherwise the values of all static <see cref="string"/> fields (including constants) will be used.
|
|||
|
/// </remarks>
|
|||
|
/// <exception cref="ArgumentNullException"/>
|
|||
|
///
|
|||
|
/// <example><code>
|
|||
|
/// public class AttackState
|
|||
|
/// {
|
|||
|
/// public static class Events
|
|||
|
/// {
|
|||
|
/// public const string HitStart = "Hit Start";
|
|||
|
/// public const string HitEnd = "Hit End";
|
|||
|
/// }
|
|||
|
///
|
|||
|
/// [SerializeField]
|
|||
|
/// [EventNames(typeof(Events))]// Use all string fields in the Events class.
|
|||
|
/// private ClipTransition _Animation;
|
|||
|
///
|
|||
|
/// private void Awake()
|
|||
|
/// {
|
|||
|
/// _Animation.Events.SetCallback(Events.HitStart, OnHitStart);
|
|||
|
/// _Animation.Events.SetCallback(Events.HitEnd, OnHitEnd);
|
|||
|
/// }
|
|||
|
///
|
|||
|
/// private void OnHitStart() { }
|
|||
|
/// private void OnHitEnd() { }
|
|||
|
/// }
|
|||
|
/// </code></example>
|
|||
|
public EventNamesAttribute(Type type)
|
|||
|
{
|
|||
|
#if UNITY_EDITOR
|
|||
|
if (type == null)
|
|||
|
throw new ArgumentNullException(nameof(type));
|
|||
|
|
|||
|
if (type.IsEnum)
|
|||
|
{
|
|||
|
Names = Enum.GetNames(type);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
Names = GatherNamesFromStaticFields(type);
|
|||
|
}
|
|||
|
|
|||
|
Names = AddSpecialItems(Names);
|
|||
|
#endif
|
|||
|
}
|
|||
|
|
|||
|
/************************************************************************************************************************/
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Creates a new <see cref="EventNamesAttribute"/> with <see cref="Names"/> from a member in the `type`
|
|||
|
/// with the specified `name`.
|
|||
|
/// </summary>
|
|||
|
/// <exception cref="ArgumentNullException"/>
|
|||
|
/// <exception cref="ArgumentException">No member with the specified `name` exists in the `type`.</exception>
|
|||
|
///
|
|||
|
/// <remarks>
|
|||
|
/// The specified member must be static and can be a Field, Property, or Method.
|
|||
|
/// <para></para>
|
|||
|
/// The member type can be anything implementing <see cref="IEnumerable"/> (including arrays, lists, and
|
|||
|
/// coroutines).
|
|||
|
/// </remarks>
|
|||
|
///
|
|||
|
/// <example><code>
|
|||
|
/// public class AttackState
|
|||
|
/// {
|
|||
|
/// public static readonly string[] Events = { "Hit Start", "Hit End" };
|
|||
|
///
|
|||
|
/// [SerializeField]
|
|||
|
/// [EventNames(typeof(AttackState), nameof(Events))]// Get the names from AttackState.Events.
|
|||
|
/// private ClipTransition _Animation;
|
|||
|
///
|
|||
|
/// private void Awake()
|
|||
|
/// {
|
|||
|
/// _Animation.Events.SetCallback(Events[0], OnHitStart);
|
|||
|
/// _Animation.Events.SetCallback(Events[1], OnHitEnd);
|
|||
|
/// }
|
|||
|
///
|
|||
|
/// private void OnHitStart() { }
|
|||
|
/// private void OnHitEnd() { }
|
|||
|
/// }
|
|||
|
/// </code></example>
|
|||
|
public EventNamesAttribute(Type type, string name)
|
|||
|
{
|
|||
|
#if UNITY_EDITOR
|
|||
|
if (type == null)
|
|||
|
throw new ArgumentNullException(nameof(type));
|
|||
|
if (name == null)
|
|||
|
throw new ArgumentNullException(nameof(name));
|
|||
|
|
|||
|
object obj;
|
|||
|
|
|||
|
var field = type.GetField(name, AnimancerEditorUtilities.StaticBindings);
|
|||
|
if (field != null)
|
|||
|
{
|
|||
|
obj = field.GetValue(null) as IEnumerable;
|
|||
|
goto GotCollection;
|
|||
|
}
|
|||
|
|
|||
|
var property = type.GetProperty(name, AnimancerEditorUtilities.StaticBindings);
|
|||
|
if (property != null)
|
|||
|
{
|
|||
|
obj = property.GetValue(null, null) as IEnumerable;
|
|||
|
goto GotCollection;
|
|||
|
}
|
|||
|
|
|||
|
var method = type.GetMethod(name, AnimancerEditorUtilities.StaticBindings, null, Type.EmptyTypes, null);
|
|||
|
if (method != null)
|
|||
|
{
|
|||
|
obj = method.Invoke(null, null) as IEnumerable;
|
|||
|
goto GotCollection;
|
|||
|
}
|
|||
|
|
|||
|
throw new ArgumentException($"{type.GetNameCS()} does not contain a member named '{name}'");
|
|||
|
|
|||
|
GotCollection:
|
|||
|
if (obj == null)
|
|||
|
throw new ArgumentException($"The collection retrieved from {type.GetNameCS()}.{name} is null");
|
|||
|
if (!(obj is IEnumerable collection))
|
|||
|
throw new ArgumentException($"The object retrieved from {type.GetNameCS()}.{name} is not an {nameof(IEnumerable)}");
|
|||
|
|
|||
|
using (ObjectPool.Disposable.AcquireList<string>(out var names))
|
|||
|
{
|
|||
|
names.Add(NoName);
|
|||
|
foreach (var item in collection)
|
|||
|
{
|
|||
|
if (item == null)
|
|||
|
continue;
|
|||
|
|
|||
|
var itemName = item.ToString();
|
|||
|
if (string.IsNullOrEmpty(itemName))
|
|||
|
continue;
|
|||
|
|
|||
|
names.Add(itemName);
|
|||
|
}
|
|||
|
|
|||
|
if (names.Count == 1)
|
|||
|
throw new ArgumentException($"The collection retrieved from {type.GetNameCS()}.{name} is empty");
|
|||
|
|
|||
|
Names = names.ToArray();
|
|||
|
}
|
|||
|
#endif
|
|||
|
}
|
|||
|
|
|||
|
/************************************************************************************************************************/
|
|||
|
#if UNITY_EDITOR
|
|||
|
/************************************************************************************************************************/
|
|||
|
|
|||
|
/// <summary>The entry used for the menu function to clear the name (U+202F Narrow No-Break Space).</summary>
|
|||
|
public const string NoName = " ";
|
|||
|
|
|||
|
/************************************************************************************************************************/
|
|||
|
|
|||
|
private static string[] AddSpecialItems(string[] names)
|
|||
|
{
|
|||
|
if (names == null)
|
|||
|
return null;
|
|||
|
|
|||
|
var newNames = new string[names.Length + 1];
|
|||
|
newNames[0] = NoName;
|
|||
|
Array.Copy(names, 0, newNames, 1, names.Length);
|
|||
|
return newNames;
|
|||
|
}
|
|||
|
|
|||
|
/************************************************************************************************************************/
|
|||
|
|
|||
|
private static string[] GatherNamesFromStaticFields(Type type)
|
|||
|
{
|
|||
|
using (ObjectPool.Disposable.AcquireList<string>(out var names))
|
|||
|
{
|
|||
|
var fields = type.GetFields(AnimancerEditorUtilities.StaticBindings);
|
|||
|
for (int i = 0; i < fields.Length; i++)
|
|||
|
{
|
|||
|
var field = fields[i];
|
|||
|
if (field.FieldType == typeof(string))
|
|||
|
{
|
|||
|
var name = (string)field.GetValue(null);
|
|||
|
if (name != null && !names.Contains(name))
|
|||
|
names.Add(name);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (names.Count > 0)
|
|||
|
return names.ToArray();
|
|||
|
else
|
|||
|
return null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/************************************************************************************************************************/
|
|||
|
#endif
|
|||
|
/************************************************************************************************************************/
|
|||
|
}
|
|||
|
}
|
|||
|
|