- 游戏开发
- 图形学
【免费下载链接】melonJS
a modern & lightweight HTML5 game engine
本篇指南围绕 melonJS 20.x 相对 19.x 及更早版本的破坏性变更展开,逐项说明哪些旧 API 已移除、已废弃或已改名,并给出可直接落地的迁移代码。读完你将掌握 20.x 强制异步
Application.init()的正确启动方式、用addPostEffect()取代renderable.shader的后处理特效新 API、video.AUTO的 WebGPU → WebGL 2 → Canvas 三级回退语义、一张完整的废弃导出对照表,以及如何避开当前仓库示例中残留的旧式写法。本文所述内容以本仓库 packages/melonjs 当前版本(20.8.0,见 package.json)为准。
为什么需要这份迁移指南
网上流传的大多数 melonJS 教程、博客与代码片段描述的仍是 19.x 甚至更早的 API。用这些"记忆中的知识"写出的代码往往看起来完全正确,却在运行时静默失败——没有 canvas 被挂载、没有帧循环启动、画面一片空白。原因在于 20.0(CHANGELOG.md 中的 "melonJS 2" 里程碑)是一次底层渲染架构重构:
- 引入了 WebGPU 后端,并退役了 WebGL 1 路径;
video.init()等旧入口被整体移除;- 引擎启动从"构造即初始化"改为"构造 + 显式
await init()"两步; - 渲染相关的类(
Compositor系列)与实体 API(Entity)被新架构替代。
下文按"从记忆中复现代码时最可能踩中的概率"排序,逐条给出新旧对照。
1.Application.init()是强制且异步的
新旧代码对照
20.0 起,new Application(...)只负责构建对象,不再替你初始化:
// ✗ pre-20.x —— 构造函数内部替你调用了 init(width, height, options) const app = new Application(800, 600, { parent: "screen" }); app.world.addChild(sprite); // 添加了子节点,但永远不会被渲染 // ✓ 20.x const app = new Application(800, 600, { parent: "screen" }); await app.init(); // REQUIRED —— 必须调用并等待 app.world.addChild(sprite);签名也变了:19.x 的init(width, height, options)在 20.x 中不接受任何参数——传入的参数会被忽略,所有设置均来自构造函数。因此原本就使用new Application(...)的 19.x 代码同样需要补上await app.init(),否则什么都不显示。
为什么是异步的
init()之所以是async,是因为获取 WebGPU 设备在 Web 平台上本质上是异步操作(需要协商 adapter/device)。这一点在源码中有明确注释:Canvas 与 WebGL 后端获取 context 时不会挂起、init()可以同步完成,但 WebGPU 不行——init()未 resolve 的 Application 没有renderer。见 application.ts 的 JSDoc 与 const.ts 的说明。
关键点是:不存在同步替代方案,也不存在替你补调用的自动回退。更隐蔽的是,缺失init()不会报错——构造函数仍然构建了app.world,子节点可以照常添加,但:
- 没有 canvas 被追加到
parent元素; - 应用从不订阅帧循环;
app.renderer与app.viewport保持undefined;- 若先构造
Sprite或Text,反而会在未初始化的全局game上抛出TypeError。
init()调用存在额外约束(application.ts):
- 重复调用
init()是被警告的空操作(会追加第二个 canvas 并抛弃当前 renderer,因此引擎拒绝执行); - 已销毁(
destroy())的实例再调用init()会抛出异常——destroy()现在是终态的,重建请构造新实例; - 若首次尝试因 WebGPU 协商失败而中途创建过 renderer,重试前会自动释放该半成品后端,避免泄漏。
video.init(...)已不存在
video.init(...)已从引擎中删除。现在的video模块只导出四个渲染器常量(AUTO、CANVAS、WEBGL、WEBGPU)——没有可供调用的init。任何形如me.video.init(800, 600, {...})的代码都来自已移除的 API,应改为构造Application并await app.init()。video.renderer、video.createCanvas()、video.getParent()也一并移除,分别改用app.renderer、app.renderer.createCanvas()与app.getParentElement()(见 CHANGELOG.md 20.0.0 的 "Changed (breaking)")。
命名空间导入仍然有效
注意:import * as me from "melonjs"依然可用,me.Sprite这样的写法完全没问题。被移除的是全局引导(globals bootstrap),而不是命名空间导入本身。迁移时无需把me.xxx改成别的形式,只需补齐启动流程。
2. 后处理特效取代了shader属性
弃用与替代
renderable.shader = ...自19.2.0起标记为废弃,当前 API 是后处理特效(post effect):
// ✗ deprecated —— 自 19.2.0 起废弃 mySprite.shader = new ShaderEffect(app.renderer, glslBody); // ✓ 20.x mySprite.addPostEffect(new ShaderEffect(app.renderer, glslBody));底层实现位于 renderable.js:shader的 getter 实际上返回postEffects[0],setter 在替换前会销毁既有特效;而addPostEffect(effect)只是把特效推入postEffects数组并返回该特效。配套 API 还包括:
getPostEffect(effectClass)——传类时返回第一个匹配的特效,不传参时返回整个特效数组;removePostEffect(effect)——移除指定特效;clearPostEffects()——清空全部特效(renderable.js)。
陷阱:removePostEffect()会销毁特效
这是迁移中最容易踩的坑:removePostEffect()会调用特效的destroy()——赋值renderable.shader同样会销毁被替换掉的对象。因此"移除再重新添加"拿回的是一个 GPU 资源已被释放的僵尸对象。
唯一例外是携带shared = true的特效:它在多个 renderable 间被复用,销毁它会破坏其它引用方,所以源码在销毁前统一判断if (typeof effect.destroy === "function" && !effect.shared)(见 renderable.js)。临时关闭某个特效,请切换effect.enabled属性,而不是移除后重新添加。
3. WebGL 2 是基线,WebGPU 是默认首选
video.AUTO的选择顺序
video.AUTO(默认值)现在的尝试顺序是WebGPU → WebGL 2 → Canvas,WebGL 1 已彻底移除。四个常量定义于 const.ts:
| 常量 | 值 | 语义 |
|---|---|---|
CANVAS | 0 | 强制 Canvas。性能较低、无可编程管线,但兼容一切环境(含 GPU 被驱动策略屏蔽的嵌入式 webview) |
WEBGL | 1 | 强制 WebGL 2。init()在 WebGL 2 不可用时直接 reject,不会静默回退到 Canvas |
AUTO | 2 | 默认。先尝试 WebGPU(完整初始化并协商设备),失败则回退 WebGL 2,再失败回退 Canvas;init()总会 resolve |
WEBGPU | 3 | 强制 WebGPU。不可用时init()同样直接 reject,不降级 |
AUTO 下的 WebGPU 尝试是一次完整的后端初始化(支持与否只能通过协商 adapter/device 来证明),失败时会释放半成品后端并打印AUTO: WebGPU unavailable (...) — falling back to WebGL,然后落到同步探测的候选上——整个逻辑可见于 application.ts。
此外,运行时还可以用URI 片段强制后端:#webgpu、#webgl、#canvas均被支持(见 const.ts),便于针对特定后端调试。
对业务代码的三点影响
- 运行在哪个后端是运行时事实。不要写假设"一定在跑 WebGL"的代码。
- 自定义着色器可以一份资源同时携带 GLSL 与 WGSL。
new ShaderEffect(renderer, { glsl, wgsl })让同一个特效在 WebGL 与 WebGPU 上都能运行,两套代码共享 uniform 名称,一个setUniform服务两个后端。 - 依赖可编程管线的特性在 Canvas 上是惰性的——只警告一次而不是抛异常。需要 GPU 的功能(
Camera3d、ShaderEffect、Light2d法线贴图、GPU tilemap)在 Canvas 上会静默失效,因此若场景依赖它们,应显式使用WEBGL或WEBGPU,让失败在启动时显性化,而不是运行时得到一张黑屏(const.ts)。
值得一提的还有:GLSL ES 1.00 的用户着色器在 WebGL 2 上可原样编译,已有着色器代码无需改动(CHANGELOG.md 20.0.0)。
4. 仍然存在、但不应在新代码中使用的废弃导出
以下符号在当前版本中依然可以解析并运行,但均已被官方替代 API 取代。废弃实现在 deprecated.js 中统一维护——每个类/函数构造或调用时都会通过warning()打印一条"已废弃、请改用 xxx"的控制台提示:
| 废弃符号 | 自版本 | 改用 |
|---|---|---|
CanvasTexture | 17.1.0 | CanvasRenderTarget |
Compositor | 18.1.0 | WebGLBatcher |
PrimitiveCompositor | 18.1.0 | PrimitiveBatcher |
QuadCompositor | 18.1.0 | QuadBatcher |
Math(大写) | 18.0.0 | math(小写) |
device.requestFullscreen/exitFullscreen | 19.7.0 | app.requestFullscreen()/app.exitFullscreen() |
renderable.shader = ... | 19.2.0 | addPostEffect() |
Entity | 18.1.0 | Sprite/Renderable+Body |
loader.onload/onProgress/onError | 18.2.0,20.3 已移除 | LOADER_*事件,或preload(assets, onloadcb) |
response.overlap/overlapN/overlapV | — | response.depth/response.normal |
setLineWidth() | 17.3.0 | lineWidth属性 |
上表同时涵盖了已被彻底移除的项(loader.onload系列),以及仍在导出的项(其余所有)。其中两点需要特别强调。
重点一:Entity仍在导出,但形态已过时
Entity自 18.1.0 起废弃,却仍然导出,因此旧代码可以"编译通过、运行正常",同时却是 20.x 的错误形态——这正是它比直接报错更危险的地方。
废弃原因记录在 entity.js:Entity在内部多包了一层——一个子Renderable(通常是Sprite)被塞进一个同时持有Body的父对象里,导致锚点语义混乱、this.renderable.xxx的间接 API、以及一套与引擎其它部分不一致的自定义坐标渲染管线。正确做法是直接继承Sprite(或Renderable)并在构造函数里挂一个Body:
class PlayerSprite extends me.Sprite { constructor(x, y, settings) { // 用图集动画帧创建 Sprite super(x, y, { ...game.texture.getAnimationSettings([ "walk0001.png", "walk0002.png", "walk0003.png" ]), anchorPoint: { x: 0.5, y: 1.0 } }); // 附加物理体(优先使用 Tiled 形状,或自行定义) this.body = new me.Body(this, settings.shapes || new me.Rect(0, 0, settings.width, settings.height) ); this.body.collisionType = me.collision.types.PLAYER_OBJECT; this.body.setMaxVelocity(3, 15); this.body.setFriction(0.4, 0); // 动画、翻转、着色全部直接调用(不再经过 this.renderable) this.addAnimation("walk", ["walk0001.png", "walk0002.png", "walk0003.png"]); this.setCurrentAnimation("walk"); } update(dt) { if (me.input.isKeyPressed("right")) { this.body.force.x = this.body.maxVel.x; this.flipX(false); } return super.update(dt); } onCollision(response, other) { return true; // solid } }完整示例(含图集动画与独立 spritesheet 两种形态)见 entity.js。这一写法也符合行业惯例——给 renderable 挂物理体是其它主流引擎的标准模式。
重点二:大写的Math会遮蔽全局Math
export * as Math from "./../math/math.ts"(deprecated.js)意味着:如果粗心地导入它,大写Math会遮蔽全局Math对象。旧文档里的me.Math.random()解析到的正是这个废弃的再导出,而不是全局Math.random()。新代码请统一使用小写math命名空间。
全屏与加载回调的迁移
- 全屏:
device.requestFullscreen(element)/device.exitFullscreen()自 19.7.0 废弃。它们在 deprecated.js 中的实现仍需从全局game反查父元素;新入口app.requestFullscreen()/app.exitFullscreen()直接使用 Application 自己的parentElement,不再依赖全局查找。 - 加载回调:
loader.onload/onProgress/onError曾是 ES 模块命名空间上的let绑定——赋值loader.onProgress = fn本身就会抛TypeError,因此这个"文档化的回调 API"从来无法真正使用,20.3 将其彻底移除(CHANGELOG.md 20.3.0)。迁移方式有二:- 监听事件:
LOADER_COMPLETE/LOADER_PROGRESS/LOADER_ERROR(定义于 event.ts,字符串值分别为"me.loader.onload"/"me.loader.onProgress"/"me.loader.onError"); - 或使用
preload(assets, onloadcb)的回调参数——它同时返回可await的 Promise(loader.js)。
- 监听事件:
- 碰撞响应:
response.overlapN/response.overlapV是传统 2D 碰撞分离向量,response.overlap是最短碰撞轴上的重叠量。响应对象 response.js 中仍保留这些字段用于兼容,但更可移植的读法是response.depth(最短轴重叠量)与response.normal(接触法线)。注意 20.0 为 3D 碰撞引入了Box3d形状,Z 方向以overlapNZ/overlapZ标量形式附加(不把overlapV拓宽成Vector3d,以免破坏所有既有 2D 消费者)——平面形状之间的碰撞行为完全不变。
5. 全局game不再是"旧形态"
game仍然导出,但自 20.0 起它的语义变了:它指向最近一次成功初始化的Application,并且在第一个init()resolve 之前是undefined。在 application.ts 中,game是let绑定,只在引擎初始化时通过setDefaultGame(app)赋值。
因此,在模块作用域读取game会得到undefined。正确做法是把Application实例作为参数传递:
Stage#onResetEvent(app, ...args)与Stage#onDestroyEvent(app)会直接收到 app 实例——签名定义于 stage.ts;- 任何 renderable 都可以通过
parentApp属性触达当前应用。
这也呼应了第 1 节:旧代码"模块顶层就game.texture.xxx"的写法在 20.x 会静默失败,因为那时game尚未就绪。
6. Node 与 SSR:开箱即用,零运行时依赖
melonJS 在 Node 环境下可以干净地导入——这对同构(isomorphic)应用与构建工具链非常有用。截至 20.3,引擎没有运行时依赖,因此不要为globalThis、String.trimStart、String.trimEnd添加 polyfill:发布产物目标为ES2022,任何能解析该产物的环境本身就已内置这些能力。
7. 浏览器支持:ES2022 目标与转译陷阱
发布产物面向ES2022,并使用了私有类成员(private class members)。早于该标准的浏览器在加载阶段就会解析失败——不是"用到某个特性时才报错",而是整份文件无法 parse。
若需要支持旧浏览器,请自行转译。但注意一个关键陷阱:转译只处理语法(syntax),内置方法(built-in methods)需要单独的 polyfill,而打包器(bundler)的target设置永远不会替你添加它们(见 CHANGELOG.md 20.3.0 的 docs 修复条目)。此外,构建配置默认跳过node_modules,所以即使把target调得很低,melonJS 也可能根本没被转译。
本仓库示例中的过时内容:不要照学
本仓库 packages/examples 中的示例是组装 melonJS 的最佳参考,但少数文件残留了过时的注释或旧式调用。以下内容不要照着学:
webgpu示例中有一条声称"video.AUTOnever selects the WebGPU backend"的注释(ExampleWebGPU.tsx)。这是 20.x 之前的遗留——AUTO优先尝试 WebGPU。代码本身是对的,注释是错的。- 若干示例仍在赋值
viewport.shader = .../sprite.shader = ...,这已废弃;addPostEffect()才是现行 API。 - 有五个文件对 HUD 设置了
this.z = Number.POSITIVE_INFINITY——该属性根本不存在,那些 HUD 能置顶仅仅是因为它们最后被添加。正确做法是让 HUD 成为最后添加的子节点(或用正确的层级顺序管理)。 platformer/entities/player.ts读取废弃字段response.overlapV(见 player.ts,enemies.ts中也有同类用法,见 enemies.ts)。应改用response.depth/response.normal。- 粒子速度:在 20.2 之前调好的粒子速度如今看起来不对——粒子变换修复改变了单次爆发的飞行距离,发射器参数已被上调以匹配新行为。若你的粒子轨迹"变短了",请重新调整速度/能量参数而不是怀疑代码出错。
排查清单:代码"应该能跑"却不工作时
按顺序核对以下五项,能覆盖绝大多数 20.x 迁移失败场景:
- 有没有
await app.init()?缺失时没有任何报错,只是什么都不渲染。 - 代码是否假设一定在 WebGL 上运行?实际可能是 Canvas 或 WebGPU。
- 是否在用
renderable.shader =而不是addPostEffect()? - 是否导入了上表中的某个废弃符号?尤其注意大写的
Math与Entity。 - 是否在引用全局
game?模块作用域读取它必得undefined,请改用Stage回调参数或parentApp。
延伸阅读
本仓库的skills目录(随包发布、随引擎版本维护)提供了与本文配套的深度指南,可继续深入:
- melonjs-getting-started——20.x 正确的完整启动引导;
- melonjs-renderables——后处理特效、指针事件与自定义绘制代码。
同时可对照 CHANGELOG.md 中 20.0.0 / 20.2.0 / 20.3.0 的 breaking changes 原文,以及 deprecated.js 中每个废弃符号的精确废弃版本与替代指引,作为迁移时的权威依据。
- 游戏开发
- 图形学
【免费下载链接】melonJS
a modern & lightweight HTML5 game engine
相关推荐
Swagger-Client 从 2.x 升级到 3.x 迁移指南
Swagger Client 从 2.x 升级到 3.x 迁移指南 前言 Swagger Client 作为处理 OpenAPI/Swagger 规范的核心工具
后端终极Bacon.js迁移指南:从2.x到3.x版本升级完全手册
终极Bacon.js迁移指南:从2.x到3.x版本升级完全手册 Bacon.js是一款功能强大的函数式响应式编程库,专为TypeScript和JavaScrip
前端Coupons项目H5与小程序双端开发:跨平台技术深度解析
Coupons项目H5与小程序双端开发:跨平台技术深度解析 GitHub加速计划下的Coupons项目是一个专注于外卖红包优惠券的跨平台应用,支持H5与小程序双
小程序电商前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考