白鹭引擎游戏开发笔记

在之前的文章 使用白鹭引擎开发微信小游戏的一些流程改进 中我介绍了基于白鹭引擎在开发微信小游戏中流程的一些改进方案,本文将介绍在开发 HTML5 游戏中遇到的一些切实的问题与解决方案。

cacheAsBitmap 的尺寸限制

当我们对一个尺寸很大的显示对象使用 cacheAsBitmap 功能时,会发现在部分浏览器中(尤其是移动端浏览器中)可能会抛出错误:

1
WebGL: INVALID_VALUE: texImage2D: width or height out of range

错误表示我们我们的 2D 位图宽度或高度超出了浏览器的限制。

据笔者测试,最新的 Safari —— iOS 12 —— 中
WebGL/Canvas 能缓存的 2D 位图宽或高不能超过 4096。当我们的游戏是一个比较大的场景,我们又想对场景的不变部分做 cacheAsBitmap 时可能触发此问题。

要规避此问题,要么对大的场景做分块,要么不使用 cacheAsBitmap

利用事件机制解耦代码

类与类或模块与模块之间的接口调用、状态传递在软件开发中时无法避免的,但是很多时候因为开发时间的限制或者自己的疏忽带来的模块之间的强耦合。

订阅-发布的设计模式较常用于模块间的解耦 —— 包括时间上(异步操作)的解耦与空间上的解耦,并且 JavaScript 本身其实也是一门基于事件驱动的语言。egret.EventDispatcher 是 egret 自带的事件派发器类,我们可以用它来实现模块间的解耦。尤其是当模块高度独立,模块有自身的生命周期(或特定时间/状态点),需要对外提供针这些时间/状态点的无侵入接口时最适合使用。

EventDispatcher.dispatchEvent('eventXXX') 会立即触发事件 eventXXX 的回调,而不是异步地执行回调函数

但是要注意下面这些问题:

  1. 及时移除不再需要的事件监听函数,减轻内存负担
  2. 发布-订阅机制减少了模块/对象之间的联系,这也表示着我们在阅读代码/排查 Bug 时要摸清模块/对象之间的联系需要花费多一些功夫

例如下图的例子:

金币组件只需订阅金币变动的事件,金币数据在被 set 时派发金币变动的事件,这样我们便只需 fetch 到最新的金币数据然后更新金币数据即可,而无需再去调用方法变更金币视图。

此外,游戏中各种状态的切换(初始、准备、匹配、游戏中、游戏成功、游戏失败)通过事件的机制也能减少很多重复的代码,可以尝试使用。

白鹭下 MVC 模式的注意事项

MVC 作为一个经典的架构,我们可以将其应用到我们的游戏开发中。

图中 Model 通知 View 便可以通过白鹭的 egret.EventDispatcher 来实现。

有一些需要注意的事项:

  1. 严格遵守 MVC 中数据的流向与调用关系,不可逾权
  2. 如果用户的操作不需要更新 Model,而只需要更新视图(例如:点击一个按钮就展示一个弹框),则直接在操作回调中更新视图,不用再走 Controller -> Model -> View 这条路径了
  3. View 持有 Controller 和 Model,读取 Model 用于视图展示,回调内调用 Controller 方法;Controll 持有 Model,调用 Model 的方法更新 Model(忌直接更改 Model 的成员);Model 只需要关注自身即可,方法内更新自身状态时,派发必要的事件供 View 响应变化

显示对象的层级

显示对象就像一棵树,我们需要维护这棵树的枝叶,让这棵树正常的生长,否则仍由其狂乱生长,会成为长期开发与维护的噩梦。。

一般在游戏中都会有两种需要维护的显示对象:场景(Scene)与场景内的弹出层(Layer),场景之间的切换与恢复,弹出层之间的叠加与切换如果没有专门的管理代码会将你的游戏代码带向深渊。

下面是两个比较简单的 SceneLayer 管理组件,能满足简单的游戏开发工作。

SceneManager

代码简单易懂,可以直接阅读代码:SceneManager

注意:上面的代码不包含场景间切换的资源管理,请自行实现

LayerManager

弹出框管理:LayerManager

不要在一整个 Scene 上绑定点击事件(目标太大),否则 Layer 上的 Touch 事件也会造成 Scene 上 Touch 事件的触发。

声音管理

白鹭通过持有音频播放后返回的 SoundChannel 对象来进行管理,其本身不提供对音频的集中式管理,例如:全局静音、全局音量调节等。

SoundManager 此组件包含了常用的音频管理功能,同时内置了缓存机制。

图片跨域

白鹭的 Canvas/WebGL 加载网络图片的原理都是使用 Image 对象,当使用的图片与当前域名不一致时会遇到跨域问题(浏览器出于内存安全的角度考虑)。白鹭的 ImageLoader 对象有一个 crossOrigin 属性,当 imgLoader.crossOrigin = 'anonymous' 来以匿名的方式访问时可以规避跨域问题,但是在使用 texture.toDataURL 时还会报跨域问题。

imgLoader.crossOrigin = 'anonymous' 表示对图片的请求不会携带 cookies 等用户信息,防止用户状态被不知情的情况下被使用;又由于纹理是存储在内存中的,WebGL 又是直接对内存的操作,所以为了防止内存的安全,跨域图片的 texture.toDataURL 不被允许。

微信小游戏场景下系列问题

重刷新机制

微信小游戏不能像在网页上一样刷新页面,在某些特定的情景下我们可能需要一个像网页刷新页面一样的刷新机制。所以我们需要提供一个刷新的接口,接口的主要工作是初始现有的软件状态、舞台上的显示对象,重新运行游戏的入口逻辑。

1
2
3
4
5
6
7
8
// Main.ts
// 用于微信小游戏的刷新
public refresh() {
this.removeChildren()
this.runGame().catch(e => {
...
})
}

长连接的重连

小程序进入后台运行后(非置顶聊天),如果 5s 内网络请求没有结束,会回调错误信息 fail interrupted;在回到前台之前,网络请求接口调用都会无法调用。

所以一旦不在小程序界面超过 5s(包括停留在分享界面),长连接便会断开,即使长连接有自动重连机制,如果在重连期间没有回到小程序界面,长连接还是会失败。所以除了长连接的重连机制,可能还需要在回到小程序界面时或者用户操作前进行连接状态的检测与必要的重连。

如果你的小程序/小游戏后端服务器使用的是 pomelo,那么建议你使用我进行了扩展增强的 pomelo 微信客户端:pomelo-client-wx,支持重连和多个 pomelo 实例。但是你还是要实现上面所说的 在回到小程序界面时或者用户操作前进行连接状态的检测与必要的重连

其他

  1. egret.Event.ENTER_FRAME 是根据配置的帧率决定触发频率的,而 egret.startTick() 则是保证回调是每秒 60 次的调用频率

  2. egret.Event.RENDER 监听此事件将会在本帧末即将开始渲染的前一刻触发回调,egret.callLater 注册的函数将延迟到屏幕重绘前执行

FuChee wechat
扫一扫,关注我