# 23 · Boss 技能模块(Boss Skill Module) > **命名空间** `BaseGames.Boss` > **程序集** `BaseGames.Enemy`(并入敌人程序集) > **依赖** `BaseGames.Core.Events` · `BaseGames.Combat`(DamageInfo · HitBox)· `BaseGames.AI`(BossOrchestrator · BehaviorDesigner)· `Kybernetik.Animancer` > **Design 来源** [47_BossSkillSystem](../Design/47_BossSkillSystem.md) --- ## 目录 1. [模块职责与层级](#1-模块职责与层级) 2. [BossSkillType 枚举](#2-bossskilltype-枚举) 3. [VulnerabilityWindow](#3-vulnerabilitywindow) 4. [BossSkillSO](#4-bossskillso) 5. [AttackPatternSO](#5-attackpatternso) 6. [SkillSequenceSO](#6-skillsequenceso) 7. [BossSkillExecutor](#7-bossskillexecutor) 8. [WeakPointSystem](#8-weakpointsystem) 9. [BossOrchestrator 集成](#9-bossorchestrator-集成) 10. [设计规则一览](#10-设计规则一览) 11. [事件频道](#11-事件频道) --- ## 1. 模块职责与层级 ``` 数据层(ScriptableObject): ┌─────────────────────────────────────────────────┐ │ BossSkillSO │ │ ├─ 技能元信息(类型/弱点窗口/互动标签) │ │ └─ SkillSequenceSO[] 伤害序列 │ │ └─ AttackPatternSO[] 单个攻击图案 │ └─────────────────────────────────────────────────┘ 运行时层(MonoBehaviour / UniTask): BossOrchestrator(AI 决策,Behavior Designer 树) └─ BossSkillExecutor(执行技能序列,处理 VulnerabilityWindow) ├─ WeakPointSystem(弱点 HurtBox 激活管理) └─ HitBox[](输出伤害) 设计原则: ① 伤害值只写在 AttackPatternSO,不在 BossSkillSO 重复 ② 每个技能必须有 ≥1 VulnerabilityWindow(后摇 ≥0.5s 或专属弱点) ③ 技能顺序由 BossOrchestrator 决定,BossSkillExecutor 只负责执行 ``` --- ## 2. BossSkillCategory 和 BossSkillType 枚举 ```csharp namespace BaseGames.Boss { /// 高层技能分类(平衡框架用)。 public enum BossSkillCategory { Melee, Ranged, Charge, AoE, Environmental, Summon, Buff, Debuff, Phase, Passive, Reactive } /// 具体技能类型(战斗设计用)。 public enum BossSkillType { // 基础攻击 MeleeSlash, // 近战斩击 ChargeAttack, // 冲刺撞击 LeapSlam, // 跳起落地震 ProjectileVolley, // 弹幕齐射 LaserBeam, // 激光扫射 // 阶段技能 PhaseTransition, // 阶段切换(无攻击,触发动画/护盾等) SummonMinion, // 召唤小兵 // 特殊 ArenaTrap, // 改变战斗区域地形 SpeedBurst, // 速度爆发(数帧内免疫/高速) DefensiveShell, // 防御壳(需攻击弱点) } /// 互动标签:定义玩家可以对该技能执行哪些反制操作。 [Flags] public enum InteractionTag { None = 0, Parryable = 1 << 0, // 可弹反 PerfectParryOnly = 1 << 1, // 仅完美弹反有效 DodgeWindow = 1 << 2, // 打开逃避窗口 Unblockable = 1 << 3, // 无法拦截 CanBeReflected = 1 << 4, // 弹幕可被反射 ExposesWeakPoint = 1 << 5, // 暴露弱点 GrantsPlayerReso = 1 << 6, // 多典:命中后给予玩家资源 ArenaHazard = 1 << 7, // 场地危机(环境相关) PhaseGate = 1 << 8, // 阶段门关(必须触发才进入下一阶段) } } ``` --- ## 3. VulnerabilityWindow ```csharp namespace BaseGames.Boss { /// 弱点出现时机类型。 public enum VulnTriggerType { OnAttackRecovery, // 攻击后摇 OnParriedSuccess, // 弹反成功 OnCounterSkillHit, // 计算技能命中 OnPhaseTransition, // 阶段切换时 OnHazardBackfire, // 场地尃8个 OnSummonDefeated, // 召唤物被击败 Manual, // BossSkillExecutor 手动触发 } /// 弱点位置类型。 public enum WeakPointType { FullBody, // 主体全身都是弱点 HeadOnly, // 仅头部 BackOnly, // 仅背部 CoreExposed, // 核心暴露(展开中心 HurtBox) CustomPoint, // 自定义弱点 HurtBox } [Serializable] public struct VulnerabilityWindow { [Tooltip("弱点触发方式")] public VulnTriggerType TriggerType; [Tooltip("触发后延迟出现(秒)")] [Min(0f)] public float TriggerDelay; [Tooltip("弱点持续时长(秒,设计约定 ≥0.5s)")] [Min(0.1f)] public float Duration; [Tooltip("弱点位置类型")] public WeakPointType WeakPointType; [Tooltip("弱点激活时 Boss 的受击乘数(1 = 正常,>1 = 额外伤害)")] [Min(0.1f)] public float DamageMultiplier; [Tooltip("命中后强制驰恶")] public bool ForceStagger; [Tooltip("驰恶时间(秒),ForceStagger=true 时生效")] [Min(0f)] public float StaggerDuration; [Tooltip("弱点开启时播放的 Feedback(光效等)")] public MMF_Player OpenFeedback; [Tooltip("弱点关闭时播放的 Feedback")] public MMF_Player CloseFeedback; [Tooltip("弱点激活时的高亮颜色(指示玩家该攻击哪里)")] public Color HighlightColor; // 与旧字段对应(保留为展示用) public bool ActivateWeakPointHurtBox => WeakPointType != WeakPointType.FullBody; } } ``` --- ## 4. BossSkillSO ```csharp namespace BaseGames.Boss { [CreateAssetMenu(menuName = "Boss/BossSkill")] public class BossSkillSO : ScriptableObject { [Header("元信息")] public string skillId; public string displayName; [TextArea(1, 4)] public string designNote; // 设计师注释(不进游戏) [Header("技能分类")] public BossSkillCategory category; // 高层分类(Melee/Ranged 等) public BossSkillType skillType; // 具体技能类型 [Header("阶段可用性")] [Tooltip("和数据层 BossPhaseConfigSO.PhaseIndex 对应;空 = 全阶段可用")] public int[] availablePhaseIndices; // 空数组 = 全阶段可用 [Header("核心攻击动作引用")] [Tooltip("构成此技能的 AttackPatternSO 序列(单个技能 = 长度 1;连击/多段 = 多个)")] public AttackPatternSO[] attackPatterns; // 按执行顺序排列 [Header("弱点窗口(至少 1 个)")] public VulnerabilityWindow[] vulnerabilityWindows; [Header("互动标签(与谜题/道具联动)")] public InteractionTag interactionTags; // [Flags] 枚举,替代旧 string interactionTag [Header("连段(执行中的子序列)")] public SkillSequenceSO sequenceOnHit; // 被玩家攻击弱点时触发的序列(可空) public SkillSequenceSO sequenceOnMiss; // 弱点未被攻击时触发(可空) [Header("玩家反制接口")] [Tooltip("被不同玩家行为反制时 Boss 的应激反应(见 §4.1)")] public PlayerCounterResponse[] counterResponses; [Header("场景联动")] [Tooltip("此技能执行时触发的场景事件(见 §4.2)")] public ArenaEventTrigger[] arenaEvents; [Header("Boss 资源")] [Tooltip("使用此技能消耗的 Boss 自身资源(见 §7)")] public BossResourceCost resourceCost; [Tooltip("使用此技能后是否积累愤怒值")] public bool buildsRage; [Header("霸体配置")] [Tooltip("此技能执行期间的霸体窗口(见 54_PoiseSystem §8);None = 可被打断")] public PoiseWindowConfig poiseWindow; [Header("动画")] public ClipTransition skillAnimation; // Animancer ClipTransition(技能播放动画) [Header("冷却")] [Min(0f)] public float cooldown; // 秒,0 = 无冷却(由 BossOrchestrator 决策层管理) } } ``` ### 4.1 PlayerCounterResponse — 玩家反制接口 ```csharp namespace BaseGames.Boss { [Serializable] public struct PlayerCounterResponse { [Header("反制条件")] public CounterType counterType; // 玩家用什么行为触发反制 public string requiredSkillId; // counterType = SpecificSkill 时填写技能 ID [Header("反制效果(对 Boss)")] public float bossStaggerDuration; // 强制硬直时长(秒,0 = 不强制) public float bossDamageBonus; // 对 Boss 的额外伤害倍率(0 = 不额外) public bool openVulnWindow; // 是否触发 VulnerabilityWindow public bool interruptSkill; // 是否打断 Boss 当前技能 [Header("反制收益(对玩家)")] public int soulPowerGrant; // 给予玩家的灵力数量 public int spiritPowerGrant; // 给予玩家的魄元数量 public MMF_Player counterFeedback; // 成功反制的特效/音效反馈 } public enum CounterType { Parry, // 弹反成功 PerfectParry, // 完美弹反 DodgeThrough, // 冲刺穿越攻击(无敌帧通过) SpecificSkill, // 使用特定玩家技能(填 requiredSkillId) WeakPointHit, // 命中暴露的弱点 HazardBackfire, // 利用场景危险伤到 Boss SummonKill, // 击败召唤物 } } ``` ### 4.2 ArenaEventTrigger — 场景联动 ```csharp namespace BaseGames.Boss { [Serializable] public struct ArenaEventTrigger { public string targetArenaObjectId; // 要影响的场景物件 ID(空 = 广播给所有) public ArenaEventType eventType; public float delay; // 从技能触发到场景事件的延迟(秒) public ArenaEventParams parameters; } public enum ArenaEventType { DestroyPlatform, // 破坏指定平台 ActivateHazard, // 激活陷阱(如喷火管) DeactivateHazard, // 关闭陷阱 SpawnHazardArea, // 生成持续危险区域(熔岩/毒液) ShakeArena, // 场景震动 ToggleLighting, // 切换场景光照 SpawnPlatform, // 生成新平台(阶段 2 变化) TriggerCutscene, // 触发小型过场 } [Serializable] public struct ArenaEventParams { public float duration; // 效果持续时间(0 = 永久) public float intensity; // 强度(震动幅度、危险区域半径等) public bool revertsOnPhaseEnd; // 阶段结束时是否恢复 } [Serializable] public struct ArenaEventData { public ArenaEventType type; public ArenaEventParams parameters; public string sourceSkillId; // 来源技能 ID(供场景物件做条件判断) } /// /// 场景中可被 Boss 技能交互的对象实现此接口。 /// Boss 通过 ArenaEventBus 广播,场景物件监听并响应。 /// public interface IArenaInteractable { string ArenaObjectId { get; } void OnBossArenaEvent(ArenaEventData data); } } ``` ### 4.3 BossResourceCost — Boss 资源消耗 ```csharp namespace BaseGames.Boss { [Serializable] public struct BossResourceCost { public string resourceId; // 对应 BossResourceConfigSO.resourceId(如 "Rage") public float cost; // 消耗量(0 = 不消耗资源) public float minRequired; // 使用此技能的最低资源要求 } [CreateAssetMenu(menuName = "Boss/ResourceConfig")] public class BossResourceConfigSO : ScriptableObject { public string resourceId; // 如 "Rage" / "PhaseCharge" public string displayName; public float maxValue; public float startValue; // 初始值(通常 0) [Header("自动变化")] public float passiveRate; // 每秒自动变化量(正=增长/负=衰减,0=不变) public float onTakeDamageGain; // 每受到 1 点伤害积累量 public float onSkillUseGain; // 每使用一次技能积累量 [Header("满值效果")] public bool autoTriggerOnFull; public BossSkillSO fullTriggerSkill; public float resetValueAfterTrigger; } } ``` --- ## 5. AttackPatternSO ```csharp namespace BaseGames.Boss { /// /// 单个攻击图案的数据。 /// 存放伤害/速度等实际参数,BossSkillSO 不存参数。 /// [CreateAssetMenu(menuName = "Boss/AttackPattern")] public class AttackPatternSO : ScriptableObject { [Header("输出")] public DamageSourceSO DamageSource; // ⚠️ HitBox.Activate() 需要 DamageSourceSO(架构 06 §4) public float KnockbackAngle; // 击退角度(度)(基础击退力由 DamageSourceSO 内配置) [Header("弹幕(若为弹幕类型)")] public AssetReferenceGameObject ProjectilePrefab; public int ProjectileCount = 1; public float SpreadAngle = 0f; // 散射角(度) public float ProjectileSpeed = 8f; [Header("范围攻击(若为 AoE 类型)")] public float AoERadius; public Vector2 AoEOffset; // 相对于 Boss 的偏移 [Header("时序")] public float WindupDuration; // 预备动作时间 public float ActiveDuration; // HitBox 激活时长 public float RecoveryDuration; // 后摇时长(VulnerabilityWindow 通常在此段) } } ``` --- ## 6. SkillSequenceSO ```csharp namespace BaseGames.Boss { /// /// 有序攻击序列(一个技能内的多段连段)。 /// [CreateAssetMenu(menuName = "Boss/SkillSequence")] public class SkillSequenceSO : ScriptableObject { [Serializable] public struct SequenceStep { public AttackPatternSO pattern; public float delayBeforeStep; // 执行本步前等待的秒数 } public SequenceStep[] steps; [Header("序列完成后的行为")] public bool RepeatIfPlayerInRange; // 若玩家仍在范围内则重复(Survival 关卡用) public float RepeatDelay; [Range(0, 10)] public int MaxRepeatCount; // 0 = 无限 } } ``` --- ## 7. BossSkillExecutor ```csharp namespace BaseGames.Boss { /// /// 挂在 Boss GameObject 上。 /// 接收 BossOrchestrator 的指令,执行指定 BossSkillSO。 /// 管理 VulnerabilityWindow 计时和 WeakPointSystem 激活。 /// public class BossSkillExecutor : MonoBehaviour { [SerializeField] HitBox[] _hitBoxes; // Boss 身上的所有 HitBox [SerializeField] WeakPointSystem _weakPointSystem; [SerializeField] AnimancerComponent _animancer; [SerializeField] private string _bossId; // Boss 资产唯一 ID [SerializeField] private BossSkillEventChannelSO _onBossSkillStarted; // 发布:技能开始 [SerializeField] private BossSkillEventChannelSO _onBossSkillEnded; // 发布:技能结束 // ⚠️ PlayerController 无 Instance(Architecture 05 §2);Boss 居小场景持有玩家 Transform 引用 [SerializeField] private Transform _playerTransform; // 由 Inspector 指定玩家 Transform BossSkillSO _currentSkill; AnimancerState _currentState; bool _isExecuting; CancellationTokenSource _cts; public bool IsExecuting => _isExecuting; // ── 公共 API ─────────────────────────────────────────────── public async UniTask ExecuteSkill(BossSkillSO skill, CancellationToken ct) { if (_isExecuting) return; _isExecuting = true; _currentSkill = skill; _onBossSkillStarted?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = skill.skillId }); // 播放技能动画(Animancer ClipTransition,直接从 BossSkillSO 引用) _currentState = _animancer.Play(skill.skillAnimation); // 执行 SkillSequenceSO(若挂载 sequenceOnMiss) if (skill.sequenceOnMiss != null) await ExecuteSequence(skill.sequenceOnMiss, ct); _isExecuting = false; _onBossSkillEnded?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = skill.skillId }); } async UniTask ExecuteSequence(SkillSequenceSO seq, CancellationToken ct) { int repeatCount = 0; do { foreach (var step in seq.steps) { ct.ThrowIfCancellationRequested(); await UniTask.WaitForSeconds(step.delayBeforeStep, cancellationToken: ct); await ExecutePattern(step.pattern, ct); } repeatCount++; if (seq.RepeatDelay > 0) await UniTask.WaitForSeconds(seq.RepeatDelay, cancellationToken: ct); } while (seq.RepeatIfPlayerInRange && repeatCount < seq.MaxRepeatCount && IsPlayerInRange() && !ct.IsCancellationRequested); } async UniTask ExecutePattern(AttackPatternSO pattern, CancellationToken ct) { // 预备 await UniTask.WaitForSeconds(pattern.WindupDuration, cancellationToken: ct); // 激活 HitBox(⚠️ 以架构 06_CombatModule §4 为准:Activate(DamageSourceSO, Transform)) foreach (var hb in _hitBoxes) hb.Activate(pattern.DamageSource, transform); await UniTask.WaitForSeconds(pattern.ActiveDuration, cancellationToken: ct); // 关闭 HitBox foreach (var hb in _hitBoxes) hb.Deactivate(); // 后摇(VulnerabilityWindow 由动画事件配合 VulnerabilityWindow 数据触发) await UniTask.WaitForSeconds(pattern.RecoveryDuration, cancellationToken: ct); } // ── VulnerabilityWindow 激活(由 ExecuteSkill 协程驱动)───── // 注意:新版 VulnerabilityWindow 改用绝对时间(TriggerDelay + Duration), // 而非归一化时间,故弃用 Update 轮询,改为 UniTask 序列激活。 async UniTask ActivateVulnerabilityWindows(BossSkillSO skill, CancellationToken ct) { foreach (var window in skill.vulnerabilityWindows) { // 等待触发延迟 await UniTask.Delay( TimeSpan.FromSeconds(window.TriggerDelay), cancellationToken: ct); // 激活弱点 bool isCustom = window.ActivateWeakPointHurtBox; _weakPointSystem.SetActive(true, window.DamageMultiplier, isCustom); window.OpenFeedback?.PlayFeedbacks(); // 持续弱点窗口 await UniTask.Delay( TimeSpan.FromSeconds(window.Duration), cancellationToken: ct); // 关闭弱点 _weakPointSystem.SetActive(false, 1f, isCustom); window.CloseFeedback?.PlayFeedbacks(); } } bool IsPlayerInRange() => _playerTransform != null && Vector2.Distance(transform.position, _playerTransform.position) < 8f; } } ``` --- ## 8. WeakPointSystem ```csharp namespace BaseGames.Boss { /// /// 管理 Boss 的专属弱点 HurtBox(如核心、眼睛等)。 /// 弱点激活期间受到的伤害会乘以 DamageMultiplier。 /// public class WeakPointSystem : MonoBehaviour { [Serializable] public struct WeakPoint { public HurtBox hurtBox; public GameObject visualIndicator; // 弱点亮光/标记(可为 null) } [SerializeField] WeakPoint[] _weakPoints; [SerializeField] private string _bossId; // Boss 资产唯一 ID [SerializeField] private StringEventChannelSO _onVulnerabilityWindowOpened; // 发布:弱点窗口开启 float _damageMultiplier = 1f; public void SetActive(bool active, float multiplier = 1f) { _damageMultiplier = multiplier; foreach (var wp in _weakPoints) { wp.hurtBox.gameObject.SetActive(active); if (wp.visualIndicator != null) wp.visualIndicator.SetActive(active); } if (active) _onVulnerabilityWindowOpened?.Raise(_bossId); } /// /// 弱点 HurtBox 受击时,由 BossStats 调用此方法获取最终伤害系数。 /// public float GetDamageMultiplier() => _damageMultiplier; } } ``` --- ## 9. BossOrchestrator 集成 `BossOrchestrator` 是 Behavior Designer 的宿主 MonoBehaviour,持有 `BossSkillExecutor` 引用: ```csharp // BossOrchestrator 片段(供 BD 节点调用) public class BossOrchestrator : MonoBehaviour { [SerializeField] BossSkillSO[] _phaseOneSkills; [SerializeField] BossSkillSO[] _phaseTwoSkills; [SerializeField] BossSkillExecutor _executor; int _currentPhase = 1; CancellationTokenSource _cts; // BD Task 节点调用:ExecuteSkill(skillId) public async UniTask ExecuteSkillById(string skillId) { var skills = _currentPhase == 1 ? _phaseOneSkills : _phaseTwoSkills; var skill = System.Array.Find(skills, s => s.skillId == skillId); if (skill == null) return; _cts?.Cancel(); _cts = new CancellationTokenSource(); await _executor.ExecuteSkill(skill, _cts.Token); } public void EnterPhaseTwo() { _currentPhase = 2; _cts?.Cancel(); // 打断当前技能 } } ``` --- ## 10. 设计规则一览 | 规则 | 说明 | |------|------| | 每个技能 ≥1 VulnerabilityWindow | 后摇窗口 ≥0.5s(归一化时长约 0.15~0.4) | | 伤害参数只在 AttackPatternSO | BossSkillSO 不存 BaseDamage 等参数 | | 技能冷却由 BossOrchestrator 管理 | BossSkillExecutor 只执行,不维护冷却 | | VulnerabilityWindow 在编辑器中校验 | 自定义 Validator Tool 检查 DurationNormalized ≥ 0.1 | | Boss 阶段切换打断当前技能 | `_cts.Cancel()` 终止 UniTask 序列 | --- ## 11. 事件频道 | 频道 SO | Payload | 发布者 | 订阅者 | |--------|---------|--------|--------| | `EVT_BossSkillStarted` | `(string bossId, string skillId)` | `BossSkillExecutor` | `BossHUD`(显示技能名)(震动通过 `CameraStateController.Instance.TriggerImpulse()` 直接调用,不订阅事件)| | `EVT_BossSkillEnded` | `(string bossId, string skillId)` | `BossSkillExecutor` | `BossOrchestrator`(BD 决策推进) | | `EVT_BossVulnerabilityWindowOpened` | `string bossId` | `WeakPointSystem` | `PlayerFeedback`(提示音效)、`HUDController` | | `EVT_BossPhaseChanged` | `(string bossId, int phase)` | `BossBase.EnterPhase()` | `BossHUD`(相机切换通过 `CameraStateController.Instance` 直接调用,不订阅事件)| --- ## 12. BossSkillSequenceEditorWindow — 技能序列可视化 > **痛点**:`SkillSequenceSO` 包含多个 `SequenceStep[]`(每步:AttackPatternSO + delayBeforeStep),但 Inspector 中只能逐字段查看数字,策划无法直觉感受技能序列的节奏感——哪段是攻击前摇、哪段是弱点窗口、总时长是否合理,全部要靠心算。本 EditorWindow 以 **甘特图(Gantt Chart)** 方式将时间线可视化。 ### 12.1 功能规格 | 功能 | 说明 | |------|------| | 时间轴刻度 | 横轴为时间(秒),最大显示 `SkillSequenceSO.TotalDuration`,刻度精度 0.1s | | 攻击阶段条 | 每个 `AttackPatternSO` 渲染为蓝色横条,长度 = `pattern.Duration` | | 延迟段 | `delayBeforeStep` 渲染为灰色空隙 | | 弱点窗口 | `VulnerabilityWindow`(`startNormalized`~`endNormalized`)渲染为绿色覆盖条 | | 选中高亮 | 点击任一条可选中对应 `AttackPatternSO`,Inspector 同步 Ping | | 验证警告 | `VulnerabilityWindow.DurationNormalized < 0.1` 时对应条变红 + Tooltip 提示 | | SO 拖放 | 将 `SkillSequenceSO` 资产拖入窗口即可加载 | ### 12.2 实现规范 ```csharp // 路径: Assets/Scripts/Editor/BossSkill/BossSkillSequenceWindow.cs // 程序集: BaseGames.Editor(Editor Only) #if UNITY_EDITOR using UnityEditor; using UnityEngine; using System.Collections.Generic; namespace BaseGames.Editor.BossSkill { public class BossSkillSequenceWindow : EditorWindow { [MenuItem("BaseGames/Tools/Boss Skill Sequence Viewer")] public static void Open() => GetWindow("技能序列查看器"); // ── 状态 ────────────────────────────────────────────────────────────── private SkillSequenceSO _target; private Vector2 _scroll; private float _zoom = 100f; // 像素/秒 private const float TrackHeight = 28f; private const float LabelWidth = 140f; private const float HeaderHeight = 30f; // 颜色 private static readonly Color ColDelay = new(0.3f, 0.3f, 0.3f, 0.5f); private static readonly Color ColAttack = new(0.2f, 0.5f, 1.0f, 0.8f); private static readonly Color ColVulnOk = new(0.2f, 0.8f, 0.3f, 0.6f); private static readonly Color ColVulnWarn = new(1.0f, 0.3f, 0.3f, 0.7f); private void OnGUI() { DrawToolbar(); if (_target == null) { EditorGUILayout.HelpBox("将 SkillSequenceSO 拖到此处或从 Toolbar 选择", MessageType.Info); HandleDragDrop(); return; } DrawTimeline(); } private void DrawToolbar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); _target = (SkillSequenceSO)EditorGUILayout.ObjectField( "技能序列", _target, typeof(SkillSequenceSO), false, GUILayout.Width(300)); GUILayout.Label($"缩放: {_zoom:0}px/s"); _zoom = GUILayout.HorizontalSlider(_zoom, 30f, 300f, GUILayout.Width(100)); EditorGUILayout.EndHorizontal(); } private void DrawTimeline() { float totalSeconds = _target.TotalDuration; float timelineWidth = totalSeconds * _zoom + LabelWidth + 20f; _scroll = EditorGUILayout.BeginScrollView(_scroll); Rect viewRect = EditorGUILayout.GetControlRect(false, HeaderHeight + _target.Steps.Length * (TrackHeight + 4f), GUILayout.Width(timelineWidth)); DrawRuler(viewRect, totalSeconds); float trackY = viewRect.y + HeaderHeight; float cursor = 0f; // 当前时间游标(秒) for (int i = 0; i < _target.Steps.Length; i++) { var step = _target.Steps[i]; Rect trackRect = new(viewRect.x, trackY + i * (TrackHeight + 4f), viewRect.width, TrackHeight); // 标签 Rect labelRect = new(trackRect.x, trackRect.y, LabelWidth, TrackHeight); GUI.Label(labelRect, step.Pattern?.name ?? "—", EditorStyles.miniLabel); // 延迟段 if (step.DelayBeforeStep > 0f) { Rect delayRect = TimeToRect(trackRect, cursor, step.DelayBeforeStep); EditorGUI.DrawRect(delayRect, ColDelay); cursor += step.DelayBeforeStep; } // 攻击段 float patternDur = step.Pattern?.Duration ?? 0f; if (patternDur > 0f) { Rect atkRect = TimeToRect(trackRect, cursor, patternDur); EditorGUI.DrawRect(atkRect, ColAttack); GUI.Label(atkRect, step.Pattern?.name ?? "", EditorStyles.centeredGreyMiniLabel); // 弱点窗口覆盖层 DrawVulnerabilityWindows(atkRect, cursor, patternDur, step.Pattern); cursor += patternDur; } } EditorGUILayout.EndScrollView(); } private void DrawVulnerabilityWindows(Rect atkRect, float patternStart, float patternDur, AttackPatternSO pattern) { if (pattern?.VulnerabilityWindows == null) return; foreach (var vw in pattern.VulnerabilityWindows) { float wStart = vw.StartNormalized * patternDur; float wDur = (vw.EndNormalized - vw.StartNormalized) * patternDur; bool warn = vw.DurationNormalized < 0.1f; Color col = warn ? ColVulnWarn : ColVulnOk; Rect wRect = TimeToRect(atkRect, patternStart + wStart, wDur); EditorGUI.DrawRect(wRect, col); if (warn) GUI.Label(wRect, "⚠", new GUIStyle { fontSize = 10, normal = { textColor = Color.white }, alignment = TextAnchor.MiddleCenter }); } } private Rect TimeToRect(Rect trackRect, float startSec, float durSec) { float x = trackRect.x + LabelWidth + startSec * _zoom; float w = Mathf.Max(2f, durSec * _zoom); return new Rect(x, trackRect.y + 2f, w, trackRect.height - 4f); } private void DrawRuler(Rect viewRect, float totalSeconds) { float step = _zoom >= 80f ? 0.5f : 1f; for (float t = 0; t <= totalSeconds; t += step) { float x = viewRect.x + LabelWidth + t * _zoom; EditorGUI.DrawRect(new Rect(x, viewRect.y, 1f, HeaderHeight), Color.gray); GUI.Label(new Rect(x + 2f, viewRect.y, 40f, 16f), $"{t:0.0}s", EditorStyles.miniLabel); } } private void HandleDragDrop() { var evt = Event.current; if (evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform) { DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (evt.type == EventType.DragPerform) { DragAndDrop.AcceptDrag(); foreach (var obj in DragAndDrop.objectReferences) if (obj is SkillSequenceSO seq) { _target = seq; break; } } evt.Use(); } } } } #endif ``` > **打开方式**:菜单 `BaseGames → Tools → Boss Skill Sequence Viewer`,将 `SkillSequenceSO` 资产拖入即可。时间轴缩放通过 Toolbar 滑条控制;弱点窗口 `DurationNormalized < 0.1` 时变红警告,对应设计规则(§10 Validator 等价)。