游戏编程模式07-设计模式-状态模式

设计模式-状态模式

参考章节:https://gpp.tkchu.me/state.html

脑内画面

状态模式把"对象当前处于什么状态,以及在该状态下如何响应输入"封装到状态对象里。它像角色动作图:站立、跳跃、下蹲、俯冲是一个个房间,输入是门,角色一次只能站在一个房间里。
#mermaid-svg-fNVDz3qEvSayLMQq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fNVDz3qEvSayLMQq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fNVDz3qEvSayLMQq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fNVDz3qEvSayLMQq .error-icon{fill:#552222;}#mermaid-svg-fNVDz3qEvSayLMQq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fNVDz3qEvSayLMQq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fNVDz3qEvSayLMQq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fNVDz3qEvSayLMQq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fNVDz3qEvSayLMQq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fNVDz3qEvSayLMQq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fNVDz3qEvSayLMQq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fNVDz3qEvSayLMQq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fNVDz3qEvSayLMQq .marker.cross{stroke:#333333;}#mermaid-svg-fNVDz3qEvSayLMQq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fNVDz3qEvSayLMQq p{margin:0;}#mermaid-svg-fNVDz3qEvSayLMQq defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-fNVDz3qEvSayLMQq g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-fNVDz3qEvSayLMQq g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-fNVDz3qEvSayLMQq g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-fNVDz3qEvSayLMQq g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-fNVDz3qEvSayLMQq g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-fNVDz3qEvSayLMQq .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-fNVDz3qEvSayLMQq .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-fNVDz3qEvSayLMQq .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-fNVDz3qEvSayLMQq .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fNVDz3qEvSayLMQq .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-fNVDz3qEvSayLMQq .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-fNVDz3qEvSayLMQq .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-fNVDz3qEvSayLMQq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fNVDz3qEvSayLMQq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fNVDz3qEvSayLMQq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fNVDz3qEvSayLMQq .edgeLabel .label text{fill:#333;}#mermaid-svg-fNVDz3qEvSayLMQq .label div .edgeLabel{color:#333;}#mermaid-svg-fNVDz3qEvSayLMQq .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-fNVDz3qEvSayLMQq .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-fNVDz3qEvSayLMQq .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-fNVDz3qEvSayLMQq .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-fNVDz3qEvSayLMQq .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-fNVDz3qEvSayLMQq .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fNVDz3qEvSayLMQq .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fNVDz3qEvSayLMQq #statediagram-barbEnd{fill:#333333;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fNVDz3qEvSayLMQq .cluster-label,#mermaid-svg-fNVDz3qEvSayLMQq .nodeLabel{color:#131300;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-fNVDz3qEvSayLMQq .note-edge{stroke-dasharray:5;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-note text{fill:black;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram-note .nodeLabel{color:black;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagram .edgeLabel{color:red;}#mermaid-svg-fNVDz3qEvSayLMQq #dependencyStart,#mermaid-svg-fNVDz3qEvSayLMQq #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-fNVDz3qEvSayLMQq .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fNVDz3qEvSayLMQq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PressJump
PressDown
ReleaseDown
PressDown
Land
Land
Standing
Jumping
Ducking
Diving

它解决的问题

角色逻辑最容易长出一堆互相排斥的布尔值:isJumpingisDuckingisDiving。状态模式把"不可能同时成立"的布尔组合压缩成"当前状态只有一个",并让每个状态管理自己的输入、更新、进入和退出逻辑。

C# 示例

csharp 复制代码
public enum HeroInput
{
    PressJump,
    PressDown,
    ReleaseDown,
    Land
}

public interface IHeroState
{
    void Enter(Hero hero);
    void HandleInput(Hero hero, HeroInput input);
    void Update(Hero hero, float dt);
    void Exit(Hero hero);
}

public sealed class Hero
{
    private IHeroState _state = new StandingState();

    public void ChangeState(IHeroState next)
    {
        _state.Exit(this);
        _state = next;
        _state.Enter(this);
    }

    public void HandleInput(HeroInput input) => _state.HandleInput(this, input);
    public void Update(float dt) => _state.Update(this, dt);

    public void SetAnimation(string name) => Console.WriteLine($"Anim: {name}");
    public void Jump() => Console.WriteLine("Jump");
    public void Dive() => Console.WriteLine("Dive");
}

public sealed class StandingState : IHeroState
{
    public void Enter(Hero hero) => hero.SetAnimation("stand");
    public void Exit(Hero hero) { }
    public void Update(Hero hero, float dt) { }

    public void HandleInput(Hero hero, HeroInput input)
    {
        if (input == HeroInput.PressJump)
        {
            hero.ChangeState(new JumpingState());
        }
        else if (input == HeroInput.PressDown)
        {
            hero.ChangeState(new DuckingState());
        }
    }
}

public sealed class JumpingState : IHeroState
{
    public void Enter(Hero hero)
    {
        hero.SetAnimation("jump");
        hero.Jump();
    }

    public void Exit(Hero hero) { }
    public void Update(Hero hero, float dt) { }

    public void HandleInput(Hero hero, HeroInput input)
    {
        if (input == HeroInput.PressDown)
        {
            hero.ChangeState(new DivingState());
        }
        else if (input == HeroInput.Land)
        {
            hero.ChangeState(new StandingState());
        }
    }
}

public sealed class DuckingState : IHeroState
{
    private float _chargeTime;

    public void Enter(Hero hero)
    {
        _chargeTime = 0f;
        hero.SetAnimation("duck");
    }

    public void Exit(Hero hero) { }

    public void Update(Hero hero, float dt)
    {
        _chargeTime += dt;
    }

    public void HandleInput(Hero hero, HeroInput input)
    {
        if (input == HeroInput.ReleaseDown)
        {
            hero.ChangeState(new StandingState());
        }
    }
}

public sealed class DivingState : IHeroState
{
    public void Enter(Hero hero)
    {
        hero.SetAnimation("dive");
        hero.Dive();
    }

    public void Exit(Hero hero) { }
    public void Update(Hero hero, float dt) { }

    public void HandleInput(Hero hero, HeroInput input)
    {
        if (input == HeroInput.Land)
        {
            hero.ChangeState(new StandingState());
        }
    }
}

什么时候用

  • 对象行为强烈依赖"当前状态"。
  • 状态之间有明确转移规则。
  • 布尔标记组合开始产生非法状态。
  • 每个状态有自己的进入、退出、更新逻辑。

使用时的锋利边

状态对象太细会造成类爆炸。简单状态机可以先用 enum + switch,当状态拥有自己的数据、生命周期或可复用逻辑时,再升级成状态对象。