一门新语言要证明自己不是"又一个玩具",最快的方式不是跑 benchmark,而是让人拿它干真活。MoonBit 软件合成挑战赛里,海外开发者交出的答卷恰好说明了这一点——三个项目分别踩进了办公软件、深度学习训练、游戏引擎这三个硬场景,而且全部跑在浏览器里。
幻灯片编辑器:把 PowerPoint 搬进浏览器
这个项目要解决的核心问题是:在浏览器里打开一份 .pptx 文件,能编辑、能高保真渲染,最终还能导出回去。听起来像是把 LibreOffice 套进 WebAssembly,但实际实现远比这轻量。
开发者选择 MoonBit 的理由很直接:MoonBit 编译到 WasmGC 后体积小、启动快,而幻灯片渲染对布局计算的实时性要求很高——每一帧拖拽、缩放、文字重排都得即时响应。用 JavaScript 写布局引擎容易陷入 GC 停顿的泥潭;用 Rust 编译到 Wasm 则要手动管理线性内存,跟 DOM 交互时又得反复跨越边界。MoonBit 的 WasmGC 后端让垃圾回收由宿主 VM 承担,同时语言侧的类型系统足够严格,布局逻辑不容易写错。
项目的技术路径大致是:解析 OOXML 格式 → 构建内部文档模型 → 用 Canvas/SVG 渲染幻灯片 → 暴露编辑操作 API。难点在第二步和第三步:OOXML 规范庞大且充满历史遗留,文档模型必须精确映射;渲染层则要处理字体回退、矢量图形合成、动画时间轴等细节。
浏览器内 Transformer 训练:把 GPU 算力拉到前端
另一个项目更激进——直接在浏览器里训练一个小型 Transformer 模型。这不是"推理",是真正的反向传播和参数更新。
实现依赖 WebGPU API。MoonBit 通过 @wgpu 包封装了 WebGPU 的核心调用:创建 device、分配 buffer、构建 compute pipeline、提交 command buffer。训练循环的每一步——前向计算、损失评估、梯度回传、权重更新——都写成 MoonBit 函数,编译为 WasmGPU kernel 在 GPU 上执行。
为什么不用 Python + PyTorch?项目作者的解释很实际:他想做一个"零安装"的 AI 教学工具。学生打开网页就能改超参数、看 loss 曲线、检查梯度分布,不需要配 CUDA、装 conda。MoonBit 在这里的优势是编译体积——整个训练程序编译后不到 200KB,而同等功能的 PyTorch Wasm 构建动辄几十 MB。
复古 2D 游戏引擎:给 AI 留好"驾驶位"
第三个项目最有趣:一个专为 AI 协作开发设计的 2D 游戏引擎。"AI 协作开发"不是让 AI 写代码然后人审查,而是让 AI agent 在运行时直接操控游戏状态——相当于 AI 是玩家,引擎是沙盒。
引擎的设计因此跟传统游戏引擎不同:
- 状态完全可观测:游戏世界没有隐藏状态,所有实体属性通过统一 schema 暴露,AI 不需要"猜"。
- 操作原子化:每个 action(移动、攻击、拾取)是独立的原子操作,AI 可以组合它们而不必理解底层渲染循环。
- 回合制节奏:引擎不是 60fps 实时循环,而是 tick-based,每个 tick 等待 AI 返回动作后再推进。这让 AI 的决策延迟不会导致"卡帧"。
引擎本身用 MoonBit 写核心逻辑(实体管理、碰撞检测、状态序列化),渲染层则委托给浏览器 Canvas。这种分工让引擎核心可以编译成纯 Wasm 模块,被任何宿主调用——不一定是浏览器,也可以是 Node.js 或者本地测试框架。
动手试:用 MoonBit 写一个 tick-based 游戏循环
上面三个项目各有深度,但入门不需要从 OOXML 解析开始。下面用一个最小可运行的 tick-based 游戏循环,演示 MoonBit 的基本项目结构和 WasmGC 编译流程。这个例子跟第三个项目(AI 协作游戏引擎)的架构思路一致:状态可观测、操作原子化、tick 驱动。
1. 初始化项目
确保已安装 MoonBit 工具链(moon CLI),然后:
moon new game-tick-demo
cd game-tick-demo
这会生成标准项目结构:
game-tick-demo/
├── moon.mod.json
├── src/
│ ├── moon.pkg.json
│ └── main.mbt
2. 编写游戏循环核心
编辑 src/main.mbt,替换为以下内容:
// 游戏世界状态:完全可观测,AI 可直接读取
struct World {
entities : Array[Entity]
tick : Int
}
struct Entity {
id : Int
x : Int
y : Int
hp : Int
tag : String
}
// 原子操作:AI 每个 tick 返回一个动作列表
enum Action {
Move(id: Int, dx: Int, dy: Int)
Attack(attacker: Int, target: Int)
Heal(id: Int, amount: Int)
}
fn init_world() -> World {
let e1 = Entity::{ id: 1, x: 0, y: 0, hp: 10, tag: "player" }
let e2 = Entity::{ id: 2, x: 5, y: 3, hp: 8, tag: "enemy" }
World::{ entities: [e1, e2], tick: 0 }
}
// 查找实体
fn find_entity(world : World, id : Int) -> Entity? {
world.entities.find_first(fn e { e.id == id })
}
// 执行单个原子动作,返回新世界状态
fn apply_action(world : World, action : Action) -> World {
match action {
Move(id, dx, dy) => {
let new_entities = world.entities.map(fn e {
if e.id == id {
Entity::{ ..e, x: e.x + dx, y: e.y + dy }
} else { e }
})
World::{ ..world, entities: new_entities }
}
Attack(attacker, target) => {
let damage = 2
let new_entities = world.entities.map(fn e {
if e.id == target {
Entity::{ ..e, hp: max(e.hp - damage, 0) }
} else { e }
})
World::{ ..world, entities: new_entities }
}
Heal(id, amount) => {
let new_entities = world.entities.map(fn e {
if e.id == id {
Entity::{ ..e, hp: min(e.hp + amount, 10) }
} else { e }
})
World::{ ..world, entities: new_entities }
}
}
}
// 一个 tick:接收动作列表,依次执行,推进时间
fn tick(world : World, actions : Array[Action]) -> World {
let mut w = world
for action in actions {
w = apply_action(w, action)
}
World::{ ..w, tick: w.tick + 1 }
}
// 打印世界状态(模拟 AI 可观测接口)
fn show_world(world : World) -> String {
let header = "=== Tick {world.tick} ==="
let body = world.entities.map(fn e {
"[{e.tag}] id={e.id} pos=({e.x},{e.y}) hp={e.hp}"
}).join("\n")
"{header}\n{body}"
}
fn main {
let world = init_world()
println(show_world(world))
// 模拟 AI 决策:玩家向敌人移动并攻击
let actions1 = [Move(1, 1, 1), Attack(1, 2)]
let world1 = tick(world, actions1)
println(show_world(world1))
// 第二个 tick:敌人反击,玩家治疗
let actions2 = [Attack(2, 1), Heal(1, 3)]
let world2 = tick(world1, actions2)
println(show_world(world2))
}
3. 运行和编译
本地运行:
moon run src
预期输出:
=== Tick 0 ===
[player] id=1 pos=(0,0) hp=10
[enemy] id=2 pos=(5,3) hp=8
=== Tick 1 ===
[player] id=1 pos=(1,1) hp=10
[enemy] id=2 pos=(5,3) hp=6
=== Tick 2 ===
[player] id=1 pos=(1,1) hp=11
[enemy] id=2 pos=(5,3) hp=6
编译为 WasmGC(浏览器可加载):
moon build --target wasm-gc
产物在 target/wasm-gc/release/ 下,是一个 .wasm 文件,体积通常在几十 KB。你可以用简单的 HTML + JavaScript 加载它并调用导出函数,把游戏循环嵌入网页——这正是那三个项目的基本部署模式。
4. 改造方向
这个骨架可以往几个方向扩展:
- 接入真实 AI:把
tick函数的actions参数改为从外部 JSON 解析,浏览器端用 fetch 调 LLM API 生成动作。 - 加碰撞检测:在
apply_action的Move分支里检查目标位置是否被占据。 - 序列化状态:给
World加to_json()方法,方便 AI 读取当前局势。
新语言的真实考验
MoonBit 目前还在快速迭代期,工具链和生态都不成熟。但这三个项目说明了一个关键信号:语言能不能活,不取决于语法多优雅,而取决于能不能让人在真实约束下把东西做出来。
浏览器里的 PPT 编辑器要处理 OOXML 的复杂性;Transformer 训练要跟 WebGPU 的底层 API 打交道;游戏引擎要为 AI agent 设计可编程接口——这些都不是"写个 Hello World 就能感受"的场景。开发者选择 MoonBit 的共性理由是:编译到 WasmGC 后体积小、GC 由宿主承担、类型系统够严格能防错。这三个属性在浏览器硬场景里确实比 JavaScript 和 Rust 各有优势。
但也要看到边界:MoonBit 的包生态还很薄,第三方库远不如 JS/npm 或 Rust/crates 丰富;调试工具链还不完善,浏览器里调试 Wasm 模块仍然痛苦;文档覆盖面有限,很多 API 只能看源码猜。如果你要上生产,这些短板会卡住进度。
采纳建议:如果你的项目本身就是浏览器端、对启动体积和 GC 停顿敏感、核心逻辑可以用纯函数式风格表达——MoonBit 值得做技术验证。先在一个隔离模块里试用,比如布局引擎、状态机、规则引擎这类"计算密集但 IO 少"的部分。不要一开始就全量替换 JavaScript,而是让 MoonBit 编译的 Wasm 模块作为"计算内核"嵌入现有前端架构,跟 DOM 交互的壳仍然用 JS。这正是那三个项目的实际做法。