前言:本书的读法与贯穿项目《合成小岛》

这一页讲清楚三件事:三卷各自负责什么、贯穿项目《合成小岛》与工具链 pebble(config / sim / core / web)长什么样、以及代码块头部的层级注释和 verify 命令该怎么读。读完你能在本机把书里任意一段代码跑起来,并知道一个数字是从哪一层出来的。

这一页不教任何设计方法,它只解决一个工程问题:书里的代码怎么读、怎么跑,一个数字到底是从哪一层算出来的。读完整套书之前先花十分钟读这一页,后面每一节都会顺很多。

一、三卷各自负责什么

《小游戏开发实战》是一套书、三卷,卷与卷之间是接力关系,不是并列关系:

卷卷名负责交付物
卷一市场、品类与产品设计做什么、给谁做、值不值得投一份可评审的立项材料与设计案
卷二引擎实现与体验打磨怎么把它做成能跑的东西一套配置驱动的内核与可玩原型
卷三上线变现与长线运营怎么上线、怎么赚钱、怎么长线一份可交付的上线方案

卷一给你「设计」,卷二给你「实现」,卷三给你「运营」。 三卷共用同一个贯穿项目与同一套工具链,所以你从卷一跑到卷三,改的是同一份配置、同一批数字。你手上这一卷是卷一,它不写引擎 API、不写渲染管线、不写服务端——那些是卷二与站内既有专书的事。

卷一内部又分三段:第 1–5 章回答「市场与约束」,第 6–10 章回答「玩法与系统」,第 11–15 章回答「立项与交付」。三段可以顺序读,也可以按目录页 给出的三条路径跳读。

二、贯穿项目:《合成小岛》

这套书不讲抽象例子,它从头到尾只做一款游戏:《合成小岛》(代号 merge-island)。它是一款轻度小游戏,玩法由四个模块叠起来:

  • 合成:把两个低级物件拖到一起合成高级物件,这是核心循环。
  • 放置:离线也在产出,回来一次性领取,这是回归钩子。
  • 关卡:一局一局推进,给核心循环一个「进度感」。
  • 装扮:用产出的资源解锁岛屿装饰,这是长线目标与付费点。

选这四个模块不是随便挑的。它们正好覆盖了小游戏里最常见的四类设计决策:核心循环、离线收益、关卡难度、长线追求,而每一类在这套书里都有一个可跑的模型。你会发现第 6 章在拆合成循环,第 8 章在算离线收益,第 7 章在校准关卡难度,第 9–10 章在把装扮接到社交与变现上。

三、工具链:pebble

支撑这套书的是一个叫 pebble 的工作区,它有四个目录,各司其职:

pebble/
  config/    # 唯一真源:economy.json / ch00_budget.json / ch00_schema.json …
  sim/       # Python:读 config 跑数值仿真,产出 JSON + CSV + SVG
  core/      # TypeScript:读 config 跑逻辑内核,node --test 断言
  web/       # 单文件 HTML:把 core/ 内联成浏览器可玩原型

关键在于 config/ 是唯一真源。sim/、core/、web/ 三处都只读它,谁也不另立一套数字。这就带来本套的第一条主张——一份 config,三种消费:同一份 config/economy.json,Python 仿真、TypeScript 内核、浏览器原型算出的 5 级产出都是 20。改一个数,三处结果一起变。

第二条主张是 一份逻辑,两种宿主。游戏逻辑不直接调用任何平台 API,只依赖一层薄薄的 Host 抽象(时钟、存储、平台能力)。开发期用 mock 宿主跑单测与仿真,上线期换成真机宿主,同一段逻辑代码,结果逐帧一致。下面这段就是最小演示,P 层,node 直接跑:

// file: core/src/ch00_two_hosts.ts
// tier: P
// verify: node core/src/ch00_two_hosts.ts
//
// 一份逻辑,两种宿主:同一段 runFrames() 分别跑在 mock 宿主与注入的平台宿主上,
// 逻辑内核只依赖 Host 抽象,不直接碰 window / wx / tt / my。
// 约束:只用可擦除语法(禁 enum / namespace / 参数属性 / 装饰器)。

export interface Host {
  now(): number;
  storageGet(key: string): string | null;
  storageSet(key: string, value: string): void;
}

export interface Frame {
  readonly tickIndex: number;
  readonly tMs: number;
  readonly dtMs: number;
}

/** 逻辑内核:只读 Host,不关心宿主是真机还是 mock。 */
export function runFrames(host: Host, frames: number, dtMs: number): Frame[] {
  const out: Frame[] = [];
  let last = host.now();
  for (let i = 0; i < frames; i += 1) {
    const t = host.now();
    out.push({ tickIndex: i, tMs: t, dtMs: t - last });
    last = t;
  }
  void dtMs;
  return out;
}

/** mock 宿主:确定性时钟,每次 now() 前进固定步长,供单测与仿真使用。 */
export function mockHost(stepMs: number): Host {
  let t = 0;
  const store = new Map<string, string>();
  return {
    now: () => {
      t += stepMs;
      return t;
    },
    storageGet: (k) => store.get(k) ?? null,
    storageSet: (k, v) => {
      store.set(k, v);
    },
  };
}

/** 平台宿主:把任意平台对象(wx / tt / my / 浏览器)适配成 Host。 */
export function platformHost(platform: {
  now(): number;
  getItem(k: string): string | null;
  setItem(k: string, v: string): void;
}): Host {
  return {
    now: () => platform.now(),
    storageGet: (k) => platform.getItem(k),
    storageSet: (k, v) => platform.setItem(k, v),
  };
}

/** 演示用的内存平台对象:模拟一个真实宿主的外部时钟。 */
export function fakePlatform(stepMs: number) {
  let t = 0;
  const store = new Map<string, string>();
  return {
    now: () => {
      t += stepMs;
      return t;
    },
    getItem: (k: string) => store.get(k) ?? null,
    setItem: (k: string, v: string) => {
      store.set(k, v);
    },
  };
}

function main(): void {
  const FRAMES = 8;
  const STEP = 16;

  const mock = mockHost(STEP);
  const device = platformHost(fakePlatform(STEP));

  const a = runFrames(mock, FRAMES, STEP);
  const b = runFrames(device, FRAMES, STEP);

  mock.storageSet("save", "island-1");
  device.storageSet("save", "island-1");

  const idxEqual = a.every((f, i) => f.tickIndex === b[i].tickIndex && f.dtMs === b[i].dtMs);
  const saveEqual = mock.storageGet("save") === device.storageGet("save");

  const report = {
    frames: FRAMES,
    stepMs: STEP,
    mockTicks: a.map((f) => f.tickIndex),
    deviceTicks: b.map((f) => f.tickIndex),
    mockDt: a[0].dtMs,
    deviceDt: b[0].dtMs,
    idxEqual,
    saveEqual,
  };
  console.log(JSON.stringify(report, null, 2));
  if (!idxEqual || !saveEqual) throw new Error("两种宿主结果不一致");
  console.log("OK 一份逻辑两种宿主:逐帧一致");
}

main();

本机实跑输出(两种宿主逐帧一致,存储往返一致):

{
  "frames": 8,
  "stepMs": 16,
  "mockTicks": [0, 1, 2, 3, 4, 5, 6, 7],
  "deviceTicks": [0, 1, 2, 3, 4, 5, 6, 7],
  "mockDt": 16,
  "deviceDt": 16,
  "idxEqual": true,
  "saveEqual": true
}
OK 一份逻辑两种宿主:逐帧一致

这段代码有三个刻意的约束,后面每一章都会遵守:只依赖 Host 抽象、只用可擦除语法(Node 26 直接跑 TS,不支持 enum / namespace / 装饰器)、逻辑里没有 Math.random()(随机数一律用配置里的 seed 加自实现的整数 LCG/PCG,确定性回放才能成立)。

四、六档可运行性怎么读

每个代码块都属于六档之一,写在块头的 tier: 注释里。读到不同档,你要做的动作不同:

档含义你要做的动作
SPython 数值仿真/opt/homebrew/bin/python3 sim/<name>.py --config config/ --out out/,看 JSON + CSV + SVG
PTypeScript 逻辑内核node --test 'core/test/*.test.ts',或直接 node core/src/<name>.ts
W单文件 HTML 原型NODE_PATH=/opt/homebrew/lib/node_modules node web/verify.mjs web/dist/index.html
D静态表 / schema / 清单只做 schema / lint 校验,不产出数字
X不可复现只读,不要试图跑:真实后台数据、真实榜单、真实投放
B基准看 JSON,软告警不硬失败,用来观察而不是卡阈值

一条纪律:看到 X 档就当它是引用,不是结论。正文里每个 X 档都会写明「需真实业务环境,命令行不可复现」,并且只把真实平台当作「公开可观察机制」引用并标注观察时点,不会写成「实测」。

五、代码块头注释怎么读

有注释的语言(Python / TypeScript)用行注释,无注释的语言(JSON / YAML / CSV)用块前的 HTML 注释。三种头注释的含义:

# file:   → 还原路径(相对 pebble/)。带 file: 的块一定是完整可运行文件。
# tier:   → S | P | W | D | X | B,缺省即报错。
# verify: → S / P / W / B 块必填,就是把它跑起来的那条命令。

无注释语言长这样:

看到 file: 就意味着这个块和 pebble/ 里的真实文件逐字节一致——你可以把它整段贴回 pebble/<file> 覆盖,也可以直接去工作区读那个文件。纯展示片段则用 snippet:,不给 tier,也不要求能跑。

六、怎么跟着跑

三步,不需要安装任何东西(本机已实测的环境见附录 A ):

# snippet: 跟着跑的三步
# 1) 环境自检(S 层,确认 python3.14 + numpy/pandas 与 node 26 都在)
/opt/homebrew/bin/python3 sim/ch00_env_check.py

# 2) 跑 S 层仿真,产出 JSON + CSV + SVG 到 out/
/opt/homebrew/bin/python3 sim/ch00_constraint_budget.py --config config/ --out out/

# 3) 跑 P 层内核断言
node --test 'core/test/*.test.ts'

一个容易踩的坑:node --test 传目录不发现 .ts 文件,必须用 glob 或显式文件名——node --test 'core/test/*.test.ts'。这是实测结论,见附录 A 。

七、读完这一页你应该知道的三件事

  1. 卷一做设计,卷二做实现,卷三做运营,三卷共用《合成小岛》与 pebble。
  2. 每个数字都来自某一层的实跑:S 层仿真、P 层内核、W 层原型,或 B 层基准;X 层只引用不实跑。
  3. 改一个配置,三处一起变——这是「一份 config 三种消费」的承诺,也是这套书敢把设计写成可复算过程的地基。

准备好了,从第 1 章 1.1 小游戏的定义与边界 开始,或者回到目录 挑一条路径。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「minigame」更多文章

  1. 附录 F:全书公式、参数与数据集索引
  2. 9.3 同意管理与数据出境
  3. 9.2 COPPA 与儿童向游戏的限制