基于电商互动场景的 Cocos 小游戏应用方案

COCOS

共 14192字,需浏览 29分钟

 ·

2020-12-21 02:45

本文转载自京东零售官方共享设计平台 Jelly

作者ID:踪跃、snandy、张宇

“大师,要如何才能做出一款人见人爱,让人爱不释手的小游戏呢?”清晨的微风吹来,柔和的阳光难挡丝丝凉意,大师轻拂白髯,着那龙头拐杖朝前一指,湖里便泛起了涟漪。众人定睛细视,不禁失声叫道:“好俊的一条鲶鱼!”且看它的遍体红麟,万分俊俏,真不愧为搅动 H5 游戏世界的魔王,前端浪潮的领舞者!诸位看官,闲言少叙,我们便来仔细瞧瞧此等神鱼终究有几多斤两。

第一章 举棋不定

技术选型

“来需求啦!”,D 哥风风火火地从卫生间出来,碰到刚沏了一杯咖啡的小 Y,见面就是一巴掌(以示友好),咖啡溢出了几滴。

“咱们这次的需求挺有意思,待会儿叫上小Z,咱们在会议室碰一下,看看怎么做。”

“你把我咖啡搞洒了!”

十分钟后,三人都来到了会议室。

D 哥先介绍需求:“这次是要做一款交互性比较强的 H5 小游戏。 基本玩法是开局给金币,用户用金币购买 joy,买来的 joy 会按照一定速率产生金币收益;合成两只相同等级的 joy 可以升一级,级别越高,每秒收益的金币也就越多。”

Y:“赚了那么多金币有锤子用?”小Y不改急躁本色,要求D哥快点。

D哥不紧不慢,整理了一下衣领接着介绍:“金币是游戏中的虚拟资产,唯一的作用就是购买 joy ,有了 joy 你就可以不断升级,而达到一定级别后,会触发随机事件得到京豆...”

Y:“喔!这才对嘛!”

D哥看了小Y一眼:“我刚刚评估了一下,这次需求的难点在于动画的制作上,动画很多也很酷炫,咱们得选择一种方式,保证我们的游戏既好看又流畅,我想听听你们的想法。”

Y:“Canvas 就行!咱们要在 H5 上开发,当然要用 H5 的画布。并且这也是当前的主流,想实现什么效果都没有问题,纯手写代码,只要代码写好点,一定能保证程序的流畅性。”

Z:“Canvas 我不是很熟练”,小Z低声说到,“并且这次的动画不是有点多?”

D:“没错,我也担心这一点,动画多是个大问题,写起来复杂,之后的调试也会比较麻烦。小 Z,你之前好像有做过 H5 上的动画?”

Z:“是的,我之前用过 lottie,不过 lottie 弊端比较明显,用 AE 生成的文件太大,对动画元素多的场景太不友好。咱们这个需求恐怕用不上,SVG 怎么样?”

Y:“那不和 Canvas 一个样?动画多起来要人命哟!你会画贝塞尔曲线吗?”

D:“嗯,我们要争取做到又快又好地开发,毕竟还有其他需求,不能在细节上花费过多的时间,但是也不能不注重细节。”

Y:“CSS 加 GIF 吧要不。”

........

Z:“要不回去调研一下?”

D:“行,咱们先各自回去研究一下,看看用哪种技术实现起来好一点,散会。”

傍晚,三个人再次来到了会议室。淡橙色的阳光铺洒在会议桌上,温暖的氛围让小Z想起了童年放学后愉快玩耍的日子。

D:“怎么样?有成果吗?”D哥又是一巴掌(以示友好)。

Y:“别说,真让我发现一个还不错的工具!”

Z:“嗯嗯,我也有收获。”

D:“好!我有个提议,我们便效仿昔日的周郎与孔明,把自己的想法写到纸上...”

Y:“不是写到手心吗?”

D:“嗯...你写手心也行,写完后大家同时亮出,看同也不同。”

于是,三人拿出纸笔,写下了自己这一下午调研的最终成果。

D:“摊开吧。”

D 哥话音一落,三张白纸便铺在了一处,上面出奇一致地各自写着:Cocos !

Y:“妈耶,就是他了!”


第二章 明察秋毫

原理探究

要说这 Cocos 究竟是什么来路,为什么这三人能够在彼此不知情的情况下都去选择了它?

D:“小Z,说说你的理由,为什么选 Cocos 呢?”

Z:“嗯嗯,那我先说下。咱们的目的是做一款 H5 手游,动画多,动画效果酷炫,游戏流畅性要求高,考虑到咱们的人力成本有限,能找到一套成熟的技术方案或者技术提供平台是最好不过的了,我们可以站在巨人的肩膀上,既减少开发时间,又能有效地避免兼容性问题和各种 bug 的出现。我朝着这个方向探索了一下,发现了不少优秀的平台,比如咱们国内做的不错的白鹭引擎,可以支持微信小游戏,百度小游戏等的开发,再比如...”

Y:“让你说为什么选 Cocos 呢,你说别的干啥,我来说吧!这种问题肯定是先逛论坛,我逛了一下午论坛,大牛们的口径很统一,Cocos 做这种小游戏没问题,技术成熟,案例丰富,用过都说好。像《梦幻西游》、《率土之滨》、《最终幻想》还有《开心消消乐》都是用 Cocos 搞的,还有一些其他的,给你们看官网的介绍图。”

D:“嗯,好,确实有很多成熟的已经落地的知名游戏。有了前人开路,我们在开发中遇到的问题应该可以迅速找到解决方案。你们有研究 Cocos 的发展历程底层原理没?”

Y:“哎!又来,这么枯燥的东西让小Z说吧。”

大家把目光转向了小 Z,作为团队中的保守一派,小Z平时不怎么发言,对待新技术的探索不如小Y有激情,但是比较热衷于研究已有技术的底层原理和来龙去脉,越古老的东西他反倒越有兴趣。发现大家都在看他,小Z的脸不争气地变红了。

Y:“你害羞个锤子!”

Z:“嗯嗯,那我说下我调研到的一些内容。咱们从刚刚到现在所说的 Cocos 其实是一种广义的说法,指的更多的是 Cocos 这个品牌或者说是我们要选用的技术栈方向是 Cocos。实际上 Cocos 就是指一个游戏引擎,开发这个引擎的团队在2011年底成立,研发出了该引擎和一系列产品,包括游戏编辑器 Cocos Creator 和全球流行的开源引擎框架 Cocos2d-x 等。

如果我们确定使用这个技术栈,那么我们后续要用到的应该是 Cocos Creator 游戏编辑器。它提供了一个完整的游戏开发解决方案,包含了轻量高效的引擎,以及能让你更快速开发游戏所需要的各种图形界面工具。界面是这样的:”

Z:“我再接着说下 Cocos Creator 游戏编辑器的工作原理。先看我从官网弄来的这张图,二位可以捋顺一下游戏编辑器整个的工作流程

Y:“太复杂了,不看,不看,你直接说。”

Z:“咳,行,简单说来分为四步:一、导入资源;二、搭建场景;三、编写脚本;四、预览发布。”

Y:“再具体点呢。”

Z:“第一步导入的资源可以是图片、图集、声音、TypeScript 脚本、JavaScript 脚本或者已有项目等。导入后,开始编辑场景。”

Y:“等一下,什么是场景?”

Z:“在 Cocos Creator 中,游戏场景( Scene )是开发时组织游戏内容的中心,也是呈现给玩家所有游戏内容的载体。游戏场景中一般会包括以下内容:

  • 场景图像和文字
  • 角色
  • 以组件形式附加在场景节点上的游戏逻辑脚本

当玩家运行游戏时,就会载入游戏场景,游戏场景加载后就会自动运行所包含组件的游戏脚本,实现各种各样开发者设置的逻辑功能。

所以除了资源以外,游戏场景是一切内容创作的基础。可以简单地把场景理解成一个总的节点,挂到这个总节点下的其他资源便都属于这个场景,我们可以预览一个场景下的动画,不同场景的动画可以切换。比如你玩游戏过了第一关,到了第二关,就是一个场景切换。”

Y:“明白!”

Z:“第三步是编写脚本,目前 Cocos Creator 支持 JavaScriptTypeScript 两种脚本语言。通过编写脚本组件,并将它赋予到场景节点中来驱动场景中的物体。对了,刚刚我们说到挂在场景下面的其他资源,在 Cocos 中都叫节点( Node ),可以看下这张图。”

Z:“我们在第一步导入的资源放在了左下角的资源管理器里并创建了一个场景 helloworld,双击这个场景文件就可以进入第二步:编辑场景。双击后,左上角那里的层级管理器就会展示当前场景的挂载情况,可以直接拖拽资源管理器中的图片等资源到该场景下,也可以直接在场景下面新建空节点。

静态资源准备好了,我们需要让动画动起来或者让节点之间有所联系,就可以进入第三步:编写脚本。我们在资源管理器中引入脚本 HelloWorld.ts,双击层级管理器中的某一个节点,在右侧的属性检查器中就可以把脚本挂载到这个节点上了。至于想实现什么样的逻辑和动画,就取决于需求和脚本的开发细节了。”

“嗯,介绍得很详细。”D哥向小Z投来赞许的目光,“最后一步预览发布就比较容易明白了,看来 Cocos Creator 想得很全面,从开始开发到最后的发布都一体化了。

Z:“嗯嗯,是的,并且他这个发布支持跨平台,开发好的小游戏可以在各种小游戏平台发布。

D:“行,现在我们已经了解了 Cocos 和一些基本的开发流程,不过虽然他有很多成熟的作品,但是我们用起来真的会很顺手吗?我们只是一个 H5 页面,放很多动画的话,会不会造成不太好的用户体验?”

Y:“啊哈!这个不会,我问了论坛里的大佬们,他们说 Cocos 引擎使用了 webGL(Web Graphics Library),webGL 是一种 3D 绘图标准,允许工程师使用 JS 去调用部分封装过的 OpenGL 标准接口提供硬件级别的 3D 图形加速功能。官网也有图介绍了这个,你们看。”

D:“好的,没问题,那我做一下总结。Cocos 开发游戏技术方案成熟,出色产品较多,开发流程一体化方便快捷,并且底层架构优秀,可以快速渲染图形,不存在性能问题的后顾之忧。那咱们就选它了?”

众人:“嗯嗯!就它了,开干!”


第三章 小试牛刀

快速入门

第二天,三个人都来得很早,小Y习惯性地去搞了一杯咖啡,小心翼翼地端到了工位上。

Y:“D哥,demo 跑完了没?”

D 哥睡眼惺忪,显然昨晚又熬夜了。“没有啊,什么 demo ?昨晚赶一个项目,没腾出时间。”

Y:“就 Cocos 官网上的那个入门 demo 。你得注意身体啊,小心猝死!”

......

Z:“那个 demo 我跑了,蛮有意思的,待会儿早会给你们演示一下?”

D:“可以。”

上午的会议室资源相对宽松,来到了会议室后,小Z就开始演示自己昨晚跑的 demo 。

Z:“因为这个是官网上的,我就不详细说每个细节了,先来看下最终效果”

Y:“我去,你这一下子就 Game Over 了啊。”

Z:“咳,别在意这些细节。我来介绍一下这个小 demo ,就挑我认为重点的说两点吧。一是主角的运动实现,二是 prefab 的使用。

紫色的小怪兽就是我们游戏的主角,我们现在要让他不断跳跃,并且可以左右移动。按照我们昨天说的,编辑器使用的前两步是导入资源和搭建场景,这两步完成后,我们得到了一个场景和一只静止在场景中的小怪兽。”

Z:“接下来我们在资源管理器中创建一个脚本准备驾驭这只小怪兽。并开始 coding 。”

Z:“先定义小怪兽的属性,包括跳跃高度,跳跃持续时间,最大移动速度和加速度。这里要注意初始值的设定最好是一个兜底值。”

properties: {
    // 主角跳跃高度
    jumpHeight: 0,
    // 主角跳跃持续时间
    jumpDuration: 0,
    // 最大移动速度
    maxMoveSpeed: 0,
    // 加速度
    accel: 0,
},

Z:“接下来编写跳跃代码,这里涉及到了缓动( cc.tween )系统,在 Cocos Creator 中,cc.tween 可以对任何对象进行操作,并且可以对对象的任意属性进行缓动。by 方法的作用是对属性进行相对值计算,表示的是变化值。比如在跳跃上升阶段,y 的值为跳跃高度,是一个正值,则小怪兽的高度会相对于当前高度增加,下落的时候 y 的值是一个负值,小怪兽会在跳跃最高点的基础上减少自己的 y 值,也就下落了。”

runJumpAction () {
    // 跳跃上升
    var jumpUp = cc.tween().by(this.jumpDuration, {y: this.jumpHeight}, {easing: 'sineOut'});
    // 下落
    var jumpDown = cc.tween().by(this.jumpDuration, {y: -this.jumpHeight}, {easing: 'sineIn'});
    // 创建一个缓动,按 jumpUp、jumpDown 的顺序执行动作
    var tween = cc.tween().sequence(jumpUp, jumpDown)
    // 不断重复
    return cc.tween().repeatForever(tween);
},

Z:“onLoad 方法会在场景加载后立刻执行,所以初始化相关的操作和逻辑都会放在这里面。我们首先将循环跳跃的动作传给了 jumpAction 变量,然后将其插入到 cc.tween 对小怪兽进行缓动的队列中,再调用 start 开始执行 cc.tween,从而让小怪兽一直跳跃。”

onLoad: function () {
    var jumpAction = this.runJumpAction();
    cc.tween(this.node).then(jumpAction).start()
},

Z:“接下来添加键盘输入,用 A 和 D 来控制它的跳跃方向。添加键盘事件响应函数 onKeyUp 和 onKeyDown。”

onKeyDown (event) {
    // set a flag when key pressed
    switch(event.keyCode) {
        case cc.macro.KEY.a:
            this.accLeft = true;
            break;
        case cc.macro.KEY.d:
            this.accRight = true;
            break;
    }
},

onKeyUp (event) {
    // unset a flag when key released
    switch(event.keyCode) {
        case cc.macro.KEY.a:
            this.accLeft = false;
            break;
        case cc.macro.KEY.d:
            this.accRight = false;
            break;
    }
},

Z:“然后修改 onLoad 方法,在其中加入向左和向右加速的开关,以及小怪兽当前在水平方向的速度。最后再调用 cc.systemEvent,在场景加载后就开始监听键盘输入。”

onLoad: function () {
    // 初始化跳跃动作
    var jumpAction = this.runJumpAction();
    cc.tween(this.node).then(jumpAction).start()

    // 加速度方向开关
    this.accLeft = false;
    this.accRight = false;
    // 主角当前水平方向速度
    this.xSpeed = 0;

    // 初始化键盘输入监听
    cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
    cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this);   
},

onDestroy () {
    // 取消键盘输入监听
    cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
    cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
},

Z:“最后修改 update 方法中的内容,添加加速度、速度和小怪兽当前位置的设置。update 会在场景加载后每帧调用一次,我们一般把需要经常计算或及时更新的逻辑内容放在 update 中。在我们的游戏里,根据键盘输入获得加速度方向后,就需要每帧在 update 中计算主角的速度和位置。”

update: function (dt) {
    // 根据当前加速度方向每帧更新速度
    if (this.accLeft) {
        this.xSpeed -= this.accel * dt;
    }
    else if (this.accRight) {
        this.xSpeed += this.accel * dt;
    }

    // 限制主角的速度不能超过最大值
    if (Math.abs(this.xSpeed) > this.maxMoveSpeed) {
        // if speed reach limit, use max speed with current direction
        this.xSpeed = this.maxMoveSpeed * this.xSpeed / Math.abs(this.xSpeed);
    }

    // 根据当前速度更新主角的位置
    this.node.x += this.xSpeed * dt;
},

Z:“到这里代码实现就完成了,我们可以在属性检查器中修改小怪兽的各项属性来优化游戏体验。”

Z:“然后,我再来说一下 prefab。对于需要重复生成的节点,我们可以将它保存成 prefab(预制资源),作为我们动态生成节点时使用的模板。在场景中编辑好节点后,直接将节点从 层级管理器 拖到 资源管理器 就可以把这个节点转变为一个预制资源。”

D:“打断一下哈,你说这个 prefab 是一个模板?”

Z:“嗯嗯,是的。比如我们这个 demo 中的星星,是要不断重复出现的,但是图案、大小、颜色每次都一致,不同的只是每次出现的位置。这种情况下,我们就可以用一个模板,包含所有的属性,需要的时候就用这个模板创建一个节点,再修改一下这个节点的某个属性(在这个 demo 里是位置属性),就可以了。在 Cocos 里 prefab 就是起到模板的作用,被称作 ‘预制资源’。”

D:“好的,请继续。”

Z:“刚刚也有说到,创建 prefab 的过程很简单,我们先编辑一个节点,然后把节点从层级管理器拖拽到资源管理器,让这个节点变成一个“资源”,一个 prefab 也就制作成功了,当我们想要在 A 节点上使用这个 prefab 的时候,先在 A 节点的挂载脚本中声明一个 prefab 属性,再在编辑器界面把资源管理器中的 prefab 拖拽到 A 节点属性管理器中,就可以在脚本中使用这个 prefab 了。代码是这样的。”

// 声明一个 prefab 属性
properties: {
    starPrefab: {
        default: null,
        type: cc.Prefab
    },
}

// newStar 就是通过 prefab 制作的新节点

var newStar = cc.instantiate(this.starPrefab);

D:“行,我基本了解了...诶?小Y ,你是不是睡着了?”

Z:“我说怎么这么安静...”

D 哥拍醒了小Y :“快起来,你的咖啡凉啦!”

Y:“啊?说完了吗?这么快!我可没睡觉啊。”

D:“行了,别狡辩了,都听到你打呼噜了...”


第四章 风起云涌

项目开发

北方的秋,天高云淡。一场风雨过后,树叶都变成了金黄。距离上次开会已经过去一月有余,几个人都在忙着自己手上的任务。这天早晨,D 哥穿上了羽绒服,依旧风风火火地走进了办公室。

D:“做的怎么样了,小伙子们?”

Y:“快来看,小 Z 这个主操作区域差不多了。”

D 哥脱下衣服,挂在了公用衣架上,用手拍了拍衣服上面的灰尘。

Y:“你快点儿撒!要我说,咱们这个项目,弄好了肯定得火!”

D 哥来到了小 Z 旁边,一起看他这几天的开发成果。

D:“还真不错,我看你这里面实现了移动、合成、购买和回收功能是吧?”

D 哥给两位的任务分配是小Y负责页面上半部分,小Z负责下半部分。所以在这里直接针对下半部分的格子区域进行询问。

Z:“嗯嗯,是的,我来简单介绍一下。这几个功能的实现都是依赖于 Cocos 碰撞组件。在 Cocos 中其实是有着丰富的组件体系的,我们之前挂到了节点上的脚本也可以称作为脚本组件。在这里,我使用的碰撞组件就是用来监测节点之间是否发生碰撞的。选中一个节点,在它的属性管理器中就可以创建一个下面这样的 Box(矩形)碰撞组件。”

Z:“节点被碰撞的过程中,会触发如下几个回调函数。”

/**
 * 当碰撞产生的时候调用
 */
onCollisionEnter: function (other, self) {
    console.log('on collision enter');
},
/**
 * 当碰撞产生后,碰撞结束前的情况下,每次计算碰撞结果后调用
 */
onCollisionStay: function (other, self) {
    console.log('on collision stay');
},
/**
 * 当碰撞结束后调用
 */
onCollisionExit: function (other, self) {
    console.log('on collision exit');
}

Z:“这里我遇到了一个坑,官方文档没有提到 other 和 self 这两个形参的含义,而我又未细致思考,理所当然地把 other 当作了主动碰撞的节点, self 当作了被碰撞的节点。结果出现了各种诡异的表现。实际上 other 代表上层的节点,self 代表下层的节点。这个可以记录下。”

D:“上层节点和下层节点具体是指什么?”

Y:“这你还要问,一看就没认真学习新知识!Cocos 在渲染节点的时候,先渲染的节点在下层,后渲染的节点在上层。层级管理器中,靠上的节点先渲染,所以在下层,靠下的节点后渲染,所以在上层。比如这里,joy 节点靠上,所以优先渲染;light 节点靠下,所以后渲染,出来的效果就是 joy 节点在 light 节点的下层。”

Z:“嗯嗯,有了碰撞组件做基础,实现这几个功能就简单很多了。比如说‘移动’,当格子中没有 joy 存在的时候,我会放一个 joy 的影子在里面。这个影子作为一个节点,被移动的 joy 是另一个节点,一旦两个节点发生碰撞,我就会根据这两个节点的属性以及碰撞的回调函数来做出接下来的操作。”

if (otherNode.getComponent('joy').level == 0 || selfNode.getComponent('joy').level == 0) {
    this.moveJoy(); 

D:“我理解一下,我看你这里是判断了节点的 level 属性为0,所以你应该是当没有 joy 存在时,把那个影子节点的 level 设置为0了,而有等级的 joy 的 level 一定大于0。碰撞发生时,一旦发现有等级为0的 joy 我们就可以断定,是一只有等级的 joy 要被移动到空格子里啦。”

Z:“没错,不愧是D哥,一看就懂。”

Y:“你这个 moveJoy 函数是咋写的,给我们瞧瞧啊。”

Z:“可以,这里我是用到了 Cocos 提供的方法 cc.tween 。”

moveJoy() {
    this.request('move')
      .then((res) => {
          this.moveJoyAction();
      })
      .catch((err) => {
      });
}

moveJoyAction() {
    cc.tween(staticNode)
      .call(function () {
        // 主动碰撞的节点回归原位,并变为无狗状态
        movingNode.setPosition(originalPosition);
        this.changeImg(movingNode);
        this.hideLabel(movingNode);
      })

      // 被碰撞的节点做动作
      .to(0, { opacity: 0 })
      .to(0, { position: movingNodePosition })
      .call(() => {
        this.changeImg(staticNode);
        this.showLabel(staticNode);
      })
      .to(0, { opacity: 255 })
      .to(0.1, { position: cc.v3(staticNodePosition.x, staticNodePosition.y, 0) })
      .start();
}

Y:“这里我懂,我来说。你先请求后端接口,通知到后端我们要进行移动了。一旦接口调通,便开始展示前端动画。这里你利用人们的视觉能力的局限性制造了一个假象,让人们觉得是把 A 放到了空格子 B 里,实际上不是,你是先把 A 变成了 B 的样子,放回了 A 自己的格子里 ,再把 B 变成了 A 的样子,放到刚刚 A 所在的位置,最后把 B 移动回自己的格子。”

Z:“嗯嗯,小 Y 说的没错,这里我考虑了一下,如果实打实地把 A 节点放到 B 节点,就需要销毁 B 节点,并且要在 A 节点原来的位置再创建一个新节点。这样太浪费性能,并且如果用户操作一旦快起来,恐怕很容易造成卡顿等问题。”

Z:“除了移动操作,其他几个操作也都是用了相似的方法,都是用 cc.tween 来控制动画的展现。”

D:“能再详细解释一下上面这个移动动画的代码吗?我没有在这做很深入的研究。”

Z:“好的,那我就解释一下 moveJoyAction 这个函数。第一行 cc.tween(staticNode) 调用方法 tween 并传入我们的被碰撞节点,因为被碰撞节点是静止不动的,所以我给它命名为 staticNode 。接下来的 call ,to 这些方法,都是链式相接的,可以保证在前一个方法执行过后,才会执行后面的方法,得益于这种链式结构,我们动画的先后顺序才是可控的。”

D:“是说传递了 staticNode 进去,之后的动作就都是控制这个节点的了吗?”

Z:“没错,像代码里面的 to 方法,可以传入两个参数,第一个参数表示动画时间,第二个参数表示该节点将要达到的状态,都是针对 staticNode 来操作的。不过像 call 方法,实际上就是去执行一下 call 里面定义的回调函数的内容,所以我直接把控制 movingNode 的操作写在里面了。这样直接控制了两个节点的一连串的动作。”

D:“好的,明白。其他几种操作也都是用这种方法写的是吧。”

Z:“嗯嗯,是的,比如合成操作,代码是这样。接口调用成功后,开始展示动效:两个 joy 彼此分开再合在一起,同时产生一个光圈特效。两个 joy 是两个不同的节点,利用 cc.tween 分别处理位置及光圈等元素。以 staticNode 为例,使用 by 方法先相对当前位置右移,再相对移动后的位置左移,之后提升等级、更新图片并展示光圈特效。”

mergeJoy() {
    this.request('merge')
      .then((res) => {
          this.levelUpAction();
      })
      .catch((err) => {
      });
}

levelUpAction( staticNode, movingNode) {
      cc.tween(movingNode)
        .by(0.2, { position: cc.v2(-50, 0) })
        .by(0.1, { position: cc.v2(50, 0) })
        .to(0, { opacity: 0 })
        .call(function () {
          // 移动的节点回归原位,并变为无狗状态
          movingNode.setPosition(originalPosition);
          this.changeImg();
        })
        .to(0, { opacity: 255 })
        .start();

      cc.tween(staticNode)
        .by(0.2, { position: cc.v2(50, 0) })
        .by(0.1, { position: cc.v2(-50, 0) })
        .call(function () {
          // 等级提升
          staticNode.getComponent('mainjoys').level++;
          this.changeImg();
          // 升级光效
          lightNode.getComponent('light').shine();
        })
        .start();
}

D:“好,看来 cc.tween 的功能还是很强大的,我感觉利用这个方法可以做任何动效了。”

Y:“这样写其实很麻烦的!”

D:“小 Y 你还有什么简单的方法吗?”

Y:“当然,其实 Cocos 还提供了另一种比较简单的动画实现方式,就是利用它自带的动画编辑器。你看上半部分那个掉落的宝箱,就是我用动画编辑器搞出来的。运行轨迹完全按照自由落体设置,十分逼真。”

D:“哦?讲讲看。”

Y:“看下面这张图。我们选中 box 节点,在下方就可以直接通过动画编辑器来编辑它。区域1里展示的是动画中需要变化的属性,我们要让宝箱掉落后上下浮动,所以改变的属性是 y 的值;区域2代表一帧动画,每一桢的属性改变就构成了整个动画过程;区域3是节点的所有属性值,我们要改变区域1中的 y 就在这里直接改就行。”

D:“嗯,的确看起来很简单,不过难点应该在每一帧的数值选取上吧,你是如何实现你的自由落体的?

Y:“首先,回想起来高中的物理知识,不过我当初没想起来,就直接百度了。初速度为0,公式是 H = 1/2 gt^2 ,我们可以求出每隔 t 秒,下落了多少距离,然后根据两桢之间的时间间隔 t 来确定出宝箱的 y 值。”

D:“不过这样的话,从一帧到另一帧的过程中是匀速运动吗,速度就是(y2 - y1)/t ?

Y:“是的,不过由于我们的 t 是足够小的,我在这令 t = 1/6s ,在这 1/6s 内的匀速运动根本无法被人感知到,我们看起来就是一个逐渐加速的过程。”

D:“不错,看来你们两个都基本掌握了使用 Cocos 开发动画的一些要领,从目前的成果来看,我们当时选择这个技术栈来开发也是没有错的,继续加油!”


第五章 尘埃落定

项目完成了

转眼到了冬天,凛冽的西伯利亚北风吹来,很多同事都感冒了。小 Y 一早便来到公司,坐在办公桌前享受着今日份咖啡和八九点钟太阳馈赠的温柔的光热。

Y:“上线!上线!这些日子 all in 开发太累了,头都秃了!”

的确,正像小 Y 说的,几个月艰苦的开发,大家都有些累了,同时也终于迎来了最后的时刻。

D:“咱们再做最后一次全流程测试,怀着要把游戏玩坏的想法走一遍,看看有没有什么隐藏的 bug 没解决,如果没问题,明早就正式上线。”

Z:“嗯嗯!”

Y:“哎呀!完蛋!”

小 Y 的一声惊呼吓出了D哥和小Z的两身冷汗。都赶紧凑了过去。

D:“怎么了?”

Y:“早饭忘记吃了。”

D & Z:“靠!”

......

Y:“哎呀!这次真完蛋了!咱们没有限制多指操作!如果同时拖动两只 joy 进行合成,会显示异常。”

Z:“我之前写了一段处理多指操作的逻辑,不过效果不好,就没加上去。”

D:“哪里不好?”

Z:“我的思路是设定一个全局开关,一旦触碰发生,就关闭开关让其他所有的触碰不再生效,只有当前的触碰停止后才打开开关。”

D:“思路是正确的,不过你在这个过程中应该还是用到了 Cocos 提供的触摸监听事件,并且你要实现这个功能,需要把所有的情况都考虑到,一旦漏掉了哪个节点或者哪种情况,你的开关可能就会失灵。“

Z:”是的,我自测的时候就发现如果操作过于频繁,会导致我的开关无法再次开启的情况发生。“

D:”我觉得这个问题比较常规,咱们先赶紧去查查,Cocos 有没有给我们提供相应的解决方案。“

Z:“好。”

......

Y:“找到啦!”

D:“这么快?说话的功夫就找到了吗?”

Y:“当然!这个问题官方就有提供解决办法,只不过也是最近几个版本才加上的。”

D:“我们的版本支持吗?”

Y:“支持。”

D:“好!快试试!”

三个人兴奋又忐忑地把代码加到了程序里。

cc.macro.ENABLE_MULTI_TOUCH = false;

D:“就一行?”

Y:“就一行!”

......

Y:“成功了!”

D:“不错,我们接着测试,看看还能找到其他的什么漏洞不。一直测试到上线,争取不留 bug!”

Y & Z:“嗯嗯!”

又是一个清晨,大师端坐在湖边,早已被冻得结实的冰层被快速地劈裂开来,一道红光跃起。

大师:“鱼儿,回来了。”

Cocos:“是呢,是呢。”

大师:“这一趟,可有收获?”

Cocos:“有呢,有呢。发生了很多事情呢......”


———— End ————

原文链接:

https://jelly.jd.com/article/5fbe616a7482df01463e27e3

复制链接在浏览器中打开,即可查看作者原文,交流更多研发经验喔!

想要了解更多 Cocos 引擎在游戏、电商、教育、展会等领域的研发应用及高能方案?点击「阅读原文」报名参加12.26广州沙龙活动吧!

浏览 52
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报