# 开放数据域框架

开放数据域 是一个封闭、独立的 JavaScript 作用域。开放数据域是微信端特有的功能,使用它可以让开发者创建一个“子域”,并且只有在这个“子域”中,才能拿到用户相关的数据。

# 工具快速上手

点击开放数据域新手引导,直接在工具中按流程学习,并查看Demo。

# 创建子域入口

要使用开放数据域,需要进行一些配置,来指定某些代码运行在开放数据域。

第一部分的工作是创建子域入口:

目录

如图,只需要创建好一个目录(注意,此目录必须是**根目录下的openDataContext**目录),并在此目录下编写子域代码即可,在子域内编写代码和主域基本没有区别,可以正常使用小游戏框架的所有功能,但资源方面会有一些限制,同时将会对接口也产生一些 限制。见后面的章节。

在创建了目录并编写好了子域代码后,只需要修改目录配置:

配置

中的设置为开放数据域文件夹勾选即可。然后工具会默认创建启动模版,开发者可以在对应的 index.ts 的engineStart()的 resolve 回调中执行自己的逻辑。比如 engine.loader.load 一个场景,并初始化它。这个场景只能加载放在子域文件夹的代码,无法加载主域的,同理,主域中也无法使用子域中的代码。

# 使用Scene/Prefab渲染内容

为了让开发者更简单地实现渲染,小游戏框架支持在开放数据域里面直接使用Scene/Prefab渲染,包括了2D/3D渲染。从而让开发者也能非常简单地实现王者荣耀一样查看好友3D皮肤等能力。

开放数据域里面的框架和主域中的框架是两个实例,完全隔离。本质上开放数据域里面的渲染实际上是渲染在一个跨域共享的离屏画布上面,然后再将上面的内容作为一个贴图绘制在主屏的画布上。对应到小游戏框架上的概念,就是将开放数据域的内容渲染到一个renderTexture上面,然后在主域中可以使用这个 renderTexture 作为纹理来任意绘制到主域的物体上。

开发者可以给想要运行在开放数据域的 Scene 或Prefab加上对应的脚本组件,这些脚本可以获取关系链这样的数据,然后实时修改 entity 来实现排行榜等能力。这些代码必须放在assets/openDataContext/目录下,并只能在开放数据域中使用。目录下面的代码会运行在开放数据域,主域和开放数据域中的代码不能相互 require。

# 资源下载

首先想要在开放数据域里面使用Scene/Prefab必须要下载对应的资源。但是游戏资源比较庞大,一般推荐打包到资源 CDN 包,并部署到开发者自己的 CDN 服务器上。但是又因为数据安全问题,开放数据域不能下载外部 CDN 保存的资源,导致能力很难实现。

所以小游戏框架针对这种情况有几种解决方案。

  • 云开发存储。小游戏开发者可以使用官方提供的 CDN 服务,把构建出来的完整的资源 CDN 包上传到官方提供的云开发存储上面。那么在开放数据域中加载任何游戏资源都能按需下载。比如想要实现王者荣耀的查看皮肤能力,因为无法知道好友皮肤是哪一个,且提前把所有的皮肤下载好会特别久,那么如果使用了云开发存储,就能按照好友的关系链数据来按需下载对应的皮肤资源。开发者的代码也只需要像主域中加载资源一样加载即可。

  • 放到主包中。对于被放在主包的入口资源,开放数据域也能直接加载到。但是因为会占用包体积大小,所以建议不放在里面。

# 数据传输和共享纹理

主域和子域可以认为是完全隔离的,但在两个域之间仍然有方法进行通信。

# 主域 -> 子域

小游戏框架提供了一个单例engine.crossContextDataBus来管理主域向子域的通信,其使用方法如下:

// 在主域,连续发送两条类型为`test`的消息
engine.crossContextDataBus.send('test', {data: 1});
engine.crossContextDataBus.send('test', {data: 2});

// 在子域,监听类型为`test`的消息
engine.crossContextDataBus.addReceiver('test', (dataList: {data: number}[]) => {
  console.log(dataList);
});

可以使用改单例的send方法在主域发送特定类型的消息,在子域则可以使用addReceiver监听消息,每次消息的派发会将该帧数据合并一次性派发,当然,也有removeReceiver方法来移除监听器。

# 子域 -> 主域

子域向主域是无法通过crossContextDataBus发送任何消息的,只能通过共享纹理RenderTexture来对绘制结果进行共享。创建共享纹理有两个方式:

  1. 在工具内创建和绑定

首先,开发者需要创建一个rendertexture资源:

创建RT

接着将其配置为共享纹理:

配置RT

再接着将资源设置为一个子域场景中的相机的渲染目标,并将其作为主域Sprite的纹理:

子域设置RT

接着创建一个 spriteframe 资源,设置他的 texture 为renderTexture,或是在 material 中使用 renderTexture 作为一个贴图 主域设置RT

即可完成共享。

  1. 使用代码创建绑定
// 在主域,创建一个共享的RenderTexture
const sharedRT = new engine.RenderTexture({width: 1024, height:1024, isShared: true, uuid: '4444'});

// 在子域,也创建一个同样`uuid`的共享RenderTexture
const sharedRT = new engine.RenderTexture({width: 1024, height:1024, isShared: true, uuid: '4444'});

如此一来,两个域的共享纹理便被关联了起来,开发者可以将子域的 RenderTexture 作为相机的RenderTarget,然后将作为Uniform进行绘制:

// 子域,将共享纹理作为 UI 相机的渲染目标(当然也可以是任何相机)
game.rootUICamera.renderTarget = shardRT;

// 主域,将共享纹理用于`SpriteFrame`的渲染
var ui = game.createEntity2D('ui');
var sp = ui.addComponent(engine.UISprite);
sp.spriteFrame = engine.SpriteFrame.createFromTexture(shardRT);

# 触摸事件传递

如果在子域里面想绘制一个排行榜,就必须让 renderTexture 上面也能监听到事件,然后传递给子域中,让排行榜能用手指滚动。

开发者可以给用来渲染子域的带有 UISprite 组件的 entity 上,再添加两个组件。

touchInputComponent,原本就是用来监听触摸的组件。

openDataContainer,用来将触摸的事件按照主域绘制区域大小来转换到子域中,子域的场景中使用的 touchInputComponent 组件就能收到回调。

# 具体运行步骤:

  • 首先开发者可以在 Project 窗口下的任意文件夹右键新建 >> image >> renderTexture,选中后就可以在 Inspector 窗口中设置 isShared 开启,从而让这个纹理变成一个跨域共享纹理的纹理。

  • 然后在一个想要渲染在子域中的3D Scene中创建一个有 Camera 组件的entity(如果是2D则是创建一个有 UICamera 的entity),并且将这个 Camera 的renderTarget属性设置为这个 renderTexture 资源,从而让相机绘制到这个纹理上。然后也可以在assets/openDataContext/目录下创建脚本组件,并添加上 entity 上,这些脚本是可以访问关系链数据,并修改 entity 的绘制内容的。最后保存场景。

  • 右键新建 >> image >> spriteframe创建一个 Spriteframe 资源,选择这个 SpriteFrame 资源,然后将最开始创建的 renderTexture 资源赋值给Image File属性。这样 spriteFrame 渲染的内容就是这个 renderTexture 被画上去的内容。

  • 在将被运行在主域的2D Scene中新建一个带有 UISprite 组件的entity,然后将 Spriteframe 资源赋值给 UISprite 组件的 spriteFrame 属性,表示这个2D组件将会用这个 spriteframe 的内容来渲染,最后保存场景。(按需使用openDataContainer)

  • 在开放数据域中加载场景。首先在一个主域的代码中,加载刚刚创建的2D场景,并触发engine.activeOpenDataContext(),这样开放数据域就会被初始化,然后在assets/openDataContext/index.ts中engineStart()返回的 Promise 的resolve函数就会被回调,开发者可以在这个回调里面加上自己的代码去加载刚刚的2D Scene。

engineStart().then(() => {
  engine.loader.load('resource/rank.scene').promise.then((s) => {
    engine.game.playScene(s as engine.Scene)
  })
})
  • 完成上面的步骤后,在游戏运行时,先由主域启动2D Scene并触发开放数据域的创建,创建回调里面会在开放数据域中创建3D Scene,然后相机组件会把开放数据域中的绘制结果画到 renderTexture 上,最后被主域的2D相机画在主屏上面。上面只是描述了一种最基本的场景,开发者可以基于 renderTexture 的通用特性,自由发挥。比如将 renderTexture 通过 material 画在3D的物体上。

# 限制和约束

子域内可以像主域一样使用engine.loader.load()去加载资源,或是使用new engine.Image()去加载图片。但是对资源的远程地址有着限制

  • 使用engine.loader.load()

    只能加载事先上传到微信云开发存储空间里的资源。

  • 使用new engine.Image()

    可以加载 微信云开发存储空间里的资源 和 微信用户头像( https://wx.qlogo.cn/mmhead/ )。

在实际开发中,考虑到主域和子域的统一,可以选择直接将微信云开发作为主力静态服务器,来减少资源上传的工作量。小游戏框架的开发工具里提供了资源上传微信云开发的辅助工具,来帮助开发者更快地上手。如果子域用到的资源非常少,甚至可以考虑直接打包到游戏代码包里。

除此之外代码接口也有限制,可以查看开放数据域基础能力

# 不使用微信云开发的方案

*该特性为实验特性。

使用微信云开发来为开放数据域提供资源,从任何方面来说都是最佳的选择。

但是如果你的游戏出于某些原因无法使用微信云开发,那么可以让主域向开放域发送下载好的文件,来为开放域提供资源。

出于安全性考虑,该方案有着以下限制:

  • 无法发送缓存的资源,只能发送本次游戏中下载的文件;
  • 同样适用于注册表文件(可能较大),会导致游戏打开变慢。
  • 同样适用于注册表文件(可能较大),在子域启动前会重新下载注册表文件,可能会导致子域启动较慢。

如果能接受以上的限制,那么可以参考以下步骤来使用该特性:

  1. 在主域内,确保需要发送的文件都下载完毕了之后,发送下载的文件到开放域。
    使用engine.loader.load(xxx, options).promise.then(...)options中 cacheable 不能为true。当进入 then 的回调时,说明该资源需要的文件都已经下载完毕了。
    之后调用engine.loader.sendDownloadedFilesToSubContext()来将主域下载好的文件发送到开放域。

  2. 在发送文件后,开放域内加载资源、播放场景。
    在主域调用engine.loader.sendDownloadedFilesToSubContext()后,开放域内才能调用engine.loader.load(xxx)
    具体时序需要用户自己保证。可以先发送文件再启动开放域;也可以先启动开放域,然后使用engine.crossContextDataBus来通信。
    注意:启动开放域时,需要使用engine.activeOpenDataContext()(代替wx.getOpenDataContext)。

  3. 正常构建项目,远程资源上传方式不要选择云开发存储。

  4. 手动修改构建出的minigame/assets/game.js
    搜索engine.loader.register(packRegisterPath, urlPrefix),替换成engine.loader.register(packRegisterPath, urlPrefix, true)
    (会导致游戏打开变慢,已改为子域启动前自动下载)

  5. 顺利的话就可以在模拟器和真机中运行了。

  • 备注: engine.activeOpenDataContext() 接口可以接收参数 { idepackRegisterURL: string, urlPrefix: string[] } 用来自定义子域远程资源的资源注册表地址,方便业务侧自己控制资源版本.