【Unity】アニメーション統合クリップについて
はじめに
初めての方も、そうでない方もこんにちは!
現役ゲームプログラマーのたむぼーです。
自己紹介を載せているので、気になる方は見ていただければ嬉しいです!
今回は
アニメーション統合クリップ
について紹介します
アニメーション統合クリップとは
アニメーションは、オブジェクト毎に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ボタンを押す
スクリプト
using UnityEngine;
using UnityEditor;
using UnityEditor.Animations;
using System.Collections.Generic;
using System.IO;
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 List<AnimationClip> _clipsToAdd = new List<AnimationClip>(); // 追加するクリップのリスト
private Dictionary<AnimationClip, string> _clipNames = new Dictionary<AnimationClip, string>(); // リスト内のクリップの名前管理
private HashSet<AnimationClip> _clipsBeingRenamed = new HashSet<AnimationClip>(); // リネーム中のクリップ
private const string AddControllerName = "Controller";
/// <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()
{
// Target Clip の設定
EditorGUILayout.LabelField("Target Animator Controller");
AnimatorController newController = EditorGUILayout.ObjectField(_controller, typeof(AnimatorController), false) as AnimatorController;
// Target Clip が設定されたかどうかで処理を分岐
if (newController != _controller)
{
_controller = newController;
CacheClips();
_clipsToAdd.Clear();
}
if (_controller == null)
{
// Target Clip が設定されていない場合のUI
EditorGUILayout.LabelField("Animator Controller Name");
_controllerName = EditorGUILayout.TextField(_controllerName);
// ドラッグアンドドロップエリアとリストを表示
DrawClipDragAndDropList();
// デフォルトクリップを追加するボタン(Show と Hide)
if (GUILayout.Button("Create Default Clips"))
{
AddDefaultClips();
}
// 新規コントローラー作成ボタン
if (GUILayout.Button("Create New Animator Controller"))
{
CreateNewAnimatorControllerWithClips();
}
}
else
{
// Target Clip が設定された場合のUI
EditorGUILayout.LabelField("Animator Controller: " + _controller.name);
// ドラッグアンドドロップエリアとリストを表示
DrawClipDragAndDropList();
// 新しいクリップ名を入力するフィールドと追加ボタン
EditorGUILayout.Space();
EditorGUILayout.LabelField("Add New Clip");
_clipName = EditorGUILayout.TextField("Clip Name", _clipName);
// ドラッグアンドドロップで追加されたリストをコントローラーに追加するボタン
if (GUILayout.Button("Add Clip"))
{
if (!string.IsNullOrEmpty(_clipName))
{
CreateNewClip(_clipName);
_clipName = string.Empty;
}
if (_clipsToAdd.Count > 0)
{
foreach (var clip in _clipsToAdd)
{
AddClipToController(clip);
}
_clipsToAdd.Clear();
}
}
// デフォルトクリップを追加するボタン(Show と Hide)
if (GUILayout.Button("Add Default Clips"))
{
CreateNewClip("Show");
CreateNewClip("Hide");
}
// 現在コントローラーに内包されているクリップリストを表示
if (_clipSet != null && _clipSet.Count > 0)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Clips in Controller");
EditorGUILayout.BeginVertical("Box");
foreach (AnimationClip clip in _clipSet)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(clip.name);
// 既存クリップのリネームボタン
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 DrawClipDragAndDropList()
{
// ドラッグアンドドロップエリア
EditorGUILayout.Space();
EditorGUILayout.LabelField("Drag and Drop Clip Here to Add to List");
var dragRect = GUILayoutUtility.GetRect(0, 50, GUILayout.ExpandWidth(true));
GUI.Box(dragRect, "Drag and Drop Animation Clips");
// ドラッグアンドドロップでリストに追加
HandleDragAndDropForList(dragRect);
// クリップリストを表示
if (_clipsToAdd.Count > 0)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Clips to Add to Controller");
EditorGUILayout.BeginVertical("Box");
for (int i = 0; i < _clipsToAdd.Count; i++)
{
var clip = _clipsToAdd[i];
EditorGUILayout.BeginHorizontal();
// クリップがリネーム中であれば、名前を編集可能に
if (_clipsBeingRenamed.Contains(clip))
{
_clipNames[clip] = EditorGUILayout.TextField(_clipNames[clip], GUILayout.Width(200));
if (GUILayout.Button("Apply", GUILayout.Width(100)))
{
_clipsBeingRenamed.Remove(clip);
}
}
else
{
EditorGUILayout.LabelField(_clipNames[clip], GUILayout.Width(200));
if (GUILayout.Button("Rename", GUILayout.Width(100)))
{
_clipsBeingRenamed.Add(clip);
}
}
// リムーブボタン
if (GUILayout.Button("Remove", GUILayout.Width(100)))
{
_clipsToAdd.RemoveAt(i);
_clipNames.Remove(clip);
_clipsBeingRenamed.Remove(clip);
i--;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
}
}
/// <summary>
/// デフォルトのShowとHideのクリップを追加するメソッド
/// </summary>
private void AddDefaultClips()
{
var showClip = new AnimationClip { name = "Show" };
var hideClip = new AnimationClip { name = "Hide" };
_clipsToAdd.Add(showClip);
_clipsToAdd.Add(hideClip);
_clipNames[showClip] = "Show";
_clipNames[hideClip] = "Hide";
}
/// <summary>
/// ドラッグアンドドロップ処理(リスト用)
/// </summary>
private void HandleDragAndDropForList(Rect dropArea)
{
Event evt = Event.current;
switch (evt.type)
{
case EventType.DragUpdated:
case EventType.DragPerform:
if (!dropArea.Contains(evt.mousePosition))
return;
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (Object dragged in DragAndDrop.objectReferences)
{
if (dragged is AnimationClip clip && !_clipsToAdd.Contains(clip))
{
_clipsToAdd.Add(clip); // リストに追加
_clipNames[clip] = clip.name; // 名前管理用
Repaint();
}
}
}
break;
}
}
/// <summary>
/// 新しいアニメーターコントローラーを作成し、リスト内のクリップを含ませる
/// </summary>
private void CreateNewAnimatorControllerWithClips()
{
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]);
if (!Directory.Exists(path))
{
path = Path.GetDirectoryName(path);
}
path = Path.Combine(path, _controllerName + ".controller");
path = AssetDatabase.GenerateUniqueAssetPath(path);
// 新しいアニメーションコントローラーを作成
_controller = AnimatorController.CreateAnimatorControllerAtPath(path);
if (_controller == null)
{
Debug.LogError("Failed to create Animator Controller.");
return;
}
CacheClips();
// BaseLayerにステートマシンを取得
AnimatorStateMachine stateMachine = _controller.layers[0].stateMachine;
// DoNothing ステートを追加して初期ステートとして設定
AnimatorState doNothingState = stateMachine.AddState("DoNothing");
doNothingState.writeDefaultValues = false;
stateMachine.defaultState = doNothingState;
// リストに追加したクリップをコントローラーに追加
foreach (var clip in _clipsToAdd)
{
// クリップを複製して新しい名前を付ける
AnimationClip newClip = new AnimationClip();
EditorUtility.CopySerialized(clip, newClip);
newClip.name = _clipNames[clip];
// アセットとして追加
AssetDatabase.AddObjectToAsset(newClip, _controller);
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
AssetDatabase.Refresh();
// 新しいステートを作成し、Motionにクリップを割り当て
AnimatorState newState = stateMachine.AddState(newClip.name);
newState.motion = newClip;
newState.writeDefaultValues = false;
_clipSet.Add(newClip);
}
// リストをクリア
_clipsToAdd.Clear();
_clipNames.Clear();
}
/// <summary>
/// ドラッグアンドドロップされたクリップを複製してアニメーションコントローラーに追加
/// </summary>
private void AddClipToController(AnimationClip originalClip)
{
if (_controller == null || originalClip == null)
{
return;
}
string name = _clipNames[originalClip];
if (ContainsClipName(name))
{
return;
}
// アニメーションクリップを複製
AnimationClip newClip = new AnimationClip();
EditorUtility.CopySerialized(originalClip, newClip);
newClip.name = name;
// アセットとして追加
AssetDatabase.AddObjectToAsset(newClip, _controller);
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
AssetDatabase.Refresh();
// ステートを追加
AnimatorStateMachine stateMachine = _controller.layers[0].stateMachine;
AnimatorState newState = stateMachine.AddState(newClip.name);
newState.motion = newClip;
newState.writeDefaultValues = false;
_clipSet.Add(newClip);
}
/// <summary>
/// 新しいアニメーションクリップを作成し、コントローラーに追加する
/// </summary>
private void CreateNewClip(string clipName)
{
if (ContainsClipName(clipName))
{
return;
}
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();
// ステートを追加
AnimatorStateMachine stateMachine = _controller.layers[0].stateMachine;
AnimatorState newState = stateMachine.AddState(newClip.name);
newState.motion = newClip;
newState.writeDefaultValues = false;
_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)
{
if (clip == null || _controller == null)
{
return;
}
// コントローラー内のすべてのレイヤーを確認
foreach (var layer in _controller.layers)
{
AnimatorStateMachine stateMachine = layer.stateMachine;
// ステートマシン内のすべてのステートを確認し、削除するクリップと一致するステートを削除
List<ChildAnimatorState> statesToRemove = new List<ChildAnimatorState>();
foreach (var state in stateMachine.states)
{
if (state.state.motion == clip)
{
statesToRemove.Add(state);
}
}
// ステートを削除
foreach (var stateToRemove in statesToRemove)
{
stateMachine.RemoveState(stateToRemove.state);
}
}
// アセットからクリップを削除
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