842 lines
32 KiB
Markdown
842 lines
32 KiB
Markdown
# 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
|
||
{
|
||
/// <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 §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(供场景物件做条件判断)
|
||
}
|
||
|
||
/// <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 无 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
|
||
{
|
||
/// <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.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<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 等价)。
|
||
|