Article

Unity 入门实战 13:Boss 多阶段机制(阶段切换、技能脚本化与演出同步)

路线阶段:Unity 入门实战第 13 章。
本章目标:把 Boss 战从“单状态高数值怪”升级为“阶段化机制战”,并保证逻辑与演出可维护。

学习目标

完成本章后,你应该能做到:

  1. 设计 Boss 阶段模型(阈值、行为包、技能池、表现参数)。
  2. 用配置驱动阶段切换,避免硬编码 if/else。
  3. 将阶段切换与技能系统、行为树、音画演出联动。
  4. 支持阶段重置与回放一致性验证。

背景问题

很多项目里的 Boss 逻辑:

if (hp < maxHp * 0.5f)
{
    speed = 2.0f;
    damage = 200;
}

问题:

  1. 阶段行为混在一个脚本里,难维护。
  2. 新增阶段要改很多旧代码。
  3. 演出触发点和战斗规则容易错位。
  4. 回放时很难还原“阶段切换瞬间发生了什么”。

阶段配置模型

[Serializable]
public sealed class BossPhaseConfig
{
    public int PhaseId;
    public float HpThreshold;           // 0~1,血量比例 <= 阈值进入
    public string BehaviorTreeKey;

    public List<int> EnabledSkillIds;
    public float MoveSpeedScale;
    public float DamageScale;

    public string EnterTimelineKey;
    public string EnterBgmLayer;
}

[Serializable]
public sealed class BossConfig
{
    public int BossId;
    public string Name;
    public List<BossPhaseConfig> Phases;
}

运行时数据

public sealed class BossRuntime
{
    public readonly Actor Actor;
    public readonly BossConfig Config;

    public int CurrentPhaseIndex;
    public bool IsTransitioning;

    public BossRuntime(Actor actor, BossConfig config)
    {
        Actor = actor;
        Config = config;
        CurrentPhaseIndex = 0;
        IsTransitioning = false;
    }
}

阶段控制器

public sealed class BossPhaseController : IUpdatable
{
    public int Order { get { return 270; } }

    private readonly BossRuntime _boss;
    private readonly BossSkillBinder _skillBinder;
    private readonly BossBehaviorBinder _behaviorBinder;
    private readonly BossPresentationBinder _presentationBinder;
    private readonly EventBus _eventBus;

    public BossPhaseController(
        BossRuntime boss,
        BossSkillBinder skillBinder,
        BossBehaviorBinder behaviorBinder,
        BossPresentationBinder presentationBinder,
        EventBus eventBus)
    {
        _boss = boss;
        _skillBinder = skillBinder;
        _behaviorBinder = behaviorBinder;
        _presentationBinder = presentationBinder;
        _eventBus = eventBus;
    }

    public void Tick(float dt, float unscaledDt)
    {
        if (_boss.IsTransitioning)
        {
            return;
        }

        var hpRate = _boss.Actor.MaxHp <= 0f ? 0f : (_boss.Actor.Hp / _boss.Actor.MaxHp);
        var nextIndex = ResolvePhaseIndex(hpRate);

        if (nextIndex == _boss.CurrentPhaseIndex)
        {
            return;
        }

        SwitchPhase(nextIndex, "hp_threshold");
    }

    private int ResolvePhaseIndex(float hpRate)
    {
        var phases = _boss.Config.Phases;

        // 默认按阈值从高到低排序
        for (var i = phases.Count - 1; i >= 0; i--)
        {
            if (hpRate <= phases[i].HpThreshold)
            {
                return i;
            }
        }

        return 0;
    }

    private void SwitchPhase(int index, string reason)
    {
        if (index < 0 || index >= _boss.Config.Phases.Count)
        {
            return;
        }

        _boss.IsTransitioning = true;

        var from = _boss.Config.Phases[_boss.CurrentPhaseIndex];
        var to = _boss.Config.Phases[index];

        _boss.CurrentPhaseIndex = index;

        _skillBinder.ApplyPhase(to);
        _behaviorBinder.ApplyPhase(to);
        _presentationBinder.ApplyPhase(to);

        _eventBus.Publish("BossPhaseChanged", new BossPhasePayload(from.PhaseId, to.PhaseId, reason));
        FoundationLog.Info("Boss", "phase_change from=" + from.PhaseId + " to=" + to.PhaseId + " reason=" + reason);

        CoroutineRunner.Instance.Delay(0.35f, delegate
        {
            _boss.IsTransitioning = false;
        });
    }
}

public struct BossPhasePayload
{
    public int From;
    public int To;
    public string Reason;

    public BossPhasePayload(int from, int to, string reason)
    {
        From = from;
        To = to;
        Reason = reason;
    }
}

技能绑定

public sealed class BossSkillBinder
{
    private readonly SkillComponent _skills;

    public BossSkillBinder(SkillComponent skills)
    {
        _skills = skills;
    }

    public void ApplyPhase(BossPhaseConfig phase)
    {
        _skills.DisableAll();

        for (var i = 0; i < phase.EnabledSkillIds.Count; i++)
        {
            _skills.EnableSkill(phase.EnabledSkillIds[i]);
        }
    }
}

行为树绑定

public sealed class BossBehaviorBinder
{
    private readonly EnemyAiBrain _brain;
    private readonly BehaviorTreeLibrary _library;

    public BossBehaviorBinder(EnemyAiBrain brain, BehaviorTreeLibrary library)
    {
        _brain = brain;
        _library = library;
    }

    public void ApplyPhase(BossPhaseConfig phase)
    {
        var tree = _library.Build(phase.BehaviorTreeKey);
        _brain.SetTree(tree);
    }
}

演出绑定

public sealed class BossPresentationBinder
{
    private readonly BossTimelinePlayer _timeline;
    private readonly BgmLayerController _bgm;
    private readonly BossView _view;

    public BossPresentationBinder(BossTimelinePlayer timeline, BgmLayerController bgm, BossView view)
    {
        _timeline = timeline;
        _bgm = bgm;
        _view = view;
    }

    public void ApplyPhase(BossPhaseConfig phase)
    {
        if (!string.IsNullOrEmpty(phase.EnterTimelineKey))
        {
            _timeline.Play(phase.EnterTimelineKey);
        }

        if (!string.IsNullOrEmpty(phase.EnterBgmLayer))
        {
            _bgm.SwitchLayer(phase.EnterBgmLayer);
        }

        _view.SetEnrageLevel(phase.PhaseId);
    }
}

阶段切换期间的战斗规则

建议默认策略:

  1. 切换期间短暂霸体(0.2~0.5s)。
  2. 切换期间暂停普通技能释放,防止动作打断。
  3. 切换结束后重置当前技能冷却下限(避免瞬发连招失控)。

与回放系统联动

阶段切换必须记录:

{
  "frame": 3250,
  "event": "BossPhaseChanged",
  "from": 1,
  "to": 2,
  "reason": "hp_threshold"
}

这样线上问题可快速确认:是阶段逻辑异常,还是技能/演出同步异常。

验收清单

  1. Boss 可按血量阈值稳定进入下一阶段。
  2. 阶段切换后技能组和行为树立即生效。
  3. 演出触发与战斗规则同步,无“先播后变/先变后播”错位。
  4. 回放同一战斗数据时阶段切换帧一致。

常见坑

坑 1:阶段阈值无序

如果阈值未排序,可能跳错阶段。加载配置时要做校验。

坑 2:切阶段不冻结输入与行为

会在过场里继续攻击,破坏演出。需要短暂门控。

坑 3:阶段切换多次触发

同一阈值反复进入。需比较当前阶段索引并去重。

本月作业

实现一个三阶段 Boss:

  1. P1:近战冲锋 + 扇形斩。
  2. P2:召唤杂兵 + 范围落雷。
  3. P3:狂暴追击 + 全屏预警技。

并输出每次阶段切换的日志、回放事件与演出截图。

下一章进入 Unity 入门实战 14:伤害数字、命中停顿与镜头震动(战斗手感打磨)。