// 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 { /// [Editor-Conditional] /// Specifies a set of acceptable names for s so they can be displayed using a dropdown /// menu instead of a text field. /// /// /// /// Placing this attribute on a type applies it to all fields in that type. /// /// 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. /// /// Documentation: Event Names /// /// /// /// [EventNames(...)]// Apply to all fields in this class. /// public class AttackState /// { /// [SerializeField] /// [EventNames(...)]// Apply to only this field. /// private ClipTransition _Action; /// } /// /// See the constructors for examples of their usage. /// /// /// 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 /// [Editor-Only] The names that can be used for events in the attributed field. public readonly string[] Names; #endif /************************************************************************************************************************/ /// Creates a new containing the specified `names`. /// /// `names` contains no elements. /// /// 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() { } /// } /// 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 } /************************************************************************************************************************/ /// Creates a new with from the `type`. /// /// /// If the `type` is an enum, all of its values will be used. /// /// Otherwise the values of all static fields (including constants) will be used. /// /// /// /// /// 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() { } /// } /// 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 } /************************************************************************************************************************/ /// /// Creates a new with from a member in the `type` /// with the specified `name`. /// /// /// No member with the specified `name` exists in the `type`. /// /// /// The specified member must be static and can be a Field, Property, or Method. /// /// The member type can be anything implementing (including arrays, lists, and /// coroutines). /// /// /// /// 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() { } /// } /// 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(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 /************************************************************************************************************************/ /// The entry used for the menu function to clear the name (U+202F Narrow No-Break Space). 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(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 /************************************************************************************************************************/ } }