MoonBit 不只是玩具语言:浏览器里做 PPT、训 Transformer、搭游戏引擎

2026-05-26 17 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:12 分钟

一门新语言要证明自己不是"又一个玩具",最快的方式不是跑 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_actionMove 分支里检查目标位置是否被占据。
  • 序列化状态:给 Worldto_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。这正是那三个项目的实际做法。


相关推荐