企业网站搭建方案,海安做网站的公司,建团购网站,海南电子商务网站简介#xff1a; 本文以「余额宝3D跑酷游戏」为例#xff0c;介绍了前端如何快速上手 Web 3D 游戏的开发。 作者 | RichLab楺楺 诚空 本文以「余额宝3D跑酷游戏」为例#xff0c;介绍了前端如何快速上手 Web 3D 游戏的开发。跑酷游戏是余额宝七周年的主玩法#xff0c;用户…简介 本文以「余额宝3D跑酷游戏」为例介绍了前端如何快速上手 Web 3D 游戏的开发。 作者 | RichLab楺楺 诚空 本文以「余额宝3D跑酷游戏」为例介绍了前端如何快速上手 Web 3D 游戏的开发。跑酷游戏是余额宝七周年的主玩法用户通过做任务来获取玩游戏的机会并且解锁游戏道具从而在游戏中获得更多的金币最终可以利用金币兑换一些权益同时我们也在游戏中植入了一些礼包先看看具体效果。 游戏设计
我们把游戏的3D场景分成了三大模块分别是赛道、金币道具和人物。
赛道设计
赛道包含了楼房和地面由于人物需要不停地往前跑基于相对运动的原理我们复制了两段楼房如图1并同时做逆时针旋转当旋转至 -theta 角度的时候把楼房的旋转角度置为0如图2。地面是一个静止的圆弧模型通过改变纹理的 UV 值来实现地面滚动的效果。 图1 赛道结构图 图2 楼房运动轨迹
金币布局
由以上图1可知我们以 theta 角度的圆弧为一个控制单元我们希望能控制游戏的总时长、每段圆弧旋转的时间以及每段圆弧摆放的金币行数这些参数如何控制3D场景的运作呢根据已知字段推导出以下几条公式蓝色字段为可配参数
需要生成金币的总行数 游戏总时长 /圆弧旋转theta角度的时间 x 每段圆弧摆放的金币行数每两行金币之间的时间间隔 游戏总时长 / 需要生成金币的总行数每行金币出现的时间 每两行金币之间的时间间隔 x 金币索引
这里主要得出 游戏总时长 和 每行金币出现时间 之间的关系而每行金币该如何摆放以及道具出现的时机由具体的业务逻辑控制这里不展开来讲。最终我们得到了一个控制金币摆放的队列
[{index: 0, // 索引代表每一行item: {position: center, // 摆放位置type: coin // 应该摆放的模型类型},time: 0 // 每行金币出现的时间},{index: 1,item: {position: left,type: coin},time: 0.25},// more......
]
这个队列如何与我们的3D场景关联呢
由以上图2可知一共有两段圆弧在交替旋转假设每段圆弧摆放的金币行数定义为 rowsPerPart当前圆弧的索引定义为 index那么每次旋转至0度的时候取 [index * rowsPerPart, (index 1), rowsPerPart] 区间的数据进行摆放。数组中 position 表示摆放位置一共有左中右三条道也可能三条道都摆放根据配置创建金币节点并设置好节点的 position。type 表示应该摆放的模型类型除了金币还可能是道具、礼包、终点线等。
开发流程
设计好游戏思路之后可以正式开始制作我们的游戏啦~
跑酷游戏是通过 Oasis Editor 开发的这是一个 web 3D 内容在线开发平台底层用的是 Oasis 3D蚂蚁自研的3D引擎。这时候你可能会问为什么要用 Oasis Editor 开发呢 接下来分为「场景搭建」、「逻辑开发」、「业务联动」来讲解整个3D工作流。
场景搭建
上传资产
在编排场景之前我们需要先上传好游戏资产一般美术提供的模型文件格式为 fbx 或 gltf纹理推荐使用 webp 格式我们在资源区右侧点击上传。 在开发过程中美术可能经常需要替换纹理所以建议美术将纹理与模型解绑通过手动上传的形式将纹理绑定到模型上避免同时加载两个纹理。
如图我们已经在资源区上传好楼房、道具、金币等模型和相应纹理。
场景编排
有了资产之后我们需要绑定到节点上然后进行场景编排如下视频以楼房和地面为例进行演示
创建场景树绑定GLTF模型编辑器PBR材质绑定纹理调整编辑器相机拷贝编辑视角转换相机视角微调相机参数按照同样的方法我们完成了整个场景的编排某些节点需要通过脚本控制展示可以点击场景树左边的小眼睛进行隐藏场景效果如下
粒子系统
游戏开发的时候经常会用到粒子系统来帮助我们实现一些比较酷炫的效果在我们这个项目中在人物节点(person)下面有2个子节点分别来负责吃到金币(coinParticle)和道具(toolParticle)时的粒子效果游戏过程中效果如下 当我们点击选中一个粒子节点的时候编辑器右侧会出来对应的属性面板属性面板中就能够看到我们的粒子组件以及相关参数通过设置参数可以调整我们的粒子效果 接下来一步就是来设置参数来控制我们的粒子效果了下面给大家介绍下几个常用参数
逻辑开发
以上场景可由前端协助美术同学进行搭建接下来这一步就正式进入编程阶段了。
脚本能力
1、cli Oasis Cli 是连接业务和 Oasis 3D 编辑器的桥梁在使用我们引擎的时候建议提前安装好 Cli 的环境
tnpm i alipay/oasis-cli -g
安装好 Cli 之后我们就可以将场景导出到我们的本地项目并且随时将最新的场景编排同步至本地。首先我们进入跑酷项目根目录并执行如下命令将我们已经建好的3D场景和当前项目连接
oasis pull sceneId
上面的 pull 命令中sceneId是我们的场景id执行完该命令后会在根目录下自动添加了1个目录和1个文件如下 当我们需要对场景进行编辑并且将最新修改同步至本地我们只需要执行如下命令即可
oasis dev
2、金币转动 这里以金币转动为例演示如何添加脚本控制首先在资源面板添加一个脚本然后在将脚本挂在节点上 完成这一步后我们就可以在coinAni的脚本中实现对coin节点的控制了金币一直旋转我们在脚本的onUpdate 中处理即可
onUpdate() {const { node } this;TWEEN.update();if (this._isRotate node.parentNode.isActive) {node.setRotationAngles(0, globalVal.coinAngle % 360, 0);}
}
碰撞检测
利用碰撞检测来反应人物与金币之间的碰撞首先需要给人物和金币都加上碰撞体包围盒。Oasis Editor 提供了立方体碰撞体和球型碰撞体引擎会在每帧更新时计算本节点的 collider 与其他 collider 的相交情况球型碰撞体之间只需要比较球心距离与两个半径之间的大小关系而立方体碰撞体需要计算八个顶点的位置关系所以使用球型碰撞体性能上会好一些。
如下图我们给人物添加了一个球型碰撞体可以调节它的球心和半径。可视化包围盒只是编辑器运行时的插件因此不会出现在我们的 H5 场景中。 编辑完碰撞体包围盒之后我们需要在脚本中进行碰撞检测监听 collision 事件
let cd node.createAbility(o3.ACollisionDetection);
cd.addEventListener(collision, e {const colliderNode e.data.collider.node; // 拿到被碰撞的节点const name colliderNode.name;// do something...
});
Shader 嘿嘿看到 Shader 别急着划走掌握了 Shader 你就可以
自定义光照、物理等模型可以开发更多酷炫的效果能够优化渲染性能能够帮助我们排查渲染上的问题
列举几个 Shader 的效果更多效果可以前往shadertoy 1、 什么是 Shader Shader着色器是运行在 GPU 上的小程序这些小程序为图形渲染管线的某个特定部分而运行它用于告诉图形硬件如何计算和输出图像。为了更深入了解 Shader 的原理我们需要了解 OpenGL 的渲染流水线这里以渲染跑酷游戏的地面模型为例
CPU 应用阶段
我们在3.1.1中上传了地面的 fbx 模型文件其中包含了顶点位置、UV、法线、切线等信息CPU 将这些信息加载到显存中然后设置渲染状态告诉 GPU 如何进行渲染工作。最后 CPU 会发出渲染命令Drawcall由GPU 接收并进行渲染。
GPU 渲染管线 GPU 渲染管线包含了几何阶段和光栅化阶段顶点着色器Vertex Shader和片元着色器Fragment Shader分别位于这两个阶段中。
几何阶段顶点着色器接收 CPU 传过来的顶点数据通常在这个阶段做一些空间变换、顶点着色等操作。接着会经过裁剪把不在相机视野中的顶点裁剪掉并剔除某些图元然后将物体坐标系转换到屏幕坐标系。
光栅化阶段两个顶点之间有很多个像素片元着色器会对像素进行处理除了进行纹理采样还会将像素与灯光进行计算产生反射、折射等效果。同一个屏幕像素点可能会有多个物体这时候需要通过 alpha 测试、深度测试、模板测试、混合blend等处理把同一位置的像素进行过滤或合并最终渲染到屏幕上。2、如何编写Shader Oasis Editor 中写 Shader 需要经过这几个步骤 1、在资源区中添加“Shader 材质”然后绑定到模型上 2、编辑 Shader 材质属性面板中提供了常见的渲染状态配置也可以直接编辑着色器定义ShaderDefine。 整个 ShaderDefine 结构如下其中 vertexShader 和 fragmentShader 分别存放顶点着色器和片元着色器代码采用 GLSL ( OpenGL 着色语言OpenGL Shading Language )编写。states 用来定义渲染状态控制对象对应上文提到的合并阶段。
export const ShaderMaterial {vertexShader: ,fragmentShader: ,states: {},uniforms: {},attributes: {},
};
3、如果要动态改变材质参数值需要创建脚本在节点每帧执行的回调函数中修改属性值。
下面通过跑道滚动和光波两个示例来讲解。
3、 跑道滚动 如2.1中所述跑道是一个静止的圆弧模型通过改变纹理的UV值来实现跑道滚动的效果。为了实现给人物打光的效果我们在基础颜色纹理上面叠加了一张渐变纹理并给人物加上了一个静态的阴影实际上是一个面片。 基础颜色纹理 渐变纹理 叠加效果
相关的Shader代码如下
export const ShaderMaterial {// Vertex Shader 代码vertexShader: uniform mat4 matModelViewProjection;uniform float utime;attribute vec3 a_position;attribute vec2 a_uv;varying vec2 v_uv;varying vec2 v_uv_run;void main() {gl_Position matModelViewProjection * vec4(a_position, 1.0 );v_uv a_uv;v_uv_run vec2( v_uv.s, v_uv.t utime );},// Fragment Shader 代码fragmentShader: varying vec2 v_uv;varying vec2 v_uv_run;uniform sampler2D texturePrimary;uniform sampler2D textureLight;void main() { vec4 texSample texture2D( texturePrimary, v_uv_run ).rgba;vec4 texLightSample texture2D( textureLight, v_uv ).rgba;gl_FragColor vec4(texSample.rgb * texSample.a texLightSample.rgb * texLightSample.a, texSample.a);},states: {},
}
Vertex Shader 和 Fragment Shader 都包含了一个 mian 入口函数。
初次看 Shader 代码会发现很多陌生的符号其中 uniform、attribute 和 varying 都是变量限定符attribute 只能存在于 Vertex Shader 中一般用来放置程序传过来的顶点、法线、颜色等数据uniform 是程序传入到 Shader 中的全局数据varying 主要负责在Vertex Shader 和 Fragment Shader 之间传递变量。
mat4、vec3、sampler2D 都是基本变量类型分别代表矩阵、向量和纹理后面的数字代表n维例如 mat4表示 4x4 矩阵。
本例的 Vertex Shader 中顶点位置 a_position 与 matModelViewProjection 矩阵相乘其实是把三维世界的物体投影到二维的屏幕上。a_uv 存放了 UV 信息我们想要把一张贴图贴到模型表面需要纹理映射坐标即UV坐标分别代表横纵两个方向。为了使地面能滚动起来我们需要每帧改变 UV 的纵坐标并通过变量 v_uv_run 传递给 Fragment Shader。
在 Fragment Shader 中texturePrimary 和 textureLight 都是从 CPU 程序传过来的纹理。通过 texture2D 采样基础颜色纹理 texturePrimary得到了纹理贴图在模型上滚动的效果。接着拿采样后的颜色值与透明渐变纹理 texLightSample 进行叠加得到了近亮远暗的效果。
最后我们在 CPU 中每帧更新 utime 的值并传入 Shader。
onUpdate(deltaTime) {if (!this.running || !this._streetMaterial) return;// 赛道滚动this._time - deltaTime * 0.0002;this._time % 1.0;this._streetMaterial.setValue(utime, this._time);
}
4、光波特效 人物吃到吸吸卡之后会有一个光波特效由于是不规则动画我们采取了帧动画来实现。首先需要拿到这样nn的帧序列。注意浏览器会对纹理尺寸进行限制可以通过 gl.MAX_TEXTURE_SIZE 拿到这个值最好别超过20482048。 接着在 Shader 中进行纹理采样。假设一个 100 * 100 的正方形它的顶点着色器运行4次因为有4个顶点但片元着色器会运行 10000 次所以尽量把 UV 等计算放在 Vertex Shader 中再通过 varying 传给 Fragment Shader。代码如下 export const ShaderMaterial {// Vertex Shader 代码vertexShader: attribute vec3 a_position;attribute vec2 a_uv;uniform mat4 matModelViewProjection;uniform float uFrame;varying vec2 v_uv;void main(void){gl_Position matModelViewProjection * vec4(a_position, 1.0);float cellCount 8.0;float row floor(uFrame / cellCount); // 当前第几行float col mod(uFrame, cellCount); // 当前第几列float cellSize 1.0 / cellCount;v_uv vec2(a_uv.s * cellSize col * cellSize, a_uv.t * cellSize row * cellSize);},// Fragment Shader 代码fragmentShader: varying vec2 v_uv;uniform sampler2D uDiffuseMap;void main(void){gl_FragColor texture2D(uDiffuseMap, v_uv);},states: {},uniforms: {uDiffuseMap: {name: uDiffuseMap,type: o3.DataType.SAMPLER_2D},uFrame: {name: uFrame,type: o3.DataType.FLOAT}},attributes: {},
};
CPU需要传入帧序列纹理uDiffuseMap还要每帧更新uFrame的值
onUpdate(deltaTime) {// update per frameif (this.material) {this.frameif (this.frame 57) {this.frame 0;}this.material this.material.setValue(uFrame, this.frame)}
}
业务联动
余额宝跑酷是一个跑在 h5 环境下的项目其中就涉及到业务层(react)和游戏层(oasis)我们在业务层和游戏层之间加了一个胶水层(gameController)来进行两者通信结构如下 从上面结构图可以看出作为胶水层的gameController主要做了2件事情一个是给业务层提供api调用并且通知游戏层另外一个是监听游戏层的消息并且通知业务层下面来看看示例 import * as o3 from alipay/o3;export default class GameController extends o3.EventDispatcher {constructor (rootNode, dispatch) {super();this._dispatch dispatch;this._oasis this._rootNode.engine;// 获取需要监听的节点this._rootNode rootNode;this._magnetCollidNode rootNode rootNode.findChildByName(magnetCollid);this._buildNNode1 rootNode rootNode.findChildByName(part1);this._buildNNode2 rootNode rootNode.findChildByName(part2);this._streetNode rootNode rootNode.findChildByName(street);// 注册监听this.getMessage(rootNode);}// 注册监听getMessage(rootNode) {// 注册监听游戏层消息this._magnetCollidNode.addEventListener(magnetCoinCollide, (event) {// 反馈给业务层this._dispatch this._dispatch({type: collideHappen, payload:{ type: coin }});});// todo 其他节点注册监听}// 给业务层调用的apigameInit(iconList, gameData) {const gameInit new o3.Event(gameInit);gameInit.data {iconList,gameData,};this._oasis this._oasis.resume();// 通知游戏层this._buildNNode1.trigger(gameInit);this._buildNNode2.trigger(gameInit);this._streetNode.trigger(gameInit);}
}
性能优化
调试工具
工欲善其事必先利其器当我们需要对项目进行性能优化的时候我们首先需要分析性能瓶颈点然后对症下药很幸运的是chrome本身就自带性能分析工具(Performance打开页面进入开发者工具即可看到)如下 除了性能调试工具外有时候我们还会遇到一些渲染异常大多是给到GPU的数据有问题而这部分数据我们没法console.logchrome提供了一个非常好用的插件(Spector.js)帮助我们查看每一帧的数据如下
降低三角面
三角面越多gpu的计算量也会越大结合游戏实际的玩法我们对三角面这块的优化主要就是不同模型进行减面最终三角面从20万降低到6万具体如下
1、人物这块因为在跑动过程中我们始终只能看到背面所以把人物前面的三角面全部去掉
2、金币这块在保证视觉效果看起来比较圆的前提下尽可能的减少三角面
3、楼房和人物类似把赛道外部的游戏过程中根本看不到的面去除 提升帧率
提升帧率本质上就是减少cpu的运算时间通过前面提到的分析工具分析我们发现节点数量过多是导致cpu运算量大的主要原因所以我们的优化重点是在降低节点数量上最终我们的 fps 在低端机上面从10优化到25下面来具体说下
1、金币模型里面有很多没有用的空节点这个我们找美术同学帮忙重新简化模型文件
2、金币模型简化后其实模型里面还有2个节点(其中有一个rootnode其实没啥用和美术同学交流反馈是目前没有办法去掉)加上挂载模型的节点我们一个金币对象其实就有3个节点为了进一步优化我们通过代码动态去掉多余节点并进行节点合并。
3、使用对象池来避免反复创建金币。在主循环中对一些循环出现的元素我们一种优化手段就是在初始化的时候事先创建一定数量的对象然后用的时候来取用完就还回来而缓存创建好的对象的结构就是我们的对象池了。对象池带来的好处减少主循环过程中创建对象带来的开销、可以有效避免因创建释放等操作带来的GC。我们游戏中金币数量很多并且是高频出现的所以要用对象池来缓存相应的设计如下 class CoinPool {private _originNode null;private _pool [];constructor () {}init (originNode: o3.Node, capacity: number 5) {this._originNode originNode;this._genNode(capacity);}destroy () {this._originNode null;this._pool.length 0;}getNode () {if (this._pool.length 0) {this._genNode();}return this._pool.shift();}putNode (node: o3.Node) {if (this._pool.indexOf(node) -1) {this._pool.push(node);}}_genNode (num: number 1) {const pool this._pool;for (let i 0; i num; i) {let node this._originNode.clone();// 对金币模型节点的优化在这里统一处理changeParent(node);purifyNode(node);pool.push(node);}}
}
对象池使用方式
// 创建并初始化
const originCoin node.findChildByName(coinParent); // 挂载金币模型的节点
const coinPool new CoinPool();
coinPool.init(originCoin, 24);// 从池子里面获取金币节点
const coinNode coinPool.getNode();// 金币节点不需要使用了进行回收
coinPool.putNode(coinNode);// 整个节点池销毁
coinPool.destroy();
其他
上述两项其实都是针对跑酷项目本身做的一些特定优化其他项目未必能够完全照搬我们的尘沫大神针对业务方面的性能优化做了比较通用全面的总结这里简单列举一下
语言
使用枚举在标记判断if或switch语句中尽量使用number型枚举,避免使用字符串作为判断标记字符串作为判断标记性能损耗较大使用Number做Object的KeyObject作为Map使用时尽量不要使用string作为Key,而是倾向使用Number作为Key其中Number的范围越小性能越高通常小于65535性能较优使用“.”访问对象属性避免使用[string]访问对象的属性和方法会导致JIT优化失效应使用“.”访问属性尽量使用for循环遍历帧级调用尽量使用for循环进行遍历操作提升性能,相对于语法糖循环更纯粹,需要提前缓存长度n进行循环判断减少纹理寻址性能损耗
逻辑
多用对象池机制由于JS本身机制和原理需要避免在帧循环中new对象,避免GC卡顿,在业务开发中的模型抽象强烈建议使用对象池机制做对象管理善用实例或静态全局变量除了对象池机制避免GC外还需要利用实例或静态全局变量减少GC损耗比如一些用于中转数学计算的临时变量可使用静态全局变量缓存另外一些可逐实例的类变量可缓存为实例全局变量减少使用时的频繁new操作带来的开销和GC。慎用事件在大型项目中慎用事件,事件本身的灵活性是一把双刃剑在解耦的同时也带来了逻辑可读性低等困难尤其在多人协作开发的项目中所以在业务系统中该解耦的模块用事件不需要的地方需要用明确的设计调用逻辑解决,切记不要因为设计的懒惰把项目搞乱
资源优化
模型合并优化美术需将不可独立移动的模型尽可能合并减少渲染批次同时注意不要合并场景范围跨度过大的模型导致模型无法裁剪的问题 材质优化 尽可能合并材质材质作为三维引擎的合并根基一切引擎级渲染批次的合并前提都是使用相同材质所以要保持材质对象尽可能的少材质模型选择需要根据美术风格尽量精简比如直接把光照合并在漫反射贴图的的卡通风格模型可以直接选择unlit材质而无需使用复杂的PBR材质模型贴图优化贴图尺寸不可能盲目追求质量使用超大尺寸需要评估实际项目贴图光栅化后的实际显示像素来使用接近的贴图尺寸否则使用过大尺寸不仅得不到效果手机还浪费显存。除此之外还可使用纹理压缩优化显存 像素填充率优化 尽量减少全屏渲染的绘制,比如UI或遮罩使用类似全屏但大部分透明的图片绘制会带来大幅的GPU渲染负担在移动端等高DPI的设备中可适当降低DPI配置减少GPU负担
玩法系统优化 碰撞系统优化 善用主动碰撞和被动碰撞概念,减少主动碰撞器可以大幅减少碰撞检测的循环遍历次数善用碰撞组概念将物体划分所属碰撞组和可与之发生碰撞的组作为过滤器根据业务规则划分可以减少不必要的碰撞检测循环跑酷弯道优化可尝试利用顶点着色器模拟弯道跑酷效果减少CPU端相关跑酷弯道逻辑的计算负担降低美术制作复杂度
Oasis 3D V2.x To V3.x
随着 Oasis 3D 服务的业务数量越来越多、业务负责度越来越大也暴露出不少问题为此我们对现有引擎进行了大重构也就是V3.x版本此版本主要目标是更快、更方便、更高效。
这里先简单介绍几个重构模块希望让大家有个初步体感。
资源管理模块
资源管理模块我们从底层实现进行了大重构主要目的是简化开发者的使用下面是v2.x版本和v3.x版本加载一个带有骨骼动画的模型示例对比可以看出v3.x版本的api是特别精简的除了api的简化外功能上我们还提供了下载重试、重试间隔、下载超时、下载进度、取消下载等。
V2.x版本加载资源 let gltfRes new Resource(skin_gltf, {type: gltf,url: xxx.gltf
});
let resourceLoader new ResourceLoader(engine);resourceLoader.load(gltfRes, (err, gltf) {if (err) return;const fairyPrefab gltf.asset.rootScene.nodes[1];const fairy1 fairyPrefab;rootNode.addChild(fairy1);const animator fairy1.addComponent(Animation);const animations gltf.asset.animations;animations.forEach((clip) {animator.addAnimationClip(clip, clip.name);});animator.playAnimationClip(Take 001);
});
V3.x版本加载资源
const { defaultSceneRoot, animations } await engine.resourceManager.load(xxx.gltf);
rootEntity.addChild(defaultSceneRoot);
const animator root.getComponent(Animation);
animator.playAnimationClip(Take 001);
数学库
数学库整个进行重构主要有2方面改善写法更简捷、性能更优。老的数学库都是函数式的并且向量、四元数等低层其实都是Array而V3.x采用Class的方式来实现底层数据结构改为object。
新的数学库不仅支持更为丰富的写法性能上面通过数学库重构以及使用数据库相关的优化性能提升比较明细下面是我们的测试结果
在线 coding
目前我们编辑器实现了在线coding意味着你只需要一台电脑并且安装一个浏览器即可完成3D项目的创建、开发、发布等界面如下 在上面的界面中即可完成在线coding然后保存即可实时查看最新的效果。进一步的我们还提供了事件面板模拟和业务层的交互这样我们就可以在3D项目中自测完整个流程然后发布给业务层使用如下 当我们开发完项目后需要交付给业务方使用在V3.x中我们只需要点击发布至对应平台即可(这块还在持续优化中)如下 原文链接 本文为阿里云原创内容未经允许不得转载。