赞
踩
字节跳动旗下的抖音等 App 在 2024 年春节期间推出了欢笑中国年系列活动,在实现增长业务目标的同时,为用户带来了全新的体验和乐趣。「招财神龙」是其中的一个重要玩法。
本次春节活动,使用到了字节内的主要前端、跨端、互动技术产品。主要涉及:
跨端框架 提供了首屏直出的方案使其具有较短的首屏时间,能够大大提升业务加载成功率。跨端框架也提供了 Canvas 作为 SAR Creator
等渲染引擎的运行环境。
SAR Creator 是抖音前端架构自研的一款基于 TypeScript 的高性能互动解决方案。SAR Creator 提供面向设计和研发同学的工作流,内置常见 2D / 3D 渲染能力、动效、粒子、物理等效果支持。
活动中,主要支持了 5 个互动玩法:“招财神龙”、“神龙探宝”、“摇福签”、“保卫现金”和“红包雨”,如下所示。
我们会通过系列文章,介绍春节玩法用到的互动技术。文章所说的互动技术指以图形 API(如:WebGL)为基础,结合前端工程化、图形渲染、引擎技术、交互能力和跨平台能力,面向前端技术栈的动效和游戏化技术,如下图所示。
在活动开发中,前端 UI 如:滑动列表、页面布局,可以用成熟的前端框架(React)。需要图形绘制的地方,如:渲染 3D 模型,就要用到互动解决方案(SAR Creator)。互动所用到的图形绘制部分往往是页面中的一个区域,我们会把互动部分封装成一个 SDK,通过使用 SDK API 和前端进行通信。
本文作为系列开篇,主要从“招财神龙”玩法视角,分享团队前端互动玩法的相关开发经验。
下面是招财神龙玩法示意,用户可以点击“去寻宝”按钮(称此时的场景为「家场景」),让神龙去寻宝(称此时的场景为「寻宝场景」),寻宝过程中神龙会遇到福袋和龙蛋。福袋自动掉落到宝箱中,而龙蛋需要用户点击。寻宝过程中,红色的主按钮上有倒计时,倒计时结束后寻宝结束,用户可以打开宝箱领取奖励。寻宝过程中,场景中会有一些可点击的发光建筑,用户点击它们,发光效果消失,可能触发任务。
在「家场景」,用户可以点击小女孩,与之产生轻互动,如下图所示。
包含四个主题的「寻宝场景」,每次寻宝会随机一个主题,如下,从左到右分别是山川、雪乡、丹霞和江南。
实现招财神龙的互动玩法,需要多个工种配合。首先产品要提出玩法需求,描述整个场景的构成要素和玩法逻辑。然后设计同学根据产品的描述,产出设计草图,逐步细化,最终通过 DCC 软件(如:C4D)生成 3D 模型、视频、2D 贴图或动画等美术资源。程序需要根据产品需求和设计产出实现互动玩法的代码逻辑。整个开发过程需要三方通力合作,尤其需要程序和设计同学的有效沟通,以确保设计方案可以用程序顺利实施。为了保障产品质量,还需要测试人员验收产品。整个开发过程大致如下图所示。开发过程是持续迭代的,比如:产品可能在开发中期提出新需求,就需要设计、程序和测试做出响应。
这里以程序的视角描述招财神龙互动玩法的实现。如上文所述,招财神龙互动部分由「家场景」和「寻宝场景」构成,两个场景通过一个转场动画过渡。每个场景使用了不同的美术资源和互动技术。程序不直接消费 DCC 软件生成的美术资源,而是消费 SAR Creator 产出的资源包(即 bundle)。
bundle: 设计在 SAR Creator 编辑器中导入 DCC 软件的产物(如:3D 模型),通过二进制序列化生成的运行时消费用的资源包。
prefab: 一个 bundle 可以包含多个 prefab(预制体),一个 prefab 可以包含 3D 模型、2D 贴图、动画甚至脚本代码等元素。
SAR Creator 为 bundle 及 prefab 提供了序列化、反序列化和管理等功能。
接下来让我们先了解一下招财神龙页面元素的构成。
招财神龙活动在抖音App及多端(抖极、头条、西瓜、番茄等)的任务页上线,为了让大家对整个招财神龙前端页面有个清晰的认识,这里我们以任务页为例,为大家讲解一下页面构成。
如上图所示:
任务页(图左):字节系 App(例如西瓜视频),大多会有一个长期在线的激励页面,如上面左图所示,用户可以通过完成任务获得现金、或者积分等虚拟货币奖励。
互动区域(图右):如上面右图所示,互动区域即为场景区域,是页面主 KV (Key Visual) 中的核心区域,用 Canvas 承载,使用 SAR Creator 来渲染互动内容。
任务页在非活动期间,以日常的形态展示(各 App 独立迭代),而在活动阶段,则以统一的活动内容展示。这是怎么做到的呢?
如上图所示,我们把任务页抽象为收益区 + 主 KV + 任务专区。在有活动的时候,我们只需要替换主 KV 对应的内容就可以了。在实际开发中,活动的主 KV 则抽象为活动 SDK。在满足活动条件时,服务端下发活动内容字段,任务页动态渲染活动组件,完成活动内容的展示;在活动结束后,服务端移除活动内容字段,页面切换回日常形态。
在任务页上开发互动内容,存在较大的性能挑战。任务页前端 UI 繁多,业务逻辑复杂,而互动的资源加载往往又是 CPU 密集型任务,所以往往在首次渲染页面时,造成页面和互动区域的 JS 线程繁忙,进而形成卡顿和渲染时间过长。同时由于任务页已经存在大量的前端 UI 和动画,留给互动部分可用的内存安全余量往往仅有 200-300 MB,稍有不慎就有可能导致 OOM。在任务页上既要完成视觉表现精美,又要保证性能良好,是一件非常有挑战的事情。
我们将前端的同学分为两部分,一部分负责处理活动的主逻辑,例如和服务端交互、处理业务元素(例如进度条、明信片等挂件、任务列表等),这一部分的工作角色,我们通常称之为“前端同学”,另一部分同学主要用来处理游戏相关的逻辑,聚焦在互动上,我们称之为“互动同学”。 他们相互协作,共同实现了招财神龙的活动玩法。二者的协作方式如下图所示。
游戏初始化阶段,游戏加载完 SAR Creator 运行框架后,向前端同学“索要”本次初始化的服务端数据,用来判断该用户进入游戏后,应该展示的是「家场景」还是「寻宝场景」。用户完成相关任务后,主接口刷新。前端同学以事件通信的形式通知互动同学渲染当前场景并播放相关动效。互动同学也会监听主接口数据,更新互动模块专有的逻辑或效果。
「家场景」是引导用户“唤醒神龙”、“去寻宝”以及“领取福袋”的核心场景,如下图所示。本章节会将介绍「家场景」的搭建过程,并分享「家场景」开发过程中有趣的实现。
整个「家场景」是由 3D 和 2D 元素混合构建的。3D 部分包括小女孩、龙、地面和雪堆。2D 元素主要有炮仗、房子以及神龙回家后小女孩头上的提示气泡,是用图片实现的。还有一些 2D 动画元素,比如房子后面一直循环播放的红包动画、龙沉睡时嘴角的“zzz”呼吸效果。
设计同学使用 SAR Creator 编辑器搭建「家场景」,包括 3D 模型/2D 精灵的摆放、灯光和相机参数的设置等。SAR Creator 编辑器提供了图形化界面,可以方便地调整场景元素的层级关系、位置、朝向、缩放比例以及材质参数等。「家场景」的 3D 模型使用透视相机渲染,而 2D 精灵等使用正交相机渲染。最终,SAR Creator 渲染出的场景画面还原了设计稿的效果。
SAR Creator 场景中所有元素,包括相机、灯光等,都以 entity(实体)的形式存在,entity 之间存在父子关系,形成一棵节点树,如下图左上角“层级”标签页下的内容。父节点 entity 的 Transform3D 组件的位置、旋转和缩放属性,会影响子节点的相同属性。Enity 上可以挂载自定义脚本,影响 enity 的行为逻辑。SAR Creator 提供了大量操纵 entity 的引擎能力。
为了呈现出精彩的效果、给用户带来尽可能好的视觉体验,我们设计了14个模型动画,并通过出色的逻辑串联,保证了动画播放流程的简洁高效。
- export enum HomeAnimName {
- HomeSleep = 'home_sleep', // 沉睡
- HomeAwake = 'home_awake', // 苏醒
- HomeIdle = 'home_idle', // 待机
- HomeClick = 'home_click', // 点击效果1
- HomeClickA = 'home_click_a', // 点击效果2
- HomeClickB = 'home_click_b', // 点击效果3
- HomeHappy = 'home_happy', // 完成任务,开心状态
- HomeGoHome = 'home_gohome', // 龙回家
- HomeHoldBox = 'home_hold_box', // 宝箱状态
- HomeOpenBox = 'home_open_box', // 龙推宝箱
- HomeCloseBox = 'home_close_box', // 关闭宝箱
- HomeCloseBoxIdle = 'home_close_box_idle', // 关闭宝箱后的待机态
- HomeOpenBoxIdle = 'home_open_box_idle', // 开完宝箱后的待机态
- HomeGoOut = 'home_goout' // 龙去寻宝
- }
我们使用了 SAR Creator 提供的动画播放能力:Animator 组件。获取到 3D 模型的 animator 组件,并调用它的crossFade
函数,在第二个参数duration
指定的时间内,从当前动画状态过渡到另一个动画状态,即下面代码中的第一个参数anim
。调用animator.on('finished',cbFunc)
可以自定义动画结束后的回调函数。
- this._dragonAnimator.crossFade(anim as string, duration);
- this._charAnimator.crossFade(anim as string, duration);
- this._dragonAnimator.on('finished', () => this.onAnimEnd(anim, params));
设置动画的loopCount
属性,可以指定该动画播放的次数。设置clampWhenFinished
可以指定播放完该动画后,是否停留在最后一帧。
- const setClip = () => {
- const loopCount = loop ? -1 : 1;
-
- const _dragonClip = this._animator.clips.find((i) => i.clip?.name === anim);
- if (_dragonClip) {
- _dragonClip.loopCount = loopCount;
- const action = this._dragonAnimator.getAction(anim);
- if (action) {
- action.clampWhenFinished = !loop;
- }
- }
- }
基于上述的这些底层的Api,我们实现了一套AnimationGraph来帮助研发和设计同学更好地开发提效。
对于设计同学使用来说,例如想实现一个龙睡觉状态到龙待机状态,我们可以将HomeAwake
HomeIdle
动画拖入到graph中,并创建动画链路。
HomeAwake
动画播完以后,会在HomeIdle
动画进行loop播放。选中链路,可以对链路进行配置和预览。
对于研发同学,可以基于graph进行逻辑条件的配置。
如上图所示,例如进入游戏后,用户可能是在“龙沉睡”或者“龙待机”的状态,我们通过在Graph的变量区建立代码运行的逻辑条件(支持Number和Boolean两种类型),可以自定义一个case
变量,当case = 1,播放“龙沉睡”、当case = 2,播放“龙待机”。
在代码中,我们可以通过使用AnimationController.setValue(variableName,value)
来触发动画执行。
- const animationController = this.entity.getScript(AnimationController);
- if(showAwake) {
- // 需要播沉睡
- animationController.setValue("case", 1)
- }else if(showIdle){
- //需要播放idle
- animationController.setValue("case", 2)
- }
再比如,在某一个时间,用户点击了“去寻宝”按钮,这时候通过设置animationController.setValue("showGoOut",true)
即可触发龙去寻宝的动画。
我们还为动画播放提供了钩子函数,在动画播放的特定时间,触发自定义的逻辑回调。
onStateEnter | 在进入状态时触发 |
---|---|
onStateExit | 在完全退出状态时触发 |
onStateUpdate | 在状态更新时触发 |
- // 获取动画控制器组件
- const animationController = this.entity.getScript(AnimationController);
-
- animationController.on('onStateEnter',(controller:AnimationController,state:AnimationState)=>{
- //在此处实现业务逻辑
- });
在实现一些特殊效果时,为了保障效果的高度还原,我们使用了坐标同步。例如小女孩头上的提示气泡和龙嘴角的“zzz”呼吸特效,接下来以气泡为例介绍一下这一部分的实现。
若用常规的模式在 3D 场景中摆放一个 2D 的片,会导致小女孩动的时候,渲染出来的气泡会穿帮或者 z-fighting。
3D-2D 坐标同步的做法是将 Bubble 节点放在 UICanvas(SAR Creator 处理 2D 元素的节点)中,每一帧将小女孩模型里的骨骼变换节点在 3D 空间中的位置转化成 UICanvas 坐标系的坐标,再实时设置 Bubble 的位置属性。
坐标同步代码如下
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。