# 2. Hierarchical FSM per Phase
Part of [[Boss Battles with Phases]]. Prev: [[1. Boss Design and the Arena]]. Next: [[3. Telegraph VFX and Fair Windows]].
[[Enemy AI State Machine]] already gives you `IState`, `StateMachine`, and a `SuperState` whose child machine runs Chase / Attack while the parent only cares about Calm / Alert. A boss is the same idea with different labels: the **outer** machine is Intro / Phase1 / Phase2 / Phase3 / Death; each phase **super-state** has a child machine for Reposition / TelegraphAttack / Recover / Vulnerable.
Reuse those types. Do not start a second state-machine framework. If you have not written the AI note's core yet, copy `IState` / `StateMachine` / `SuperState` from it and keep the names stable so students can jump between notes.
## 2.1 Outer graph
```
[Intro] --(Timeline stopped)--> [Phase 1]
|
health <= threshold 2
v
[Phase 2]
|
health <= threshold 3
v
[Phase 3]
|
coup de grace / health 0
v
[Death]
```
`AddAnyTransition(death, () => WantsDeath)` so a freak fall or a scripted kill can leave any phase. Phase transitions are **not** Any-State spam: they are checked after damage, once, in the brain.
## 2.2 Phase super-state
The child graph is intentionally small. Fancy move lists are data on `PhaseDefinition.attacks`, not twelve child states.
```csharp
using System;
using UnityEngine;
/// <summary>
/// Super-state for one boss phase. Child machine: Reposition -> TelegraphAttack -> Recover.
/// NOT compiled in Unity. Depends on IState / StateMachine / SuperState from Enemy AI State Machine.
/// </summary>
public sealed class BossPhaseState : SuperState
{
public readonly PhaseDefinition Phase;
readonly BossBrain brain;
public BossPhaseState(BossBrain brain, PhaseDefinition phase, IState initialChild)
: base(phase != null ? phase.displayName : "Phase", initialChild, resumeChild: false)
{
this.brain = brain;
Phase = phase;
}
public override void Enter()
{
brain.ApplyPhase(Phase);
base.Enter();
}
}
```
If your `SuperState` methods are not `virtual` in the AI note, either make `Enter` virtual there (preferred - one line) or wrap the call:
```csharp
public sealed class BossPhaseState : IState
{
public string Name => phase.displayName;
public StateMachine Child { get; } = new();
readonly PhaseDefinition phase;
readonly BossBrain brain;
readonly IState initial;
public BossPhaseState(BossBrain brain, PhaseDefinition phase, IState initial)
{
this.brain = brain; this.phase = phase; this.initial = initial;
}
public void Enter()
{
brain.ApplyPhase(phase);
Child.SetState(initial);
}
public void Tick(float dt) => Child.Tick(dt);
public void Exit() => Child.CurrentState?.Exit();
}
```
The second form does not require editing the AI note. Use it if you want this series to stay additive.
## 2.3 The brain
```csharp
using System;
using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// Owns health, the outer state machine, and phase application.
/// NOT compiled in Unity.
/// </summary>
public sealed class BossBrain : MonoBehaviour, IDamageable
{
[SerializeField] BossDefinition definition;
[SerializeField] TeamId team = TeamId.Enemy;
[SerializeField] NavMeshAgent agent;
[SerializeField] Animator animator;
[SerializeField] BossIntroDirector intro;
[SerializeField] ArenaHazardDirector hazards;
[SerializeField] CombatVictim victimForward; // optional: reuse CombatVictim for feel
public event Action<PhaseDefinition, PhaseDefinition> PhaseChanged; // previous, current
public event Action Died;
StateMachine _outer;
PhaseDefinition _phase;
int _health;
bool _introDone;
bool _coupReady;
public TeamId Team => team;
public bool IsAlive => _health > 0;
public bool IsInvulnerable { get; set; }
public BossDefinition Definition => definition;
public PhaseDefinition Phase => _phase;
public Transform Target { get; set; }
public AttackDefinition CurrentAttack { get; set; }
void Start()
{
_health = definition.maxHealth;
BuildMachine();
if (definition.introTimeline != null && intro != null)
{
IsInvulnerable = true;
intro.PlayIntro(definition.introTimeline, OnIntroFinished);
}
else OnIntroFinished();
}
void OnIntroFinished()
{
_introDone = true;
IsInvulnerable = false;
EnterPhase(definition.phases[0], playSting: false);
}
void BuildMachine()
{
_outer = new StateMachine();
// Child states are constructed in the worked example; the outer graph only
// needs a Death state here plus whatever EnterPhase assigns with SetState.
_outer.AddAnyTransition(new BossDeathState(this), () => !IsAlive && _coupReady);
}
void Update()
{
if (_outer?.CurrentState == null) return;
_outer.Tick(Time.deltaTime);
}
public void ApplyPhase(PhaseDefinition phase)
{
if (agent != null) agent.speed = phase.moveSpeed;
hazards?.SetActiveHazards(phase.enableHazardIds, phase.disableHazardIds);
if (animator != null) animator.SetInteger("Phase", IndexOf(phase));
}
public void ApplyDamage(in DamageInfo info)
{
if (!IsAlive || IsInvulnerable) return;
if (_health - info.amount <= 0 && definition.coupDeGraceAttack != null
&& info.attack != definition.coupDeGraceAttack)
{
// Clamp to 1 HP until the designed finishing attack lands.
_health = 1;
_coupReady = true;
victimForward?.ApplyDamage(WithAmount(info, 0)); // feel only
return;
}
_health = Mathf.Max(0, _health - info.amount);
victimForward?.ApplyDamage(info);
if (_health <= 0)
{
_coupReady = true;
Died?.Invoke();
return;
}
TryAdvancePhase();
}
void TryAdvancePhase()
{
if (definition.phases == null || definition.phases.Length == 0) return;
float n = (float)_health / definition.maxHealth;
PhaseDefinition next = _phase;
for (int i = 0; i < definition.phases.Length; i++)
{
var p = definition.phases[i];
if (n <= p.enterAtHealthNormalized) next = p;
}
if (next != _phase) EnterPhase(next, playSting: true);
}
public void EnterPhase(PhaseDefinition phase, bool playSting)
{
var previous = _phase;
_phase = phase;
var child = BuildChildMachine(phase);
var state = new BossPhaseState(this, phase, child.initial);
// Copy transitions from the helper onto state.Child in the worked example.
_outer.SetState(state);
PhaseChanged?.Invoke(previous, phase);
if (playSting) intro?.PlaySting(phase.phaseEnterTimeline);
}
(IState initial, StateMachine unused) BuildChildMachine(PhaseDefinition phase)
{
var reposition = new BossRepositionState(this, phase);
var attack = new BossTelegraphAttackState(this, phase);
var recover = new BossRecoverState(this, phase);
var machine = new StateMachine();
machine.AddTransition(reposition, attack, () => InAttackRange());
machine.AddTransition(attack, recover, () => attack.Done);
machine.AddTransition(recover, reposition, () => recover.Done);
machine.SetState(reposition);
return (reposition, machine);
}
public bool InAttackRange()
{
if (Target == null || _phase == null) return false;
return Vector3.Distance(transform.position, Target.position) <= _phase.attackRange;
}
int IndexOf(PhaseDefinition phase)
{
if (definition.phases == null) return 0;
return Mathf.Max(0, Array.IndexOf(definition.phases, phase));
}
static DamageInfo WithAmount(in DamageInfo info, int amount)
{
var copy = info;
copy.amount = amount;
return copy;
}
}
```
`BuildChildMachine` as written builds a machine *and* a `BossPhaseState` that will build another child in `Enter`. That double-build is easy to mess up. The worked example (part 6) uses the cleaner form: construct children, hook transitions on `BossPhaseState.Child`, then `SetState`. Treat the snippet above as the *policy* (when to swap phases, how coup de grace works), not as the final wiring.
Policy worth keeping:
- **Invulnerable during intro.** The player should not cheese the cutscene.
- **Coup de grace.** If `coupDeGraceAttack` is set, health will not drop below 1 until that attack lands. That is Rogers' "let the player administer the coup de grace" as a rule, not a cutscene skip.
- **Phase change after damage is applied and the victim is still alive.** Do not start Phase 3 on the same frame as Death.
## 2.4 Child state sketches
Keep each child short. Telegraph details live in part 3.
```csharp
public sealed class BossRepositionState : IState
{
public string Name => "Reposition";
readonly BossBrain brain;
readonly PhaseDefinition phase;
public BossRepositionState(BossBrain brain, PhaseDefinition phase)
{ this.brain = brain; this.phase = phase; }
public void Enter() { /* agent.isStopped = false; destination = flank or close */ }
public void Tick(float dt) { /* face the target; retarget if needed */ }
public void Exit() { /* agent.isStopped = true if the attack is rooted */ }
}
public sealed class BossRecoverState : IState
{
public string Name => "Recover";
public bool Done { get; private set; }
readonly PhaseDefinition phase;
float _t;
public BossRecoverState(BossBrain brain, PhaseDefinition phase) { this.phase = phase; }
public void Enter() { Done = false; _t = 0.4f; }
public void Tick(float dt) { _t -= dt; if (_t <= 0f) Done = true; }
public void Exit() { }
}
```
`BossTelegraphAttackState` is the next part.
## 2.5 Exercise
On a whiteboard, draw the outer graph and one phase's child graph. If you need more than five child states, you are probably encoding the *move list* as states. Put the move list in `PhaseDefinition.attacks` and keep the child graph stable.
## Sources
- [[Enemy AI State Machine]] Step 6
- [[Game Programming Patterns (Review)]] - State