Files
zeling_v2/Docs/Architecture/23_BossSkillModule.md
2026-05-08 11:04:00 +08:00

842 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
BossOrchestratorAI 决策Behavior Designer 树)
└─ BossSkillExecutor执行技能序列处理 VulnerabilityWindow
├─ WeakPointSystem弱点 HurtBox 激活管理)
└─ HitBox[](输出伤害)
设计原则:
① 伤害值只写在 AttackPatternSO不在 BossSkillSO 重复
② 每个技能必须有 ≥1 VulnerabilityWindow后摇 ≥0.5s 或专属弱点)
③ 技能顺序由 BossOrchestrator 决定BossSkillExecutor 只负责执行
```
---
## 2. BossSkillCategory 和 BossSkillType 枚举
```csharp
namespace BaseGames.Boss
{
/// <summary>高层技能分类(平衡框架用)。</summary>
public enum BossSkillCategory
{
Melee, Ranged, Charge, AoE, Environmental, Summon,
Buff, Debuff, Phase, Passive, Reactive
}
/// <summary>具体技能类型(战斗设计用)。</summary>
public enum BossSkillType
{
// 基础攻击
MeleeSlash, // 近战斩击
ChargeAttack, // 冲刺撞击
LeapSlam, // 跳起落地震
ProjectileVolley, // 弹幕齐射
LaserBeam, // 激光扫射
// 阶段技能
PhaseTransition, // 阶段切换(无攻击,触发动画/护盾等)
SummonMinion, // 召唤小兵
// 特殊
ArenaTrap, // 改变战斗区域地形
SpeedBurst, // 速度爆发(数帧内免疫/高速)
DefensiveShell, // 防御壳(需攻击弱点)
}
/// <summary>互动标签:定义玩家可以对该技能执行哪些反制操作。</summary>
[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
{
/// <summary>弱点出现时机类型。</summary>
public enum VulnTriggerType
{
OnAttackRecovery, // 攻击后摇
OnParriedSuccess, // 弹反成功
OnCounterSkillHit, // 计算技能命中
OnPhaseTransition, // 阶段切换时
OnHazardBackfire, // 场地尃8个
OnSummonDefeated, // 召唤物被击败
Manual, // BossSkillExecutor 手动触发
}
/// <summary>弱点位置类型。</summary>
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 §8None = 可被打断")]
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供场景物件做条件判断
}
/// <summary>
/// 场景中可被 Boss 技能交互的对象实现此接口。
/// Boss 通过 ArenaEventBus 广播,场景物件监听并响应。
/// </summary>
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
{
/// <summary>
/// 单个攻击图案的数据。
/// 存放伤害/速度等实际参数BossSkillSO 不存参数。
/// </summary>
[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
{
/// <summary>
/// 有序攻击序列(一个技能内的多段连段)。
/// </summary>
[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
{
/// <summary>
/// 挂在 Boss GameObject 上。
/// 接收 BossOrchestrator 的指令,执行指定 BossSkillSO。
/// 管理 VulnerabilityWindow 计时和 WeakPointSystem 激活。
/// </summary>
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 无 InstanceArchitecture 05 §2Boss 居小场景持有玩家 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
{
/// <summary>
/// 管理 Boss 的专属弱点 HurtBox如核心、眼睛等
/// 弱点激活期间受到的伤害会乘以 DamageMultiplier。
/// </summary>
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);
}
/// <summary>
/// 弱点 HurtBox 受击时,由 BossStats 调用此方法获取最终伤害系数。
/// </summary>
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.EditorEditor 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<BossSkillSequenceWindow>("技能序列查看器");
// ── 状态 ──────────────────────────────────────────────────────────────
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 等价)。