从零开始开发3D游戏,助力B类场景互动创新尝试

简介: B类场景里的互动创新。

作者 | 楺楺

image.png
近些年,越来越多的业务都引入了互动游戏,相信大家都玩过双十一盖楼、养猫、蚂蚁森林等小游戏。那么互动在B类场景中是否也能带来价值,我们发起了第一次尝试:一达通 0-1 破蛋结合互动游戏做客户激励,拉动客户从准入到上行,游戏部分的效果如下:

点击查看视频

👇 微信或钉钉扫码玩游戏,挑战最高分2500~
image.png
截止目前的数据,线上每人平均玩了10次,说明这种竞技类小游戏对提高用户粘性和活跃度发挥了较大的作用。当然只有挑战性和趣味性是不够的,再结合任务道具、组队PK、自选赛道、分享/传播等手段,将带来更好的业务效果。

场景分析

整个游戏界面分成了3层:
image.png

  • DOM UI层:这一层主要来放置2D相关的UI元素,比如分数面板、游戏说明面板、游戏结束面板等,然后通过事件与3D场景进行通信。

  • 3D场景层:包含了阿牛、赛道、能量块在内的整个3D场景,主要通过 Oasis Engine 和 Oasis 3D Editor 实现,最终表现为一个 canvas 组件,下文会重点介绍这一层的实现。

  • 背景层:即放置在最底层的蓝天大海背景。

⚠️ 值得注意的是,不要直接 css 设置 canvas 的背景,这样会导致微信或 Safari 压后台之后出现场景重叠的异常现象。推荐的做法是把 gl.clearColor 的背景色设置为透明,然后在 canvas 外部放置一个背景层,让 canvas 专注于3D渲染。

实现 3D 场景

对比我们平常的业务开发,3D场景开发包含了以下几个流程:
image.png

场景搭建

我们从最简单的3D场景搭建开始。得益于 Oasis Editor 的功能,我们可以直接编辑3D场景,所见即所得。

首先我们要理解好 ECS(Entity-Component-System)架构,在 Oasis 3D 中,万物皆组件,一个实体代表什么取决于它身上挂载了什么组件。比如我们创建了一个实体,上面挂载了相机组件,那么它就是一个相机实体。如果再给它添加飞行组件,那么它就是会飞的相机。

基于这样的思想,我搭建出一棵完整的场景树(左侧红色框内):
image.png
其中,角色实体上挂载了 GLTF 模型组件,最终渲染出阿牛的模型。一般设计师在3D软件中导出 FBX,Oasis Editor 会将它转换成适合 Web 环境的 GLTF,并解析出骨骼动画、材质、纹理等信息。另外角色实体上挂载了不少脚本组件,这些都是包含了游戏逻辑的自定义组件。

选择脚本开发方式

Oasis Editor 包含了云端代码编辑器,而且提供了事件测试面板。3D场景中监听的事件会出现在输入事件列表中,我们可以配置事件参数进行触发;3D场景中触发的事件则出现在输出事件面板中。
image.png
针对本游戏,我们希望能通过 git 管理项目,并且为了使接入方只需关心3D通信部分,把强制横屏、降级、赛道配置化等交给游戏组件本身,而这些并不属于 Oasis 3D 的脚本,所以选择了本地开发的形式。

脚本创建及绑定是在 Oasis 场景编辑器里面操作,然后将项目下载到本地进行开发。之后我们会经常需要调整3D场景或者添加一些新的脚本,这时候就需要用 @alipay/oasis-cli 把3D场景拉取到本地。⚠️ 需要注意的是,oasis pull -s 会覆盖掉本地的脚本,所以一般执行 oasis pull,只拉取 schema 配置。

逻辑开发

1 、控制角色运动轨迹

利用碰撞检测来获取3D空间中两个物体之间的相交情况,需要碰撞体包围盒和碰撞检测器。为了实现不同碰撞带来的不同动作,我把角色的碰撞体包围盒设置得比实际大一点,所以角色和蓝色方块的碰撞体默认是相交的。如图所示,深蓝色立方体是角色的碰撞体包围盒
image.png
用户第一次点击的时候做 x 轴直线运动,第二次点击时增加 y 轴分量,做上抛运动。
image.png
image.png
检测角色与方块碰撞的 end_overlop 事件,如果碰撞体对象为蓝色方块,则直接做自由下落运动。
image.png
检测角色与方块碰撞 begin_overlop 事件,如果撞到蓝色方块,则计算方块与角色的相对位置。

  • 如果角色在方块上方,只做x轴直线运动。

image.png

  • 如果角色在侧方或下方,则做自由下落运动。

image.png
image.png

  • 检测角色与终点站台的碰撞,如果角色悬空位于站台上方,则做自由落体运动,同时切换相机视角。起始状态和终点状态都是可以计算出来,所以这里利用 tween 动画实现平滑的自由落体运动。

image.png
2、 相机跟随

相机是3D场景中必不可少的元素,它相当于一个观察3D世界的眼睛,只有在相机锥形体内的区域才会被渲染到屏幕上。在角色奔跑的过程中,我们希望相机永远跟随在角色的右后方,所以在每一帧都把相机的位置设置为角色的相对位置。

在 Oasis 3D 脚本的生命周期中,onUpdate 和 onLateUpdate 都是在每帧更新中执行,区别在于 onLateUpdate 是晚于所有 onUpdate 之后更新的。假设角色运动由两个脚本的 onUpdate 同时控制,相机在 onUpdate 进行跟随,这时候就可能出现抖动的现象,所以相机跟随一般放在 onLateUpdate 之中。

3、 骨骼动画

我们的角色有多种骨骼动画,比如水平运动时的奔跑动画、达到终点时的招手动画等,这些动画信息都包含在 FBX 文件中。设计师从 C4D 导出的文件把所有骨骼动画都包含在同一个动画片段中,导致我们无法分段播放。所以还需要借助 Blender(一款开源的跨平台全能三维动画制作软件),在 Blender 的动画摄影表中有一个动作编辑器,在里面复制一个时间轴,删去多余的、只保留需要的动作。依次分好后,导出 FBX。

我们通过动画片段的名称来播放(playAnimationClip),那这些名称如何获取呢?可以打开 GLTF 源文件,找到 animation 属性。samplers 描述动画数据的来源;channels 建立输入(即从采样器计算的值)和输出(即动画节点属性)之间的连接;而 name 就是动画片段的名称了。
image.png
image.png
4、 粒子动画

640.gif

粒子系统重点要理解好它的发射参数,引擎会根据配置参数,自动生成几何体和材质。在 Oasis Editor 中,我们可以直接为实体绑定粒子系统组件,以能量块的爆炸粒子为例:爆炸是从一个点往四周发射的效果,所以把所有粒子的初始速度都置零,x 轴和 y 轴加上速度随机因子,给 y 轴负方向加了微小的加速度模拟重力和空气摩擦力。
image.png
image.png
所有粒子默认都不播放的,只有在检测到碰撞才会发射:

const cd = this.entity.addComponent(o3.CollisionDetection);
cd.on("begin_overlop", e => {
  const colliderNode = e.collider._entity;
  // 获取碰撞实体的类型
  const entityType = this.getEntityType(colliderNode);

  // 撞到能量块
  if (entityType === "reward") {
    // 获取粒子组件
    const particleComponent = colliderNode.getComponent(o3.GPUParticleSystem);
    // 发射粒子
    particleComponent && particleComponent.start();
    this.engine.dispatch("gotReward");
  }
});

5、 Shader 动画

640 (1).gif

角色碰到红色方块之后的消失动画采用 Shader 帧动画,具体实现参考我的另一篇文章

6、 赛道配置化

基于 3.1 搭建出来的场景树,我们可以通过克隆、拖拽的形式配置我们的赛道。

640 (2).gif

为了能够灵活调整游戏难度,我们需要把赛道配置抽象出来。赛道主要由两部分组成:方块和能量块。实体之间的区别主要是位置,针对方块来说,有危险方块和普通方块,各自比例也不同,我们通过实体名字进行区分,比如 normalBox11 代表 11:1:1的普通方块,dangerousBox2 代表 2:1:1 的危险方块。危险方块可能包含旋转、位移等动画,所以都做成单独的脚本组件。最终抽象出数据结构:


interface IGameConfig {
  // 方块配置
  boxsConfig: {
    name: string;
    position: {
      x: number;
      y: number;
      z: number;
    };
    action?: string[];
  }[];
  // 能量块配置
  rewardsConfig: {
    name: string;
    position: {
      x: number;
      y: number;
      z: number;
    };
  }[];
}

游戏内部设置了多个难度等级的配置,接入方可以传入对应的 level,也可以自定义赛道。
7、 游戏换肤
我们在 Oasis Editor 中编辑完场景之后,可以导出项目,其中 schema.json 是关键产物,里面包含了实体(nodes)、组件(abilities)、资源(assets)、动画(animator)等信息,它们之间的关系:
image.png
其中,asset 包含了GLTF、材质、纹理、脚本等多种类型的资源。runtime 会根据 schema 配置来构建实体树,并加载所有资源,资源与组件进行关联,再将组件挂载到对应的实体上。所以我们需要在场景初始化之前,把 schema 中的相关纹理替换掉,从而达到自定义换肤的功能。

8、 手机横屏方案

游戏是横屏显示的,用户可能会开启手机的自动旋转设置,我们可以通过监听屏幕旋转事件(onorientationchange)来设置我们的游戏。如果屏幕的宽度大于屏幕的高度,游戏容器的宽高直接等于屏幕宽高。如果屏幕的宽度小于屏幕高度,游戏容器的宽设置为屏幕的高,游戏容器的高设置为屏幕的宽,然后将游戏容器旋转90度,由于是围绕中心点进行旋转,所以还需要将游戏容器往左下角偏移,如下图所示:
image.png

const clientWidth = document.documentElement.clientWidth;
const clientHeight = document.documentElement.clientHeight;
gameContainer.style.top = (clientHeight - clientWidth) / 2 + "px";
gameContainer.style.left = 0 - (clientHeight - clientWidth) / 2 + "px";

onorientationchange 在低端机上会有兼容性问题,可以借助 resize 来触发。这时候发现转屏之后无法马上获取屏幕宽高,所以还需要延迟监听。

优化

1、 画布节能模式

在 Retina 屏中,一个逻辑像素等于多个物理像素,取决于设备像素比。如果不对画布做处理,就会出现下图这种模糊的效果。通常我们会选择高清模式,即画布像素 1:1 填充到屏幕物理像素。具体做法是将画布的宽高按设备像素比来放大:webcanvas.width = canvas.clientWidth * window.devicePixelRatio 。设置之后发现帧率下降,手机容易发烫,因为渲染的压力和屏幕物理像素高宽成正比,物理像素越大,渲染压力越大。权衡性能和效果,我们选择了节能模式,即在高清模式的基础上对画布按照某个比例进行缩放 webcanvas.width = canvas.clientWidth * window.devicePixelRatio / scale。一般 scale 设置为 3/2 即可。
image.png
2、对象池

由 3.3.6 分析得到了赛道的两个主要素:方块和能量块,而且他们在场景中存在大量类似的节点。为了减少主循环过程中创建对象带来的开销、避免因创建释放等操作带来的GC,我们要用对象池来进行优化。如下图中的能量块对象池,在大池子中有几个小池子,方便集中管理。我们在大池子中保留了几个原始实体,这些实体在 3.1 中被我们挂载到同一个父实体中。我们先在每个小池子中克隆三个实体,需要的时候往池子中取,如果池子已经被取空,则先克隆之后再取出。当不需要的时候归还实体,并恢复实体的初始状态。
image.png
这时候我们发现存在大量 Drawcall,因为场景中存在太多的实体。所以我们进一步优化,以角色的位置为中心设置一个可视区域,只有在可视区范围内才从对象池中获取,可视区外则归还实体。比如我把可视区的范围缩小,可以更直观的看出效果:

640 (3).gif

我们在每局游戏开始的时候得到一个实体配置队列,实体配置结构见 3.3.6,这些数据可以在 Oasis Editor 可视化编排之后通过脚本输出。在角色奔跑过程中,我们从队列中取出可视区的实体配置,然后从对象池中取出对应实体进行摆放,当超出可视区之外则归还实体。


/**
 * 获取可见范围内的实体配置
 * @param positionX - 角色在x轴的位置
 */
getVisibleRewardConfig(positionX: number) {
  return this.rewardsConfig.filter(item => {
    const { position, entity } = item;
    // 已经不可见的实体隐藏掉
    if (position.x < positionX + SHOW_RANGE[0] && entity) {
      // 对象池归还实体
      this.entityPool.putEntity(entity);
      item.entity = null;
    }
    return (
      position.x >= positionX + SHOW_RANGE[0] &&
      position.x <= positionX + SHOW_RANGE[1]
    );
  });
}

3、 垂直同步

游戏运行在移动端上会出现很明显的抖动,严重影响用户体验。抖动有多种原因,先拿比较容易复现的场景来讲,当能量块和方块离得近的时候会抖动(如下图)。因为触发了能量块的碰撞结束事件,导致角色做自由下落运动。但此时角色是在方块上方,又触发了水平奔跑。所以“水平跑-下落-水平跑-下落”,循环引起了抖动。

640 (4).gif

另一种是跳跃过程中存在的抖动现象,由于找不到复现规律,排查起来也比较费劲,一开始是往应用层的逻辑思考,后来发现当用户不进行任何操作也会出现场景抖动,所以怀疑是屏幕刷新率不同步导致。由于我们一开始设置了目标帧率,所以主动关闭了垂直同步。垂直同步即场同步(Vertical synchronization),我们看到的游戏动画,都是经过用户的交互、显卡的计算和渲染,再由屏幕刷新,最终才呈现出来。我们所说的帧率是由屏幕的刷新率决定的,而显卡的渲染速率取决于某一帧的复杂程度。如果显卡在 60/1 s 的时间里渲染了两张图片,但屏幕的刷新率只有 60Hz,这时候屏幕就只能把两帧图片拼接在一起,造成抖动或画面撕裂。为了解决这个问题,我们可以开启垂直同步,让显卡每次渲染完成后,必须挂机休息,等待屏幕刷新结束再渲染下一张图片。
4、 WebGL 版本切换

一开始游戏运行在安卓版的钉钉上会出现闪退现象。经排查,钉钉使用的是旧版本的U4内核,该版本运行 WebGL2.0 会导致闪退。在钉钉未升级最新版的U4内核的情况下,我们需要针对这部分用户使用 WebGL1.0 处理。引擎的 WebGLMode 默认为 Auto,即设备支持优先选择 WebGL2.0,不支持 WebGL2.0 会回滚至 WebGL1.0。当检测到游戏运行在安卓的钉钉上时,需要将 WebGLMode 切换到 WebGL1.0。

沉淀

目前游戏已经抽成一个通用组件,支持多种业务场景接入,支持配置游戏难度等级、换肤、自定义赛道。业务接入十分很简单,只需要准备一个 canvas,当用户点击时触发游戏开始或重置,同时监听得分、结束等事件。业务联动关键代码如下:


const gameContainer = canvas.parentElement;
// 游戏初始化
const oasis = await boot({
  canvas,
  gameContainer,
  level: 2,
  debug: false,
  handleDowngrade: () => {
    setDowngrade(true);
  }
});
const { engine } = oasis;

// 开始游戏
engine.dispatch("start");
// 再玩一次
engine.dispatch("reset");
// 获得能量块
engine.on("gotReward", () => {
  setScore(x => x + 1);
});
// 游戏结束:死亡
engine.on("gameDie", () => {
  // do something
});
// 游戏结束:到达终点
engine.on("gameOver", () => {
  // do something
});

3D 研发过程的一些思考

本次项目时间十分紧张,开发和联调只用了一周半,加上ICBU跨供第一次尝试 3D 互动游戏,整个研发流程暴露了不少问题,这里提下自己对研发流程的一些总结 。

设计阶段

在拿到3D美术资源后,我们需要针对性的做一些优化。事实证明,3D资源交接这个流程耗费了大量时间,很多资源来回修改,所以我们应该在设计之前就跟美术同学约定好规范。这里提一些资源优化的参考:

  • FBX 或 GLTF 不能包含相机,会导致重复渲染;

  • 纹理不要绑在模型里面,后期经常要替换纹理,避免重复加载了两张;

  • 优先使用非透明材质,较少性能消耗;

  • 光照信息已经合并在漫反射贴图,使用 Unlit 材质替代 PBR 材质;

  • 较少三角形面数,把不在用户视野内的面挖空,整体三角面控制在5w以内;

  • 尽量合并材质,减少渲染批次;

  • 删除模型文件里面包含的多余节点,较少transform计算;

  • 纹理都用 2 的 n 次方,避免使用太大的纹理,尽量用 jpg 或 webp 格式,并进行压缩。像能量块这种小的模型一般 256*256 就够了;

  • 粒子纹理用透明的,去掉四周多余空间。多粒子的情况下使纹理合并到一张 sprite 图。

开发阶段

复杂的3D场景不建议直接用API来开发,Oasis Editor 很好的帮我们解决了开发效率低、效果还原差的两大痛点。在开始开发之前,我们需要跟营销前端同学划分好场景,3D部分尽量不要包含业务逻辑,并约定好业务H5与3D场景之间的通信事件。3D开发过程中我们很难去定位问题,这里推荐一个 Chrome 插件----SpectorJS,我们可以方便地查看当前的渲染状态、Drawcall 等信息。

image.png
在做性能优化时,我们可以利用 Chrome 的 Performance 工具,把 CPU 设置成 6x slowdown 来模拟一些比较低端的机型,然后看每一帧的渲染时长,理想情况下每帧 16.6ms,我们可以展开看具体耗时是哪个接口然后对其优化。同时我们也可以设置 3G 网络来模拟弱网情况。
image.png

测试阶段

不同于前端页面的测试,3D场景测试的重点是性能和兼容性。正常来说,WebGL 支持率在99%以上,不支持 WebGL 我们也做了降级处理,所以更多的是关注上层应用的兼容性。对于用户一些可能的操作流,比如压后台、省电模式、竖排方向锁定状态、切换APP等,测试过程中也需要覆盖到。

性能方面,重点关注白屏时间、Crash率、FPS、内存。可以引入 Oasis 3D 提供的性能查看面板(@oasis-engine/stats),FPS 在低端手机上需要稳定在30以上,Memory 在 60M 以下,DrawCall 在 50 以下,总的三角面在 5w 以内。还可以利用 MPerf 采集更具体的数据,复杂的场景需要出性能报告,对于超标的情况需要优化后才能上线。
image.png

发布阶段

线上要做好降级,本次游戏针对安卓 6.0.0 以下、IOS 10 以下的设备做了降级,不支持webGL、资源加载异常也做了降级,最终降级率在 3% 左右。同时要监控脚本异常、白屏及 crash。我们这次游戏运行在微信、钉钉、阿里卖家 APP 上,通过 EMAS 可以看到 APP 整体 crash,不过不能区分是不是h5引起的。另外,观察线上的得分情况,可以动态调整我们的游戏难度。

总结

互动游戏在传播、促活上面起到了明显的效果,后续3D互动将作为用户成长体系的一个能力,结合内容营销、搭建能力、成长激励,支撑运营进行内容营销推广和客户成长体系建设。

⭐️ 最后预告一下,Oasis 3D 将在今年2月1日举行开源发布会,届时将在B站进行全网直播,欢迎加入钉钉群 31360432 了解更多开源信息~ 🥰


image.png
关注「Alibaba F2E」
把握阿里巴巴前端新动态

相关文章
|
4月前
|
开发工具 Android开发 开发者
移动应用开发之旅:从概念到市场的全景解析
在数字化浪潮的推动下,移动应用成为了我们日常生活的一部分。本文将带你穿越移动应用开发的迷宫,探索那些让应用从一个简单的想法变成数百万用户手中宝贝的秘密。我们将一探究竟,了解移动操作系统的基础、开发工具的选择、设计原则的应用,以及市场策略的实施。无论你是开发者还是对移动应用充满好奇的旁观者,这篇文章都将为你揭示移动应用背后的魔法。
55 0
|
1月前
|
前端开发 测试技术 Android开发
移动应用开发之旅:从概念到市场
在数字化时代的浪潮中,移动应用已成为日常生活和商业活动不可或缺的一部分。本文将引导读者穿越移动应用开发的迷宫,从最初的灵感迸发到最终的产品发布。我们将探索移动操作系统的多样性,理解不同平台的独特需求,并深入剖析开发过程中的关键步骤。你将学习如何将一个简单想法转化为现实,包括市场调研、设计原则、编码实践、测试策略以及上线后的推广和维护。加入我们,启航你的移动应用开发之旅,让你的梦想在用户的手掌中闪耀。
|
3月前
|
开发框架 前端开发 JavaScript
探索移动应用开发之旅:从概念到市场
【9月更文挑战第17天】在这篇文章中,我们将一起揭开移动应用开发的神秘面纱,从构思的火花到市场上的应用。我们将通过一个实际的代码示例,展示如何将一个简单的想法转化为现实。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和知识,帮助你在移动应用开发的道路上更进一步。
36 0
|
4月前
|
机器人 C# 人工智能
智能升级:WPF与人工智能的跨界合作——手把手教你集成聊天机器人,打造互动新体验与个性化服务
【8月更文挑战第31天】聊天机器人已成为现代应用的重要组成部分,提供即时响应、个性化服务及全天候支持。随着AI技术的发展,聊天机器人的功能日益强大,不仅能进行简单问答,还能实现复杂对话管理和情感分析。本文通过具体案例分析,展示了如何在WPF应用中集成聊天机器人,并通过示例代码详细说明其实现过程。使用Microsoft的Bot Framework可以轻松创建并配置聊天机器人,增强应用互动性和用户体验。首先,需在Bot Framework门户中创建机器人项目并编写逻辑。然后,在WPF应用中添加聊天界面,实现与机器人的交互。
127 0
|
4月前
|
图形学 C# 开发者
全面掌握Unity游戏开发核心技术:C#脚本编程从入门到精通——详解生命周期方法、事件处理与面向对象设计,助你打造高效稳定的互动娱乐体验
【8月更文挑战第31天】Unity 是一款强大的游戏开发平台,支持多种编程语言,其中 C# 最为常用。本文介绍 C# 在 Unity 中的应用,涵盖脚本生命周期、常用函数、事件处理及面向对象编程等核心概念。通过具体示例,展示如何编写有效的 C# 脚本,包括 Start、Update 和 LateUpdate 等生命周期方法,以及碰撞检测和类继承等高级技巧,帮助开发者掌握 Unity 脚本编程基础,提升游戏开发效率。
103 0
|
5月前
|
人工智能 搜索推荐 开发者
AI驱动的游戏设计:创造更智能、更沉浸的游戏体验
【7月更文第31天】人工智能(AI)技术正在深刻地改变游戏行业,不仅为游戏设计师提供了创造更丰富、更动态游戏世界的工具,也为玩家带来了更加个性化和沉浸式的体验。本文将探讨AI在游戏设计中的应用案例,并展示一些具体的实现方法。
685 2
|
7月前
|
机器学习/深度学习 人工智能 前端开发
前沿技术探索:构建更智能的前端开发体验
【2月更文挑战第11天】 本文深入探讨了利用人工智能(AI)和机器学习(ML)技术革新前端开发过程的最新趋势。通过分析当前的开发挑战,文章提出了一种结合AI辅助设计、代码自动生成和智能测试的全新前端开发框架。这不仅能显著提升开发效率,还能优化用户体验,为企业创造更大的商业价值。文章的核心在于展示如何通过技术创新,将复杂的前端开发任务简化,从而使开发者能够更加专注于创意和策略的实现。
105 3
|
7月前
|
人工智能 小程序 算法
探索AI动画与小程序的应用创新-产品面对面系列直播第四期
探索AI动画与小程序的应用创新-产品面对面系列直播第四期
128 6
|
7月前
|
搜索推荐 安全 视频直播
摆脱自研难题,AUI Kit助力企业快速搭建专属互动课堂
深度剖析音视频开发利器的技术架构、技术性能、开发能效和最佳实践,从互动课堂AUI Kit的核心功能、技术架构、快速集成等方面,介绍如何通过低代码快速接入专属互动课堂视频业务。
139 1
|
存储 数据可视化 搜索推荐
《2022中国云游戏行业认知与观察》——第二章、云游戏应用场景与技术实践——2.1 云端游 & 云手游:定义全新业务模式 提升游戏 ROI——2.1.3 应用案例:云游戏技术支持bilibili 游戏创作大赛试玩体验区
《2022中国云游戏行业认知与观察》——第二章、云游戏应用场景与技术实践——2.1 云端游 & 云手游:定义全新业务模式 提升游戏 ROI——2.1.3 应用案例:云游戏技术支持bilibili 游戏创作大赛试玩体验区
238 0