卷二从这里开始。卷一把《合成小岛》从一个想法写成了设计案——品类、平台约束、玩法闭环、变现结构都算过账;卷二要把它落成能跑的代码。落成的第一步不是写代码,而是选一条技术路线:用哪个引擎、还是不用引擎。本节只解决这一件事——把「小游戏引擎」这个词从一个模糊的品牌名,变成一张能被程序读进来做统计、打分、画图的格局表。它不写任何引擎 API(组件怎么写、生命周期怎么挂),那是引擎专书的活。
先说分工,三处都要写清。本站有一篇小程序小游戏开发综述 ,那是覆盖引擎选型、性能、变现的单篇综述;本节给的是可跑的能力覆盖统计,第 1.2 节再给可跑的加权评分与敏感度模型——综述告诉你「有哪些引擎」,这里告诉你「把它们的差异算成可比较的数字」。本站还有 Phaser 工程实操系列 共 127 篇,那是在 Phaser 里写游戏的工程书;本节只在选型层面横向对比 Phaser,不重复它的 API 教程。本站另有一篇 Defold 引擎入门 ,那是单一引擎的入门;本节给的是跨引擎的选型方法——怎么把八个引擎放在同一张表里比。三条内容可以并读,但口径不同:一个是单篇综述、一个是单引擎工程书、一个是单引擎入门,本节是跨引擎的选型方法论。
引擎、框架、渲染库、无引擎:先画一条谱系
「小游戏引擎」这个词经常被用得太宽。团队说「我们用引擎」,可能指三件完全不同的事:一套带编辑器的完整游戏引擎,一个只提供运行时的 2D 框架,或者一个只负责把画面画出来的渲染库。把它们混为一谈,选型讨论就会鸡同鸭讲。
本书把这条谱系切成四段:
- 完整引擎(full-engine):自带可视化编辑器、场景 / 组件系统、资源管线,通常还带物理、音频、动画。你搭积木,引擎给你地基、墙体、水电。代表是 Cocos Creator、LayaAir、Egret、Defold、Unity WebGL。
- 框架(framework):提供运行时(渲染循环、输入、物理),但没有官方编辑器与资源管线,工程结构由你自己定。代表是 Phaser。
- 渲染库(render-lib):只负责「把一批显示对象画到屏幕上」,物理、音频、资源、平台适配全都要自建。代表是 PixiJS。
- 无引擎(no-engine):连渲染库都不用,直接调平台的 Canvas / WebGL API。包体最小,代价是每一层都要自己写。代表是原生小程序 Canvas 路线。
这条谱系的关键性质是:从右往左,包体与首包越来越小,但你自己的代码量越来越大。选型从来不是「哪个引擎好」,而是「你愿意用多少自研成本,换多少包体与启动速度」。这就是为什么本节要先把格局画成一张表——只有把八条路线放在同一组坐标上,后面的评分与分界分析才有意义。
把格局写成一张可被程序消费的表(D 层)
下面这段是 D 层静态配置,即引擎格局对照表本身。它记录八个引擎的形态、脚本语言、是否开源,以及每一条能力轴是否开箱内置;同时附带第 1.2 节要用的八条选择维度与一组整数权重。要点:引擎能力是公开可观察口径的行业归纳,观察时点 2026-10,不是任何平台或引擎后台的实测;权重是设计假设,合计 100;所有数字都是整数,无浮点。
{
"version": 1,
"series": "《小游戏开发实战》",
"volume": "卷二《引擎实现与体验打磨》",
"targetGame": "merge-island",
"observedAt": "2026-10",
"note": "小游戏引擎 / 框架格局对照表。引擎形态、语言、开源与能力覆盖为【公开可观察】口径的行业归纳,观察时点 2026-10,不是任何平台或引擎后台实测;选择维度权重是整数百分比,合计 100,属【设计假设】。",
"units": { "weight": "整数百分比,合计 100", "score": "0–100 整数有利度分", "capability": "是否开箱内置(true/false)" },
"capabilityAxes": [
{ "id": "editor", "cn": "可视化编辑器" },
{ "id": "physics", "cn": "内置物理" },
{ "id": "animation", "cn": "动画 / 时间轴" },
{ "id": "audio", "cn": "音频系统" },
{ "id": "assetPipeline", "cn": "资源 / 图集管线" },
{ "id": "hotUpdate", "cn": "热更新支持" },
{ "id": "tsSupport", "cn": "TypeScript 一等支持" },
{ "id": "miniGameAdapter", "cn": "小游戏平台适配层" }
],
"engines": [
{ "id": "cocos", "cn": "Cocos Creator", "kind": "full-engine", "lang": "TypeScript", "openSource": true, "caps": ["editor", "physics", "animation", "audio", "assetPipeline", "hotUpdate", "tsSupport", "miniGameAdapter"], "note": "组件化全功能引擎,编辑器与平台适配成熟,当前小游戏主流选择之一。" },
{ "id": "laya", "cn": "LayaAir", "kind": "full-engine", "lang": "TypeScript", "openSource": true, "caps": ["editor", "physics", "animation", "audio", "assetPipeline", "hotUpdate", "tsSupport", "miniGameAdapter"], "note": "同样主打小游戏,强调小包体与运行性能,官方适配层完整。" },
{ "id": "egret", "cn": "Egret", "kind": "full-engine", "lang": "TypeScript", "openSource": true, "caps": ["editor", "animation", "audio", "assetPipeline", "hotUpdate", "tsSupport", "miniGameAdapter"], "note": "老牌 H5 引擎,生态活跃度较巅峰期回落,内置物理缺失。" },
{ "id": "defold", "cn": "Defold", "kind": "full-engine", "lang": "Lua", "openSource": true, "caps": ["editor", "physics", "animation", "audio", "assetPipeline", "miniGameAdapter"], "note": "轻量引擎,包体小、启动快,脚本语言是 Lua,无内置热更新与 TS 支持。" },
{ "id": "phaser", "cn": "Phaser", "kind": "framework", "lang": "TypeScript", "openSource": true, "caps": ["physics", "animation", "audio", "tsSupport"], "note": "H5 2D 框架,无官方编辑器与资源管线,小游戏适配需自行搭建。" },
{ "id": "pixi", "cn": "PixiJS", "kind": "render-lib", "lang": "TypeScript", "openSource": true, "caps": ["tsSupport"], "note": "纯渲染库,只负责把画面画出来,物理 / 音频 / 资源 / 适配都要自建。" },
{ "id": "unity-webgl", "cn": "Unity WebGL / 团结引擎", "kind": "full-engine", "lang": "C#", "openSource": false, "caps": ["editor", "physics", "animation", "audio", "assetPipeline"], "note": "重型引擎,包体与首包对小游戏极不友好,无官方小游戏适配层。" },
{ "id": "native", "cn": "原生小程序 Canvas API", "kind": "no-engine", "lang": "JavaScript", "openSource": true, "caps": [], "note": "不用引擎,直接调平台 Canvas / WebGL API,包体最小但一切自建。" }
],
"dimensions": [
{ "id": "packageSize", "cn": "包体可控度", "direction": "higher_better", "desc": "越高表示基线包体越小、越容易塞进主包限额" },
{ "id": "firstLoad", "cn": "首包 / 冷启动", "direction": "higher_better", "desc": "越高表示冷启动越快、首屏越早可交互" },
{ "id": "runtimePerf", "cn": "运行性能", "direction": "higher_better", "desc": "越高表示同机帧率与内存表现越可控" },
{ "id": "ecosystem", "cn": "生态与文档", "direction": "higher_better", "desc": "越高表示文档、插件、社区问答越丰富" },
{ "id": "learningCost", "cn": "学习成本可控度", "direction": "higher_better", "desc": "越高表示上手越快、团队迁移成本越低" },
{ "id": "platformFit", "cn": "平台适配度", "direction": "higher_better", "desc": "越高表示对小游戏平台 API 与审核的适配越省事" },
{ "id": "iterationSpeed", "cn": "迭代效率", "direction": "higher_better", "desc": "越高表示改一处、看一次、发一版越快" },
{ "id": "longTermSupport", "cn": "长期维护", "direction": "higher_better", "desc": "越高表示版本维护与向后兼容越有保障" }
],
"weights": { "packageSize": 18, "firstLoad": 14, "runtimePerf": 16, "ecosystem": 15, "learningCost": 13, "platformFit": 12, "iterationSpeed": 7, "longTermSupport": 5 }
}
表把「格局」和「评价」分开了:engines 描述每个引擎是什么(形态、语言、开源、能力),dimensions 与 weights 描述怎么比(第 1.2 节用)。分开的好处是,改引擎清单不需要动权重,调权重也不需要动引擎清单。注意 native 的 caps 是空数组——这不是遗漏,而是这条路线最本质的特征:没有任何一条能力轴是平台白送的。
读表统计:能力维度覆盖(S 层)
下面这段是 S 层仿真脚本,读 config/e01_engines.json,对八个引擎与八条能力轴做交叉统计:每个引擎开箱内置了几条能力轴、每条能力轴被多少个引擎内置,并产出 JSON 与一张纯 Python SVG 条形图。它回答的问题是「这条谱系上的能力覆盖是怎么衰减的」。
#!/usr/bin/env python3
# file: sim/e01_landscape.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/e01_landscape.py --config config/ --out out/
"""引擎格局的能力维度覆盖统计:读 config/e01_engines.json(引擎 / 能力轴 / 维度 / 权重)。
对 8 个引擎与 8 条能力轴做交叉统计,产出:
- byKind / byLang / byOpenSource:引擎形态 / 语言 / 开源的计数分布;
- perEngine:每个引擎覆盖了多少条能力轴(coverage),并列出缺失的能力;
- perAxis:每条能力轴被多少个引擎开箱内置(providerCount);
- coverageByKind:按引擎形态看平均覆盖度(整数地板除)。
产出 JSON + SVG(各引擎能力覆盖数条形图)。引擎能力为【公开可观察】口径,观察时点 2026-10。
纯标准库 + xml.etree 手写 SVG,零依赖;全整数,无浮点。
"""
from __future__ import annotations
import argparse
import json
import os
import xml.etree.ElementTree as ET
def load(path: str) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)
def count_by(items: list[dict], key: str) -> dict:
out: dict[str, int] = {}
for it in items:
out[it[key]] = out.get(it[key], 0) + 1
return out
def svg(path: str, order: list[str], cov: dict, cn: dict, total_axes: int) -> int:
W, H, PAD, gap = 640, 420, 150, 12
n = len(order)
plot_w = W - PAD - 60
plot_h = H - 2 * 40
bar_h = (plot_h - gap * (n + 1)) / n
vmax = total_axes or 1
root = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
"height": str(H), "viewBox": f"0 0 {W} {H}"})
ET.SubElement(root, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
ET.SubElement(root, "text", {"x": "40", "y": "26", "font-size": "15",
"font-family": "sans-serif"}).text = (
f"各引擎开箱内置的能力轴数量(共 {total_axes} 条)")
for i, eid in enumerate(order):
y = 40 + gap + i * (bar_h + gap)
w = cov[eid] / vmax * plot_w
ET.SubElement(root, "rect", {"x": str(PAD), "y": f"{y:.1f}",
"width": f"{max(w, 1):.1f}", "height": f"{bar_h:.1f}",
"fill": "#0ea5e9"})
ET.SubElement(root, "text", {"x": str(PAD - 8), "y": f"{y + bar_h / 2 + 4:.1f}",
"font-size": "12", "text-anchor": "end",
"font-family": "sans-serif"}).text = cn[eid]
ET.SubElement(root, "text", {"x": f"{PAD + w + 8:.1f}", "y": f"{y + bar_h / 2 + 4:.1f}",
"font-size": "12", "font-family": "sans-serif"}).text = str(cov[eid])
ET.ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
return os.path.getsize(path)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
os.makedirs(args.out, exist_ok=True)
cfg = load(os.path.join(args.config, "e01_engines.json"))
engines = cfg["engines"]
axes = [a["id"] for a in cfg["capabilityAxes"]]
cn = {e["id"]: e["cn"] for e in engines}
per_engine, cov = {}, {}
for e in engines:
have = [a for a in axes if a in e["caps"]]
miss = [a for a in axes if a not in e["caps"]]
per_engine[e["id"]] = {"cn": e["cn"], "kind": e["kind"], "lang": e["lang"],
"coverage": len(have), "has": have, "missing": miss}
cov[e["id"]] = len(have)
per_axis = {a: sum(1 for e in engines if a in e["caps"]) for a in axes}
by_kind, kind_cov = count_by(engines, "kind"), {}
for e in engines:
kind_cov.setdefault(e["kind"], []).append(cov[e["id"]])
coverage_by_kind = {k: sum(v) // len(v) for k, v in kind_cov.items()}
order = sorted(engines, key=lambda e: (-cov[e["id"]], e["id"]))
order_ids = [e["id"] for e in order]
svg_bytes = svg(os.path.join(args.out, "e01_landscape.svg"), order_ids, cov, cn, len(axes))
report = {
"version": 1, "source": "sim/e01_landscape.py", "observedAt": cfg["observedAt"],
"engineCount": len(engines), "axisCount": len(axes),
"byKind": by_kind, "byLang": count_by(engines, "lang"),
"byOpenSource": count_by(engines, "openSource"),
"coverageByKind": coverage_by_kind,
"perEngine": per_engine, "perAxis": per_axis,
"order": order_ids,
"mostCovered": [e for e in order_ids if cov[e] == max(cov.values())],
"leastCovered": [e for e in order_ids if cov[e] == min(cov.values())],
"svg": os.path.join(args.out, "e01_landscape.svg"), "svgBytes": svg_bytes,
}
with open(os.path.join(args.out, "e01_landscape.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps({
"byKind": by_kind, "byLang": report["byLang"], "byOpenSource": report["byOpenSource"],
"coverageByKind": coverage_by_kind,
"coverage": {cn[e["id"]]: cov[e["id"]] for e in order},
"perAxis": {a: per_axis[a] for a in axes},
"svgBytes": svg_bytes,
}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
脚本刻意把「形态分布」(byKind)和「覆盖度」(coverage)分两个口径给出:形态是分类,覆盖度是量。前者告诉你生态里有多少条完整引擎,后者告诉你完整引擎平均送了多少条能力轴。
实跑结果
在本机工作区执行 /opt/homebrew/bin/python3 sim/e01_landscape.py --config config/ --out out/,输出如下(同机相对量级,绝对数值不可移植):
{
"byKind": { "full-engine": 5, "framework": 1, "render-lib": 1, "no-engine": 1 },
"byLang": { "TypeScript": 5, "Lua": 1, "C#": 1, "JavaScript": 1 },
"byOpenSource": { "true": 7, "false": 1 },
"coverageByKind": { "full-engine": 6, "framework": 4, "render-lib": 1, "no-engine": 0 },
"coverage": {
"Cocos Creator": 8,
"LayaAir": 8,
"Egret": 7,
"Defold": 6,
"Unity WebGL / 团结引擎": 5,
"Phaser": 4,
"PixiJS": 1,
"原生小程序 Canvas API": 0
},
"perAxis": {
"editor": 5, "physics": 5, "animation": 6, "audio": 6,
"assetPipeline": 5, "hotUpdate": 3, "tsSupport": 5, "miniGameAdapter": 4
},
"svgBytes": 2245
}
几个可以直接读出的事实:
- 能力覆盖从 8 一路衰减到 0:Cocos Creator 与 LayaAir 各内置 8 条能力轴,Egret 7、Defold 6、Unity WebGL 5、Phaser 4、PixiJS 1、原生 Canvas 0。这条数列就是前面那条「引擎—框架—渲染库—无引擎」谱系的量化版本:每往右走一步,就少一条平台白送的能力轴。
- 形态分布偏「完整引擎」:八个里五个是完整引擎,框架、渲染库、无引擎各一个。这说明小游戏生态的主流仍然是「用引擎」,但两端各有一条少人走、却各有用处的路——PixiJS 与原生 Canvas 主打极小包体。
- 语言高度集中:TypeScript 五个(Cocos、LayaAir、Egret、Phaser、PixiJS),Lua、C#、JavaScript 各一个。对一支熟悉 TS 的团队来说,多数选项不需要换语言,这本身就是一种隐性成本优势。
- 开箱能力轴里,最稀缺的是热更新:八条轴里
hotUpdate只有 3 个引擎内置(Cocos、LayaAir、Egret),其余五条都在 4–6 之间。这意味着**「能不能不发版就改内容」在小游戏里并不是默认能力**,而是少数引擎才送的。这条结论会在第 4 章的资源热更新里兑现。 miniGameAdapter只有 4 个:Cocos、LayaAir、Egret、Defold 有官方小游戏适配层。Phaser、PixiJS、Unity、原生 Canvas 都需要你自己处理平台 API 差异——这直接指向本卷第 10 章的「平台 API 抽象层」。
这张表怎么用,边界在哪
用法上,这张表是后续两步的输入:第 1.2 节给八条选择维度填分数、算加权总分与敏感度,第 1.3 节回答「什么时候值得离开引擎、自己写」。格局表本身不含任何「谁更好」的判断——它只负责把八条路线放进同一组坐标。
边界要说清三件事。第一,能力「有没有」不等于「好不好用」:Cocos 和 LayaAir 都内置了 8 条轴,但两条轴背后的成熟度并不相同,这是第 1.2 节要处理的(用分数区分)。第二,这里的八条能力轴是覆盖主流、便于教学的集合,不是穷举:真实引擎还有 shader 系统、3D 管线、粒子编辑器等,本书按「小游戏用得上」的标准做了裁剪。第三,observedAt 标了 2026-10,意味着这是观察时点的快照:引擎的形态、能力与活跃度都会迁移,任何「现在某引擎内置了 X」的结论都要带着时点读。这也正是把格局做成 config 而非写死在正文里的原因——更新一次数据,下游的统计、评分、分界会一起更新。
小结
本节把「小游戏引擎」从品牌名变成了四段谱系:完整引擎 / 框架 / 渲染库 / 无引擎,并把八条路线落成一张机器可读的 config。S 层脚本证明这张表能被程序消费,读出了能力覆盖从 8 衰减到 0、形态以完整引擎为主、TS 语言占多数、热更新与平台适配层最稀缺等结构性事实。下一节在这个骨架上填入八条维度的分数,把「格局」算成一张可调权重、可做敏感度分析的选型评分表。
阅读导航:上一节:目录 · 下一节:选型评分模型与敏感度分析 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。