commit a2808b627d1d7d19398acae47bb1f39210c243da Author: 陈思海 <1464576565@qq.com> Date: Wed Apr 15 19:47:09 2026 +0800 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da556c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# UnityProject + +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Mm]emoryCaptures/ +/EditorBuild/ +/[Aa]ssets/StreamingAssets +/[Aa]ssets/StreamingAssets.meta +/BuildBundleInfo/ + +# Asset meta data should only be ignored when the corresponding asset is also ignored +!/[Aa]ssets/**/*.meta + +# Uncomment this line if you wish to ignore the asset store tools plugin +# /[Aa]ssets/AssetStoreTools* + +# Autogenerated Jetbrains Rider plugin +[Aa]ssets/Plugins/Editor/JetBrains* + +# Visual Studio cache directory +.vs/ + +# Gradle cache directory +.gradle/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db + +# Unity3D generated meta files +*.pidb.meta +*.pdb.meta +*.mdb.meta + +# Unity3D generated file on crash reports +sysinfo.txt + +# Builds +*.apk + +# Crashlytics generated file +crashlytics-build.properties + +#HybirdCLR(HuaTuo) +/HybirdCLRData/ +[Hh]ybridCLRData/ + +#AATemp +[Aa]ssets/AATemp/ +[Aa]ssets/AATemp.meta + +# Custom AATest +[Aa]ssets/AATest/ +[Aa]ssets/AATest.meta + + + +#Sandbox +Sandbox/ + +#MAC +.DS_Store + +#Rider +.idea/ diff --git a/Assets/.claude/settings.local.json b/Assets/.claude/settings.local.json new file mode 100644 index 0000000..bc469a0 --- /dev/null +++ b/Assets/.claude/settings.local.json @@ -0,0 +1,22 @@ +{ + "permissions": { + "allow": [ + "Bash(where pdftotext:*)", + "Bash(pdftotext \"G:\\\\\\\\UnityProject\\\\\\\\CapabilitySystem\\\\\\\\Assets\\\\\\\\技术文档.pdf\" \"G:\\\\\\\\UnityProject\\\\\\\\CapabilitySystem\\\\\\\\Assets\\\\\\\\技术文档.txt\")", + "Bash(ls -1 CapabilitySystem/Core/*.cs)", + "Bash(pdftotext \"技术文档.pdf\" - | head -n 500)", + "Bash(pdftotext \"技术文档.pdf\" - | tail -n +500 | head -n 500)", + "Bash(pdftotext \"技术文档.pdf\" /tmp/tech_doc.txt)", + "Read(//tmp/**)", + "Bash(pdftotext \"技术文档.pdf\" - 2>/dev/null)", + "Bash(grep '\"role\":\"user\"' \"C:\\\\Users\\\\admin\\\\.claude\\\\projects\\\\G--UnityProject-CapabilitySystem-Assets\\\\8203d8cd-1a37-42bd-8ba3-21ec01f49b28.jsonl\" | tail -1 | python -c \"import sys, json; data = json.load\\(sys.stdin\\); print\\(data['message']['content'][0]['text']\\)\")", + "Bash(grep '\"role\":\"user\"' \"C:\\\\Users\\\\admin\\\\.claude\\\\projects\\\\G--UnityProject-CapabilitySystem-Assets\\\\8203d8cd-1a37-42bd-8ba3-21ec01f49b28.jsonl\" | tail -1 | python -c \"import sys, json; data = json.load\\(sys.stdin\\); content = data['message']['content']; print\\(content if isinstance\\(content, str\\) else content[0] if isinstance\\(content, list\\) else content\\)\")", + "Bash(python3 -c \"import PyPDF2; pdf = PyPDF2.PdfReader\\('/g/UnityProject/CapabilitySystem/Assets/技术文档.pdf'\\); print\\(f'页数: {len\\(pdf.pages\\)}'\\); [print\\(f'--- 第{i+1}页 ---\\\\n{pdf.pages[i].extract_text\\(\\)}'\\) for i in range\\(min\\(5, len\\(pdf.pages\\)\\)\\)]\" 2>/dev/null || python3 -c \"import pdfplumber; pdf = pdfplumber.open\\('/g/UnityProject/CapabilitySystem/Assets/技术文档.pdf'\\); print\\(f'页数: {len\\(pdf.pages\\)}'\\); [print\\(f'--- 第{i+1}页 ---\\\\n{pdf.pages[i].extract_text\\(\\)}'\\) for i in range\\(min\\(5, len\\(pdf.pages\\)\\)\\)]\" 2>/dev/null || echo \"需要安装PDF解析库\")", + "Bash(pip install:*)", + "Bash(python3 -c \"\nimport pdfplumber\npdf = pdfplumber.open\\('/g/UnityProject/CapabilitySystem/Assets/技术文档.pdf'\\)\ntotal = len\\(pdf.pages\\)\nprint\\(f'总页数: {total}'\\)\nfor i in range\\(min\\(10, total\\)\\):\n text = pdf.pages[i].extract_text\\(\\)\n if text:\n print\\(f'\\\\n===== 第{i+1}页 ====='\\)\n print\\(text[:2000]\\)\n\" 2>&1)", + "Bash(python --version 2>&1; where python 2>&1; ls /g/UnityProject/CapabilitySystem/Assets/技术文档.pdf)", + "Bash(python -c \"import pdfplumber; print\\('pdfplumber ok'\\)\" 2>&1)", + "Bash(python -c \"\nimport pdfplumber\npdf = pdfplumber.open\\('G:/UnityProject/CapabilitySystem/Assets/技术文档.pdf'\\)\ntotal = len\\(pdf.pages\\)\nprint\\(f'总页数: {total}'\\)\nfor i in range\\(min\\(8, total\\)\\):\n text = pdf.pages[i].extract_text\\(\\)\n if text:\n print\\(f'\\\\n===== 第{i+1}页 ====='\\)\n print\\(text[:3000]\\)\npdf.close\\(\\)\n\" 2>&1)" + ] + } +} diff --git a/Assets/CapabilitySystem.meta b/Assets/CapabilitySystem.meta new file mode 100644 index 0000000..81fb0b2 --- /dev/null +++ b/Assets/CapabilitySystem.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bbdaf55c33b53e34b81f7e2588d66cf7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Attributes.meta b/Assets/CapabilitySystem/Attributes.meta new file mode 100644 index 0000000..14b1190 --- /dev/null +++ b/Assets/CapabilitySystem/Attributes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5ea2f924fbe109c44be5ebb61f540d30 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Attributes/TickOrderAttribute.cs b/Assets/CapabilitySystem/Attributes/TickOrderAttribute.cs new file mode 100644 index 0000000..034f7ef --- /dev/null +++ b/Assets/CapabilitySystem/Attributes/TickOrderAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace CapabilitySystem +{ + /// + /// 标记Capability的Tick执行顺序 + /// + [AttributeUsage(AttributeTargets.Class, Inherited = true)] + public class TickOrderAttribute : Attribute + { + public TickGroup TickGroup { get; } + public int Order { get; } + + /// + /// 定义Capability的执行顺序 + /// + /// Tick组 + /// 组内顺序(默认0,数值越小越先执行) + public TickOrderAttribute(TickGroup tickGroup, int order = 0) + { + TickGroup = tickGroup; + Order = order; + } + } +} diff --git a/Assets/CapabilitySystem/Attributes/TickOrderAttribute.cs.meta b/Assets/CapabilitySystem/Attributes/TickOrderAttribute.cs.meta new file mode 100644 index 0000000..86ca2eb --- /dev/null +++ b/Assets/CapabilitySystem/Attributes/TickOrderAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 08c8672f9dd000447aef58db8940efd1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Core.meta b/Assets/CapabilitySystem/Core.meta new file mode 100644 index 0000000..5cb8606 --- /dev/null +++ b/Assets/CapabilitySystem/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e49e089b37c87894696d6f3b49563abf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Core/ActionCapability.cs b/Assets/CapabilitySystem/Core/ActionCapability.cs new file mode 100644 index 0000000..2f81212 --- /dev/null +++ b/Assets/CapabilitySystem/Core/ActionCapability.cs @@ -0,0 +1,73 @@ +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// Action Capability基类 + /// 配合ActionQueue使用,用于队列化的行为 + /// + public abstract class ActionCapability : Capability + { + protected ActionQueue actionQueue; + protected bool isActionComplete; + + protected override void Setup() + { + base.Setup(); + actionQueue = new ActionQueue(); + } + + protected override bool ShouldActivate() + { + // 由外部控制激活(通过ActionQueue) + return false; + } + + protected override void OnActivated() + { + base.OnActivated(); + isActionComplete = false; + SetupActions(); + actionQueue.Start(); + } + + protected override void TickActive(float deltaTime) + { + base.TickActive(deltaTime); + actionQueue.Update(deltaTime); + + // 检查队列是否完成 + if (!actionQueue.IsRunning) + { + isActionComplete = true; + } + } + + protected override bool ShouldDeactivate() + { + return isActionComplete; + } + + protected override void OnDeactivated() + { + actionQueue.Clear(); + base.OnDeactivated(); + } + + /// + /// 子类实现:设置Action队列 + /// + protected abstract void SetupActions(); + + /// + /// 手动激活此ActionCapability + /// + public void Execute() + { + if (!IsActive) + { + Activate(); + } + } + } +} diff --git a/Assets/CapabilitySystem/Core/ActionCapability.cs.meta b/Assets/CapabilitySystem/Core/ActionCapability.cs.meta new file mode 100644 index 0000000..4b4d012 --- /dev/null +++ b/Assets/CapabilitySystem/Core/ActionCapability.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2c5f43ff96d913240bd0d7d270a05814 \ No newline at end of file diff --git a/Assets/CapabilitySystem/Core/ActionQueue.cs b/Assets/CapabilitySystem/Core/ActionQueue.cs new file mode 100644 index 0000000..44f0e73 --- /dev/null +++ b/Assets/CapabilitySystem/Core/ActionQueue.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// Action队列系统 + /// 用于管理顺序执行的Action,适用于Boss战斗和Puzzle系统 + /// + public class ActionQueue + { + /// Action定义 + public class ActionDefinition + { + public string Name; + public Action OnStart; + public Action OnUpdate; + public Func IsComplete; + public Action OnEnd; + public float Duration; + public bool UsesDuration; + + public ActionDefinition(string name) + { + Name = name; + Duration = 0f; + UsesDuration = false; + } + } + + private Queue actionQueue = new Queue(); + private ActionDefinition currentAction; + private float currentActionTime; + private bool isRunning; + + #region Properties + + public bool IsRunning => isRunning; + public int QueueCount => actionQueue.Count; + public ActionDefinition CurrentAction => currentAction; + + #endregion + + #region Queue Management + + /// + /// 添加Action到队列 + /// + public void Enqueue(ActionDefinition action) + { + if (action == null) + { + Debug.LogWarning("[ActionQueue] Cannot enqueue null action"); + return; + } + + actionQueue.Enqueue(action); + } + + /// + /// 添加基于时长的Action + /// + public void EnqueueDuration(string name, float duration, Action onStart = null, Action onUpdate = null, Action onEnd = null) + { + var action = new ActionDefinition(name) + { + Duration = duration, + UsesDuration = true, + OnStart = onStart, + OnUpdate = onUpdate, + OnEnd = onEnd + }; + Enqueue(action); + } + + /// + /// 添加基于条件的Action + /// + public void EnqueueCondition(string name, Func isComplete, Action onStart = null, Action onUpdate = null, Action onEnd = null) + { + var action = new ActionDefinition(name) + { + IsComplete = isComplete, + UsesDuration = false, + OnStart = onStart, + OnUpdate = onUpdate, + OnEnd = onEnd + }; + Enqueue(action); + } + + /// + /// 清空队列 + /// + public void Clear() + { + actionQueue.Clear(); + if (currentAction != null) + { + currentAction.OnEnd?.Invoke(); + currentAction = null; + } + currentActionTime = 0f; + isRunning = false; + } + + #endregion + + #region Execution + + /// + /// 开始执行队列 + /// + public void Start() + { + if (isRunning) + { + Debug.LogWarning("[ActionQueue] Already running"); + return; + } + + isRunning = true; + StartNextAction(); + } + + /// + /// 停止执行 + /// + public void Stop() + { + if (currentAction != null) + { + currentAction.OnEnd?.Invoke(); + currentAction = null; + } + isRunning = false; + currentActionTime = 0f; + } + + /// + /// 更新队列(需要在外部每帧调用) + /// + public void Update(float deltaTime) + { + if (!isRunning || currentAction == null) + return; + + currentActionTime += deltaTime; + + // 调用更新回调 + currentAction.OnUpdate?.Invoke(deltaTime); + + // 检查是否完成 + bool isComplete = false; + if (currentAction.UsesDuration) + { + isComplete = currentActionTime >= currentAction.Duration; + } + else if (currentAction.IsComplete != null) + { + isComplete = currentAction.IsComplete(); + } + + if (isComplete) + { + // 当前Action完成 + currentAction.OnEnd?.Invoke(); + currentAction = null; + currentActionTime = 0f; + + // 开始下一个Action + StartNextAction(); + } + } + + private void StartNextAction() + { + if (actionQueue.Count == 0) + { + // 队列为空,停止 + isRunning = false; + return; + } + + currentAction = actionQueue.Dequeue(); + currentActionTime = 0f; + currentAction.OnStart?.Invoke(); + } + + #endregion + } +} + diff --git a/Assets/CapabilitySystem/Core/ActionQueue.cs.meta b/Assets/CapabilitySystem/Core/ActionQueue.cs.meta new file mode 100644 index 0000000..884e767 --- /dev/null +++ b/Assets/CapabilitySystem/Core/ActionQueue.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 590dcae1712665a49ae5ffced1edce04 \ No newline at end of file diff --git a/Assets/CapabilitySystem/Core/Capability.cs b/Assets/CapabilitySystem/Core/Capability.cs new file mode 100644 index 0000000..68c4210 --- /dev/null +++ b/Assets/CapabilitySystem/Core/Capability.cs @@ -0,0 +1,314 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// Capability基类 + /// Capability是一个单一的功能单元,包含自身所有的状态和决策逻辑 + /// + public abstract class Capability + { + #region Properties + + /// 是否激活 + public bool IsActive { get; private set; } + + /// 拥有者GameObject + public GameObject Owner { get; private set; } + + /// Tick组 + public TickGroup TickGroup { get; private set; } + + /// Tick顺序 + public int TickOrder { get; private set; } + + /// 标签列表 + public List Tags { get; private set; } + + /// 配置数据 + public CapabilityData Data { get; private set; } + + /// 关联的CapabilityComponent + protected CapabilityComponent CapabilityComponent { get; private set; } + + /// 黑板系统(快捷访问) + protected CapabilityBlackboard Blackboard => CapabilityComponent?.Blackboard; + + /// 时间记录器(快捷访问) + protected TemporalLogger TemporalLogger => CapabilityComponent?.TemporalLogger; + + /// 激活历史记录(用于调试) + public List ActivationHistory { get; private set; } + + /// 最大历史记录数量 + private const int MaxHistoryCount = 500; // 增加到500条,支持更长时间的调试 + + /// IsBlocked缓存 + private bool cachedIsBlocked; + private int lastBlockCheckFrame = -1; + + #endregion + + #region Lifecycle + + /// + /// 初始化Capability + /// + internal void Initialize(GameObject owner, CapabilityComponent component, CapabilityData data = null) + { + Owner = owner; + CapabilityComponent = component; + Data = data; + Tags = new List(); + IsActive = false; + ActivationHistory = new List(); + + // 读取TickOrder特性 + var attribute = GetType().GetCustomAttributes(typeof(TickOrderAttribute), true); + if (attribute.Length > 0) + { + var tickOrderAttr = (TickOrderAttribute)attribute[0]; + TickGroup = tickOrderAttr.TickGroup; + TickOrder = tickOrderAttr.Order; + } + else + { + // 默认值 + TickGroup = TickGroup.Gameplay; + TickOrder = 0; + } + + // 注册到系统 + CapabilitySystem.Instance.RegisterCapability(this); + + // 调用子类Setup + Setup(); + } + + /// + /// 销毁Capability + /// + internal void Destroy() + { + if (IsActive) + { + Deactivate(); + } + + CapabilitySystem.Instance.UnregisterCapability(this); + } + + /// + /// 激活Capability + /// + internal void Activate() + { + if (IsActive) return; + + IsActive = true; + + // 记录激活时间 + var record = new CapabilityActivationRecord + { + ActivateTime = Time.time, + ActivateFrame = Time.frameCount, + Position = Owner.transform.position + }; + ActivationHistory.Add(record); + + // 限制历史记录数量 + if (ActivationHistory.Count > MaxHistoryCount) + { + ActivationHistory.RemoveAt(0); + } + + // 记录到TemporalLogger + if (TemporalLogger != null) + { + string key = $"{Owner.name}.{GetType().Name}"; + TemporalLogger.Log($"{key}.Active", true); + TemporalLogger.LogPosition(key, Owner.transform.position); + TemporalLogger.LogRotation(key, Owner.transform.rotation); + } + + OnActivated(); + } + + /// + /// 失活Capability + /// + internal void Deactivate() + { + if (!IsActive) return; + + IsActive = false; + + // 记录失活时间 + if (ActivationHistory.Count > 0) + { + var lastRecord = ActivationHistory[ActivationHistory.Count - 1]; + lastRecord.DeactivateTime = Time.time; + lastRecord.DeactivateFrame = Time.frameCount; + lastRecord.Duration = lastRecord.DeactivateTime - lastRecord.ActivateTime; + } + + // 记录到TemporalLogger + if (TemporalLogger != null) + { + string key = $"{Owner.name}.{GetType().Name}"; + TemporalLogger.Log($"{key}.Active", false); + } + + OnDeactivated(); + } + + /// + /// 每帧更新(仅在激活时调用) + /// + internal void Tick(float deltaTime) + { + TickActive(deltaTime); + } + + #endregion + + #region Virtual Methods (子类重写) + + /// + /// 初始化时调用,用于获取组件引用等 + /// + protected virtual void Setup() { } + + /// + /// 检查是否应该激活(未激活时每帧调用) + /// + protected virtual bool ShouldActivate() { return false; } + + /// + /// 检查是否应该失活(激活时每帧调用) + /// + protected virtual bool ShouldDeactivate() { return false; } + + /// + /// 激活时调用 + /// + protected virtual void OnActivated() { } + + /// + /// 失活时调用 + /// + protected virtual void OnDeactivated() { } + + /// + /// 激活状态下每帧调用 + /// + protected virtual void TickActive(float deltaTime) { } + + #endregion + + #region Tag Blocking + + /// + /// 阻塞带有指定Tag的所有Capability + /// + protected void BlockCapabilitiesWithTag(CapabilityTag tag, Instigator instigator) + { + CapabilityComponent.BlockTag(tag, instigator); + } + + /// + /// 解除阻塞带有指定Tag的Capability + /// + protected void UnblockCapabilitiesWithTag(CapabilityTag tag, Instigator instigator) + { + CapabilityComponent.UnblockTag(tag, instigator); + } + + /// + /// 检查是否被阻塞(带缓存优化) + /// + public bool IsBlocked() + { + // 同一帧内使用缓存结果 + if (lastBlockCheckFrame == Time.frameCount) + { + return cachedIsBlocked; + } + + cachedIsBlocked = false; + foreach (var tag in Tags) + { + if (CapabilityComponent.IsTagBlocked(tag)) + { + cachedIsBlocked = true; + break; + } + } + + lastBlockCheckFrame = Time.frameCount; + return cachedIsBlocked; + } + + #endregion + + #region Dynamic Priority + + /// + /// 运行时设置Tick顺序 + /// + public void SetTickOrder(TickGroup group, int order) + { + if (TickGroup != group || TickOrder != order) + { + TickGroup = group; + TickOrder = order; + CapabilitySystem.Instance.MarkNeedsSort(); + } + } + + #endregion + + #region Helper Methods + + /// + /// 获取组件(泛型方法) + /// + protected T GetComponent() where T : Component + { + return Owner.GetComponent(); + } + + /// + /// 尝试获取组件 + /// + protected bool TryGetComponent(out T component) where T : Component + { + return Owner.TryGetComponent(out component); + } + + #endregion + + #region Internal Update Methods + + /// + /// 内部更新方法,由CapabilitySystem调用 + /// + internal bool InternalShouldActivate() + { + if (IsBlocked()) return false; + return ShouldActivate(); + } + + /// + /// 内部失活检查,由CapabilitySystem调用 + /// + internal bool InternalShouldDeactivate() + { + return ShouldDeactivate(); + } + + #endregion + } +} diff --git a/Assets/CapabilitySystem/Core/Capability.cs.meta b/Assets/CapabilitySystem/Core/Capability.cs.meta new file mode 100644 index 0000000..d75f412 --- /dev/null +++ b/Assets/CapabilitySystem/Core/Capability.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4dbadc84377efab488b761cd541fc9b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Core/CapabilityActivationRecord.cs b/Assets/CapabilitySystem/Core/CapabilityActivationRecord.cs new file mode 100644 index 0000000..df6963c --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityActivationRecord.cs @@ -0,0 +1,41 @@ +namespace CapabilitySystem +{ + /// + /// Capability激活记录 + /// 用于调试和时间轴显示 + /// + public class CapabilityActivationRecord + { + /// 激活时间 + public float ActivateTime { get; set; } + + /// 失活时间 + public float DeactivateTime { get; set; } + + /// 激活帧 + public int ActivateFrame { get; set; } + + /// 失活帧 + public int DeactivateFrame { get; set; } + + /// 持续时间 + public float Duration { get; set; } + + /// 是否仍在激活中 + public bool IsStillActive => DeactivateTime == 0; + + /// 阻塞原因(如果有) + public string BlockReason { get; set; } + + /// 激活时的位置(可选) + public UnityEngine.Vector3? Position { get; set; } + + /// 自定义数据(用于扩展) + public System.Collections.Generic.Dictionary CustomData { get; set; } + + public CapabilityActivationRecord() + { + CustomData = new System.Collections.Generic.Dictionary(); + } + } +} diff --git a/Assets/CapabilitySystem/Core/CapabilityActivationRecord.cs.meta b/Assets/CapabilitySystem/Core/CapabilityActivationRecord.cs.meta new file mode 100644 index 0000000..819bb25 --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityActivationRecord.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 504183dddb5845f4fb0aa0beedf7302a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Core/CapabilityBlackboard.cs b/Assets/CapabilitySystem/Core/CapabilityBlackboard.cs new file mode 100644 index 0000000..1e19ccc --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityBlackboard.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// Capability黑板系统 + /// 用于Capability之间共享数据和通信 + /// + public class CapabilityBlackboard + { + private Dictionary data = new Dictionary(); + private Dictionary>> listeners = new Dictionary>>(); + + #region Data Access + + /// + /// 设置数据 + /// + public void Set(string key, T value) + { + if (string.IsNullOrEmpty(key)) + { + Debug.LogWarning("[CapabilityBlackboard] Key cannot be null or empty"); + return; + } + + data[key] = value; + + // 通知监听者 + if (listeners.ContainsKey(key)) + { + foreach (var listener in listeners[key]) + { + listener?.Invoke(value); + } + } + } + + /// + /// 获取数据 + /// + public T Get(string key, T defaultValue = default) + { + if (string.IsNullOrEmpty(key)) + { + Debug.LogWarning("[CapabilityBlackboard] Key cannot be null or empty"); + return defaultValue; + } + + if (data.TryGetValue(key, out object value)) + { + if (value is T typedValue) + { + return typedValue; + } + else + { + Debug.LogWarning($"[CapabilityBlackboard] Type mismatch for key '{key}'. Expected {typeof(T)}, got {value.GetType()}"); + return defaultValue; + } + } + + return defaultValue; + } + + /// + /// 尝试获取数据 + /// + public bool TryGet(string key, out T value) + { + value = default; + + if (string.IsNullOrEmpty(key)) + return false; + + if (data.TryGetValue(key, out object objValue) && objValue is T typedValue) + { + value = typedValue; + return true; + } + + return false; + } + + /// + /// 检查是否包含指定键 + /// + public bool Contains(string key) + { + return !string.IsNullOrEmpty(key) && data.ContainsKey(key); + } + + /// + /// 移除数据 + /// + public bool Remove(string key) + { + if (string.IsNullOrEmpty(key)) + return false; + + return data.Remove(key); + } + + /// + /// 清空所有数据 + /// + public void Clear() + { + data.Clear(); + } + + #endregion + + #region Event System + + /// + /// 监听数据变化 + /// + public void AddListener(string key, Action callback) + { + if (string.IsNullOrEmpty(key) || callback == null) + return; + + if (!listeners.ContainsKey(key)) + { + listeners[key] = new List>(); + } + + if (!listeners[key].Contains(callback)) + { + listeners[key].Add(callback); + } + } + + /// + /// 移除监听 + /// + public void RemoveListener(string key, Action callback) + { + if (string.IsNullOrEmpty(key) || callback == null) + return; + + if (listeners.ContainsKey(key)) + { + listeners[key].Remove(callback); + + if (listeners[key].Count == 0) + { + listeners.Remove(key); + } + } + } + + /// + /// 移除指定键的所有监听 + /// + public void RemoveAllListeners(string key) + { + if (!string.IsNullOrEmpty(key)) + { + listeners.Remove(key); + } + } + + #endregion + + #region Debug + + /// + /// 获取所有键 + /// + public IEnumerable GetAllKeys() + { + return data.Keys; + } + + /// + /// 获取数据数量 + /// + public int Count => data.Count; + + #endregion + } +} diff --git a/Assets/CapabilitySystem/Core/CapabilityBlackboard.cs.meta b/Assets/CapabilitySystem/Core/CapabilityBlackboard.cs.meta new file mode 100644 index 0000000..41d424c --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityBlackboard.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: baf276d6a762e61489825601a75f1562 \ No newline at end of file diff --git a/Assets/CapabilitySystem/Core/CapabilityComponent.cs b/Assets/CapabilitySystem/Core/CapabilityComponent.cs new file mode 100644 index 0000000..59e85c1 --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityComponent.cs @@ -0,0 +1,343 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// Capability组件,挂载到GameObject上管理Capability + /// + public class CapabilityComponent : MonoBehaviour + { + [SerializeField] + [Tooltip("默认应用的Sheet列表")] + private List defaultSheets = new List(); + + // 所有Capability实例 + private List capabilities = new List(); + + // Capability引用计数:Type -> (Capability, RefCount) + private Dictionary capabilityRegistry = new Dictionary(); + + // 标签阻塞器:Tag -> Instigator列表 + private Dictionary> tagBlockers = new Dictionary>(); + + // 已应用的Sheet(用于运行时管理) + private HashSet appliedSheets = new HashSet(); + + // 黑板系统(用于Capability间通信) + private CapabilityBlackboard blackboard = new CapabilityBlackboard(); + + // 时间记录器(用于调试和回放) + private TemporalLogger temporalLogger; + + #region Unity Lifecycle + + private void Awake() + { + // 初始化 TemporalLogger(不能在字段初始化器中创建,因为会调用 Time.time) + temporalLogger = new TemporalLogger(); + + // 应用默认Sheet + foreach (var sheet in defaultSheets) + { + if (sheet != null) + { + AddSheet(sheet); + } + } + } + + private void OnDestroy() + { + // 销毁所有Capability + foreach (var capability in capabilities.ToList()) + { + capability.Destroy(); + } + capabilities.Clear(); + } + + #endregion + + #region Sheet Management + + /// + /// 添加Sheet + /// + public void AddSheet(CapabilitySheet sheet) + { + if (sheet == null || appliedSheets.Contains(sheet)) + return; + + appliedSheets.Add(sheet); + sheet.ApplyToGameObject(gameObject, this); + } + + /// + /// 移除Sheet + /// + public void RemoveSheet(CapabilitySheet sheet) + { + if (sheet == null || !appliedSheets.Contains(sheet)) + return; + + appliedSheets.Remove(sheet); + + // 移除Sheet创建的Capability(通过引用计数) + sheet.RemoveFromGameObject(this); + } + + #endregion + + #region Capability Management + + /// + /// 添加Capability实例(内部使用,支持引用计数) + /// + internal void AddCapability(Capability capability, bool incrementRefCount = true) + { + if (capability == null) + return; + + Type capabilityType = capability.GetType(); + + // 检查是否已存在 + if (capabilityRegistry.ContainsKey(capabilityType)) + { + if (incrementRefCount) + { + // 增加引用计数 + var entry = capabilityRegistry[capabilityType]; + capabilityRegistry[capabilityType] = (entry.capability, entry.refCount + 1); + } + return; + } + + // 添加新Capability + capabilities.Add(capability); + capabilityRegistry[capabilityType] = (capability, 1); + } + + /// + /// 移除Capability实例(内部使用,支持引用计数) + /// + internal void RemoveCapability(Capability capability, bool decrementRefCount = true) + { + if (capability == null) + return; + + Type capabilityType = capability.GetType(); + + if (!capabilityRegistry.ContainsKey(capabilityType)) + return; + + if (decrementRefCount) + { + var entry = capabilityRegistry[capabilityType]; + int newRefCount = entry.refCount - 1; + + if (newRefCount > 0) + { + // 还有其他引用,只减少计数 + capabilityRegistry[capabilityType] = (entry.capability, newRefCount); + return; + } + } + + // 引用计数为0,真正移除 + capabilities.Remove(capability); + capabilityRegistry.Remove(capabilityType); + capability.Destroy(); + } + + /// + /// 运行时添加Capability(泛型方法) + /// + public T AddCapability() where T : Capability, new() + { + return AddCapability(null); + } + + /// + /// 运行时添加Capability(泛型方法,带配置数据) + /// + public T AddCapability(CapabilityData data) where T : Capability, new() + { + Type capabilityType = typeof(T); + + // 检查是否已存在 + if (capabilityRegistry.ContainsKey(capabilityType)) + { + Debug.LogWarning($"[CapabilityComponent] Capability of type {capabilityType.Name} already exists on {gameObject.name}"); + return capabilityRegistry[capabilityType].capability as T; + } + + // 创建新实例 + T capability = new T(); + capability.Initialize(gameObject, this, data); + AddCapability(capability, false); + + return capability; + } + + /// + /// 运行时移除Capability(泛型方法) + /// + public void RemoveCapability() where T : Capability + { + Type capabilityType = typeof(T); + + if (capabilityRegistry.TryGetValue(capabilityType, out var entry)) + { + RemoveCapability(entry.capability, false); + } + } + + /// + /// 获取Capability(泛型方法) + /// + public T GetCapability() where T : Capability + { + Type capabilityType = typeof(T); + + if (capabilityRegistry.TryGetValue(capabilityType, out var entry)) + { + return entry.capability as T; + } + + return null; + } + + /// + /// 检查是否有指定类型的Capability + /// + public bool HasCapability() where T : Capability + { + return capabilityRegistry.ContainsKey(typeof(T)); + } + + /// + /// 检查是否有指定类型的Capability(非泛型版本) + /// + public bool HasCapabilityOfType(Type capabilityType) + { + return capabilityRegistry.ContainsKey(capabilityType); + } + + /// + /// 获取所有Capability + /// + public IReadOnlyList GetAllCapabilities() + { + return capabilities.AsReadOnly(); + } + + /// + /// 获取黑板系统 + /// + public CapabilityBlackboard Blackboard => blackboard; + + /// + /// 获取时间记录器 + /// + public TemporalLogger TemporalLogger => temporalLogger; + + /// + /// 获取所有Capability的激活历史记录(用于调试器) + /// + public List<(Capability capability, CapabilityActivationRecord record)> GetActivationHistory() + { + var allRecords = new List<(Capability capability, CapabilityActivationRecord record)>(); + + foreach (var capability in capabilities) + { + foreach (var record in capability.ActivationHistory) + { + allRecords.Add((capability: capability, record: record)); + } + } + + // 按激活时间排序 + allRecords.Sort((a, b) => a.record.ActivateTime.CompareTo(b.record.ActivateTime)); + + return allRecords; + } + + #endregion + + #region Tag Blocking + + /// + /// 阻塞指定Tag + /// + public void BlockTag(CapabilityTag tag, Instigator instigator) + { + if (tag == null || instigator == null) + return; + + if (!tagBlockers.ContainsKey(tag)) + { + tagBlockers[tag] = new List(); + } + + if (!tagBlockers[tag].Contains(instigator)) + { + tagBlockers[tag].Add(instigator); + } + } + + /// + /// 解除阻塞指定Tag + /// + public void UnblockTag(CapabilityTag tag, Instigator instigator) + { + if (tag == null || instigator == null) + return; + + if (tagBlockers.ContainsKey(tag)) + { + tagBlockers[tag].Remove(instigator); + + // 如果没有阻塞者了,移除字典项 + if (tagBlockers[tag].Count == 0) + { + tagBlockers.Remove(tag); + } + } + } + + /// + /// 检查Tag是否被阻塞 + /// + public bool IsTagBlocked(CapabilityTag tag) + { + if (tag == null) + return false; + + return tagBlockers.ContainsKey(tag) && tagBlockers[tag].Count > 0; + } + + /// + /// 获取阻塞指定Tag的所有Instigator + /// + public IReadOnlyList GetTagBlockers(CapabilityTag tag) + { + if (tag == null || !tagBlockers.ContainsKey(tag)) + return new List().AsReadOnly(); + + return tagBlockers[tag].AsReadOnly(); + } + + /// + /// 获取所有被阻塞的Tag + /// + public IEnumerable GetBlockedTags() + { + return tagBlockers.Keys; + } + + #endregion + } +} diff --git a/Assets/CapabilitySystem/Core/CapabilityComponent.cs.meta b/Assets/CapabilitySystem/Core/CapabilityComponent.cs.meta new file mode 100644 index 0000000..2a486fe --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d38e65e95dde13e42af0ad02fe3af814 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Core/CapabilityData.cs b/Assets/CapabilitySystem/Core/CapabilityData.cs new file mode 100644 index 0000000..092167c --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityData.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// Capability配置数据基类 + /// 用于存储Capability的参数配置,实现数据与逻辑分离 + /// + public abstract class CapabilityData : ScriptableObject + { + [Header("Basic Settings")] + [SerializeField] + [Tooltip("Capability的显示名称")] + private string displayName; + + [SerializeField] + [Tooltip("Capability的描述")] + [TextArea(3, 5)] + private string description; + + public string DisplayName => displayName; + public string Description => description; + + /// + /// 验证配置数据的有效性 + /// + public virtual bool Validate() + { + return true; + } + + private void OnValidate() + { + if (string.IsNullOrEmpty(displayName)) + { + displayName = name; + } + } + } +} diff --git a/Assets/CapabilitySystem/Core/CapabilityData.cs.meta b/Assets/CapabilitySystem/Core/CapabilityData.cs.meta new file mode 100644 index 0000000..6c46630 --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2d12e212f8a01fd4f992c5f66087a888 \ No newline at end of file diff --git a/Assets/CapabilitySystem/Core/CapabilitySheet.cs b/Assets/CapabilitySystem/Core/CapabilitySheet.cs new file mode 100644 index 0000000..eba6cf8 --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilitySheet.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// Capability Sheet资源 + /// 用于组织和管理一组相关的Capability和Component + /// + [CreateAssetMenu(fileName = "NewCapabilitySheet", menuName = "Capability/Sheet")] + public class CapabilitySheet : ScriptableObject + { + [Header("Capabilities")] + [SerializeField] + [Tooltip("Capability类的完整名称列表(包含命名空间)")] + private List capabilityClassNames = new List(); + + [SerializeField] + [Tooltip("Capability配置数据列表(与capabilityClassNames对应)")] + private List capabilityDatas = new List(); + + [Header("Components")] + [SerializeField] + [Tooltip("需要添加的Component类型列表")] + private List componentTypeNames = new List(); + + [Header("Sub Sheets")] + [SerializeField] + [Tooltip("子Sheet列表")] + private List subSheets = new List(); + + // 记录Sheet创建的Capability类型(用于移除时的引用计数) + private List createdCapabilityTypes = new List(); + + #region Public Properties + + public IReadOnlyList CapabilityClassNames => capabilityClassNames.AsReadOnly(); + public IReadOnlyList CapabilityDatas => capabilityDatas.AsReadOnly(); + public IReadOnlyList ComponentTypeNames => componentTypeNames.AsReadOnly(); + public IReadOnlyList SubSheets => subSheets.AsReadOnly(); + + #endregion + + #region Apply to GameObject + + /// + /// 应用Sheet到GameObject + /// + public void ApplyToGameObject(GameObject target, CapabilityComponent capabilityComponent) + { + if (target == null || capabilityComponent == null) + { + Debug.LogError($"[CapabilitySheet] Cannot apply sheet '{name}': target or component is null"); + return; + } + + // 1. 递归应用子Sheet + foreach (var subSheet in subSheets) + { + if (subSheet != null) + { + subSheet.ApplyToGameObject(target, capabilityComponent); + } + } + + // 2. 添加Component + foreach (var componentTypeName in componentTypeNames) + { + if (string.IsNullOrEmpty(componentTypeName)) + continue; + + try + { + Type componentType = Type.GetType(componentTypeName); + if (componentType == null) + { + Debug.LogWarning($"[CapabilitySheet] Component type not found: {componentTypeName}"); + continue; + } + + if (!typeof(Component).IsAssignableFrom(componentType)) + { + Debug.LogWarning($"[CapabilitySheet] Type is not a Component: {componentTypeName}"); + continue; + } + + // 检查是否已存在 + if (target.GetComponent(componentType) == null) + { + target.AddComponent(componentType); + } + } + catch (Exception e) + { + Debug.LogError($"[CapabilitySheet] Failed to add component '{componentTypeName}': {e.Message}"); + } + } + + // 3. 创建Capability实例 + createdCapabilityTypes.Clear(); + + for (int i = 0; i < capabilityClassNames.Count; i++) + { + string capabilityClassName = capabilityClassNames[i]; + if (string.IsNullOrEmpty(capabilityClassName)) + continue; + + try + { + Type capabilityType = Type.GetType(capabilityClassName); + if (capabilityType == null) + { + Debug.LogWarning($"[CapabilitySheet] Capability type not found: {capabilityClassName}"); + continue; + } + + if (!typeof(Capability).IsAssignableFrom(capabilityType)) + { + Debug.LogWarning($"[CapabilitySheet] Type is not a Capability: {capabilityClassName}"); + continue; + } + + // 检查是否已存在(去重) + if (capabilityComponent.HasCapabilityOfType(capabilityType)) + { + // 已存在,增加引用计数 + var existingCapability = capabilityComponent.GetAllCapabilities() + .FirstOrDefault(c => c.GetType() == capabilityType); + if (existingCapability != null) + { + capabilityComponent.AddCapability(existingCapability, true); + createdCapabilityTypes.Add(capabilityType); + } + continue; + } + + // 获取对应的配置数据 + CapabilityData data = null; + if (i < capabilityDatas.Count) + { + data = capabilityDatas[i]; + } + + // 创建实例 + Capability capability = (Capability)Activator.CreateInstance(capabilityType); + + // 初始化 + capability.Initialize(target, capabilityComponent, data); + + // 添加到Component + capabilityComponent.AddCapability(capability, true); + createdCapabilityTypes.Add(capabilityType); + } + catch (Exception e) + { + Debug.LogError($"[CapabilitySheet] Failed to create capability '{capabilityClassName}': {e.Message}"); + } + } + } + + #endregion + + #region Remove from GameObject + + /// + /// 从GameObject移除Sheet创建的Capability(通过引用计数) + /// + internal void RemoveFromGameObject(CapabilityComponent capabilityComponent) + { + if (capabilityComponent == null) + return; + + // 递归移除子Sheet + foreach (var subSheet in subSheets) + { + if (subSheet != null) + { + subSheet.RemoveFromGameObject(capabilityComponent); + } + } + + // 移除此Sheet创建的Capability + foreach (var capabilityType in createdCapabilityTypes) + { + var capability = capabilityComponent.GetAllCapabilities() + .FirstOrDefault(c => c.GetType() == capabilityType); + if (capability != null) + { + capabilityComponent.RemoveCapability(capability, true); + } + } + + createdCapabilityTypes.Clear(); + } + + #endregion + + #region Editor Helper Methods + +#if UNITY_EDITOR + /// + /// 添加Capability类名 + /// + public void AddCapabilityClassName(string className) + { + if (!string.IsNullOrEmpty(className) && !capabilityClassNames.Contains(className)) + { + capabilityClassNames.Add(className); + UnityEditor.EditorUtility.SetDirty(this); + } + } + + /// + /// 移除Capability类名 + /// + public void RemoveCapabilityClassName(string className) + { + if (capabilityClassNames.Remove(className)) + { + UnityEditor.EditorUtility.SetDirty(this); + } + } + + /// + /// 添加Component类型名 + /// + public void AddComponentTypeName(string typeName) + { + if (!string.IsNullOrEmpty(typeName) && !componentTypeNames.Contains(typeName)) + { + componentTypeNames.Add(typeName); + UnityEditor.EditorUtility.SetDirty(this); + } + } + + /// + /// 移除Component类型名 + /// + public void RemoveComponentTypeName(string typeName) + { + if (componentTypeNames.Remove(typeName)) + { + UnityEditor.EditorUtility.SetDirty(this); + } + } + + /// + /// 添加子Sheet + /// + public void AddSubSheet(CapabilitySheet sheet) + { + if (sheet != null && !subSheets.Contains(sheet) && sheet != this) + { + subSheets.Add(sheet); + UnityEditor.EditorUtility.SetDirty(this); + } + } + + /// + /// 移除子Sheet + /// + public void RemoveSubSheet(CapabilitySheet sheet) + { + if (subSheets.Remove(sheet)) + { + UnityEditor.EditorUtility.SetDirty(this); + } + } +#endif + + #endregion + } +} diff --git a/Assets/CapabilitySystem/Core/CapabilitySheet.cs.meta b/Assets/CapabilitySystem/Core/CapabilitySheet.cs.meta new file mode 100644 index 0000000..2b0622b --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilitySheet.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ffab8a3c2ebe1c442b4912eefbab7a54 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Core/CapabilitySystem.cs b/Assets/CapabilitySystem/Core/CapabilitySystem.cs new file mode 100644 index 0000000..8d353db --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilitySystem.cs @@ -0,0 +1,202 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// Capability全局管理系统(单例) + /// 负责所有Capability的注册、注销和更新 + /// + public class CapabilitySystem : MonoBehaviour + { + private static CapabilitySystem instance; + + public static CapabilitySystem Instance + { + get + { + if (instance == null) + { + // 查找场景中的实例 + instance = FindObjectOfType(); + + // 如果没有,自动创建 + if (instance == null) + { + var go = new GameObject("[CapabilitySystem]"); + instance = go.AddComponent(); + DontDestroyOnLoad(go); + } + } + return instance; + } + } + + // 所有Capability实例,按TickOrder排序 + private List allCapabilities = new List(); + + // 按TickGroup分组的Capability(性能优化) + private Dictionary> capabilitiesByGroup = new Dictionary>(); + + // 是否需要重新排序 + private bool needsSort = false; + + #region Unity Lifecycle + + private void Awake() + { + if (instance != null && instance != this) + { + Destroy(gameObject); + return; + } + + instance = this; + DontDestroyOnLoad(gameObject); + } + + private void Update() + { + // 如果需要排序,先排序 + if (needsSort) + { + SortCapabilities(); + needsSort = false; + } + + float deltaTime = Time.deltaTime; + + // 遍历所有Capability + for (int i = 0; i < allCapabilities.Count; i++) + { + var capability = allCapabilities[i]; + + if (!capability.IsActive) + { + // 未激活:检查是否应该激活 + if (capability.InternalShouldActivate()) + { + capability.Activate(); + } + } + else + { + // 已激活:先检查是否应该失活 + if (capability.InternalShouldDeactivate()) + { + capability.Deactivate(); + } + else + { + // 执行Tick + capability.Tick(deltaTime); + } + } + } + } + + #endregion + + #region Capability Registration + + /// + /// 注册Capability + /// + public void RegisterCapability(Capability capability) + { + if (capability == null || allCapabilities.Contains(capability)) + return; + + allCapabilities.Add(capability); + needsSort = true; + } + + /// + /// 注销Capability + /// + public void UnregisterCapability(Capability capability) + { + if (capability == null) + return; + + allCapabilities.Remove(capability); + } + + /// + /// 按TickOrder排序Capability + /// + private void SortCapabilities() + { + allCapabilities = allCapabilities + .OrderBy(c => (int)c.TickGroup) + .ThenBy(c => c.TickOrder) + .ToList(); + + // 重建分组字典 + capabilitiesByGroup.Clear(); + foreach (var capability in allCapabilities) + { + if (!capabilitiesByGroup.ContainsKey(capability.TickGroup)) + { + capabilitiesByGroup[capability.TickGroup] = new List(); + } + capabilitiesByGroup[capability.TickGroup].Add(capability); + } + } + + /// + /// 标记需要重新排序(公开给Capability使用) + /// + public void MarkNeedsSort() + { + needsSort = true; + } + + #endregion + + #region Query Methods + + /// + /// 获取所有Capability(只读) + /// + public IReadOnlyList GetAllCapabilities() + { + return allCapabilities.AsReadOnly(); + } + + /// + /// 获取指定GameObject的所有Capability + /// + public List GetCapabilitiesForGameObject(GameObject gameObject) + { + return allCapabilities.Where(c => c.Owner == gameObject).ToList(); + } + + /// + /// 获取所有激活的Capability + /// + public List GetActiveCapabilities() + { + return allCapabilities.Where(c => c.IsActive).ToList(); + } + + #endregion + + #region Debug Info + + /// + /// 获取统计信息 + /// + public (int total, int active, int blocked) GetStatistics() + { + int total = allCapabilities.Count; + int active = allCapabilities.Count(c => c.IsActive); + int blocked = allCapabilities.Count(c => c.IsBlocked()); + + return (total, active, blocked); + } + + #endregion + } +} diff --git a/Assets/CapabilitySystem/Core/CapabilitySystem.cs.meta b/Assets/CapabilitySystem/Core/CapabilitySystem.cs.meta new file mode 100644 index 0000000..8b2e5a9 --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilitySystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 84c9abe5e6bf1244d8842b430e369f78 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Core/CapabilityTag.cs b/Assets/CapabilitySystem/Core/CapabilityTag.cs new file mode 100644 index 0000000..77460d6 --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityTag.cs @@ -0,0 +1,29 @@ +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// Capability标签,用于标识和阻塞Capability + /// + [CreateAssetMenu(fileName = "NewCapabilityTag", menuName = "Capability/Tag")] + public class CapabilityTag : ScriptableObject + { + [SerializeField] + private string tagName; + + public string TagName => tagName; + + private void OnValidate() + { + if (string.IsNullOrEmpty(tagName)) + { + tagName = name; + } + } + + public override string ToString() + { + return tagName; + } + } +} diff --git a/Assets/CapabilitySystem/Core/CapabilityTag.cs.meta b/Assets/CapabilitySystem/Core/CapabilityTag.cs.meta new file mode 100644 index 0000000..b16236c --- /dev/null +++ b/Assets/CapabilitySystem/Core/CapabilityTag.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ec7408feedfe9234b9e54ea5fedfd004 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Core/CompoundCapability.cs b/Assets/CapabilitySystem/Core/CompoundCapability.cs new file mode 100644 index 0000000..439b073 --- /dev/null +++ b/Assets/CapabilitySystem/Core/CompoundCapability.cs @@ -0,0 +1,177 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// 复合Capability基类 + /// 用于管理多个子Capability,适用于AI系统和复杂行为 + /// + public abstract class CompoundCapability : Capability + { + /// 子Capability列表 + protected List childCapabilities = new List(); + + /// 当前激活的子Capability + protected Capability activeChild; + + #region Child Management + + /// + /// 添加子Capability + /// + protected void AddChild(Capability child) + { + if (child == null || childCapabilities.Contains(child)) + return; + + childCapabilities.Add(child); + } + + /// + /// 移除子Capability + /// + protected void RemoveChild(Capability child) + { + if (child == null) + return; + + if (activeChild == child) + { + DeactivateChild(child); + } + + childCapabilities.Remove(child); + } + + /// + /// 激活子Capability + /// + protected void ActivateChild(Capability child) + { + if (child == null || !childCapabilities.Contains(child)) + { + Debug.LogWarning($"[CompoundCapability] Cannot activate child: not in child list"); + return; + } + + // 如果有其他子Capability激活,先停用 + if (activeChild != null && activeChild != child) + { + DeactivateChild(activeChild); + } + + if (!child.IsActive) + { + child.Activate(); + activeChild = child; + OnChildActivated(child); + } + } + + /// + /// 停用子Capability + /// + protected void DeactivateChild(Capability child) + { + if (child == null) + return; + + if (child.IsActive) + { + child.Deactivate(); + if (activeChild == child) + { + activeChild = null; + } + OnChildDeactivated(child); + } + } + + /// + /// 停用所有子Capability + /// + protected void DeactivateAllChildren() + { + foreach (var child in childCapabilities.ToList()) + { + if (child.IsActive) + { + DeactivateChild(child); + } + } + } + + /// + /// 获取所有子Capability + /// + public IReadOnlyList GetChildren() + { + return childCapabilities.AsReadOnly(); + } + + #endregion + + #region Lifecycle Overrides + + protected override void OnActivated() + { + base.OnActivated(); + OnCompoundActivated(); + } + + protected override void OnDeactivated() + { + // 停用所有子Capability + DeactivateAllChildren(); + OnCompoundDeactivated(); + base.OnDeactivated(); + } + + protected override void TickActive(float deltaTime) + { + base.TickActive(deltaTime); + + // 更新激活的子Capability + if (activeChild != null && activeChild.IsActive) + { + activeChild.Tick(deltaTime); + } + + TickCompound(deltaTime); + } + + #endregion + + #region Virtual Methods for Subclasses + + /// + /// 复合Capability激活时调用 + /// + protected virtual void OnCompoundActivated() { } + + /// + /// 复合Capability停用时调用 + /// + protected virtual void OnCompoundDeactivated() { } + + /// + /// 复合Capability每帧更新 + /// + protected virtual void TickCompound(float deltaTime) { } + + /// + /// 子Capability激活时调用 + /// + protected virtual void OnChildActivated(Capability child) { } + + /// + /// 子Capability停用时调用 + /// + protected virtual void OnChildDeactivated(Capability child) { } + + #endregion + } +} + diff --git a/Assets/CapabilitySystem/Core/CompoundCapability.cs.meta b/Assets/CapabilitySystem/Core/CompoundCapability.cs.meta new file mode 100644 index 0000000..b52f84a --- /dev/null +++ b/Assets/CapabilitySystem/Core/CompoundCapability.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ad7a50e648a6a2f4081d5496a96d1704 \ No newline at end of file diff --git a/Assets/CapabilitySystem/Core/Instigator.cs b/Assets/CapabilitySystem/Core/Instigator.cs new file mode 100644 index 0000000..8a0a150 --- /dev/null +++ b/Assets/CapabilitySystem/Core/Instigator.cs @@ -0,0 +1,70 @@ +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// 发起者,用于追踪谁发起了某个操作(如阻塞Tag) + /// + public class Instigator + { + public enum InstigatorType + { + GameObject, + Capability, + String + } + + public InstigatorType Type { get; private set; } + public object Source { get; private set; } + + private Instigator(InstigatorType type, object source) + { + Type = type; + Source = source; + } + + public static Instigator FromGameObject(GameObject obj) + { + return new Instigator(InstigatorType.GameObject, obj); + } + + public static Instigator FromCapability(Capability capability) + { + return new Instigator(InstigatorType.Capability, capability); + } + + public static Instigator FromString(string name) + { + return new Instigator(InstigatorType.String, name); + } + + public override string ToString() + { + switch (Type) + { + case InstigatorType.GameObject: + return $"GameObject: {(Source as GameObject)?.name ?? "null"}"; + case InstigatorType.Capability: + return $"Capability: {Source?.GetType().Name ?? "null"}"; + case InstigatorType.String: + return $"String: {Source as string ?? "null"}"; + default: + return "Unknown"; + } + } + + public override bool Equals(object obj) + { + if (obj is Instigator other) + { + return Type == other.Type && Equals(Source, other.Source); + } + return false; + } + + public override int GetHashCode() + { + return (Type, Source).GetHashCode(); + } + } +} diff --git a/Assets/CapabilitySystem/Core/Instigator.cs.meta b/Assets/CapabilitySystem/Core/Instigator.cs.meta new file mode 100644 index 0000000..0efcb7a --- /dev/null +++ b/Assets/CapabilitySystem/Core/Instigator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 522dd182cb6f1f644a756270b7ac0111 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Core/TemporalLogger.cs b/Assets/CapabilitySystem/Core/TemporalLogger.cs new file mode 100644 index 0000000..78f37ee --- /dev/null +++ b/Assets/CapabilitySystem/Core/TemporalLogger.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace CapabilitySystem +{ + /// + /// 时间数据记录器 + /// 用于记录任意类型的数据到时间轴,支持回放 + /// + public class TemporalLogger + { + /// + /// 数据记录项 + /// + [Serializable] + public class DataRecord + { + public float Time; + public int Frame; + public string Key; + public object Value; + public Type ValueType; + + public DataRecord(string key, object value) + { + Time = UnityEngine.Time.time; + Frame = UnityEngine.Time.frameCount; + Key = key; + Value = value; + ValueType = value?.GetType(); + } + } + + /// + /// 数据通道(每个Key一个通道) + /// + public class DataChannel + { + public string Key; + public Type DataType; + public List Records = new List(); + public int MaxRecords = 1000; + + public void AddRecord(DataRecord record) + { + Records.Add(record); + + // 限制记录数量 + if (Records.Count > MaxRecords) + { + Records.RemoveAt(0); + } + } + + /// + /// 获取指定时间的数据(插值) + /// + public object GetValueAtTime(float time) + { + if (Records.Count == 0) + return null; + + // 找到时间前后的记录 + DataRecord before = null; + DataRecord after = null; + + for (int i = 0; i < Records.Count; i++) + { + if (Records[i].Time <= time) + { + before = Records[i]; + } + else + { + after = Records[i]; + break; + } + } + + // 如果只有一个记录或时间在范围外 + if (before == null) + return after?.Value; + if (after == null) + return before.Value; + + // 尝试插值(仅支持特定类型) + return InterpolateValue(before, after, time); + } + + private object InterpolateValue(DataRecord before, DataRecord after, float time) + { + float t = Mathf.InverseLerp(before.Time, after.Time, time); + + // 根据类型进行插值 + if (DataType == typeof(Vector3)) + { + return Vector3.Lerp((Vector3)before.Value, (Vector3)after.Value, t); + } + else if (DataType == typeof(Quaternion)) + { + return Quaternion.Slerp((Quaternion)before.Value, (Quaternion)after.Value, t); + } + else if (DataType == typeof(float)) + { + return Mathf.Lerp((float)before.Value, (float)after.Value, t); + } + else if (DataType == typeof(Color)) + { + return Color.Lerp((Color)before.Value, (Color)after.Value, t); + } + + // 不支持插值的类型,返回最近的值 + return t < 0.5f ? before.Value : after.Value; + } + } + + private Dictionary channels = new Dictionary(); + private float startTime; + private bool isRecording = true; + private bool isInitialized = false; + + public IReadOnlyDictionary Channels => channels; + public float StartTime => startTime; + public bool IsRecording => isRecording; + + public TemporalLogger() + { + // 不在构造函数中调用 Time.time,延迟到第一次使用时初始化 + } + + /// + /// 确保已初始化 + /// + private void EnsureInitialized() + { + if (!isInitialized) + { + startTime = Time.time; + isInitialized = true; + } + } + + #region Recording + + /// + /// 记录数据 + /// + public void Log(string key, object value) + { + if (!isRecording || value == null) + return; + + EnsureInitialized(); + + if (!channels.ContainsKey(key)) + { + channels[key] = new DataChannel + { + Key = key, + DataType = value.GetType() + }; + } + + var record = new DataRecord(key, value); + channels[key].AddRecord(record); + } + + /// + /// 记录位置 + /// + public void LogPosition(string key, Vector3 position) + { + Log($"{key}.Position", position); + } + + /// + /// 记录旋转 + /// + public void LogRotation(string key, Quaternion rotation) + { + Log($"{key}.Rotation", rotation); + } + + /// + /// 记录Transform + /// + public void LogTransform(string key, Transform transform) + { + LogPosition(key, transform.position); + LogRotation(key, transform.rotation); + } + + /// + /// 记录动画状态 + /// + public void LogAnimation(string key, Animator animator, string stateName) + { + if (animator == null) + return; + + var stateInfo = animator.GetCurrentAnimatorStateInfo(0); + Log($"{key}.AnimState", stateName); + Log($"{key}.AnimTime", stateInfo.normalizedTime); + } + + /// + /// 暂停记录 + /// + public void PauseRecording() + { + isRecording = false; + } + + /// + /// 恢复记录 + /// + public void ResumeRecording() + { + isRecording = true; + } + + /// + /// 清空所有记录 + /// + public void Clear() + { + channels.Clear(); + startTime = Time.time; + isInitialized = true; + } + + #endregion + + #region Playback + + /// + /// 获取指定时间的数据 + /// + public object GetValueAtTime(string key, float time) + { + if (!channels.ContainsKey(key)) + return null; + + return channels[key].GetValueAtTime(time); + } + + /// + /// 获取时间范围 + /// + public (float min, float max) GetTimeRange() + { + EnsureInitialized(); + + float min = float.MaxValue; + float max = float.MinValue; + + foreach (var channel in channels.Values) + { + if (channel.Records.Count > 0) + { + min = Mathf.Min(min, channel.Records[0].Time); + max = Mathf.Max(max, channel.Records[channel.Records.Count - 1].Time); + } + } + + return min == float.MaxValue ? (0, 0) : (min, max); + } + + /// + /// 获取所有数据通道的Key + /// + public List GetAllKeys() + { + return new List(channels.Keys); + } + + #endregion + } +} diff --git a/Assets/CapabilitySystem/Core/TemporalLogger.cs.meta b/Assets/CapabilitySystem/Core/TemporalLogger.cs.meta new file mode 100644 index 0000000..e89d67c --- /dev/null +++ b/Assets/CapabilitySystem/Core/TemporalLogger.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: de209aa30b789fa44bfd9c23e7de4fe0 \ No newline at end of file diff --git a/Assets/CapabilitySystem/Editor.meta b/Assets/CapabilitySystem/Editor.meta new file mode 100644 index 0000000..6a4efdc --- /dev/null +++ b/Assets/CapabilitySystem/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 474d0f25701409945a5dd85127ce7be4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Editor/CapabilitySheetEditor.cs b/Assets/CapabilitySystem/Editor/CapabilitySheetEditor.cs new file mode 100644 index 0000000..0a44105 --- /dev/null +++ b/Assets/CapabilitySystem/Editor/CapabilitySheetEditor.cs @@ -0,0 +1,223 @@ +using UnityEngine; +using UnityEditor; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace CapabilitySystem.Editor +{ + /// + /// CapabilitySheet自定义编辑器 + /// + [CustomEditor(typeof(CapabilitySheet))] + public class CapabilitySheetEditor : UnityEditor.Editor + { + private CapabilitySheet sheet; + private List availableCapabilityTypes; + private List availableComponentTypes; + private int selectedCapabilityIndex = 0; + private int selectedComponentIndex = 0; + + private void OnEnable() + { + sheet = (CapabilitySheet)target; + RefreshAvailableTypes(); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Capability Sheet", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // Capabilities部分 + DrawCapabilitiesSection(); + EditorGUILayout.Space(); + + // Components部分 + DrawComponentsSection(); + EditorGUILayout.Space(); + + // Sub Sheets部分 + DrawSubSheetsSection(); + + serializedObject.ApplyModifiedProperties(); + + if (GUI.changed) + { + EditorUtility.SetDirty(sheet); + } + } + + private void DrawCapabilitiesSection() + { + EditorGUILayout.LabelField("Capabilities", EditorStyles.boldLabel); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // 显示现有Capability + var capabilityNames = sheet.CapabilityClassNames.ToList(); + for (int i = 0; i < capabilityNames.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + + string className = capabilityNames[i]; + string displayName = GetShortTypeName(className); + + EditorGUILayout.LabelField($"{i + 1}. {displayName}"); + + if (GUILayout.Button("X", GUILayout.Width(25))) + { + sheet.RemoveCapabilityClassName(className); + break; + } + + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(); + + // 添加新Capability + EditorGUILayout.BeginHorizontal(); + selectedCapabilityIndex = EditorGUILayout.Popup("添加Capability", selectedCapabilityIndex, + availableCapabilityTypes.Select(t => t.Name).ToArray()); + + if (GUILayout.Button("添加", GUILayout.Width(50))) + { + if (selectedCapabilityIndex >= 0 && selectedCapabilityIndex < availableCapabilityTypes.Count) + { + var selectedType = availableCapabilityTypes[selectedCapabilityIndex]; + sheet.AddCapabilityClassName(selectedType.AssemblyQualifiedName); + } + } + EditorGUILayout.EndHorizontal(); + + if (GUILayout.Button("刷新类型列表")) + { + RefreshAvailableTypes(); + } + + EditorGUILayout.EndVertical(); + } + + private void DrawComponentsSection() + { + EditorGUILayout.LabelField("Components", EditorStyles.boldLabel); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // 显示现有Component + var componentNames = sheet.ComponentTypeNames.ToList(); + for (int i = 0; i < componentNames.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + + string typeName = componentNames[i]; + string displayName = GetShortTypeName(typeName); + + EditorGUILayout.LabelField($"{i + 1}. {displayName}"); + + if (GUILayout.Button("X", GUILayout.Width(25))) + { + sheet.RemoveComponentTypeName(typeName); + break; + } + + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(); + + // 添加新Component + EditorGUILayout.BeginHorizontal(); + selectedComponentIndex = EditorGUILayout.Popup("添加Component", selectedComponentIndex, + availableComponentTypes.Select(t => t.Name).ToArray()); + + if (GUILayout.Button("添加", GUILayout.Width(50))) + { + if (selectedComponentIndex >= 0 && selectedComponentIndex < availableComponentTypes.Count) + { + var selectedType = availableComponentTypes[selectedComponentIndex]; + sheet.AddComponentTypeName(selectedType.AssemblyQualifiedName); + } + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.EndVertical(); + } + + private void DrawSubSheetsSection() + { + EditorGUILayout.LabelField("Sub Sheets", EditorStyles.boldLabel); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + var subSheets = sheet.SubSheets.ToList(); + for (int i = 0; i < subSheets.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + + var subSheet = subSheets[i]; + EditorGUILayout.ObjectField($"{i + 1}.", subSheet, typeof(CapabilitySheet), false); + + if (GUILayout.Button("X", GUILayout.Width(25))) + { + sheet.RemoveSubSheet(subSheet); + break; + } + + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(); + + // 添加新Sub Sheet + EditorGUILayout.BeginHorizontal(); + var newSubSheet = (CapabilitySheet)EditorGUILayout.ObjectField("添加Sub Sheet", null, typeof(CapabilitySheet), false); + if (newSubSheet != null) + { + sheet.AddSubSheet(newSubSheet); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.EndVertical(); + } + + private void RefreshAvailableTypes() + { + // 查找所有Capability子类 + availableCapabilityTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsClass && !type.IsAbstract && typeof(Capability).IsAssignableFrom(type)) + .OrderBy(type => type.Name) + .ToList(); + + // 查找所有Component类型(排除Unity内置的) + availableComponentTypes = AppDomain.CurrentDomain.GetAssemblies() + .Where(assembly => !assembly.FullName.StartsWith("Unity") && !assembly.FullName.StartsWith("System")) + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsClass && !type.IsAbstract && typeof(Component).IsAssignableFrom(type) && type != typeof(Transform)) + .OrderBy(type => type.Name) + .ToList(); + } + + private string GetShortTypeName(string assemblyQualifiedName) + { + if (string.IsNullOrEmpty(assemblyQualifiedName)) + return "Unknown"; + + var parts = assemblyQualifiedName.Split(','); + if (parts.Length > 0) + { + var fullName = parts[0].Trim(); + var lastDot = fullName.LastIndexOf('.'); + return lastDot >= 0 ? fullName.Substring(lastDot + 1) : fullName; + } + + return assemblyQualifiedName; + } + } +} diff --git a/Assets/CapabilitySystem/Editor/CapabilitySheetEditor.cs.meta b/Assets/CapabilitySystem/Editor/CapabilitySheetEditor.cs.meta new file mode 100644 index 0000000..e767099 --- /dev/null +++ b/Assets/CapabilitySystem/Editor/CapabilitySheetEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 209106279e66f3349b8b1d62cb9285be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Editor/Debugger.meta b/Assets/CapabilitySystem/Editor/Debugger.meta new file mode 100644 index 0000000..12a2e1c --- /dev/null +++ b/Assets/CapabilitySystem/Editor/Debugger.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 836d4cfce7d4ec543baa74d91d396954 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CapabilitySystem/Editor/Debugger/CapabilityDebuggerStyle.uss b/Assets/CapabilitySystem/Editor/Debugger/CapabilityDebuggerStyle.uss new file mode 100644 index 0000000..70e5e91 --- /dev/null +++ b/Assets/CapabilitySystem/Editor/Debugger/CapabilityDebuggerStyle.uss @@ -0,0 +1,197 @@ +/* Capability Debugger Styles */ + +.toolbar { + flex-direction: row; + background-color: rgb(56, 56, 56); + border-bottom-width: 1px; + border-bottom-color: rgb(26, 26, 26); + padding: 5px; +} + +.control-button { + width: 40px; + height: 30px; + margin-left: 2px; + margin-right: 2px; + font-size: 16px; +} + +.time-label { + -unity-text-align: middle-left; + font-size: 12px; + color: rgb(200, 200, 200); +} + +.section-header { + font-size: 13px; + -unity-font-style: bold; + background-color: rgb(64, 64, 64); + padding: 8px; + border-bottom-width: 1px; + border-bottom-color: rgb(40, 40, 40); +} + +.capability-row { + flex-direction: row; + height: 50px; + border-bottom-width: 1px; + border-bottom-color: rgb(76, 76, 76); +} + +.capability-row:hover { + background-color: rgb(80, 80, 80); +} + +.capability-name { + width: 200px; + padding-left: 10px; + -unity-text-align: middle-left; + background-color: rgb(70, 70, 70); + font-size: 12px; +} + +.channel-row { + padding: 8px; + margin-bottom: 4px; + background-color: rgb(60, 60, 60); + border-radius: 4px; +} + +.channel-row:hover { + background-color: rgb(70, 70, 70); +} + +/* Timeline specific */ +.timeline-track { + height: 40px; + margin-bottom: 2px; + flex-direction: row; +} + +.timeline-label { + width: 200px; + background-color: rgb(70, 70, 70); + padding-left: 10px; + -unity-text-align: middle-left; +} + +.timeline-area { + flex-grow: 1; + position: relative; + background-color: rgb(56, 56, 56); +} + +.activation-bar { + position: absolute; + height: 70%; + top: 15%; + background-color: rgba(76, 204, 76, 0.8); + border-radius: 4px; +} + +.activation-bar-blocked { + background-color: rgba(204, 76, 76, 0.8); +} + +/* Performance panel */ +.performance-stat { + padding: 5px; + margin-bottom: 3px; + background-color: rgb(60, 60, 60); + border-left-width: 3px; + border-left-color: rgb(76, 204, 76); +} + +.performance-stat-warning { + border-left-color: rgb(255, 165, 0); +} + +.performance-stat-critical { + border-left-color: rgb(204, 76, 76); +} + +/* Tag blocker panel */ +.tag-blocker-item { + padding: 10px; + margin-bottom: 8px; + background-color: rgb(64, 64, 64); + border-radius: 4px; + border-left-width: 4px; + border-left-color: rgb(255, 128, 64); +} + +.tag-blocker-header { + font-size: 13px; + -unity-font-style: bold; + color: rgb(255, 200, 150); + margin-bottom: 5px; +} + +.instigator-item { + margin-left: 15px; + font-size: 11px; + color: rgb(180, 180, 180); +} + +/* Inspector panel */ +.inspector-foldout { + margin-bottom: 10px; +} + +.inspector-property { + flex-direction: row; + padding: 3px; + margin-left: 10px; +} + +.inspector-property-label { + width: 120px; + color: rgb(180, 180, 180); +} + +.inspector-property-value { + flex-grow: 1; + color: rgb(220, 220, 220); +} + +/* Status indicators */ +.status-active { + color: rgb(76, 204, 76); +} + +.status-inactive { + color: rgb(128, 128, 128); +} + +.status-blocked { + color: rgb(204, 76, 76); +} + +/* Utility classes */ +.flex-row { + flex-direction: row; +} + +.flex-column { + flex-direction: column; +} + +.flex-grow { + flex-grow: 1; +} + +.text-center { + -unity-text-align: middle-center; +} + +.text-bold { + -unity-font-style: bold; +} + +.margin-small { + margin: 5px; +} + +.padding-small { + padding: 5px; +} diff --git a/Assets/CapabilitySystem/Editor/Debugger/CapabilityDebuggerStyle.uss.meta b/Assets/CapabilitySystem/Editor/Debugger/CapabilityDebuggerStyle.uss.meta new file mode 100644 index 0000000..6c78b9b --- /dev/null +++ b/Assets/CapabilitySystem/Editor/Debugger/CapabilityDebuggerStyle.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf93baf8c45a5a54a84abff32e2433ac +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/Assets/CapabilitySystem/Editor/Debugger/CapabilitySceneGizmos.cs b/Assets/CapabilitySystem/Editor/Debugger/CapabilitySceneGizmos.cs new file mode 100644 index 0000000..d333de9 --- /dev/null +++ b/Assets/CapabilitySystem/Editor/Debugger/CapabilitySceneGizmos.cs @@ -0,0 +1,185 @@ +using UnityEngine; +using UnityEditor; +using System.Linq; + +namespace CapabilitySystem.Editor +{ + /// + /// Scene视图中的Capability可视化工具 + /// 显示激活状态、Tag阻塞、时间轴等信息 + /// + [InitializeOnLoad] + public static class CapabilitySceneGizmos + { + private static bool showGizmos = true; + private static bool showActivationState = true; + private static bool showTagBlockers = true; + private static bool showActivationHistory = false; + private static float gizmoSize = 0.5f; + + static CapabilitySceneGizmos() + { + SceneView.duringSceneGui += OnSceneGUI; + } + + [MenuItem("Window/Capability System/Toggle Scene Gizmos")] + private static void ToggleGizmos() + { + showGizmos = !showGizmos; + SceneView.RepaintAll(); + } + + private static void OnSceneGUI(SceneView sceneView) + { + if (!showGizmos) return; + + var allComponents = Object.FindObjectsOfType(); + + foreach (var component in allComponents) + { + DrawComponentGizmos(component); + } + + // 绘制控制面板 + Handles.BeginGUI(); + DrawControlPanel(); + Handles.EndGUI(); + } + + private static void DrawComponentGizmos(CapabilityComponent component) + { + if (component == null || component.gameObject == null) return; + + Vector3 position = component.transform.position; + var capabilities = component.GetAllCapabilities(); + + // 绘制激活状态 + if (showActivationState) + { + DrawActivationState(position, capabilities); + } + + // 绘制Tag阻塞信息 + if (showTagBlockers) + { + DrawTagBlockers(position, component); + } + + // 绘制激活历史 + if (showActivationHistory) + { + DrawActivationHistory(position, capabilities); + } + } + + private static void DrawActivationState(Vector3 position, System.Collections.Generic.IReadOnlyList capabilities) + { + int activeCount = capabilities.Count(c => c.IsActive); + int totalCount = capabilities.Count; + + if (totalCount == 0) return; + + // 绘制圆环表示激活比例 + float radius = gizmoSize; + Vector3 offset = Vector3.up * 2f; + Vector3 center = position + offset; + + // 背景圆 + Handles.color = new Color(0.3f, 0.3f, 0.3f, 0.5f); + Handles.DrawSolidDisc(center, Camera.current.transform.forward, radius); + + // 激活部分 + if (activeCount > 0) + { + float fillAngle = 360f * activeCount / totalCount; + Handles.color = new Color(0.3f, 1f, 0.3f, 0.8f); + Handles.DrawSolidArc(center, Camera.current.transform.forward, Vector3.up, fillAngle, radius); + } + + // 文字标签 + Handles.Label(center + Vector3.up * (radius + 0.2f), + $"{activeCount}/{totalCount}", + new GUIStyle() + { + normal = { textColor = Color.white }, + fontSize = 12, + fontStyle = FontStyle.Bold, + alignment = TextAnchor.MiddleCenter + }); + } + + private static void DrawTagBlockers(Vector3 position, CapabilityComponent component) + { + var blockedTags = component.GetBlockedTags().ToList(); + + if (blockedTags.Count == 0) return; + + Vector3 offset = Vector3.up * 2.5f; + Vector3 labelPos = position + offset; + + // 绘制阻塞图标 + Handles.color = new Color(1f, 0.5f, 0.3f, 0.8f); + float iconSize = gizmoSize * 0.3f; + Handles.DrawSolidDisc(labelPos, Camera.current.transform.forward, iconSize); + + // 绘制阻塞信息 + string blockerText = $"🚫 {blockedTags.Count} Tags Blocked"; + Handles.Label(labelPos + Vector3.right * (iconSize + 0.2f), + blockerText, + new GUIStyle() + { + normal = { textColor = new Color(1f, 0.7f, 0.5f) }, + fontSize = 11, + fontStyle = FontStyle.Bold + }); + } + + private static void DrawActivationHistory(Vector3 position, System.Collections.Generic.IReadOnlyList capabilities) + { + foreach (var capability in capabilities) + { + if (capability.ActivationHistory.Count == 0) continue; + + // 绘制最近的激活位置 + var recentRecords = capability.ActivationHistory + .Where(r => r.Position.HasValue) + .TakeLast(5) + .ToList(); + + for (int i = 0; i < recentRecords.Count - 1; i++) + { + var from = recentRecords[i].Position.Value; + var to = recentRecords[i + 1].Position.Value; + + float alpha = (i + 1) / (float)recentRecords.Count; + Handles.color = new Color(0.5f, 0.5f, 1f, alpha * 0.5f); + Handles.DrawLine(from, to, 2f); + } + } + } + + private static void DrawControlPanel() + { + GUILayout.BeginArea(new Rect(10, 10, 250, 200)); + + GUILayout.BeginVertical(GUI.skin.box); + GUILayout.Label("Capability Gizmos", EditorStyles.boldLabel); + + showGizmos = GUILayout.Toggle(showGizmos, "Show Gizmos"); + + GUI.enabled = showGizmos; + showActivationState = GUILayout.Toggle(showActivationState, "Show Activation State"); + showTagBlockers = GUILayout.Toggle(showTagBlockers, "Show Tag Blockers"); + showActivationHistory = GUILayout.Toggle(showActivationHistory, "Show Activation History"); + + GUILayout.Space(5); + GUILayout.Label($"Gizmo Size: {gizmoSize:F2}"); + gizmoSize = GUILayout.HorizontalSlider(gizmoSize, 0.1f, 2f); + + GUI.enabled = true; + + GUILayout.EndVertical(); + GUILayout.EndArea(); + } + } +} diff --git a/Assets/CapabilitySystem/Editor/Debugger/CapabilitySceneGizmos.cs.meta b/Assets/CapabilitySystem/Editor/Debugger/CapabilitySceneGizmos.cs.meta new file mode 100644 index 0000000..e96dded --- /dev/null +++ b/Assets/CapabilitySystem/Editor/Debugger/CapabilitySceneGizmos.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5e639ccc748f78a40ac9c54298796d68 \ No newline at end of file diff --git a/Assets/CapabilitySystem/Editor/Debugger/EnhancedCapabilityDebugger.cs b/Assets/CapabilitySystem/Editor/Debugger/EnhancedCapabilityDebugger.cs new file mode 100644 index 0000000..8fdf8c1 --- /dev/null +++ b/Assets/CapabilitySystem/Editor/Debugger/EnhancedCapabilityDebugger.cs @@ -0,0 +1,1723 @@ +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; +using System.Collections.Generic; +using System.Linq; + +namespace CapabilitySystem.Editor +{ + /// + /// 增强的Capability调试器 - 完整的UIElements实现 + /// 提供时间轴、性能分析、Tag阻塞可视化等功能 + /// + public class EnhancedCapabilityDebugger : EditorWindow + { + [MenuItem("Window/Capability System/Enhanced Debugger")] + public static void ShowWindow() + { + var window = GetWindow(); + window.titleContent = new GUIContent("Capability Debugger"); + window.minSize = new Vector2(1200, 700); + } + + // 主要UI元素 + private VisualElement root; + private TwoPaneSplitView mainSplitView; + private TwoPaneSplitView leftSplitView; + + // 左侧面板 + private VisualElement hierarchyPanel; + private ListView gameObjectList; + private VisualElement inspectorPanel; + + // 右侧面板 + private VisualElement timelinePanel; + private VisualElement performancePanel; + private VisualElement tagBlockerPanel; + + // 选中的对象 + private GameObject selectedGameObject; + private CapabilityComponent selectedComponent; + private Capability selectedCapability; + + // 时间轴相关 + private ScrollView contentScrollView; + private ScrollView rulerScrollView; + private VisualElement scrubber; + private float currentTime = 0f; + private float minTime = 0f; + private float maxTime = 10f; + private float zoomLevel = 1f; // 缩放级别 + + // 详细信息面板 + private VisualElement detailsPanel; + + // 自动刷新相关 + private float lastRefreshTime = 0f; + private float autoRefreshInterval = 0.1f; // 每0.1秒自动刷新一次(提高频率) + private bool autoRefreshEnabled = true; // 自动刷新开关 + private bool isFollowingTime = true; // 是否跟随当前时间 + private float lastTimelineRefreshMaxTime = 0f; // 上次刷新时间轴时的maxTime + private float lastTimelineRefreshTime = 0f; // 上次刷新时间轴的时间 + + // 标签按钮 + private ToolbarButton timelineTabButton; + private ToolbarButton performanceTabButton; + private ToolbarButton tagBlockerTabButton; + private string currentTab = "timeline"; + private string hierarchySearchText = ""; + + // 性能数据 + private Dictionary performanceStats = new Dictionary(); + + private class PerformanceData + { + public int CallCount; + public float TotalTime; + public float AverageTime => CallCount > 0 ? TotalTime / CallCount : 0f; + public float MaxTime; + } + + private void CreateGUI() + { + root = rootVisualElement; + + // 加载样式表 + LoadStyleSheet(); + + // 创建工具栏 + CreateToolbar(); + + // 创建主分割视图(左右分割) + mainSplitView = new TwoPaneSplitView(0, 400, TwoPaneSplitViewOrientation.Horizontal); + mainSplitView.style.flexGrow = 1; + root.Add(mainSplitView); + + // 创建左侧分割视图(上下分割:层级+Inspector) + leftSplitView = new TwoPaneSplitView(0, 300, TwoPaneSplitViewOrientation.Vertical); + mainSplitView.Add(leftSplitView); + + // 创建各个面板 + CreateHierarchyPanel(); + CreateInspectorPanel(); + CreateRightPanel(); + + // 注册更新回调 + EditorApplication.update += OnEditorUpdate; + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + + // 初始刷新 + RefreshAll(); + } + + private void OnDestroy() + { + EditorApplication.update -= OnEditorUpdate; + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + } + + private void OnPlayModeStateChanged(PlayModeStateChange state) + { + // 进入或退出播放模式时刷新 + if (state == PlayModeStateChange.EnteredPlayMode || state == PlayModeStateChange.EnteredEditMode) + { + RefreshAll(); + } + } + + private void LoadStyleSheet() + { + var styleSheet = AssetDatabase.LoadAssetAtPath( + "Assets/CapabilitySystem/Editor/Debugger/CapabilityDebuggerStyle.uss"); + if (styleSheet != null) + { + root.styleSheets.Add(styleSheet); + } + } + + private void CreateToolbar() + { + var toolbar = new Toolbar(); + toolbar.style.paddingLeft = 5; + toolbar.style.paddingRight = 5; + toolbar.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f); + + // 标签页按钮(Timeline, Performance, Tag Blocker) + timelineTabButton = new ToolbarButton(() => ShowTab("timeline")) { text = "Timeline" }; + timelineTabButton.style.marginLeft = 5; + toolbar.Add(timelineTabButton); + + performanceTabButton = new ToolbarButton(() => ShowTab("performance")) { text = "Performance" }; + toolbar.Add(performanceTabButton); + + tagBlockerTabButton = new ToolbarButton(() => ShowTab("tagblocker")) { text = "Tag Blocker" }; + toolbar.Add(tagBlockerTabButton); + + root.Add(toolbar); + } + + private void CreateHierarchyPanel() + { + hierarchyPanel = new VisualElement(); + hierarchyPanel.style.backgroundColor = new Color(0.18f, 0.18f, 0.18f); + + var header = new Label("GameObject Hierarchy"); + header.style.unityFontStyleAndWeight = FontStyle.Bold; + header.style.paddingLeft = 10; + header.style.paddingTop = 5; + header.style.paddingBottom = 5; + header.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f); + hierarchyPanel.Add(header); + + // 搜索框 + var searchField = new TextField(); + searchField.name = "hierarchySearchField"; + searchField.style.marginLeft = 5; + searchField.style.marginRight = 5; + searchField.style.marginTop = 5; + searchField.style.marginBottom = 5; + searchField.value = hierarchySearchText; + searchField.RegisterValueChangedCallback(evt => + { + hierarchySearchText = evt.newValue; + RefreshHierarchy(); + }); + hierarchyPanel.Add(searchField); + + // 创建GameObject列表 + gameObjectList = new ListView(); + gameObjectList.style.flexGrow = 1; + gameObjectList.selectionType = SelectionType.Single; + gameObjectList.onSelectionChange += OnGameObjectSelected; + hierarchyPanel.Add(gameObjectList); + + leftSplitView.Add(hierarchyPanel); + } + + private void CreateInspectorPanel() + { + inspectorPanel = new VisualElement(); + inspectorPanel.style.backgroundColor = new Color(0.16f, 0.16f, 0.16f); + + var header = new Label("Inspector"); + header.style.unityFontStyleAndWeight = FontStyle.Bold; + header.style.paddingLeft = 10; + header.style.paddingTop = 5; + header.style.paddingBottom = 5; + header.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f); + inspectorPanel.Add(header); + + var scrollView = new ScrollView(); + scrollView.name = "inspectorScrollView"; + scrollView.style.flexGrow = 1; + inspectorPanel.Add(scrollView); + + leftSplitView.Add(inspectorPanel); + } + + private void CreateRightPanel() + { + var rightContainer = new VisualElement(); + rightContainer.style.flexGrow = 1; + + // 直接创建内容容器,不需要标签按钮(已移到工具栏) + var tabContent = new VisualElement(); + tabContent.name = "tabContent"; + tabContent.style.flexGrow = 1; + + // 创建各个标签页内容 + CreateTimelinePanel(tabContent); + CreatePerformancePanel(tabContent); + CreateTagBlockerPanel(tabContent); + + rightContainer.Add(tabContent); + mainSplitView.Add(rightContainer); + + // 默认显示Timeline + ShowTab("timeline"); + } + + private void ShowTab(string tabName) + { + currentTab = tabName; + + // 隐藏所有面板 + if (timelinePanel != null) timelinePanel.style.display = DisplayStyle.None; + if (performancePanel != null) performancePanel.style.display = DisplayStyle.None; + if (tagBlockerPanel != null) tagBlockerPanel.style.display = DisplayStyle.None; + + // 更新按钮样式 + UpdateTabButtonStyles(); + + // 显示选中的面板 + switch (tabName) + { + case "timeline": + if (timelinePanel != null) timelinePanel.style.display = DisplayStyle.Flex; + break; + case "performance": + if (performancePanel != null) performancePanel.style.display = DisplayStyle.Flex; + break; + case "tagblocker": + if (tagBlockerPanel != null) tagBlockerPanel.style.display = DisplayStyle.Flex; + break; + } + } + + private void UpdateTabButtonStyles() + { + // 重置所有按钮样式 + if (timelineTabButton != null) + { + timelineTabButton.style.backgroundColor = currentTab == "timeline" + ? new Color(0.3f, 0.5f, 0.7f) + : StyleKeyword.Null; + } + if (performanceTabButton != null) + { + performanceTabButton.style.backgroundColor = currentTab == "performance" + ? new Color(0.3f, 0.5f, 0.7f) + : StyleKeyword.Null; + } + if (tagBlockerTabButton != null) + { + tagBlockerTabButton.style.backgroundColor = currentTab == "tagblocker" + ? new Color(0.3f, 0.5f, 0.7f) + : StyleKeyword.Null; + } + } + + private void CreateTimelinePanel(VisualElement parent) + { + timelinePanel = new VisualElement(); + timelinePanel.style.flexGrow = 1; + timelinePanel.style.backgroundColor = new Color(0.22f, 0.22f, 0.22f); + timelinePanel.style.flexDirection = FlexDirection.Column; + + // 顶部:控制栏 + var headerContainer = new VisualElement(); + headerContainer.style.flexDirection = FlexDirection.Row; + headerContainer.style.justifyContent = Justify.SpaceBetween; + headerContainer.style.paddingLeft = 10; + headerContainer.style.paddingTop = 10; + headerContainer.style.paddingBottom = 10; + headerContainer.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f); + + timelinePanel.Add(headerContainer); + + // 时间轴控制栏(只显示信息) + var controlBar = new VisualElement(); + controlBar.name = "timelineControlBar"; + controlBar.style.flexDirection = FlexDirection.Row; + controlBar.style.paddingLeft = 10; + controlBar.style.paddingRight = 10; + controlBar.style.paddingTop = 5; + controlBar.style.paddingBottom = 5; + controlBar.style.backgroundColor = new Color(0.18f, 0.18f, 0.18f); + controlBar.style.alignItems = Align.Center; + + // 当前时间显示 + var timeLabel = new Label($"Time: {currentTime:F2}s"); + timeLabel.name = "currentTimeLabel"; + timeLabel.style.color = Color.gray; + timeLabel.style.marginLeft = 0; + controlBar.Add(timeLabel); + + // 弹性空间 + controlBar.Add(new VisualElement { style = { flexGrow = 1 } }); + + // 缩放控制(最右侧) + var zoomSlider = new Slider(0.1f, 10f) { value = zoomLevel }; + zoomSlider.name = "zoomSlider"; + zoomSlider.style.width = 120; + zoomSlider.style.marginRight = 8; + zoomSlider.RegisterValueChangedCallback(evt => + { + zoomLevel = evt.newValue; + UpdateZoomUI(); + RefreshTimeline(); + }); + controlBar.Add(zoomSlider); + + var zoomLabel = new Label($"Zoom: {zoomLevel:F1}x"); + zoomLabel.name = "zoomLevelLabel"; + zoomLabel.style.fontSize = 11; + zoomLabel.style.color = Color.gray; + zoomLabel.style.minWidth = 80; + controlBar.Add(zoomLabel); + + timelinePanel.Add(controlBar); + + // 时间轴视图容器 + var timelineViewContainer = new VisualElement(); + timelineViewContainer.name = "timelineViewContainer"; + timelineViewContainer.style.flexGrow = 1; + timelineViewContainer.style.flexDirection = FlexDirection.Column; + timelineViewContainer.style.overflow = Overflow.Hidden; + timelineViewContainer.style.position = Position.Relative; + + // 创建playhead但先不添加,在RefreshTimeline中添加 + scrubber = new VisualElement(); + scrubber.name = "playhead"; + scrubber.style.position = Position.Absolute; + scrubber.style.width = 2; + scrubber.style.height = Length.Percent(100); + scrubber.style.backgroundColor = new Color(1f, 0.3f, 0.3f, 0.9f); + scrubber.style.left = 200; + scrubber.style.top = 0; + scrubber.pickingMode = PickingMode.Ignore; + + timelinePanel.Add(timelineViewContainer); + + // 底部:详细信息面板 + CreateDetailsPanel(); + + parent.Add(timelinePanel); + } + + private void CreateDetailsPanel() + { + detailsPanel = new VisualElement(); + detailsPanel.style.height = 35; + detailsPanel.style.backgroundColor = new Color(0.14f, 0.14f, 0.14f); + detailsPanel.style.borderTopWidth = 1; + detailsPanel.style.borderTopColor = new Color(0.2f, 0.2f, 0.2f); + detailsPanel.style.paddingLeft = 10; + detailsPanel.style.paddingRight = 10; + detailsPanel.style.paddingTop = 3; + detailsPanel.style.paddingBottom = 3; + + // 单行容器,包含所有图例和状态指示 + var infoContainer = new VisualElement(); + infoContainer.style.flexDirection = FlexDirection.Row; + infoContainer.style.alignItems = Align.Center; + infoContainer.style.flexWrap = Wrap.Wrap; + + // 颜色图例标题 + var legendLabel = new Label("Activation:"); + legendLabel.style.color = Color.gray; + legendLabel.style.fontSize = 11; + legendLabel.style.marginRight = 10; + infoContainer.Add(legendLabel); + + // 添加图例项 + AddLegendItem(infoContainer, "● Active", new Color(0.3f, 0.8f, 0.3f), "Normal activation"); + AddLegendItem(infoContainer, "● Quick", new Color(0.4f, 0.7f, 0.9f), "Duration < 0.1s"); + AddLegendItem(infoContainer, "● Long", new Color(0.2f, 0.7f, 0.3f), "Duration > 2s"); + AddLegendItem(infoContainer, "● Blocked", new Color(0.9f, 0.4f, 0.3f), "Has block reason"); + + // 分隔符 + var separator = new VisualElement(); + separator.style.width = 2; + separator.style.height = 16; + separator.style.backgroundColor = new Color(0.3f, 0.3f, 0.3f); + separator.style.marginLeft = 15; + separator.style.marginRight = 15; + infoContainer.Add(separator); + + // 状态指示标题 + var statusLabel = new Label("Status:"); + statusLabel.style.color = Color.gray; + statusLabel.style.fontSize = 11; + statusLabel.style.marginRight = 10; + infoContainer.Add(statusLabel); + + // 状态指示器 + AddStatusIndicator(infoContainer, new Color(0.3f, 0.9f, 0.3f), "Active"); + AddStatusIndicator(infoContainer, new Color(0.5f, 0.5f, 0.5f), "Inactive"); + AddStatusIndicator(infoContainer, new Color(0.9f, 0.3f, 0.3f), "Blocked"); + + detailsPanel.Add(infoContainer); + + timelinePanel.Add(detailsPanel); + } + + private void AddLegendItem(VisualElement parent, string text, Color color, string tooltip) + { + var item = new VisualElement(); + item.style.flexDirection = FlexDirection.Row; + item.style.alignItems = Align.Center; + item.style.marginRight = 20; + item.tooltip = tooltip; + + var label = new Label(text); + label.style.color = color; + label.style.fontSize = 11; + item.Add(label); + + parent.Add(item); + } + + private void AddStatusIndicator(VisualElement parent, Color color, string text) + { + var container = new VisualElement(); + container.style.flexDirection = FlexDirection.Row; + container.style.alignItems = Align.Center; + container.style.marginRight = 15; + + var indicator = new VisualElement(); + indicator.style.width = 4; + indicator.style.height = 16; + indicator.style.backgroundColor = color; + indicator.style.marginRight = 5; + container.Add(indicator); + + var label = new Label(text); + label.style.color = Color.gray; + label.style.fontSize = 11; + container.Add(label); + + parent.Add(container); + } + + // 鼠标拖拽相关 + private bool isDragging = false; + private Vector2 dragStartPosition; + + private void OnTimelineWheel(WheelEvent evt) + { + if (selectedComponent == null || contentScrollView == null) return; + + // 获取鼠标在时间轴上的位置(相对于contentScrollView) + Vector2 mousePos = evt.localMousePosition; + float mouseX = mousePos.x - 200; // 减去左侧标签宽度 + + // 只在鼠标在时间轴内容区域时才缩放 + if (mouseX < 0) return; + + // 计算鼠标位置对应的时间点(缩放前) + float scrollOffset = contentScrollView.scrollOffset.x; + float pixelsPerSecondOld = 100f * zoomLevel; + float mouseTimeOffset = (mouseX + scrollOffset) / pixelsPerSecondOld; + float mouseTime = minTime + mouseTimeOffset; + + // 鼠标滚轮缩放 + float zoomDelta = -evt.delta.y * 0.01f; + float oldZoom = zoomLevel; + zoomLevel = Mathf.Clamp(zoomLevel + zoomDelta, 0.1f, 10f); + + if (Mathf.Abs(oldZoom - zoomLevel) > 0.01f) + { + // 更新缩放级别显示 + UpdateZoomUI(); + + // 刷新时间轴 + RefreshTimeline(); + + // 计算鼠标位置对应的时间点在新缩放级别下的像素位置 + float pixelsPerSecondNew = 100f * zoomLevel; + float newPixelPos = (mouseTime - minTime) * pixelsPerSecondNew; + + // 调整滚动位置,使鼠标下的时间点保持不变 + // 新的滚动偏移 = 新的像素位置 - 鼠标在视口中的位置 + float newScrollOffset = newPixelPos - mouseX; + + // 使用 EditorApplication.delayCall 确保在UI更新后设置滚动位置 + EditorApplication.delayCall += () => + { + if (contentScrollView != null && contentScrollView.horizontalScroller != null) + { + contentScrollView.horizontalScroller.value = Mathf.Max(0, newScrollOffset); + } + }; + } + + evt.StopPropagation(); + } + + private void OnTimelineMouseDown(MouseDownEvent evt) + { + if (evt.button == 0 && selectedComponent != null) // 左键 + { + isDragging = true; + dragStartPosition = evt.mousePosition; + + // 点击时自动暂停跟随模式 + if (isFollowingTime) + { + isFollowingTime = false; + UpdatePlayButton(); + + // 显示scrubber和滚动条 + if (scrubber != null) + { + scrubber.style.display = DisplayStyle.Flex; + } + if (contentScrollView != null) + { + contentScrollView.horizontalScrollerVisibility = ScrollerVisibility.Auto; + } + } + + // 立即更新时间到点击位置 + UpdateTimeFromMousePosition(evt.localMousePosition); + + evt.StopPropagation(); + } + } + + private void OnTimelineMouseMove(MouseMoveEvent evt) + { + if (isDragging && selectedComponent != null) + { + // 直接根据鼠标位置更新时间 + UpdateTimeFromMousePosition(evt.localMousePosition); + + evt.StopPropagation(); + } + } + + private void OnTimelineMouseUp(MouseUpEvent evt) + { + if (evt.button == 0) + { + isDragging = false; + evt.StopPropagation(); + } + } + + private void UpdateTimeFromMousePosition(Vector2 localMousePosition) + { + float clickX = localMousePosition.x - 200; // 减去左侧标签宽度 + + if (clickX >= 0 && contentScrollView != null) + { + // 加上滚动偏移 + float scrollOffset = contentScrollView.scrollOffset.x; + float adjustedX = clickX + scrollOffset; + + // 应用缩放:从像素位置计算时间 + float pixelsPerSecond = 100f * zoomLevel; + float timeOffset = adjustedX / pixelsPerSecond; + currentTime = minTime + timeOffset; + currentTime = Mathf.Clamp(currentTime, minTime, maxTime); + + UpdateScrubberPosition(); + UpdateCurrentTimeLabel(); + UpdateDetailsPanel(); + } + } + + private void UpdatePlayButton() + { + var playPauseButton = root.Q