前言
本次春节活动中,我们大部分场景使用内部的 SAR Creator互动方案来实现。
SAR Creator 是一款基于 TypeScript 的高性能、轻量化的互动解决方案,目前支持了Web和字节内部跨端框架平台,服务于字节内部的各种互动业务,包括但不限于抖音春节、抖音直播礼物、抖音UG活动等。
SAR Creator 编辑器支持了图形化界面,提供了各类完善的系统(光照、动画、脚本等)供用户快速便捷的搭建一个高质量的互动场景,集成了场景搭建、资源配置、预览调试等功能,并提供了打包构建等工程化插件,可配合业务定制高效2D、3D内容生产工作流。
本文将就本次春节活动中编辑器的工作流程、开发常用功能、以及配套的工程配置方案等进行介绍。
工作流程
目前编辑器的工作流程能极大的提升开发效率,在于研发和设计能够极好的解耦工作。对于设计同学来说,不仅能在研发同学在线编辑器上协作,也能独立制作通过Asset Package包进行资产的交付;而对于开发来说,编辑器能够可视化的进行大部分场景搭建以及资产配置,能直观的检查资产信息。
资产制作
由于设计同学本地没有开发环境,为了方便设计同学脱离研发单独使用SAR Creator制作美术资产,我们提供了SAR Creator Launcher工具供美术使用,使用 Sar Creator Launcher 可以管理 Sar Creator 编辑器的多个版本安装、创建新项目、快速启动项目、查看编辑器版本的更新信息等功能。
设计同学使用Launcher搭建一个项目之后,可以将他从PS、Blender、Spine制作的资源导入到SAR Creator当中进行组合到场景当中,并导出成预制体资源,例如春节活动以下场景就是由设计同学制作然后交付给研发使用。
动效预览
设计同学产出的资源可以直接导入到SAR Creator中进行播放预览以及调整,美术可以确保最终交付给研发的动画资源是符合预期的。
目前支持的动画有 lottie、spine、帧动画、模型动画、组合动画以及帧差动画。
资产导出
设计同学最终的导出产物一般为prefabs资源,因为设计和研发的项目不互通,需要一种更快捷的方式同步资源,设计同学可以在创建的预制体右键导出资源包,此功能会将预制体依赖的所有资源导出为一个asset压缩包,设计同学可以直接将此压缩包发送给研发同学导入。
研发同学直接在game目录上右键选择导入资源包,即可选择设计同学交付的文件将资源导入。
功能介绍
画布适配
画布适配是指调整互动区域的视觉输出以适应不同的屏幕尺寸和分辨率,这样可以确保互动区域在不同分辨率的设备上看起来运行都正常。
SAR Creator 提供了简单的解决方案,首先需要指定设计的宽度和高度,它们定义了游戏将以何种分辨率进行开发,通过设定这些值可以确保互动区域在所在目标设备上正确缩放并保持一致的视觉呈现。
除此之外除了为了进一步增强画布的适配功能,我们提供了根据特定需求的各种适配策略。这些策略包括:
- FixedWidth(固定宽度):在此策略中,互动区域保持其设计宽度,高度根据纵横比自动调整。这确保互动区域始终占据屏幕的整个宽度,没有重要信息被裁剪掉。
- FixedHeight(固定高度):与固定宽度策略类似,互动区域保持其设计高度,而宽度则根据纵横比调整。这可以确保互动区域始终垂直填充屏幕,并在各种设备上提供一致的体验。
- Auto(自动适应):此策略将互动区域按比例缩放以适应可用屏幕空间的宽度和高度。取决于设备的纵横比,两侧或上下可能会出现一些空白区域,但整个互动区域内容都是可见的且无失真。
2D&3D混合开发
互动场景的编辑中,经常需要2D和3D元素相互结合才能创造出更好的交互体验,3D模型的移动经常能带来更逼真的效果,2D元素经常应用于背景图或者UI元素。
通过使用SAR Creator编辑器,可以很轻松的在同一场景中同时添加2D和3D对象。而且SAR Creator编辑器还允许自由调整这些对象之间的层级关系、渲染顺序以及视觉表现。此外,还可以为场景设置多种灯光和相机效果,进一步丰富场景表现力。
使用2D用来编辑器场景UI,对比使用前端技术编写UI界面逻辑,可视化拖拽编辑能大大提升开发速度,拿之前的一个项目举例:
- 左下界面UI是使用前端技术方案代码编写界面,耗时3天。
- 右下UI是使用SAR Creator 2D场景制作,耗时0.5天。
Bundle分包
在项目开发过程中,资源管理和加载优化是至关重要的。随着互动内容变得越来越丰富,会出现资源体积庞大、加载速度缓慢等问题。为了解决这些问题,SAR Creator编辑器提供了Bundle分包能力,帮助开发者轻松地对游戏资源进行分组、管理和优化。
Bundle分包使用起来非常简便,只需要选中文件夹,然后如左上图所示,勾选上配置为Bundle,就可以在构建的时候将这个文件夹内的资源拆分为独立的Bundle包,分组选项中有三种资源合并类型,功能介绍如下:
- 无:不做任何优化;
- 合并依赖:将Bundle中相互依赖的资源二进制文件合并在一起,从而减少运行时加载请求次数;
- 全部合并:将Bundle中所有资源二进制文件合并为一个,最大化减少请求数量,但可能会增加单个资源加载时间。
用户可以根据自己bundle包的资源情况合理选择分组方式。使用Bundle分包可以让开发者按需加载分包资源,无需项目初始化时就加载所有资源,这样不仅能减少项目首包的大小,还能提升游戏的启动速度和运行效率。
图片分级
图片资源的大小不仅影响着项目最终的包体大小,分辨率大的图片同样会占用设备更多的内存以及性能,而不同的设备的性能是不一样的,为了达到最好的显示效果又要满足用户设备的性能要求,SAR Creator编辑器对图片提供了分级功能,可以使不同性能的设备加载不同分辨率级别的图片,这在春节活动中得到了广泛使用。
当勾选去掉禁用分级后,构建的时候会生成normal、medium、low三个级别的文件资源,其中medium、low的图片分辨率会低于原始图片,会根据用户手机的性能评分下发不同等级的图片资源,而无需研发同学关心下发的内容。
需要注意的是,在使用图片分级时,实际上是会修改图片的分辨率大小,而当我们使用spine或者序列帧这类动画文件时,图片资源为图集,如果降低了图片的分辨率,它们就无法在图集上获取正确的像素数据,导致动画播放异常,因此针对图集文件或者动画文件的图片资源,我们需要禁用分级。
压缩纹理
压缩纹理,是一种 GPU 能直接读取并显示的格式,使得图像无需在 CPU 侧解压成 bitmap 即可直接传入 GPU 进行渲染,节约大量的显存开销,而在移动端即内存开销。
在图片的检查器中,勾选上使用压缩纹理即可开启使用,压缩纹理在运行时会根据运行环境自动判断是否使用压缩纹理,使用时用户无需关注。
启动图片上的压缩纹理配置后,可以配置选择etc、astc类型的压缩纹理,并通过排序决定运行时加载的优先级。同时可以配置压缩纹理的压缩质量、是否翻转等属性。
另外需要注意的是:
- 压缩纹理不能翻转纹理,即
flipY
表现始终为false
- 压缩纹理不能生成 mipmap,即
generateMipmaps
始终为false
- 为了适配不兼容压缩纹理的机型,图片资源仍然会被打进包体当中,所以包体会存在图片和图片的压缩纹理资源,包体体积会变得更大
透明像素扩边
在Canvas中绘制图片纹理时,图片边缘的像素会比较难处理,当我们选择边缘像素插值方式为linear时,虽然边缘会更光滑,但是由于插值方式的原因,图片的像素边缘会有一圈黑边,如下左图所示:
因此,为了解决这个问题,SAR Creator编辑器提供了透明像素扩边的能力,我们可以通过图片配置勾选上透明像素扩边,将图片的边缘增加一圈纯白像素,使插值的像素可以不为黑色,如果扩边单个像素为白色不明显时,我们也可以选择扩全部像素,将透明像素图片的所有透明像素点填充为通过插值计算的带有颜色透明度的像素点,避免边缘渲染时产生黑边。
不过透明像素扩边同样存在着一些问题,经过扩边操作的图片因为会修改原先的图片文件,所以会导致图片体积增大,具体的增大情况要看最终的图片产物。
工程配置
配置文件
SAR Creator的配置文件主要分为两块:
- SAR Creator项目的配置文件,其指定了项目的路径、入口文件等基础信息以及SAR Creator中预览的一些基础配置,方便运行项目的时候找到项目的位置。
- SAR Creator项目构建插件的配置,因为SAR Creator可以适配各种平台,因此SAR Creator的构建流程通过插件的形式嵌入到不同平台的构建流程当中,并可以在插件中深度集成平台的相关能力。
SAR Creator项目配置文件通常叫做sar.config.js
,这边以春节的摇签项目配置举例,项目的配置文件如下所示:
const path = require('path');
module.exports = {
// 指定入口文件目录
projectRoot: path.resolve(__dirname, 'game'),
previewConfig: {
previewDevtool: true,
resolve: {
alias: {
// 别名,脚本中使用@lottery代替绝对路径,用来避免sar creator报错
'@lottery': path.resolve(__dirname, './')
}
}
}
};
插件通常在平台的配置文件中引入,因此插件的配置通常也在平台的配置文件中,此次春节活动是基于字节内的跨端框架平台开发,所以接下来会结合跨端框架平台做插件配置的功能介绍。
分包配置
跨端框架平台的项目会主动下发资源到用户手机,但是为了提高资源包下发的成功率,单包的限制通常在5M,因为如果我们的项目资源大于5M时,就需要进行拆包,在上面我们已经介绍过了Bundle分包,其能很好的将互动资源进行拆分并动态加载,但是一个Bundle包不一定有5M,因此为了让资源包收益最大化,一个资源包可以由几个Bundle包组成,这个拆分手动的话会很麻烦,因此我们通过工程配置在,构建完成后进行分包,配置信息如下:
const lotterySarConfig = {
// ......
/** 分包策略,构建才会生效 **/
subPackages: {
// 可以过滤掉一些包,比如测试包需要被剔除
excludeBundles: ['test'],
// 组合bundle包为gecko包的规则
combineBundleRules: [
{
// 拆分到的gecko包名
channelName: 'pitaya-lottery-game-2',
// bundle包匹配符合条件的正则规则
test: [/mascot/]
},
{
channelName: 'pitaya-lottery-game',
test: [/main-scene/, /resources/],
preload: true,
preloadPriority: 96
}
]
}
};
内联配置
在Bundle包中,会存在各种config或者json配置文件,文件数量较多时,每次访问都需要请求一个链接地址,为了减少请求次数,我们可以通过配置插件构建,将每个Bundle包中的json文件合并成一个二进制文件,减少请求的次数和资源的大小,并且为了防止单个二进制文件太大,我们仍可以限制合并的二进制的文件大小,以确保不影响请求资源下载的速度,配置如下:
const lotterySarConfig = {
// ....
ejectOptions: {
// bundle的config文件不生成
disableGenConfigWhenInline: true,
// group分组配置信息
groupConfig: {
// 针对 bundle 下 group 的体积限制,优先级更高
groupPackingSizeLimitBundleConfig: {
// 名为mascot的 bundle 下单个group限制400k大小
mascot: 400,
// 同上
resources: 400
}
},
// 内置bundle文件的config配置到entry.ts里
inlineConfig: {
'main-scene': true,
resources: true,
mascot: true
}
},
}
预加载配置
跨端框架平台支持资源的预加载,为了配合平台特性,我们还可以在构建生成游戏的时候就配置Bundle里的资源支持预加载,然后在构建完成后就可以生成预加载的配置文件,可以使用户在打开容器时就可以让容器提前加载好配置文件里的资源,使项目内加载到资源的时候,能更快的加载资源,其配置如下:
const lotterySarPreloadUrlList = [];
const lotterySarConfig = {
// ......
//
preload: {
// 预加载资源前缀
publicPath: `https://${CDN_DOMAIN}${GECKO_PREFIX}/pitaya-lottery-main/`,
// 限制预加载资源个数
limit: 300,
// 限制预加载文件总体积, 默认就是3M
sizeLimit: 3 * 1024,
// 配置文件输出目录
outputDir: 'merge-js/lottery',
// 过滤资源的规则
asset: {
rules: [
{
// 匹配规则
test: /.*/,
// 可以匹配重写该资源是否加入 preload.json,返回 { enableMemory, url, priority } 或 null
rewrite: (item) => {
if (
item.url.endsWith('.png') ||
item.url.endsWith('.jpg') ||
item.url.endsWith('.jpeg') ||
// 已经加入的不再次加入preload
lotterySarPreloadUrlList.includes(item.url)
) {
// 不写入预加载配置
return;
}
lotterySarPreloadUrlList.push(item.url);
item.enableMemory = true;
// 写入预加载配置
return item;
}
}
]
}
}
}
最后预加载的配置文件如下所示:
{
// 跨端框架页面路径
"***/index/template.js": {
"image": [
{
// 是否开启内存缓存
"enableMemory": true,
// 资源加载路径
"url": "***/resource/image/back.bcd0a82e.png",
// 资源大小
"size": 280
},
// ....
],
"other": [
{
"enableMemory": true,
"url": "***/main-scene/g_0.cc991.group",
"priority": 96,
"size": 143296,
"assetBundleType": "group"
},
// ......
]
}
}
依据跨端框架的页面路径,来预加载配置的URL链接以及优先级,能够在页面打开前就提前加载好资源内容。
开发实践
脚本编写
在资产平台右键我们可以创建默认脚本,脚本格式如下所示:
// newscript.ts 脚本文件
import { Script, ScriptUtil } from '@sar-creator/toolkit/engine/core';
@ScriptUtil.Register('NewScript')
export default class NewScript extends Script {
onStart() {
const scene = this.world.getEntitiesByName('Scene')[0];
if (scene) {
// TODO:
}
}
}
SAR Creator 脚本的生命周期分为以下几个阶段:
- onAwake:节点挂载时执行一次
- onDestroy:节点销毁时执行
- onPause:暂停时执行
- onResume:恢复时执行
- onStart:第一次帧动画开始前执行一次
- onUpdate:每帧开始时执行
- onLateUpdate:每帧结束时执行
我们常用的生命周期有onStart
、onUpdate
、onResume
这个三个生命周期。
onStart
是在节点首帧时激活一次,表明游戏已经可以开始运行,我们一般在此阶段动态加载资源,将获取到的资源添加到场景中,或者获取场景的某些节点实例。因为我们的项目是SAR
Creator +
跨端框架组合,所以在跨端框架阶段初始化引擎的时候,我们会同步去获取服务端数据,当满足onStart触发以及数据获取成功两个条件时,我们一般会在脚本内依据数据初始化场景内容。
onUpdate
会在节点的每帧渲染前触发一次,由requestAnimationFrame
驱动,当我们需要变换场景中的元素的时候,就可以依靠每帧触发修改微小的变量,达到最终看起来连贯变换的样子,例如在摇签玩法中我们会依据陀螺仪的数据来确定签筒的状态,因此我们在此阶段每帧获取陀螺仪的数据,然后对其操作同步转换签筒的状态,让签筒有一种跟手的感觉。
onResume
会在游戏暂停后重启时触发,一般来说当我们有些定时操作会在暂停阶段销毁,在恢复阶段触发,除此之外,当节点隐藏恢复时也会触发此阶段,当一个节点启动时播放动画,播放完成后隐藏,当恢复时需要重置动画状态并播放就可以在这个阶段进行。
预制体制作
当我们将层级面板中的节点拖拽到资产面板中就会自动生成预制体资产,预制体资产储存了这个节点的所有的数据以及依赖的资源信息。
双击预制体资源,我们仍可以对预制体进行二次编辑等操作。
使用预制体资源可以帮我们可视化的预设大量的节点数据,我们可以通过直接加载制作的预制体,节省大量的实体修改参数配置等操作,从而提高我们的开发速度。
另外将场景中的非必要资源拆成预制体加载,可以有效提升主场景的加载速度,并渐进式加载其他装饰节点预制体到场景当中,并且可以依据用户手机性能,选择性加载不同复杂程度的预制体到场景当中。
例如在摇签玩法中,我们预设了不同的背景烟花以及签筒的预制体,使用时我们依据时机判断加载不同的烟花到场景当中,以及依据用户的手机性能,选择性加载要求更高性能的签筒还是性能要求更低的签筒。
动态加载
在项目搭建中我们已经介绍了Bundle的配置方式,用户可以将文件夹配置成动态资源包,那么加载动态资源包我们提供了资源管理器assetManager挂载在world上,例如加载一个预制体资源如下所示:
const {
bundle,
error: bundleError
} = await this.world.assetManager.loadBundle('resources');
if(!bundle || errror) throw bundleError
const {
asset,
error: prefabError
} = await bundle.load<Entity>(`prefabs/fireworks/${prefabName}.prefab`);
if (!asset?.get?.() || prefabError) throw prefabError;
const entity = asset.get() as Entity
我们需要先使用loadBundle获取到这个Bundle包的实例,loadBundle支持传入Bundle的名称或者链接,拿到Bundle实例后,我们按照资源在bundle包中的路径加载,就可以获得到一个asset资产对象,asset调用get方法可以获取当前加载资产的实例,例如加载预制体获取到Entity实例。
另外当我们加载图片资源时,我们从资产面板可以看到图片的texture虚拟资产Texture对象,如果我们想要直接加载图片的Texture对象时,就可以在加载图片的路径中拼接上index.texture,例如下所示:
const {
bundle,
error: bundleError
} = await this.world.assetManager.loadBundle('resources');
if(!bundle || errror) throw bundleError
const {
asset,
error: textureError
} = await bundle.load<Texture>(`texture/lamp/rect.png/index.texture`);
if (!asset?.get?.() || textureError) throw textureError;
const texture = asset.get() as Texture
未来展望
在过去的一年中,我们的SAR Creator产品经历了三个重要的大版本更新,分别是1.0、1.1、以及1.2,三个版本的迭代都是我们基于用户需求并不断优化用户体验推出来的阶段性版本。
在三个版本的迭代中,我们满足了研发同学的一些重要需求,例如脚本配置、bundle分包、性能优化等;增添了一些新功能,例如动画编辑器、VFX Particle、SAR Creator Launcher等;优化了SAR Creator的使用体验,例如多语言版本、材质属性分组、资产预览面板等。
基于过去一年的迭代,SAR Creator为春节的活动的开发带来了稳定的功能支持,在2024年中,SAR Creator将会持续优化引擎性能以及编辑器的易用性。
除此之外动画将是编辑器能力的核心,在2024年中我们将会不断扩展动画编辑器的能力,使用SAR Creator能够实现更加丰富的动画编辑功能,简单的动画元素将直接可以使用编辑器完成开发和预览。