404 lines
15 KiB
C#
404 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Cielonos.MainGame.Characters;
|
|
using Sirenix.OdinInspector;
|
|
using SLSFramework.General;
|
|
using UnityEngine;
|
|
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
using UnityEditor.Callbacks;
|
|
#endif
|
|
|
|
namespace SLSUtilities.FunctionalAnimation
|
|
{
|
|
public enum TimeDisplayMode
|
|
{
|
|
[InspectorName("秒 (s)")] Seconds,
|
|
[InspectorName("帧 (f)")] Frames
|
|
}
|
|
|
|
[CreateAssetMenu(fileName = "NewFuncAnimData", menuName = "Functional Animation/FuncAnimData", order = 1)]
|
|
public class FuncAnimData : SerializedScriptableObject
|
|
{
|
|
public static bool EditorWantsRepaint = false;
|
|
|
|
[Title("编辑器设置")]
|
|
[EnumToggleButtons]
|
|
[OnValueChanged("RequestRepaint")] // <-- 修改
|
|
public TimeDisplayMode timeMode;
|
|
|
|
[Title("核心动画")]
|
|
[Required("必须指定一个动画片段")]
|
|
[OnValueChanged("RequestRepaint")] // <-- 修改
|
|
public AnimationClip animationClip;
|
|
|
|
[Title("编辑时信息")]
|
|
public FuncAnimInfo animInfo = new FuncAnimInfo("AnimationName", "StateName", true, DisruptionType.NormalAction, 1.0f, 0, true);
|
|
|
|
[Title("技能区间")]
|
|
[ListDrawerSettings(
|
|
ListElementLabelName = "@this.GetIntervalLabel()", // <-- 修改为调用方法
|
|
AddCopiesLastElement = true)]
|
|
public List<FuncAnimInterval> intervals = new List<FuncAnimInterval>()
|
|
{
|
|
new FuncAnimInterval(IntervalType.Cancellable),
|
|
new FuncAnimInterval(IntervalType.Startup),
|
|
new FuncAnimInterval(IntervalType.ExternalDisruption),
|
|
new FuncAnimInterval(IntervalType.Active),
|
|
new FuncAnimInterval(IntervalType.Invincible),
|
|
new FuncAnimInterval(IntervalType.Preinput),
|
|
new FuncAnimInterval(IntervalType.ActionDisruption),
|
|
new FuncAnimInterval(IntervalType.MovementDisruption),
|
|
new FuncAnimInterval(IntervalType.RootMotion)
|
|
};
|
|
|
|
[Title("动画事件")]
|
|
public EventCollection eventCollection;
|
|
|
|
[Title("变量存储")]
|
|
public VariableCollection variableCollection;
|
|
|
|
// (新增) 核心方法:将父级引用传递给子级
|
|
// 这让子对象 (Event/Interval) 能够访问 animationClip 来计算帧
|
|
[OnInspectorGUI("UpdateChildReferences")] // 在绘制其他所有内容之前运行
|
|
private void UpdateChildReferences()
|
|
{
|
|
if (intervals != null)
|
|
{
|
|
foreach (var interval in intervals)
|
|
{
|
|
if (interval != null) interval.parentData = this;
|
|
}
|
|
}
|
|
if (eventCollection.animEvents != null)
|
|
{
|
|
foreach (var evt in eventCollection.animEvents)
|
|
{
|
|
if (evt != null) evt.parentData = this;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RequestRepaint()
|
|
{
|
|
#if UNITY_EDITOR
|
|
EditorWantsRepaint = true;
|
|
#endif
|
|
}
|
|
|
|
public FuncAnimInterval Interval(IntervalType type, string name = "", int index = 0)
|
|
{
|
|
List<FuncAnimInterval> find = intervals.FindAll(interval => interval.intervalType == type);
|
|
|
|
if (find.Count == 1)
|
|
{
|
|
if (type != IntervalType.Custom)
|
|
{
|
|
return find[0];
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(name) && find[0].intervalName == name)
|
|
{
|
|
return find[0];
|
|
}
|
|
|
|
Debug.LogWarning($"[FuncAnimData.Interval] 找不到名为 '{name}' 的自定义区间。");
|
|
return null;
|
|
}
|
|
|
|
if (find.Count == 0)
|
|
{
|
|
Debug.LogWarning($"[FuncAnimData.Interval] 找不到类型为 '{type}' 的区间。");
|
|
return null;
|
|
}
|
|
|
|
if (index < 0 || index >= find.Count)
|
|
{
|
|
Debug.LogWarning($"[FuncAnimData.Interval] 类型为 '{type}' 的区间数量不足,无法获取索引 {index}。");
|
|
return null;
|
|
}
|
|
|
|
return find[index];
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace SLSUtilities.FunctionalAnimation
|
|
{
|
|
public enum IntervalType
|
|
{
|
|
[InspectorName("可取消窗口 (Cancellable)")]
|
|
Cancellable = 0,
|
|
[InspectorName("前摇 (Startup)")]
|
|
Startup = 10,
|
|
[InspectorName("外部打断窗口 (External Disruption)")]
|
|
ExternalDisruption = 11,
|
|
[InspectorName("攻击判定 (Active)")]
|
|
Active = 20,
|
|
[InspectorName("无敌帧 (Invincible)")]
|
|
Invincible = 21,
|
|
[InspectorName("预输入窗口 (Pre-Input)")]
|
|
Preinput = 22,
|
|
[InspectorName("新动作打断窗口 (Action Disruption)")]
|
|
ActionDisruption = 30,
|
|
[InspectorName("移动打断窗口 (Movement Disruption)")]
|
|
MovementDisruption = 31,
|
|
[InspectorName("强制动作打断窗口 (Forced Action Disruption) 如果没有此区间,默认整个动作都可被强制打断")]
|
|
ForcedActionDisruption = 32,
|
|
[InspectorName("Root Motion 作用区间,角色会使用动画中的位移")]
|
|
RootMotion = 40,
|
|
[InspectorName("自定义 (Custom)")]
|
|
Custom = 100
|
|
}
|
|
|
|
[Serializable]
|
|
public class FuncAnimInterval // <-- 关键改动: struct 变为 class
|
|
{
|
|
[NonSerialized, HideInInspector]
|
|
public FuncAnimData parentData;
|
|
|
|
// (新增) 辅助函数
|
|
private float GetClipDuration() => (parentData == null || parentData.animationClip == null)
|
|
? 10.0f : parentData.animationClip.length; // 默认10s
|
|
private bool ShowSeconds() => parentData == null || parentData.timeMode == TimeDisplayMode.Seconds || parentData.animationClip == null;
|
|
private bool ShowFrames() => parentData != null && parentData.timeMode == TimeDisplayMode.Frames && parentData.animationClip != null;
|
|
private int GetMaxFrames() => (parentData == null || parentData.animationClip == null || parentData.animationClip.frameRate == 0)
|
|
? 100 : Mathf.RoundToInt(parentData.animationClip.length * parentData.animationClip.frameRate);
|
|
|
|
public string GetIntervalLabel()
|
|
{
|
|
return intervalType == IntervalType.Custom ? (string.IsNullOrEmpty(intervalName) ? "Custom" : intervalName) : intervalType.ToString();
|
|
}
|
|
|
|
public IntervalType intervalType;
|
|
|
|
[ShowIf("intervalType", IntervalType.Custom)]
|
|
public string intervalName;
|
|
|
|
// (修改) 仅在 "Seconds" 模式下显示
|
|
[ShowIf("ShowSeconds")]
|
|
[MinMaxSlider(0f, "@GetClipDuration()", ShowFields = true)]
|
|
[LabelText("Time Range (s)")]
|
|
[SuffixLabel("秒")]
|
|
public Vector2 timeRange;
|
|
|
|
// (新增) 在 "Frames" 模式下显示的 "帧" 属性
|
|
[ShowInInspector, ShowIf("ShowFrames"), MinMaxSlider(0, "@GetMaxFrames()", ShowFields = true)]
|
|
[LabelText("Frame Range")]
|
|
public Vector2Int FrameRange
|
|
{
|
|
get
|
|
{
|
|
if (parentData == null || parentData.animationClip == null) return Vector2Int.zero;
|
|
float frameRate = parentData.animationClip.frameRate;
|
|
if (frameRate == 0) return Vector2Int.zero;
|
|
|
|
// (修改) 从 timeRange (秒) 计算
|
|
return new Vector2Int(
|
|
Mathf.RoundToInt(timeRange.x * frameRate),
|
|
Mathf.RoundToInt(timeRange.y * frameRate)
|
|
);
|
|
}
|
|
set
|
|
{
|
|
if (parentData == null || parentData.animationClip == null) return;
|
|
float frameRate = parentData.animationClip.frameRate;
|
|
if (frameRate == 0) return;
|
|
|
|
// (修改) 转换回 timeRange (秒)
|
|
timeRange.x = (float)value.x / frameRate;
|
|
timeRange.y = (float)value.y / frameRate;
|
|
}
|
|
}
|
|
|
|
// (修改) 属性现在直接返回 timeRange
|
|
public float StartTime => timeRange.x;
|
|
public float EndTime => timeRange.y;
|
|
|
|
public FuncAnimInterval(IntervalType intervalType)
|
|
{
|
|
this.parentData = null;
|
|
this.intervalType = intervalType;
|
|
this.intervalName = "";
|
|
this.timeRange = Vector2.zero;
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace SLSUtilities.FunctionalAnimation
|
|
{
|
|
[Serializable]
|
|
public class EventCollection : ICloneable<EventCollection>
|
|
{
|
|
[TitleGroup("时间轴事件")]
|
|
[ListDrawerSettings(ShowFoldout = true, DraggableItems = true)]
|
|
public List<FuncAnimEvent> animEvents = new List<FuncAnimEvent>();
|
|
|
|
[TitleGroup("时间轴事件")]
|
|
[DisableInEditorMode]
|
|
[ShowInInspector]
|
|
[Tooltip("在动作被打断时触发的事件列表")]
|
|
public List<FuncAnimEvent> endEvents => animEvents.FindAll(evt => evt.isEnd);
|
|
|
|
[TitleGroup("非时间轴事件")]
|
|
[Tooltip("在技能开始播放动画 *之前* 立即执行")]
|
|
[ListDrawerSettings(ShowFoldout = true, DraggableItems = false)]
|
|
[SerializeReference]
|
|
public List<FuncAnimPayloadBase> startEvents = new List<FuncAnimPayloadBase>();
|
|
|
|
[TitleGroup("非时间轴事件")]
|
|
[Tooltip("在技能被(StopSkill) *打断* 时执行")]
|
|
[ListDrawerSettings(ShowFoldout = true, DraggableItems = false)]
|
|
[SerializeReference]
|
|
public List<FuncAnimPayloadBase> disruptionEvents = new List<FuncAnimPayloadBase>();
|
|
|
|
[TitleGroup("非时间轴事件")]
|
|
[Tooltip("技能播放期间每帧执行")]
|
|
[ListDrawerSettings(ShowFoldout = true, DraggableItems = false)]
|
|
[SerializeReference]
|
|
public List<FuncAnimPayloadBase> updateEvents = new List<FuncAnimPayloadBase>();
|
|
|
|
[TitleGroup("非时间轴事件")]
|
|
[Tooltip("技能播放期间每帧执行")]
|
|
[ListDrawerSettings(ShowFoldout = true, DraggableItems = false)]
|
|
[SerializeReference]
|
|
public List<FuncAnimPayloadBase<bool>> updateUntilEvents = new List<FuncAnimPayloadBase<bool>>();
|
|
|
|
public EventCollection Clone()
|
|
{
|
|
EventCollection clone = new EventCollection();
|
|
clone.animEvents = new List<FuncAnimEvent>(this.animEvents);
|
|
clone.startEvents = new List<FuncAnimPayloadBase>(this.startEvents);
|
|
clone.disruptionEvents = new List<FuncAnimPayloadBase>(this.disruptionEvents);
|
|
clone.updateEvents = new List<FuncAnimPayloadBase>(this.updateEvents);
|
|
clone.updateUntilEvents = new List<FuncAnimPayloadBase<bool>>(this.updateUntilEvents);
|
|
return clone;
|
|
}
|
|
}
|
|
|
|
|
|
[Serializable]
|
|
public class FuncAnimEvent
|
|
{
|
|
// (新增) 非序列化的父级引用
|
|
[NonSerialized]
|
|
public FuncAnimData parentData;
|
|
|
|
// (新增) 辅助方法
|
|
private bool ShowSeconds() => parentData == null || parentData.timeMode == TimeDisplayMode.Seconds || parentData.animationClip == null;
|
|
private bool ShowFrames() => parentData != null && parentData.timeMode == TimeDisplayMode.Frames && parentData.animationClip != null;
|
|
private int GetMaxFrames() => (parentData == null || parentData.animationClip == null || parentData.animationClip.frameRate == 0)
|
|
? 100 : Mathf.RoundToInt(parentData.animationClip.length * parentData.animationClip.frameRate);
|
|
|
|
|
|
// (修改) 仅在 "Seconds" 模式下显示
|
|
[HorizontalGroup("EventLine", Width = 0.15f)]
|
|
[Tooltip("事件触发时间 (秒)")] [SuffixLabel("秒", Overlay = true)]
|
|
[LabelText("时间")] [LabelWidth(40)]
|
|
[ShowIf("ShowSeconds")]
|
|
public float triggerTime;
|
|
|
|
// (新增字段)
|
|
[HorizontalGroup("EventLine", Width = 0.15f)]
|
|
[ShowInInspector, ShowIf("ShowFrames"), SuffixLabel("f", Overlay = true)]
|
|
[LabelText("帧")] [LabelWidth(40)]
|
|
[MinValue(0)] [MaxValue("@GetMaxFrames()")]
|
|
public int TriggerFrame
|
|
{
|
|
get
|
|
{
|
|
if (parentData == null || parentData.animationClip == null) return 0;
|
|
float frameRate = parentData.animationClip.frameRate;
|
|
return Mathf.RoundToInt(triggerTime * frameRate);
|
|
}
|
|
set
|
|
{
|
|
if (parentData == null || parentData.animationClip == null) return;
|
|
float frameRate = parentData.animationClip.frameRate;
|
|
if (frameRate > 0)
|
|
{
|
|
triggerTime = (float)value / frameRate;
|
|
}
|
|
}
|
|
}
|
|
|
|
// (修改) "Bool" 是第二列
|
|
[HorizontalGroup("EventLine", Width = 0.15f)]
|
|
[Tooltip("如果技能被打断,是否也触发此事件?(即'End'事件)")]
|
|
[LabelWidth(40)]
|
|
[PropertyOrder(1)]
|
|
public bool isEnd;
|
|
|
|
// (修改) "Payload" 是第三列
|
|
[HorizontalGroup("EventLine")]
|
|
[HideLabel]
|
|
[SerializeReference]
|
|
[PropertyOrder(2)]
|
|
public FuncAnimPayloadBase payload;
|
|
|
|
public FuncAnimEvent(FuncAnimData parentData, float triggerTime, FuncAnimPayloadBase payload, bool isEnd)
|
|
{
|
|
this.parentData = parentData;
|
|
this.triggerTime = triggerTime;
|
|
this.isEnd = isEnd;
|
|
this.payload = payload;
|
|
}
|
|
|
|
public void Invoke()
|
|
{
|
|
payload?.Invoke();
|
|
}
|
|
}
|
|
|
|
public class VariableCollection
|
|
{
|
|
public Dictionary<string, string> variables = new Dictionary<string, string>();
|
|
|
|
public T GetVariable<T>(string key, T defaultValue = default)
|
|
{
|
|
if (variables.TryGetValue(key, out string valueStr))
|
|
{
|
|
try
|
|
{
|
|
return (T)Convert.ChangeType(valueStr, typeof(T));
|
|
}
|
|
catch
|
|
{
|
|
Debug.LogWarning($"[FuncAnimData.VariableCollection] 无法将变量 '{key}' 的值转换为类型 {typeof(T)},返回默认值。");
|
|
return defaultValue;
|
|
}
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
public VariableCollection Clone()
|
|
{
|
|
VariableCollection clone = new VariableCollection();
|
|
clone.variables = new Dictionary<string, string>(this.variables);
|
|
return clone;
|
|
}
|
|
}
|
|
|
|
public static class FuncAnimExtensions
|
|
{
|
|
public static void Invoke(this IList<FuncAnimEvent> events)
|
|
{
|
|
if (events == null) return;
|
|
foreach (FuncAnimEvent evt in events)
|
|
{
|
|
evt.Invoke();
|
|
}
|
|
}
|
|
|
|
public static void Invoke(this IList<FuncAnimPayloadBase> events)
|
|
{
|
|
if (events == null) return;
|
|
foreach (FuncAnimPayloadBase evt in events)
|
|
{
|
|
evt.Invoke();
|
|
}
|
|
}
|
|
}
|
|
} |