12. Subclass Sandbox

12. Subclass Sandbox:由基类提供受保护原语,让子类在有限能力内组合行为,通过可复位因果实验和反例证据验收。

12. Subclass Sandbox

学习目标

  • 能说明沙箱基类的作用
  • 能解释引擎改动如何被隔离
  • 能选择操作粒度与注入方式

为什么"12. Subclass Sandbox"从问题证据开始

你的游戏有几十种"超能力":火球、冰霜、治疗、传送……每种能力都要调用引擎的音频、粒子、伤害系统。最直觉的实现是让每个能力类直接调用引擎 API:AudioSystem::play("fire")ParticleSystem::spawn("flame")。代码今天能跑,但每次引擎改 API(比如音频系统重构),几十个能力类全部要跟着改——改动波及面巨大,而且漏改一个就是运行时崩溃。

本页把"无模式基线"定义为"子类直接调用引擎 API"的代码,然后引入沙箱基类作为候选机制,验证它是否真的让"引擎改动只波及一处"。通过条件是引擎 API 变化时只需改基类一个文件——而不是每个能力类。

🔮 先预测:引擎音频 API 改名,沙箱方案要改几个文件?

来源、版本与独立重写边界

本页用作者完整在线正文核对正式标题、设计分叉和时代语境,并以作者源码仓库交叉检查结构。仓库许可证明确正文、HTML与样式为 CC BY-NC-ND 4.0,示例程序等其他文件为 MIT;因此下列中文解释、图示、交互和代码均为独立教学重写,不翻译、拼接或改写受 ND 限制的原文表达。

本章机制与术语

理解 Subclass Sandbox 需要四个核心概念:

  • :提供一组受保护操作(playSound/spawnParticle/damage)的基类——子类唯一能碰引擎的通道。
  • :继承基类、实现具体行为(activate())的能力类——只调用基类提供的操作。
  • :基类封装引擎调用的方法——子类调用它们,但不直接碰引擎。
  • :子类把多个沙箱操作组合成完整效果——行为 = 沙箱操作的编排。

四个概念共同约束"由基类提供受保护原语,让子类在有限能力内组合行为":任何结论都必须回到"引擎改动波及范围、子类对引擎的耦合度、行为组合自由度"三个可观察量。

Behavioral Pattern · Subclass Sandbox

Subclass Sandbox — 子类只会在沙箱里玩

▷ 可交互
引擎把"能做的事"收进沙箱基类,子类只负责组合子类不碰引擎 API——安全边界 + 解耦一次到位Superpower(沙箱基类)protected: 可调操作playSound()spawnParticle()damageTarget()abstract activate()FireballPower(子类)override activate()playSound("fire")spawnParticle("flame")damageTarget(25)引擎power.activate()子类只碰沙箱改引擎 API 只需改沙箱基类一处——所有子类自动适配,不会漏改

第 1 / 4 步 · ① Sandbox 基类:提供受保护操作(playSound/spawnParticle/damageTarget)

子类能做的所有事都来自沙箱——引擎改动被隔离在基类,子类零波及。

💡 对着动画看:动画是三栏结构——左侧 Superpower 沙箱基类(protected 操作清单),中间 FireballPower 子类(override activate() 里组合 playSound/spawnParticle/damageTarget),右侧引擎。注意子类到引擎之间没有直接连线——它只认识沙箱。读"引擎改动如何隔离"时回到这张图:改引擎只动基类那一栏。

官方结构逐项深读

12. Subclass Sandbox

Subclass Sandbox 的主张:把子类对引擎的直接依赖,收进一个沙箱基类。基类提供受保护的操作方法(内部调用引擎 API),子类只调用这些方法组合行为。这样子类不认识引擎——它认识的是"能播放声音、能生成粒子、能造成伤害"这套抽象能力。引擎内部怎么实现(音频用 OpenAL 还是 DirectSound)与子类无关。

Intent

意图:解耦"行为定义"(子类)与"引擎能力"(底层 API)。子类专心定义"火球术做什么",基类专心封装"引擎能做什么"。引擎改版时,只有基类需要适配;子类代码一行不动。这也是"稳定抽象层"思想的体现:沙箱是行为与引擎之间的稳定接口。

Motivation

回到超能力系统的现实:火球术要播音效、生成粒子、对目标造成伤害。如果三个操作直接散落在 FireballPower 里调用引擎 API,那么引擎音频系统一重构,FireballPower、IcePower、TeleportPower……所有能力类都要改。动机就是消灭这种"引擎改动波及所有行为"的脆弱性——把变化收敛到基类。

The Pattern

模式结构:抽象基类 Superpower(protected 方法 playSound/spawnParticle/damageTarget + 抽象方法 activate)→ 具体子类 FireballPower(实现 activate,调用基类方法)→ 引擎(创建能力实例并调用 activate())。子类永远不直接调用引擎 API——所有能力通过基类的受保护方法间接获得。基类是唯一认识引擎的类。

When to Use It

何时用沙箱:有一族行为类(技能、AI 状态、任务)共享同一批引擎能力,且引擎 API 频繁变动。何时不用:行为只有一两个类(没必要建抽象层)、行为需要直接操作引擎细粒度接口(沙箱太粗)。判断标准:"改动引擎会波及多少行为类"——波及面大就用沙箱收敛。

Keep in Mind

沙箱的代价:间接层(调用多一层)、基类膨胀(所有能力要的操作都堆进基类)、能力受限(子类只能做基类允许的事——这既是安全也是限制)。作者强调:沙箱是"为了解耦而增加的一层"——当引擎真的很稳定、行为真的很少时,这层就是浪费。

Sample Code

示例代码:Superpower 基类持引擎引用(构造时注入),protected 方法内部调用引擎;FireballPower::activate() 组合三个操作。核心结构:class Superpower { protected: void playSound(id); void spawnParticle(id); void damage(target, amount); }; class FireballPower : Superpower { void activate() override; };——子类看不到引擎,只看到能力。

Design Decisions

三个关键设计决策:提供什么操作(粗粒度 vs 细粒度)、方法直接提供还是通过对象提供(基类方法 vs 能力对象)、基类怎么拿状态(构造注入 vs 全局获取)。前两者决定沙箱的"边界形状",后者决定基类与引擎的耦合方式——下面三节展开。

What operations should be provided?

操作粒度:粗粒度操作(playSound(id)——一条命令封装音效系统调用,子类好写、引擎好改)与细粒度操作(openAudioChannel/loadSample/play——子类灵活但直接暴露引擎结构)。作者建议:从粗粒度起步——沙箱的意义就是让子类简单,细粒度操作会把它拉回"子类碰引擎"的老路。需要更细控制时,再向基类加粗粒度方法。

Should methods be provided directly, or through objects that contain them?

两种提供方式:直接方法playSound(id) 是基类方法——简单直接,但所有操作挤在基类,类会膨胀)与通过能力对象(基类持有 Audio 对象,子类调 audio->play(id)——把操作分组到对象,基类瘦身,子类语义更清晰:audio.play() 明确是音频能力)。作者建议:操作多了以后用能力对象分组(audio/particle/combat 各一对象),基类只做装配。

How does the base class get the state that it needs?

基类拿引擎状态的方式:构造注入(Superpower 构造时接收引擎引用——显式、可测,但每个能力创建时都要传)与全局获取(基类内部用 Service Locator 或单例拿引擎——方便,但隐藏依赖、难测试)。作者建议:构造注入优先——子类创建时显式传入所需系统,测试时容易替换替身;全局获取是便利但耦合的退路。

See Also

  • Template Method:沙箱的基类定义操作骨架、子类实现步骤——本质是模板方法的"行为组合"用法。
  • Strategy:策略模式用组合替换行为,沙箱用继承提供能力——一个选行为,一个给能力。
  • Component:组件模式把能力拆到组件,与沙箱的"能力收进基类"互补——超复杂实体可用组件,简单行为族用沙箱。

可迁移实现或计算骨架

initial state -> 基类封装引擎操作(protected)-> 子类组合操作实现行为 -> 引擎调用 activate()
fault injection -> 子类直接调用引擎 API
pass condition -> 引擎 API 改动只波及基类一个文件
reset -> initial state

对应实现要点:基类构造注入引擎引用;protected 方法粗粒度封装引擎调用;子类只调基类方法;操作多时用能力对象分组。该骨架只保存实验合同;真实项目还要固定能力清单、注入路径与测试替身,并保留基线实现以便回退。

本章练习与节点验证矩阵

练习

问题 1:波及面对比。

操作:模拟引擎音频 API 改名,统计基线与沙箱方案各要改几个文件

问题 2:能力边界。

操作:在动画沙箱中"新增"一个"传送"能力,只用现有操作能否表达

问题 3:注入 vs 全局。

操作:分别用构造注入与 Service Locator 实现基类拿引擎,各写一个测试

术语复核

名词解释

本章出现的专业名词,用大白话再讲一遍。

沙箱基类
受保护操作
组合

练习答案参考

练习判据即答案:每个练习的"验证判据"列给出了通过标准——先自己动手,再对照判据核验。

术语复核与本章回顾

掌握"12. Subclass Sandbox"意味着能从"子类直接调用引擎 API 让引擎改动波及所有行为"出发,解释沙箱基类、子类、受保护操作与组合四者的关系,再用"引擎改动波及范围、子类对引擎的耦合度、行为组合自由度"三个可观察量推翻或保留实现。若三个判据不能同时满足,本章仍未通过。

一句话回顾:引擎把"能做的事"全收进沙箱基类,子类只会组合这些能力——引擎改动被基类一堵墙挡住,行为类从此稳如泰山。

阅读导航

← 上一页:11. Bytecode · 下一页:13. Type Object → ← 上一页:11. Bytecode · 下一页:13. Type Object →

资料与写作方式声明

本章以Robert Nystrom《Game Programming Patterns》(游戏编程模式)权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…