WebAssembly 游戏开发技术专题全层级14 / 31 已发布

WebAssembly GC 提案正式落地:托管语言的 Wasm 原生化新纪元

· 0 分钟阅读
WebAssembly GC 提案正式落地:托管语言的 Wasm 原生化新纪元 When Managed Languages Go Native on Wasm — WebAssembly 游戏开发技术专题

WebAssembly GC 提案正式落地:托管语言的 Wasm 原生化新纪元 When Managed Languages Go Native on Wasm

为什么 GC 提案是托管语言做游戏的第一块基石

先说个我自己的糗事。2021 年我跟着一个小队做网页端卡牌游戏,主程拍板用 C#,理由朴素得很:组里三个策划两个程序都熟这套。结果第一个能玩的版本打出来,玩家从点开链接到看见主菜单,要下载将近 9MB、白屏六秒多。发行看完数据,说了句我记到今天的话:「你们做的不是网页游戏,是网页下载器。」

那次之后我给自己立了条规矩:在浏览器里做游戏,第一道门槛不是画面,不是玩法,是「点开链接到看见第一个像素」之间那段空白。这段空白有多长,直接决定你的次日留存是 30% 还是 12%。

WebAssembly 的 GC 提案(垃圾回收提案,下称 GC 提案)解决的正是这段空白背后的东西。很多人把它当成编译器内部的技术活,觉得离做游戏很远。恰恰相反——它决定的不是「跑得多快」,而是「你的游戏逻辑能用什么形状写」。

过去这些年,能在 Wasm 上体面跑游戏的语言基本只有 Rust 和 C++。原因很土:Wasm 的线性内存是一块扁平的字节地毯,它只认字节,不认对象。而卡牌、Roguelike、模拟经营这些品类,游戏逻辑天生就是一张密密麻麻的对象网——一件装备有词条,词条有效果,效果指向一个 buff,buff 又挂回角色身上。你要把这张网塞进字节地毯,要么自己手搓一套对象管理,要么把整个语言运行时背进浏览器。

GC 提案干的事,是把结构体、数组、引用变成 Wasm 的一等公民,让对象不用再从外面搬进来。用一句话概括本文的立场:它不是给 Wasm 加了台垃圾车,而是给「对象」发了身份证。有了身份证,Dart、C#、Kotlin 写的游戏,才有资格说自己「原生跑在 Wasm 上」。

下面分四层拆。第一层先算清旧世界那笔搬家税到底多贵;第二层看 GC 提案的三块积木和一个特别容易被低估的小聪明;第三层算 GC 偷走的帧预算——这层跟做游戏的人关系最大;第四层看真实世界里谁在跑、谁在观望。最后给一张能直接抄的参数表,和我自己踩过的几个坑。

第一层:先算清旧世界的搬家税

要理解 GC 提案的价值,得先理解旧世界有多难受。

Wasm 模块看到的是一块连续的线性内存,它只能干两件事:按偏移量读一个字节,或者按偏移量写一个字节。没有类型,没有边界检查,没有「这个位置放的是一个玩家对象」这种概念。这套设计非常干净、非常快,也非常不近人情——它假设你是一个编译器,你自己知道自己在干什么。

C++ 和 Rust 确实是这么活的:结构体字段的偏移在编译期就算好了,堆上的对象谁分配谁释放,编译器替你盯着。但托管语言的世界观完全反着来:对象在堆上随便飘,谁都不用管释放,运行时负责在合适的时候扫一遍,把没人引用的收走。这套东西要搬进浏览器,只有三条路,每条都在收税。

路线代表代价玩家实际感受到什么
把整个运行时编译进 WasmBlazor(精简的 .NET 运行时)、Unity WebGL(IL2CPP 路线)包体多几 MB,启动时要唤醒运行时、初始化堆首屏白屏从 1 秒变成 5 秒
先编译成 JavaScriptDart 的旧 Web 目标、Kotlin/JS 一线语义翻译有损耗,类型信息在 JS 里被抹平,靠运行时补丁兜底逻辑一复杂帧率就不稳,长尾卡顿
手动把对象摊平成字节手写对象池 + 手写序列化的 C++ 风格实现开发效率暴跌,每种类型都要手写回收和序列化跑得挺快,但策划提需求时程序的眼神会变

这三条路我跟团队都走过。说得直白点:第一条是花玩家的流量和耐心,第二条是花程序的头发,第三条是花策划的想象力——因为每次你想加新系统,程序的第一反应是「这个能不能不做」。

拿 Unity WebGL 当例子,它是最典型的「第一条路」。Unity 的做法叫 IL2CPP:先把 C# 编译出来的中间代码翻译成 C++,再让 Emscripten 把 C++ 编译成 Wasm。听起来很聪明,实际上是把「C# 跑在浏览器里」偷换成了「C# 先变成 C++ 再跑」。

代价藏在细节里。C# 的泛型在 C++ 里没有直接对应物,得靠代码生成穷举;反射要元数据表撑着;异常处理得走模拟路径。更要紧的是垃圾回收:IL2CPP 用的是一套保守式回收器(后来也有增量式版本可选)。保守式是什么意思?它没法精确知道内存里哪块是指针、哪块是数字,只能把整块内存当成可能的指针扫一遍,猜哪个位置像地址。

这个「猜」字带来两个直接后果。一是扫描成本随堆大小线性上涨——堆到 300MB 以后,每次回收都可能吃掉十几毫秒,正好撞上你要出招的那一帧。二是误判——某个数值碰巧长得像个地址,那块内存就永远回收不掉。所以 Unity WebGL 项目有个业内公开的秘密:内存水位普遍比原生高,移动端 Safari 上尤其容易崩。

我跟过一个 2D 像素风项目,原生包在 iPhone 上稳稳跑,转成 WebGL 之后在 Safari 上平均十几分钟闪退一次。查了两周,最后发现是纹理加载后那块内存被误判为可达,堆一路涨上去撞了标签页配额。这不是 Unity 的锅,是「在字节地毯上模拟对象世界」这条路本身就有的税。

Blazor 走的是另一条同样收费的路:把一个精简的 .NET 运行时打包进去,一个 Hello World 级别的项目压缩后下载量在 2MB 上下,首屏还得先把这个运行时唤醒。整套结构挺优雅,但你也得承认:玩家下载的不是你的游戏,是你的运行时。

这就是旧世界的账单——托管语言想在 Wasm 上跑,就得把自己整个家搬过去。GC 提案想让这笔账消失。

第二层:三块积木,和一个了不起的小聪明

GC 提案往 Wasm 里塞的东西,说穿了就三样:结构体、数组、引用类型。听着平平无奇,但配上 Wasm 原有的「模块自己声明类型」机制,味道就出来了。

结构体是一组有名字的字段,字段可以是数字,也可以是引用。布局由模块自己声明——也就是说,一个「玩家」结构体里哪个字段落在第几个字节,是模块说了算,运行时照单执行。这一条很关键:字段访问是直接按偏移取,不是 JS 那种「先问一下这个对象长什么样」的动态查找。

数组是定长的同类型元素序列,元素可以是数字或引用。一个背包格子数组、一个粒子池、一张寻路节点的邻接表,都能直接表达。

引用类型是这套设计的灵魂,它是一棵树:最顶上是能装任何东西的引用;往下是可比较的引用(能判等);再往下分出结构体引用和数组引用;另外有一条独立枝杈叫外部引用,专门指代宿主(也就是 JS)给的对象。

这棵树里有个设计我要单独表扬——31 位整数引用。它把一个小整数直接放进引用位里:不分配内存,不装箱,就是个引用,只不过这个引用恰好代表一个 31 位的数。

为什么这个不起眼的东西重要?因为游戏里遍地是小整数。道具编号、阵营 ID、状态枚举、稀有度、buff 层数……你写一个「给角色挂三个状态」的系统,如果每个状态都必须是对象,那就要为三个数字各分配一个包装对象,再挂到链上。一帧挂十次、六十帧就是六百次分配,全是一堆活不过两秒的小垃圾。

有了 31 位整数引用,这类值根本不用分配——它就是引用本身。我第一次看这设计时的反应是:「这不就是把枚举塞进指针低位的那个老技巧嘛。」没错,但这次它是规范里写清楚的、被所有引擎一致实现的、带类型检查的版本。老技巧靠程序员自律,新特性靠平台保证——这个差别在团队协作里是天地之别,因为你没法指望每个新同事都记得「这里别装箱」。

还有一件事比这三块积木更重要,但它不在规范里,而在实现里:这些对象和宿主语言的对象住在同一个堆里。以 V8 为例,Wasm 的结构体、数组和 JS 对象共用同一套分代回收机制。这意味着什么?

以前 Wasm 和 JS 之间只能传数字。你想把一份「关卡配置」交给前端 UI 去渲染,得把它序列化成字节,传过去,再在 JS 那边解析回来,一来一回还得管谁负责释放。跨边界的成本直接决定了你的架构长什么样——很多 Wasm 游戏之所以长成一个巨大的单文件逻辑块,就是因为边界太贵,谁都不想切。

现在可以直接传引用。代价是这条边界必须被严格遵守(引用不能随便跨模块长期持有),但至少在架构上,你可以放心地把 UI 放在 JS/TS 侧、把逻辑放在 Wasm 侧,而不用为每次数据交换付过路费。

落到玩家能感知的层面,大概是三件事:

  • 加载变快:不用再下载并初始化一个自带 GC 的运行时,那几 MB 的固定开销没了。
  • 帧率曲线变平:对象分配交给成熟的引擎 GC 处理,不再是你自己写的回收器在关键帧上抽风。
  • 内存天花板抬高:没有「猜指针」的误判,该回收的能回收掉,移动端闪退概率明显下降。

第三件事最不显眼,但对留存的影响最大。一个玩家玩到第 20 分钟突然白屏,他不会觉得是内存问题,他只觉得这游戏很烂。

第三层:GC 不是免费的,它偷的是每帧那 16.6 毫秒

前面都是好消息,这层说点扎心的:GC 不是免费的,它偷的是你每帧那 16.6 毫秒里的一块。

先把回收的物理过程讲清楚。分代回收把堆分成年轻代和老年代。新对象丢进年轻代,年轻代满了就扫一遍——因为绝大多数对象活不过几秒,这一遍扫得又快又便宜,通常在一毫秒以内。活下来的对象被「晋升」到老年代。老年代满了就得做标记:从根出发,把所有还能摸到的对象标一遍,然后清扫或整理。整理意味着移动对象,移动意味着暂停,暂停意味着你这一帧会晚到。

所以 GC 成本不是一个固定数值,它由三个变量决定:分配速率、存活率、堆大小。而这三个数字,是能被游戏设计影响的,而且影响很大。

举个例子。一个弹幕射击,屏幕两百颗子弹,每颗每帧更新位置,打中之后爆开三颗碎片粒子,两秒后消失。如果你按「每颗子弹是一个对象」来写,每帧分配量并不高;但如果你按「每颗子弹每帧生成一个不可变的位置快照」来写(有些偏函数式的代码风格会这么干),那一秒就是上万次分配,全进年轻代,年轻代被反复打满,GC 触发频率高到帧率曲线像心电图。

同一个玩法,两种写法,玩家体感能差出一个档次。这就是为什么我说 GC 提案是设计问题不只是技术问题——它把「对象怎么用」的选择权,从内存模型的限制里解放出来交还给你,同时也把责任一并交还给你。

Unity 的增量式回收是个很好的参照系。它做的事是把一次长暂停切成一堆小暂停,分帧执行。代价是总工作量变大(要维护写屏障去追踪跨代引用),而且如果某一帧新分配太多,小暂停还是会挤在一起。

但它带来了一个很有意思的设计判断:玩家对「持续略卡」的容忍度,往往高于「偶发顿挫」。一次 60 毫秒的停顿在动作游戏里足够让你判定失误,玩家会骂「手感垃圾」;而整体掉 5 帧,玩家的感受是「这游戏在网页上还行」。所以很多团队宁可开增量式回收,用一点点平均性能换帧时间的稳定。

Wasm GC 交给宿主引擎之后,这个权衡由引擎替你做了——V8 那套并发标记、并行清扫是十几年打磨出来的东西,你自己写的回收器很难打得过。但别把这当成免费午餐:并发标记会占用 CPU,抢的正是你渲染线程的时间;引擎的默认参数是按网页平均负载调出来的,不是按你的 60 帧射击游戏调的。你还是得看指标、调分配。

说句实在话,我见过太多团队在「上不上新特性」上纠结三个月,最后败在没人愿意去看一眼堆的增长曲线。工具变了,看数据的习惯不能变。

第四层:谁真的在跑——Dart 领跑,Kotlin 追赶,C# 观望

这层讲真实世界。GC 提案不是纸面标准,浏览器那头的开关已经拨了:Chrome 和 Firefox 在 2023 年底同年打开了默认支持,Safari 拖到 2024 年底才跟上。也就是说今天你写的东西,三大浏览器都跑得起来——但 Safari 这关最难过,而 Safari 恰好是移动端的主战场。这个细节会直接影响你的机型测试清单。

真正吃上头啖汤的是 Flutter。

Dart 的处境很典型。以前 Dart 编译到 Web 走的是「翻译成 JavaScript」这条路,Dart 对象在运行时变成 JS 对象,用 JS 的 GC。跑是能跑,但类型、数字、类的形状这些 Dart 语义,在 JS 里要靠一堆运行时检查兜着。现在 Dart 有了直通 Wasm 的编译目标:类直接变成 Wasm 结构体,列表变成 Wasm 数组,用的是 V8 那套 GC。语义翻译这一层的损耗被抹掉了。

Flutter 在 3.22 版本把 Wasm 编译目标推到了稳定,同时还上了新的渲染器。官方给出的基准里,部分场景接近两倍提升——注意「部分场景」这四个字,我不建议拿这个数字去说服老板,因为真实项目的瓶颈往往不在渲染管线,而在你那堆每帧都在重建的组件。

Flame 是 Dart 生态里的游戏引擎,纯 Dart、不要额外运行时,靠 Flutter 的渲染管线画东西。它的核心是一棵组件树:每个精灵、每个碰撞体、每段动画都是一个组件,每帧遍历更新。这套结构在 Wasm GC 下明显更舒服——每帧生成的那些中间对象(变换、颜色、矩形)变成了结构体,分配更便宜,回收更可预测。这是那种「玩家不会夸你、但少骂你两句」的改进,做过长线运营的人都懂它值多少钱。

但我要给想上车的同学泼盆水。Flame 在 Web 上的帧率依然打不过同水平的原生游戏,主要开销在 Flutter 的渲染管线和画布合成,那部分跟 GC 一点关系没有。另一个更现实的坑是构建成本:因为 Wasm 目标和旧的 HTML 渲染路径不能共用同一份代码,你等于要维护两条构建线、两套测试。独立团队两三个人,这笔账得算清楚再拍板。

Kotlin 这边的情况更有意思。Kotlin 的 Wasm 目标已经能用,Compose Multiplatform 的 Web 版本也到了可以拿给人看的阶段:动画、布局、手势都能跑。但社区抱怨最多的不是性能,是包体和首屏——一个简单页面动辄几 MB 下载,打开要等。

这件事特别值得说,因为它戳破了一个很容易搞混的错觉:GC 提案解决的是「对象能不能原生存在」,它完全没解决「包体有多大」。这是两笔独立的账。Compose 的运行时、Kotlin 标准库、Skia 渲染器,这些东西不管你用不用 GC 都得打包进去。你要是把这两件事混着谈,就会得出「上了 GC 提案包体自然就小了」这种错误结论,然后在项目中期被打脸——本系列前面聊包体优化那篇已经把这块讲透了,这两篇建议一起看。

再说句让 C# 同学不太舒服的话:.NET 和 Unity 短期内吃不到这波红利。Blazor 至今还是自带精简运行时加自带 GC;.NET 那边有一条直编 Wasm 的实验路线,但离「能用」还有距离。Unity 更是被 IL2CPP 整条工具链锁死了,换 GC 模型等于重做导出管线,商业上不划算。Godot 的 C# Web 导出到现在也没真正落地。

这说明 GC 提案的收益是新项目红利,不是存量项目升级。你手上那个跑了三年的 Unity 项目,别指望换版本就变快;但如果你今天从零做一个 2D 或轻量 3D 的网页游戏,Dart 这条线是目前唯一「GC 提案已落地、而且真的有人在跑游戏」的选择。这个判断我给得很保守,因为我知道一年后它可能就变了。

参数化设计工具:一张能直接抄的 GC 预算表

下面这张表是我根据几个项目复盘出来的,不是标准答案,但能当起点用。用法是:先按品类定预算上限,然后在开发期接一个分配统计,每帧把新分配对象数和堆水位打到屏幕上——就跟你打 FPS 一样。连续跑三个典型场景(主菜单、战斗高峰、大世界传送),哪一段超了就去那段找原因。这个习惯比任何优化技巧都好使。

维度可调参数建议区间为什么是这个数
分配速率每秒新分配的对象总量轻量 2D:低于 15MB/s;3D:低于 30MB/s超过这个量级,年轻代会被频繁打满,触发频率失控
单帧分配量一帧内新创建的对象个数2D 低于 500;3D 低于 2000与分配速率互为校验,专防某一帧突然爆量
存活率活过年轻代的对象比例低于 10%高了说明你在批量制造长寿垃圾,晋升压力会全压到老年代回收上
堆目标托管堆上限移动端低于 150MB;桌面低于 400MB浏览器标签页有隐形配额,超了直接崩,不给你任何提示
单帧 GC 预算一帧允许花在回收上的时间60fps 下 1~2ms;120fps 下 0.5~1ms渲染要 6~9ms、逻辑要 3~5ms,剩下的真不多
对象池阈值同类对象创建频率超过多少就进池高于 100 次/秒子弹、粒子、伤害飘字这类高频同类对象,池化收益依然明显
小整数表示多大范围的整数走 31 位引用绝对值小于 2 的 30 次方ID、枚举、层数直接塞进引用,免掉包装对象
结构体规模单个结构体的字段个数低于 16,超了就拆字段太多缓存不友好,也容易一次性晋升到老年代

再补两条经验值。第一,对象池没失业。GC 变好不代表高频同类对象就该放开 new。子弹、粒子、飘字一秒生成几百上千个,池化依然能省下可观的分配压力,而且池化之后对象地址稳定、缓存局部性更好。第二,别把结构体当万能药。有些团队一听「原生结构体」就兴奋,把所有类都改成结构体,结果一套复杂的组合逻辑变成了一堆手写分支。引用类型就该拿来表达「有行为、有关系」的东西,这是两种工具,不是新旧替代。

两条阅读路径:你该关心哪几件事

初级路径:先想清楚「要不要上车」

你现在最该关心三件事:能不能跑(浏览器版本覆盖)、包体多大、首屏多久。这三件事直接决定玩家愿不愿意留下来,比任何特性都重要。

什么情况下值得考虑 GC 这条线?三个条件:从零起新项目、游戏逻辑对象多且关系复杂、想在网页上做非三消类的品类(卡牌、Roguelike、模拟经营、策略、叙事驱动)。你要是做个五分钟一局的休闲小游戏,用不用这套差别不大。

最省事的验证路径是 Flutter 加 Flame:装好工具链,把游戏图标和一张编辑器截图换成能跑的空场景,先量出加载时间和内存水位。别一上来就写战斗系统,先用一个空架子把「首屏多久」这个数字拿到手,这是我用六秒白屏换来的教训。

千万别干的事:为了尝鲜把跑了半年的存量项目迁过去。工具链、调试、构建脚本全套要重来,成本高得离谱,收益还不一定兑现。

进阶路径:把 GC 当系统来调

如果你已经把项目跑起来了,接下来是这几件事:

第一,给对象图做一次体检。把每帧创建的对象按类型分类统计,找出分配榜前三名,通常你会发现 80% 的垃圾来自 20% 的代码——十有八九是某个每帧新建的临时列表,或者某个为了「写法优雅」而每帧生成快照的函数。

第二,把 GC 预算写进你的帧预算表,跟渲染、逻辑、物理、音频并列,而不是等卡了再想办法。我习惯在帧预算表里单独列一行给 GC,写上 1.5ms,谁超了谁改。

第三,开始考虑跨模块的对象所有权。这里就是下一块拼图的入口——当你要把游戏拆成多个 Wasm 模块(比如主逻辑一个、插件一个、渲染一个),对象在模块之间传递时谁负责释放、能不能长期持有引用,这些问题会立刻从「理论」变成「编译不过」。这个坑,本系列讲 Component Model 的那篇会接着往下挖。

第四,自己写存档序列化。GC 对象对模块外部是不透明的,你没法像读线性内存那样直接整块 dump 出去。想存档就得显式定义每一类对象怎么写成字节、怎么读回来。听着是倒退,其实是好事——这逼着你把数据格式和对象结构解耦,将来改结构不容易炸存档。

争议与坑:那些让我深夜复盘的瞬间

坑一:以为「有 GC 就不用管内存了」。这是我最想拍桌子的误解。GC 只负责回收没人用的对象,它管不了你「为什么要造这么多对象」。而且浏览器标签页有隐形内存配额,移动端尤其紧,超了就是直接崩,连个错误码都不给你。GC 把内存管理从「手动挡」变成了「自动挡」,但方向盘还在你手上。

坑二:把引用当免费通道随便长期持有。跨模块或跨宿主边界传的引用,生命周期规则比你想的严格得多。我见过一个项目为了省事,把 Wasm 侧的对象引用挂在 JS 的全局表里当缓存,结果那批对象永远回收不掉,内存一路涨。正确姿势是:跨越边界的东西,尽量在边界两侧各自留一份明确的所有权说明。

坑三:调试器看到的是 Wasm 对象,不是你的类。你在堆快照里看到的是一堆结构体和数组,字段名可能是编译器生成的编号。想看「这个玩家身上挂了几个 buff」,还得靠你自己打点。工具链这块的成熟度,是这套技术目前最短板的地方,比性能更值得你提前做心理准备。

坑四:双构建成本被严重低估。为了让不支持 Wasm GC 的浏览器也能玩,你多半得同时出一份 JS 版本的构建。两套构建、两套测试、两倍的上线验证时间,还有那些「在 A 上好好的、在 B 上就崩」的诡异 bug。两三个人的独立团队,这个成本能吃掉你 20% 的开发时间。

再说个真正的争议:Wasm 到底该不该有 GC?

有相当一部分人认为这是对初心的背叛——Wasm 起家靠的就是「贴近机器、可预测、够快」,现在把托管语言那套搬进来,等于把它变成一个平庸的通用虚拟机,跟当年的 Flash 有什么区别?

我的态度很明确:我站 GC 这边,理由两条。第一条是零成本的:不用就不生成。你写 Rust、写 C++,编译器根本不会给你造出 GC 对象,一毛钱不掏。Wasm 能不能「贴近机器」,取决于你用什么语言写,不取决于规范里允许了什么。第二条更本质:Wasm 的护城河从来不是「快」,是「可移植的安全沙盒」。一个能在任何设备、任何平台、任何宿主上安全跑起来的东西,天然就该装得下更多语言。只让 C++ 和 Rust 上车,那不是安全沙盒,那是男士俱乐部。

真正的风险不在 GC 本身,在于有人会把它当成「性能问题已经被解决了」的许可证,然后写出每帧分配 50MB 的代码。技术解放生产力,也解放了糟糕的设计——这两件事总是同时发生。

FAQ

Q1:我是 Rust / C++ 开发者,GC 提案会不会让我的东西变慢?

不会。GC 类型系统是可选用的——你的模块不声明结构体类型,编译器就不生成,运行时那套东西跟你没有交集点。真正会间接影响到你的是引擎内部实现的变化(比如 V8 的堆管理策略调整),但那是量级很小的波动,不用为它焦虑。

Q2:我用 Unity,什么时候能吃到这波红利?

短期内别指望。IL2CPP 这条管线把 C# 的语义整个搬进 C++ 世界,换 GC 模型等于把导出管线重做一遍。真有那么一天,也是下一代大版本的事。你现在能做的优化还是老三样:砍包体、压纹理、控内存峰值。

Q3:GC 暂停真的能稳定在 60fps 以内吗?

能,但有条件。条件是分配速率和存活率都在合理区间(参考上面那张表)。如果你一秒分配几百 MB,任何 GC 都救不了你——那不是 GC 的问题,是对象设计的问题。另外提醒一句:引擎的默认参数是按网页平均负载调的,不是按你的游戏调的,你得自己看曲线。

Q4:存档怎么办?对象都在 GC 堆里,我怎么导出?

自己写序列化。定义好每类对象怎么写、怎么读,别指望直接 dump 内存。这活儿听着烦,但好处是它强制你把数据格式和对象结构解耦——将来改内部结构时,老存档不会全炸。很多项目都是在做云存档的时候才被迫面对这件事,早点做省事得多。

Q5:现在入场 Flutter / Flame 做网页游戏靠谱吗?

看你要做什么。2D、UI 密集、卡牌和经营类,靠谱,而且能吃到 GC 提案的第一波红利。要写重度 3D 或者对帧时间极度敏感的动作游戏,现在还是 Rust 或 C++ 更稳。还有一个现实考量:Flame 的生态比 Unity 小太多,遇到问题得自己去翻 issue,这个隐性成本要算进去。我一般建议团队先用它做一个小体量的实验项目,跑通了再谈主力项目。

最后抛个问题给你琢磨:过去十年,我们把「能不能在浏览器里跑」当成技术问题;GC 提案落地之后,它越来越像一个纯粹的设计问题——当语言不再是限制,你打算用那些被省下来的精力,去做一个什么样的游戏?这个答案,比任何技术选型都更能决定你的项目能走多远。

文章标签
WebAssemblyWasmEmscriptenWASIwasm-bindgenComponent ModelSharedArrayBufferAtomicsWasm 多线程Wasm SIMDwasm-packRust Wasm
更多专题全部专题
觉得有价值?点赞或收藏支持内容持续产出。
← 返回专题:WebAssembly 游戏开发技术专题