unity3d 避免使用Animator触发器硬编码值Unity

3duebb1j  于 2023-01-21  发布在  其他
关注(0)|答案(3)|浏览(176)

在更新Unity Animator的触发器时,有没有推荐的方法来避免硬编码值?
目前,我采用硬编码变量,基本上是从Animator编辑器中复制它们的名称,然后这样调用它们:

animator.SetBool("PurchaseAvailable", isUnlockPossible);

这样做的问题是,触发器的名称可能会更改,或者在某个时候被完全删除,然后我只能在运行时发现问题,甚至更糟,在生产中。
如有任何建议,我们将不胜感激。
谢谢。

mf98qq94

mf98qq941#

不幸的是,没有。它们不是可以通过正常的var_of_type.name_of_prop_or_var方式访问的变量。有一个抽象层将字符串转换为Map属性,即实际触发器。如果您使用AudioMixer,则暴露音频属性(例如音量)的变量以相同的方式工作。
虽然你可以建立一个集中的关系,比如Dictionary将你自己的属性名Map到一个内部的animator属性,但是结果仍然是一个字符串。不同的是它是集中的,当填充字典时的一个改变不会影响使用键的代码,而不是值。问题仍然存在,虽然代码的风险会更小,Animator仍将使用其自己的参数列表,因此,您也需要在那里更改它。
不过,我不想在没有解决您的顾虑的情况下用一个基本的“不”来结束这个答案,您提到过您担心在运行时或生产中会发生错误。
受控的运行时会话,如单元测试或全面测试,正是为了达到这个目的。(端到端测试、A/B测试和最后但并非最不重要的回归测试),他们应该能够发现代码中可能发生此类问题的任何不一致之处,在我看来,这是值得鼓励的,因为这是测试的重点之一。如果测试做得正确,它发生在最终产品发布的机会是渺茫的。
所以简而言之:虽然没有办法在构建时自动检查,但只要测试遵循正确的指导原则,我不会担心测试过程中的错误。然而,如果您觉得需要过于频繁地更改参数名称,则可能存在不同的问题,无论是在一致性、设计还是影响您的开发的任何其他方面,我会首先解决这个问题。

ffvjumwh

ffvjumwh2#

这是你在测试时不能忽略的,因为它会破坏你游戏的动画行为,如果你使用了错误的字符串,你会看到错误。只是为了让事情更容易,从一个地方处理,为了优化的目的,我会使用Animator.StringToHash函数。

[SerializeField] private Animator _animator;
    
private const string AnimParam1 = "test1";
private const string AnimParam2 = "test2";
private const string AnimParam3 = "test3";
    
private static readonly int Test1 = Animator.StringToHash(AnimParam1);
private static readonly int Test2 = Animator.StringToHash(AnimParam2);
private static readonly int Test3 = Animator.StringToHash(AnimParam3);

private void Awake()
{
    _animator.SetBool(Test1, true);
    _animator.SetInteger(Test2, 2);
    _animator.SetFloat(Test3, 3.0f);
}

如果你愿意,你也可以在运行时使用Animator.parameters来运行一些手动检查,但我认为这不值得。如果这些参数在构建时也准备好了,那么你可以明确地使用它们。

olmpazwi

olmpazwi3#

如前所述,没有内置的东西,但您可以使用自定义编辑器脚本来实现类似的功能。
例如,您可以有一个自定义属性类型,它带有一个自定义编辑器,该编辑器可以提取Animator.parameters,过滤特定类型,并显示一个包含名称的下拉列表。
然后在运行时缓存Aniamtor.StringToHash

[Serializable]
public class AnimatorParameter
{
    [SerializeField]
    private RuntimeAnimatorController animator;

    [SerializeField]
    private AnimatorControllerParameterType type;

    [SerializeField]
    private string name;

    private int? hash;

    public AnimatorControllerParameterType Type => type;
    public string Name => name;
    public RuntimeAnimatorController RuntimeAnimatorController;

    public int Hash
    {
        get
        {
            if (hash == null)
            {
                hash = Animator.StringToHash(name);
            }

            return hash.Value;
        }
    }

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(AnimatorParameter))]
    private class AnimatorParameterDrawer : PropertyDrawer
    {
        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            return EditorGUIUtility.singleLineHeight * 2;
        }

        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            using (new EditorGUI.PropertyScope(position, label, property))
            {
                position = EditorGUI.PrefixLabel(position, label);

                var controllerRect = position;
                controllerRect.height = EditorGUIUtility.singleLineHeight;
                position.y += controllerRect.height;

                var controllerProperty = property.FindPropertyRelative(nameof(animator));

                using (var change = new EditorGUI.ChangeCheckScope())
                {
                    EditorGUI.PropertyField(controllerRect, controllerProperty, GUIContent.none);

                    if (!controllerProperty.objectReferenceValue)
                    {
                        var infoRect = position;
                        infoRect.height = EditorGUIUtility.singleLineHeight;
                        EditorGUI.HelpBox(infoRect, "No animator referenced!", MessageType.Error);
                    }
                    else
                    {
                        var typeRect = position;
                        typeRect.width = 80;
                        typeRect.height = EditorGUIUtility.singleLineHeight;
                        position.x += typeRect.width;

                        var nameRect = position;
                        nameRect.height = EditorGUIUtility.singleLineHeight;
                        nameRect.width -= typeRect.width;

                        var typeProperty = property.FindPropertyRelative(nameof(type));

                        EditorGUI.PropertyField(typeRect, typeProperty, GUIContent.none);

                        var options = ((AnimatorController)controllerProperty.objectReferenceValue).parameters
                            .Where(p => p.type == typeProperty.GetEnumValue<AnimatorControllerParameterType>())
                            .Select(p => p.name)
                            .ToList();

                        var nameProperty = property.FindPropertyRelative(nameof(name));
                        var currentIndex = options.IndexOf(nameProperty.stringValue);

                        var newIndex = EditorGUI.Popup(nameRect, currentIndex, options.ToArray());

                        nameProperty.stringValue = newIndex >= 0 && newIndex < options.Count ? options[newIndex] : "";
                    }

                    if (change.changed)
                    {
                        var target = (AnimatorParameter)fieldInfo.GetValue(property.serializedObject.targetObject);
                        target.hash = null;
                        fieldInfo.SetValue(property.serializedObject.targetObject, target);
                    }
                }
            }
        }
    }
#endif
}

为了让事情变得简单一点,你甚至可以添加一些扩展方法,比如:

public static class AnimatorExtensions
{
    public static void SetFloat(this Animator animator, AnimatorParameter animatorParameter, float value)
    {
        if (animatorParameter.Type != AnimatorControllerParameterType.Float)
        {
            throw new ArgumentException("Given parameter is not of type Float!");
        }

        if (animator.runtimeAnimatorController != animatorParameter.RuntimeAnimatorController)
        {
            Debug.LogWarning("AnimatorControllers do not match!");
        }

        animator.SetFloat(animatorParameter.Hash, value);
    }

    public static void SetInteger(this Animator animator, AnimatorParameter animatorParameter, int value)
    {
        if (animatorParameter.Type != AnimatorControllerParameterType.Int)
        {
            throw new ArgumentException("Given parameter is not of type Int!");
        }
        
        if (animator.runtimeAnimatorController != animatorParameter.RuntimeAnimatorController)
        {
            Debug.LogWarning("AnimatorControllers do not match!");
        }

        animator.SetInteger(animatorParameter.Hash, value);
    }

    public static void SetBool(this Animator animator, AnimatorParameter animatorParameter, bool value)
    {
        if (animatorParameter.Type != AnimatorControllerParameterType.Bool)
        {
            throw new ArgumentException("Given parameter is not of type Bool!");
        }
        
        if (animator.runtimeAnimatorController != animatorParameter.RuntimeAnimatorController)
        {
            Debug.LogWarning("AnimatorControllers do not match!");
        }

        animator.SetBool(animatorParameter.Hash, value);
    }

    public static void SetTrigger(this Animator animator, AnimatorParameter animatorParameter)
    {
        if (animatorParameter.Type != AnimatorControllerParameterType.Trigger)
        {
            throw new ArgumentException("Given parameter is not of type Trigger!");
        }
        
        if (animator.runtimeAnimatorController != animatorParameter.RuntimeAnimatorController)
        {
            Debug.LogWarning("AnimatorControllers do not match!");
        }

        animator.SetTrigger(animatorParameter.Hash);
    }

    public static void ResetTrigger(this Animator animator, AnimatorParameter animatorParameter)
    {
        if (animatorParameter.Type != AnimatorControllerParameterType.Trigger)
        {
            throw new ArgumentException("Given parameter is not of type Trigger!");
        }
        
        if (animator.runtimeAnimatorController != animatorParameter.RuntimeAnimatorController)
        {
            Debug.LogWarning("AnimatorControllers do not match!");
        }

        animator.ResetTrigger(animatorParameter.Hash);
    }

    public static float GetFloat(this Animator animator, AnimatorParameter animatorParameter)
    {
        if (animatorParameter.Type != AnimatorControllerParameterType.Float)
        {
            throw new ArgumentException("Given parameter is not of type Float!");
        }
        
        if (animator.runtimeAnimatorController != animatorParameter.RuntimeAnimatorController)
        {
            Debug.LogWarning("AnimatorControllers do not match!");
        }

        return animator.GetFloat(animatorParameter.Hash);
    }

    public static int GetInteger(this Animator animator, AnimatorParameter animatorParameter)
    {
        if (animatorParameter.Type != AnimatorControllerParameterType.Int)
        {
            throw new ArgumentException("Given parameter is not of type Integer!");
        }
        
        if (animator.runtimeAnimatorController != animatorParameter.RuntimeAnimatorController)
        {
            Debug.LogWarning("AnimatorControllers do not match!");
        }

        return animator.GetInteger(animatorParameter.Hash);
    }

    public static bool GetBool(this Animator animator, AnimatorParameter animatorParameter)
    {
        if (animatorParameter.Type != AnimatorControllerParameterType.Bool)
        {
            throw new ArgumentException("Given parameter is not of type Bool!");
        }
        
        if (animator.runtimeAnimatorController != animatorParameter.RuntimeAnimatorController)
        {
            Debug.LogWarning("AnimatorControllers do not match!");
        }

        return animator.GetBool(animatorParameter.Hash);
    }
}

这里有一个小演示,它现在看起来会是什么样子(可能会改进)

public class Example : MonoBehaviour
{
    public Animator animator;
    public AnimatorParameter animatorParameter;

    [ContextMenu(nameof(Apply))]
    public void Apply()
    {
        switch (animatorParameter.Type)
        {
            case AnimatorControllerParameterType.Float:
                animator.SetFloat(animatorParameter, Random.value);
                break;
            case AnimatorControllerParameterType.Int:
                animator.SetInteger(animatorParameter, Random.Range(0, 5));
                break;
            case AnimatorControllerParameterType.Bool:
                animator.SetBool(animatorParameter, !animator.GetBool(animatorParameter));
                break;
            case AnimatorControllerParameterType.Trigger:
                animator.SetTrigger(animatorParameter);
                break;
        }
    }
}

这样做的问题是,触发器的名称可能会更改,或者在某个时候被完全删除,然后我只能在运行时发现问题,甚至更糟,在生产中。
当然,同样的问题仍然存在。不幸的是,动画中的很多东西(和其他)系统是基于字符串的(即使是renaming/re-arranging of objects within the hierarchy也是一个问题)。你 * 可以 * 可能在它周围实现一些 Package 器,而不是直接删除/重命名触发器等,只需要通过你的自定义代码,基本上通知/验证AnimatorParameter的每个示例,更新它们的值并重新序列化它们的资产。这将变得相当复杂,虽然可能不值得努力。
例如,您可以在测试中进行简单的验证,如

public static class AnimatorParameterExtensions
{
    public static void Validate(this AnimatorParameter animatorParameter)
    {
        Assert.IsNotNull(animatorParameter);
        Assert.AreEqual(true, (bool)animatorParameter.Animator);
        Assert.AreEqual(false, string.IsNullOrWhiteSpace(animatorParameter.Name));
        Assert.AreEqual(true, animatorParameter.Animator.parameters.Where(p => p.type == animatorParameter.Type).Any(p => p.name == animatorParameter.Name));
    }
}

我实际上希望看到他们实现一些东西,比如在新的输入系统中,你的整个自定义输入操作都会生成到c#类中,所以如果你重命名它们,你也会立即得到一堆错误。
实际上,您可以尝试实现类似的东西-我不是代码生成Maven,但这可能会给您一个很好的起点

public static class AnimatorFancyCodeGenerator
{
    private const string className  = nameof(AnimatorFancyCodeGenerator);

    [MenuItem("Tools/Generate Animator Parameters Code")]
    private static void Generate()
    {
        var stringBuilder = new StringBuilder();
        stringBuilder
            .Append("using System;\n")
            .Append("using UnityEngine;\n\n")
            .Append("public class ").Append(className).Append("Result\n")
            .Append("{\n");

        var controllers = AssetDatabase.FindAssets("t:" + nameof(AnimatorController));
        foreach (var guid in controllers)
        {
            var path = AssetDatabase.GUIDToAssetPath(guid);
            var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(path);

            stringBuilder.Append($"    public class {controller.name.Replace(" ", "")}").Append("{\n");

            foreach (var parameter in controller.parameters)
            {
                stringBuilder.Append($"        private static int? _{parameter.type}_{parameter.name.Replace(" ", "")};\n")
                    .Append($"        public static int {parameter.type}_{parameter.name.Replace(" ", "")}\n")
                    .Append("        { get{\n")
                    .Append($"            if(_{parameter.type}_{parameter.name.Replace(" ", "")} == null)\n")
                    .Append("            {\n")
                    .Append($"                _{parameter.type}_{parameter.name.Replace(" ", "")} = Animator.StringToHash(\"{parameter.name}\");\n")
                    .Append("            }\n")
                    .Append($"            return _{parameter.type}_{parameter.name.Replace(" ", "")}.Value;")
                    .Append("       }}\n");
            }

            stringBuilder.Append("}\n");
        }

        stringBuilder.Append("}");

        File.WriteAllBytes(Application.dataPath + "/" + className + "Result.cs", Encoding.UTF8.GetBytes(stringBuilder.ToString()));
        
        AssetDatabase.Refresh();
    }
}

然后将所有控制器生成到一个类中(对我来说,重新格式化后看起来像e.g.)

using System;
using UnityEngine;

public class AnimatorFancyCodeGeneratorResult
{
    public class NewAnimatorController
    {
        private static int? _Trigger_SomeTrigger;

        public static int Trigger_SomeTrigger
        {
            get
            {
                if (_Trigger_SomeTrigger == null)
                {
                    _Trigger_SomeTrigger = Animator.StringToHash("Some Trigger");
                }

                return _Trigger_SomeTrigger.Value;
            }
        }

        private static int? _Trigger_AnotherTrigger;

        public static int Trigger_AnotherTrigger
        {
            get
            {
                if (_Trigger_AnotherTrigger == null)
                {
                    _Trigger_AnotherTrigger = Animator.StringToHash("Another Trigger");
                }

                return _Trigger_AnotherTrigger.Value;
            }
        }

        private static int? _Bool_BoolFlag;

        public static int Bool_BoolFlag
        {
            get
            {
                if (_Bool_BoolFlag == null)
                {
                    _Bool_BoolFlag = Animator.StringToHash("Bool Flag");
                }

                return _Bool_BoolFlag.Value;
            }
        }

        private static int? _Bool_OtherBoolFlag;

        public static int Bool_OtherBoolFlag
        {
            get
            {
                if (_Bool_OtherBoolFlag == null)
                {
                    _Bool_OtherBoolFlag = Animator.StringToHash("Other Bool Flag");
                }

                return _Bool_OtherBoolFlag.Value;
            }
        }

        private static int? _Int_SomeInt;

        public static int Int_SomeInt
        {
            get
            {
                if (_Int_SomeInt == null)
                {
                    _Int_SomeInt = Animator.StringToHash("Some Int");
                }

                return _Int_SomeInt.Value;
            }
        }

        private static int? _Int_OtherInt;

        public static int Int_OtherInt
        {
            get
            {
                if (_Int_OtherInt == null)
                {
                    _Int_OtherInt = Animator.StringToHash("Other Int");
                }

                return _Int_OtherInt.Value;
            }
        }

        private static int? _Float_NiceFloat;

        public static int Float_NiceFloat
        {
            get
            {
                if (_Float_NiceFloat == null)
                {
                    _Float_NiceFloat = Animator.StringToHash("Nice Float");
                }

                return _Float_NiceFloat.Value;
            }
        }

        private static int? _Float_OtherFloat;

        public static int Float_OtherFloat
        {
            get
            {
                if (_Float_OtherFloat == null)
                {
                    _Float_OtherFloat = Animator.StringToHash("Other Float");
                }

                return _Float_OtherFloat.Value;
            }
        }
    }
}

(of课程名称冲突目前可能是一个问题-正如所说,远非完美),您可以稍后(例如

animator.SetTrigger(AnimatorFancyCodeGeneratorResult.NewAnimatorController.Trigger_SomeTrigger);

这样,如果参数被重命名/删除/重新键入等,您将得到实际的编译器错误

相关问题