📗

【Unity】アニメーション統合クリップについて

2024/08/29に公開

はじめに

初めての方も、そうでない方もこんにちは!
現役ゲームプログラマーのたむぼーです。
自己紹介を載せているので、気になる方は見ていただければ嬉しいです!

今回は
 アニメーション統合クリップ
について紹介します

https://zenn.dev/tmb/articles/1072f8ea010299

アニメーション統合クリップとは

アニメーションは、オブジェクト毎にAnimatorControllerとAnimationClipを使います

例①:ホーム画面にあるバナーのアニメーションの場合
画像①のように名前による管理が必要になります

このような状態だと、オブジェクト毎にファイルが増えていき、管理が大変になる。
そのため、AnimatorControllerに紐づく(オブジェクトに紐づく)AnimationClipは統合して管理します

先ほどの例①のAnimationClipをAnimatorControllerに統合した場合
画像②のように、HomeBannerAnimatorに内包する形で紐づくアニメーションを持たせる

このようにすることで、オブジェクト毎のアニメーションの管理がしやすくなる。

使い方

・AnimatorControllerを置きたいProjectの階層で
 右クリックし、AssetsメニューからAnimationCombineClipを選択

・AnimationCombineClipウィンドウが立ち上がるので
 Target Clipに作成済みのAnimatorControllerを設定するか、
 Animator Controller Nameに作成したいAnimatorControllerの名前を入力し、Createを押す

・作成されたAnimatorControllerはデフォルトで、ShowとHideを持っています。
 また、ShowとHideはAnimatorにも自動で設定されます。

【Clipの追加、リネーム、削除について】
・Clipの追加はAdd New Clipに名前を入力し、Createを押す(名前を入力するとCreateボタンが出ます)
・Clipのリネームは対象のクリップのRenameボタンを押し、入力欄に変更後の名前を入力する
・Clipの削除は対象のクリップのRemoveボタンを押す

スクリプト

AnimationCombineClip.cs
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;
using UnityEditor.Animations;
using System.Linq;

namespace Editor
{
    sealed public class AnimationCombineClip : EditorWindow
    {
        private string _controllerName;
        private AnimatorController _controller;
        private string _clipName;
        private HashSet<AnimationClip> _clipSet;
        private List<AnimationClip> _clipsToRemove;
        private Dictionary<AnimationClip, string> _renameClips;

        private const string AddControllerName = "Animator";

        /// <summary>
        /// メニュー項目を追加し、ウィンドウを作成するメソッド
        /// </summary>
        [MenuItem("Assets/AnimationCombineClip")]
        private static void Create()
        {
            AnimationCombineClip window = GetWindow<AnimationCombineClip>();
            if (Selection.activeObject is AnimatorController)
            {
                window._controller = Selection.activeObject as AnimatorController;
                window.CacheClips();
            }
        }

        /// <summary>
        /// アニメーターコントローラーに含まれるアニメーションクリップをキャッシュするメソッド
        /// </summary>
        private void CacheClips()
        {
            // クリップのセットを初期化
            _clipSet = new HashSet<AnimationClip>();
            if (_controller == null)
            {
                return;
            }

            // コントローラーに含まれる全てのアセットを取得
            Object[] allAssets = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(_controller));
            foreach (Object asset in allAssets)
            {
                // アセットがアニメーションクリップの場合、セットに追加
                if (asset is AnimationClip clip)
                {
                    _clipSet.Add(clip);
                }
            }

            _clipsToRemove = new List<AnimationClip>();
            _renameClips = new Dictionary<AnimationClip, string>();
        }

        /// <summary>
        /// エディタウィンドウのGUIを描画するメソッド
        /// </summary>
        private void OnGUI()
        {
            EditorGUILayout.LabelField("Target Clip");

            AnimatorController newController = EditorGUILayout.ObjectField(_controller, typeof(AnimatorController), false) as AnimatorController;

            // 新しいコントローラーが選択された場合、キャッシュを更新
            if (newController != _controller)
            {
                _controller = newController;
                CacheClips();
            }

            if (_controller == null)
            {
                EditorGUILayout.Space();
                EditorGUILayout.HelpBox("Please select an Animator Controller.", MessageType.Warning);

                EditorGUILayout.Space();
                EditorGUILayout.LabelField("Animator Controller Name");
                _controllerName = EditorGUILayout.TextField(_controllerName);

                if (GUILayout.Button("Create New Animator Controller"))
                {
                    CreateNewAnimatorController();
                }
                return;
            }

            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Add New Clip");
            EditorGUILayout.BeginVertical("Box");

            if (GUILayout.Button("CreateDefaultClip"))
            {
                if (!ContainsClipName("Show"))
                {
                    CreateNewClip("Show");
                }
                if (!ContainsClipName("Hide"))
                {
                    CreateNewClip("Hide");
                }
            }

            _clipName = EditorGUILayout.TextField(_clipName);
            if (_clipSet == null || ContainsClipName(_clipName) || string.IsNullOrEmpty(_clipName))
            {
                EditorGUILayout.LabelField("Cannot create duplicate or empty names");
            }
            else if (GUILayout.Button("Create"))
            {
                CreateNewClip(_clipName);
                _clipName = string.Empty;
            }
            EditorGUILayout.EndVertical();

            // クリップが存在しない場合は終了
            if (_clipSet == null || _clipSet.Count == 0)
            {
                return;
            }

            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Remove or Rename Clip");
            EditorGUILayout.BeginVertical("Box");

            foreach (AnimationClip clip in _clipSet)
            {
                EditorGUILayout.BeginHorizontal();
                EditorGUILayout.LabelField(clip.name);

                // Rename機能の追加
                if (_renameClips.ContainsKey(clip))
                {
                    _renameClips[clip] = EditorGUILayout.TextField(_renameClips[clip], GUILayout.Width(100));
                    if (GUILayout.Button("Apply", GUILayout.Width(100)))
                    {
                        RenameClip(clip, _renameClips[clip]);
                        _renameClips.Remove(clip);
                    }
                }
                else
                {
                    if (GUILayout.Button("Rename", GUILayout.Width(100)))
                    {
                        _renameClips[clip] = clip.name;
                    }
                }

                if (GUILayout.Button("Remove", GUILayout.Width(100)))
                {
                    _clipsToRemove.Add(clip);
                }
                EditorGUILayout.EndHorizontal();
            }
            EditorGUILayout.EndVertical();

            foreach (AnimationClip clip in _clipsToRemove)
            {
                RemoveClip(clip);
            }

            _clipsToRemove.Clear();
        }

        /// <summary>
        /// 新しいアニメーターコントローラーを作成する
        /// </summary>
        private void CreateNewAnimatorController()
        {
            var selects = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.Assets);
            if (selects.Length == 0)
            {
                EditorUtility.DisplayDialog("Error", "Please select a folder in the Project window.", "OK");
                return;
            }

            if (!_controllerName.EndsWith(AddControllerName))
            {
                _controllerName += AddControllerName;
            }

            string path = AssetDatabase.GetAssetPath(selects[0]);
            Debug.Log(path);
            if (!Directory.Exists(path))
            {
                path = Path.GetDirectoryName(path);
            }

            path = Path.Combine(path, _controllerName + ".controller");
            path = AssetDatabase.GenerateUniqueAssetPath(path);

            _controller = AnimatorController.CreateAnimatorControllerAtPath(path);

            CacheClips();

            // BaseLayerにDoNothingステートを追加
            AnimatorStateMachine stateMachine = _controller.layers[0].stateMachine;
            AnimatorState doNothingState = stateMachine.AddState("DoNothing");
            doNothingState.writeDefaultValues = false;

            // ShowとHideクリップを作成し、BaseLayerに追加
            if (!ContainsClipName("Show"))
            {
                CreateNewClip("Show");
            }
            if (!ContainsClipName("Hide"))
            {
                CreateNewClip("Hide");
            }

            // ShowとHideステートを追加
            AddAnimatorState(stateMachine, "Show");
            AddAnimatorState(stateMachine, "Hide");
        }

        /// <summary>
        /// AnimatorStateを追加
        /// </summary>
        private AnimatorState AddAnimatorState(AnimatorStateMachine stateMachine, string name)
        {
            AnimatorState state = stateMachine.AddState(name);
            state.motion = _clipSet.FirstOrDefault(clip => clip.name == name);
            state.writeDefaultValues = false;
            return state;
        }

        /// <summary>
        /// Exitに遷移するように設定
        /// </summary>
        private void AddExitTransition(AnimatorState state)
        {
            AnimatorStateTransition transition = state.AddExitTransition();
            transition.hasExitTime = true;
            transition.exitTime = 0;
            transition.duration = 0;
            transition.offset = 0;
            transition.interruptionSource = TransitionInterruptionSource.None;
            transition.orderedInterruption = false;
        }

        /// <summary>
        /// 新しいアニメーションクリップを作成し、コントローラーに追加する
        /// </summary>
        private void CreateNewClip(string clipName)
        {
            AnimationClip newClip = AnimatorController.AllocateAnimatorClip(clipName);

            AnimationClipSettings settings = AnimationUtility.GetAnimationClipSettings(newClip);
            settings.loopTime = false;
            AnimationUtility.SetAnimationClipSettings(newClip, settings);

            // アセットとして追加
            AssetDatabase.AddObjectToAsset(newClip, _controller);
            AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
            AssetDatabase.Refresh();

            _clipSet.Add(newClip);
        }

        /// <summary>
        /// 指定されたアニメーションクリップの名前を変更する
        /// </summary>
        private void RenameClip(AnimationClip clip, string newName)
        {
            string oldName = clip.name;

            clip.name = newName;
            EditorUtility.SetDirty(clip);

            foreach (AnimatorControllerLayer layer in _controller.layers)
            {
                UpdateStateMachine(layer.stateMachine, oldName, newName);
            }

            AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
            AssetDatabase.Refresh();
        }

        /// <summary>
        /// 状態マシン内のすべての状態を更新してクリップ名を変更する
        /// </summary>
        private void UpdateStateMachine(AnimatorStateMachine stateMachine, string oldName, string newName)
        {
            foreach (ChildAnimatorState state in stateMachine.states)
            {
                AnimationClip stateClip = state.state.motion as AnimationClip;
                if (stateClip != null && stateClip.name == newName && state.state.name == oldName)
                {
                    state.state.name = newName;
                    EditorUtility.SetDirty(state.state);
                }
            }

            foreach (ChildAnimatorStateMachine childStateMachine in stateMachine.stateMachines)
            {
                UpdateStateMachine(childStateMachine.stateMachine, oldName, newName);
            }
        }

        /// <summary>
        /// 指定されたアニメーションクリップを削除する
        /// </summary>
        private void RemoveClip(AnimationClip clip)
        {
            DestroyImmediate(clip, true);
            AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
            AssetDatabase.Refresh();

            _clipSet.Remove(clip);
        }

        /// <summary>
        /// 指定された名前のアニメーションクリップが存在するか
        /// </summary>
        private bool ContainsClipName(string name)
        {
            foreach (AnimationClip clip in _clipSet)
            {
                if (clip.name == name)
                {
                    return true;
                }
            }
            return false;
        }
    }
}

Discussion