303 lines
10 KiB
C#
303 lines
10 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using JetBrains.Annotations;
|
|
using UnityEditor;
|
|
using UnityEditor.Animations;
|
|
using UnityEditor.UIElements;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace AibisDream.SystemEditor
|
|
{
|
|
public class AnimatorClipLink : EditorWindow
|
|
{
|
|
[SerializeField] private VisualTreeAsset visualTreeAsset;
|
|
|
|
private ObjectField _animatorField;
|
|
private Label _animatorMessage;
|
|
private ListView _stateInfoView;
|
|
|
|
private TextField _clipsPathField;
|
|
private Label _clipsMessage;
|
|
private ListView _clipsNameList;
|
|
|
|
private HelpBox _matchMessageBox;
|
|
|
|
#region 部分缓存
|
|
|
|
private static AnimatorController _animatorCache;
|
|
private static string _clipsPathCache;
|
|
|
|
private ChildAnimatorState[] _statesCache;
|
|
private string[] _clipsCache;
|
|
|
|
private bool _isChecked;
|
|
private bool _allMatched;
|
|
private bool _isAllLoad;
|
|
|
|
#endregion
|
|
|
|
[MenuItem("Tools/AnimatorClipLink")]
|
|
public static void ShowExample()
|
|
{
|
|
AnimatorClipLink wnd = GetWindow<AnimatorClipLink>();
|
|
wnd.titleContent = new GUIContent("AnimatorClipLink");
|
|
}
|
|
|
|
public void CreateGUI()
|
|
{
|
|
// Each editor window contains a root VisualElement object
|
|
VisualElement root = rootVisualElement;
|
|
|
|
// Instantiate UXML
|
|
root.Add(visualTreeAsset.Instantiate());
|
|
|
|
// 注册一些事件
|
|
_animatorField = root.Q<ObjectField>("animator-field");
|
|
_animatorField.RegisterValueChangedCallback(OnAnimatorChanged);
|
|
_animatorMessage = root.Q<Label>("animator-message");
|
|
_stateInfoView = root.Q<ListView>("state-info-list");
|
|
|
|
_clipsPathField = root.Q<TextField>("clips-path-field");
|
|
_clipsPathField.RegisterValueChangedCallback(OnClipsPathChanged);
|
|
_clipsMessage = root.Q<Label>("clips-message");
|
|
_clipsNameList = root.Q<ListView>("clips-name-list");
|
|
|
|
_matchMessageBox = root.Q<HelpBox>("match-message-box");
|
|
|
|
root.Q<Button>("import-merge-button").clicked += ImportMerge;
|
|
|
|
// 初始化
|
|
_animatorMessage.text = "请添加AnimatorController";
|
|
_clipsMessage.text = "请输入Clips路径";
|
|
// 加载AnimatorState
|
|
if (_animatorCache != null)
|
|
{
|
|
_animatorField.value = _animatorCache;
|
|
LoadAnimatorController(_animatorCache);
|
|
}
|
|
else
|
|
{
|
|
UpdateStateInfos(Array.Empty<ChildAnimatorState>());
|
|
}
|
|
|
|
// 加载路径下的Clips
|
|
if (!string.IsNullOrWhiteSpace(_clipsPathCache))
|
|
{
|
|
_clipsPathField.value = _clipsPathCache;
|
|
LoadClipPath(_clipsPathCache);
|
|
}
|
|
else
|
|
{
|
|
UpdateClipInFolder(Array.Empty<string>());
|
|
}
|
|
}
|
|
|
|
private void UpdateStateInfos(ChildAnimatorState[] states)
|
|
{
|
|
_statesCache = states;
|
|
// 总条目
|
|
var stateList = states.ToList();
|
|
|
|
_stateInfoView.makeItem = MakeItem;
|
|
_stateInfoView.bindItem = BindItem;
|
|
_stateInfoView.itemsSource = stateList;
|
|
_stateInfoView.selectionType = UnityEngine.UIElements.SelectionType.None;
|
|
|
|
MatchStateAndClip();
|
|
return;
|
|
|
|
// 链接
|
|
void BindItem(VisualElement item, int index)
|
|
{
|
|
if (index >= stateList.Count) return;
|
|
|
|
var state = stateList[index];
|
|
if (item is ObjectField field)
|
|
{
|
|
field.value = state.state.motion;
|
|
field.label = state.state.name;
|
|
}
|
|
}
|
|
|
|
// 创建
|
|
VisualElement MakeItem()
|
|
{
|
|
var item = new ObjectField
|
|
{
|
|
objectType = typeof(AnimationClip)
|
|
};
|
|
item.SetEnabled(false);
|
|
return item;
|
|
}
|
|
}
|
|
|
|
private void OnAnimatorChanged(ChangeEvent<Object> evt)
|
|
{
|
|
if (evt.newValue is AnimatorController animatorController)
|
|
{
|
|
LoadAnimatorController(animatorController);
|
|
}
|
|
else
|
|
{
|
|
LoadAnimatorController(null);
|
|
}
|
|
}
|
|
|
|
private void LoadAnimatorController([CanBeNull] AnimatorController animatorController)
|
|
{
|
|
// 判断新的值是不是空的,是就清空State信息
|
|
if (animatorController == null)
|
|
{
|
|
_animatorCache = null;
|
|
_animatorMessage.text = "AnimatorController 为空";
|
|
// 清空State信息
|
|
UpdateStateInfos(Array.Empty<ChildAnimatorState>());
|
|
return;
|
|
}
|
|
|
|
_animatorCache = animatorController;
|
|
// 值更新的时候,更新State信息
|
|
var states = animatorController.layers[0].stateMachine.states;
|
|
_animatorMessage.text = $"BaseLayer共有{states.Length}个State";
|
|
UpdateStateInfos(states);
|
|
}
|
|
|
|
private void OnClipsPathChanged(ChangeEvent<string> evt)
|
|
{
|
|
LoadClipPath(evt.newValue);
|
|
}
|
|
|
|
private void LoadClipPath([CanBeNull] string clipsPath)
|
|
{
|
|
_clipsPathCache = clipsPath;
|
|
// 先判断路径是否存在
|
|
if (!AssetDatabase.IsValidFolder(clipsPath))
|
|
{
|
|
_clipsMessage.text = "路径不存在";
|
|
// 清空Clip信息
|
|
UpdateClipInFolder(Array.Empty<string>());
|
|
return;
|
|
}
|
|
|
|
// 从路径中获取Clip
|
|
var clipIds = AssetDatabase.FindAssets("t:AnimationClip", new[] { clipsPath });
|
|
// 获取名称
|
|
var clipNames = clipIds.Select(AssetDatabase.GUIDToAssetPath)
|
|
.Where(item => item.EndsWith(".anim"))
|
|
.ToArray();
|
|
|
|
_clipsMessage.text = $"共有{clipNames.Length}个Clip";
|
|
UpdateClipInFolder(clipNames);
|
|
}
|
|
|
|
private void UpdateClipInFolder(string[] clipNames)
|
|
{
|
|
_clipsCache = clipNames;
|
|
var clipNameList = clipNames.Select(Path.GetFileNameWithoutExtension).ToList();
|
|
|
|
_clipsNameList.makeItem = MakeItem;
|
|
_clipsNameList.bindItem = BindItem;
|
|
_clipsNameList.itemsSource = clipNameList;
|
|
|
|
MatchStateAndClip();
|
|
return;
|
|
|
|
void BindItem(VisualElement item, int index)
|
|
{
|
|
if (index >= clipNameList.Count) return;
|
|
|
|
var clipName = clipNameList[index];
|
|
if (item is Label label)
|
|
{
|
|
label.text = clipName;
|
|
}
|
|
}
|
|
|
|
VisualElement MakeItem() => new Label();
|
|
}
|
|
|
|
private void MatchStateAndClip()
|
|
{
|
|
if (_statesCache is not { Length: > 0 } || _clipsCache is not { Length: > 0 })
|
|
{
|
|
_matchMessageBox.text = "请先选择AnimatorController和Clips路径";
|
|
_matchMessageBox.messageType = HelpBoxMessageType.Warning;
|
|
_allMatched = false;
|
|
_isAllLoad = false;
|
|
return;
|
|
}
|
|
|
|
var clipNames = _clipsCache.Select(Path.GetFileNameWithoutExtension).ToArray();
|
|
// 匹配state和clips
|
|
var notMatchStates =
|
|
(from state in _statesCache where !clipNames.Contains(state.state.name) select state.state.name)
|
|
.ToList();
|
|
|
|
if (notMatchStates.Count == 0)
|
|
{
|
|
_matchMessageBox.messageType = HelpBoxMessageType.Info;
|
|
_matchMessageBox.text = "所有State都已经匹配";
|
|
_allMatched = true;
|
|
}
|
|
else
|
|
{
|
|
_matchMessageBox.messageType = HelpBoxMessageType.Warning;
|
|
_matchMessageBox.text = $"未匹配的State有{notMatchStates.Count}个:\n{string.Join(", ", notMatchStates)}";
|
|
_allMatched = false;
|
|
}
|
|
|
|
_isAllLoad = true;
|
|
_isChecked = false;
|
|
}
|
|
|
|
private void ImportMerge()
|
|
{
|
|
if (!_isAllLoad)
|
|
{
|
|
_matchMessageBox.messageType = HelpBoxMessageType.Warning;
|
|
_matchMessageBox.text = "请先选择AnimatorController和Clips路径";
|
|
return;
|
|
}
|
|
|
|
if (_isChecked || _allMatched)
|
|
{
|
|
MergeClips();
|
|
_isChecked = false;
|
|
|
|
_matchMessageBox.messageType = HelpBoxMessageType.Info;
|
|
_matchMessageBox.text = "匹配完成";
|
|
}
|
|
else
|
|
{
|
|
_matchMessageBox.messageType = HelpBoxMessageType.Warning;
|
|
_matchMessageBox.text = "仍有未匹配到Clip的State,确认要继续吗?";
|
|
_isChecked = true;
|
|
}
|
|
}
|
|
|
|
private void MergeClips()
|
|
{
|
|
var clipNames = _clipsCache.Select(Path.GetFileNameWithoutExtension).ToArray();
|
|
foreach (var state in _statesCache)
|
|
{
|
|
var idx = Array.IndexOf(clipNames, state.state.name);
|
|
if (idx != -1)
|
|
{
|
|
var newClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(_clipsCache[idx]);
|
|
var oldClip = state.state.motion;
|
|
|
|
var settings = AnimationUtility.GetAnimationClipSettings(newClip);
|
|
settings.loopTime = oldClip.isLooping;
|
|
AnimationUtility.SetAnimationClipSettings(newClip, settings);
|
|
|
|
state.state.motion = newClip;
|
|
}
|
|
}
|
|
|
|
LoadAnimatorController(_animatorCache);
|
|
}
|
|
}
|
|
} |