游戏编程模式04-设计模式-观察者模式

设计模式-观察者模式

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

脑内画面

观察者模式让一个系统宣布"发生了某件事",而不需要知道谁在乎这件事。它像游戏世界里的公告牌:物理系统贴出"角色开始坠落",成就、音效、教程系统各自来看,公告牌不认识它们。
音频系统 成就系统 事件发布点 物理系统 音频系统 成就系统 事件发布点 物理系统 #mermaid-svg-gnFEibEllrnr7frR{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-gnFEibEllrnr7frR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gnFEibEllrnr7frR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gnFEibEllrnr7frR .error-icon{fill:#552222;}#mermaid-svg-gnFEibEllrnr7frR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gnFEibEllrnr7frR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gnFEibEllrnr7frR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gnFEibEllrnr7frR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gnFEibEllrnr7frR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gnFEibEllrnr7frR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gnFEibEllrnr7frR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gnFEibEllrnr7frR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gnFEibEllrnr7frR .marker.cross{stroke:#333333;}#mermaid-svg-gnFEibEllrnr7frR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gnFEibEllrnr7frR p{margin:0;}#mermaid-svg-gnFEibEllrnr7frR .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gnFEibEllrnr7frR text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-gnFEibEllrnr7frR .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gnFEibEllrnr7frR .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-gnFEibEllrnr7frR .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-gnFEibEllrnr7frR .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-gnFEibEllrnr7frR #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-gnFEibEllrnr7frR .sequenceNumber{fill:white;}#mermaid-svg-gnFEibEllrnr7frR #sequencenumber{fill:#333;}#mermaid-svg-gnFEibEllrnr7frR #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-gnFEibEllrnr7frR .messageText{fill:#333;stroke:none;}#mermaid-svg-gnFEibEllrnr7frR .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gnFEibEllrnr7frR .labelText,#mermaid-svg-gnFEibEllrnr7frR .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-gnFEibEllrnr7frR .loopText,#mermaid-svg-gnFEibEllrnr7frR .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-gnFEibEllrnr7frR .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gnFEibEllrnr7frR .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-gnFEibEllrnr7frR .noteText,#mermaid-svg-gnFEibEllrnr7frR .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-gnFEibEllrnr7frR .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gnFEibEllrnr7frR .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gnFEibEllrnr7frR .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gnFEibEllrnr7frR .actorPopupMenu{position:absolute;}#mermaid-svg-gnFEibEllrnr7frR .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-gnFEibEllrnr7frR .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gnFEibEllrnr7frR .actor-man circle,#mermaid-svg-gnFEibEllrnr7frR line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-gnFEibEllrnr7frR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Notify(EntityFell) OnNotify(EntityFell) OnNotify(EntityFell)

它解决的问题

如果物理代码里直接写 Achievements.Unlock(...),物理系统就知道了成就系统的存在。观察者模式把这种依赖反过来:成就系统主动订阅物理事件,物理系统只负责通知。

C# 示例

csharp 复制代码
public sealed class Subject<TEvent>
{
    private readonly List<Action<TEvent>> _listeners = new();

    public IDisposable Subscribe(Action<TEvent> listener)
    {
        _listeners.Add(listener);
        return new Subscription(() => _listeners.Remove(listener));
    }

    public void Notify(TEvent gameEvent)
    {
        foreach (var listener in _listeners.ToArray())
        {
            listener(gameEvent);
        }
    }

    private sealed class Subscription : IDisposable
    {
        private Action? _dispose;

        public Subscription(Action dispose)
        {
            _dispose = dispose;
        }

        public void Dispose()
        {
            _dispose?.Invoke();
            _dispose = null;
        }
    }
}

public readonly record struct EntityFell(int EntityId, bool IsHero);

public sealed class PhysicsSystem
{
    public Subject<EntityFell> EntityFell { get; } = new();

    public void SimulateFall(int entityId, bool isHero)
    {
        EntityFell.Notify(new EntityFell(entityId, isHero));
    }
}

public sealed class AchievementSystem
{
    private IDisposable? _subscription;

    public void Connect(PhysicsSystem physics)
    {
        _subscription = physics.EntityFell.Subscribe(OnEntityFell);
    }

    private void OnEntityFell(EntityFell e)
    {
        if (e.IsHero)
        {
            Unlock("FellFromBridge");
        }
    }

    private void Unlock(string id)
    {
        Console.WriteLine($"Achievement unlocked: {id}");
    }
}

什么时候用

  • 一个系统需要广播状态变化,但不想依赖具体接收者。
  • 事件必须同步处理,并且处理成本可控。
  • 订阅者数量不固定,例如成就、统计、音效、教程。

使用时的锋利边

观察者默认是同步调用。发布者会等所有观察者执行完才继续,所以观察者里不要做加载资源、网络请求、复杂寻路这类重操作。需要异步、跨线程、延迟处理时,优先考虑事件队列。

订阅也要有退出机制。C# 的事件和委托很容易因为忘记取消订阅导致对象无法被回收。