为了媳妇,熬夜撸了一个合成大西瓜!

开发 前端 开发工具
最近微博上曝出了很多瓜,"合成大西瓜"这个游戏也很火热,玩了一阵还挺有意思的。

 [[380153]] 

图片来自 Pexels

研究了一下原理,发现目前流传的版本都是魔改编译后的版本,代码经过压缩不具备可读性,因此决定自己照着实现一个。

本项目主要用作 cocos creator 练手使用,所有美术素材和音频材料均来源于:

  1. www.wesane.com/game/654/ 

感谢原作者,向每一位游戏开发者致敬!

本文所有代码及素材都放在 Github 上:

  1. https://github.com/tangxiangmin/cocos-big-watermelon 

也可以通过在线预览地址体验:

  1. https://web-game-9gh6nrus14fec37e-1252170212.tcloudbaseapp.com/ 

游戏逻辑

整个游戏逻辑比较简单,结合了俄罗斯方块与消除游戏的核心玩法:

  • 在生成一个水果
  • 点击屏幕,水果移动到对应 x 轴位置并自由下落
  • 每个水果会与其他水果发生碰撞,两个相同的水果碰撞时会发生合并,升级成更高一级的水果

水果共有 11 种类型:

 

游戏目标是合成最高级的水果:大西瓜!当堆积的水果超过顶部红线时则游戏结束。

整理出需要实现的核心逻辑:

  • 生成水果
  • 水果下落与碰撞
  • 水果消除动画效果及升级逻辑

预备工作

cocos creator 基本概念

整个项目使用 cocos creator v2.4.3 实现,建议初次了解的同学可以先过一下官方文档,本文不会过多介绍 creator 的使用(主要是我也不太熟练)。

官方文档链接:

  1. https://docs.cocos.com/creator/2.3/manual/zh/ 

游戏素材

首先需要准备美术资源,本位所有美术素材和音频材料均来源于:

  1. www.wesane.com/game/654/ 

首先访问游戏网站,打开 network 面板,可以看见游戏依赖的所有美术资源,我们下载自己所需的文件即可。

 

所需的图片资源包括:

  • 11 张水果贴图
  • 每种水果合成效果贴图,均包含:一张果粒图片,一张圆形水珠图片,一张爆炸贴图
  • 两个西瓜合成时有灯光和撒花的效果,时间有限暂不实现
  • 音频文件同理,可以在 Filter 栏选择 .mp3 后缀的请求快速筛选对应资源。水果消除时的爆炸声和水声,音频文件同理,可以在 Filter 栏选择 .mp3 后缀的请求快速筛选对应资源

创建游戏场景和背景

打开 cocos creator,新建一个项目(也可以直接导入从 github 下载的项目源码):

https://github.com/tangxiangmin/cocos-big-watermelon

然后记得将刚才下载的素材资源拖拽到右下角的资源管理器中。

创建 scene 和背景节点

项目初始化之后,在左下角资源管理器新建一个游戏 Scene,取名 game 作为游戏主场景。

 

创建完毕后就可以在资源管理器的 assets 中看见刚才创建的名为 game 的 scene。

选择 game 场景,在左上角的层级管理器中可以看见场景的 Canvas 画布根节点,cocos 默认画布是横屏的 960*640,可以选择根节点然后再右侧属性检查器中调整宽高为 640*960。

 

接下来创建背景层,我们在 Canvas 节点下面新建一个 background 节点,由于整个背景是纯色 #FBE79D 的,因此使用一个单色 Sprite 填充即可。

 

同样将 background 节点宽高调整为整个画布的大小,由于默认锚点均为 0.5*0.5,此时整个画布会被完全填充。

现在整个游戏场景大概是这个样子的:

 

接下来设计游戏的逻辑脚本部分。

场景脚本组件

在 assets 目录下新建一个 js 脚本,按照惯例命令成 Game.js,creator 会生成一个带基础 cc.Class 的模板文件。

 

先将脚本组件与节点关联起来,选择 Canvas 根节点,在右侧属性检查器中添加组件,然后选择刚才创建的这个 Game 组件。

 

然后编写具体的代码逻辑,打开 Game.js 文件(建议使用 vscode 或者 webstrom 打开整个项目的根目录进行编辑)。

里面的初始代码大概长这样:

// Game.jscc.Class({ extends: cc.Component, properties: { }, onLoad(){ }, start(){ }})

我们需要在这里维护整个游戏的逻辑,后面逐步添加代码内容。

创建水果

水果是整个游戏的核心元素,在游戏中被频繁创建和销毁。

生成单个水果预制资源

这种动态创建的节点可以通过预制资源 Prefab 来控制,制作 Prefab 最简单的方式就是将资源从资源管理器拖动到场景编辑器中,然后再将层级管理器中的节点拖回资源管理器。

这里以等级最低的水果“葡萄”为例:

 

 

然后将层级管理器中的节点删除,这样我们就得到了一个 fruit 的预制资源,在脚本组件中,就可以使用代码通过预制资源动态生成节点了。

修改 Game.js,添加一个属性 fruitPrefab,其类型为 cc.Prefab:

  1. // Game.js 
  2. properties: { 
  3.     fruitPrefab: { 
  4.         defaultnull
  5.         type: cc.Prefab 
  6.     }, 

回到 creator,选择 Canvas 节点,可以在属性检查器中的 Game 组件栏目看见和修改该属性了。

我们将刚才制作的 prefab 资源从资源管理器拖动到这里,在初始化的时候,有 cocos 负责初始化对应的属性数据:

创建单个水果 

回到 Game.js,开始编写真正的逻辑:创建一个葡萄。

  1. // Game.js 
  2. onLoad(){ 
  3.     let fruit = cc.instantiate(this.fruitPrefab); 
  4.     fruit.setPosition(cc.v2(0, 400)); 
  5.  
  6.     this.node.addChild(fruit); 

预览模式下就可以看见屏幕正上方有一个葡萄了:

 

Nice,非常好的开始!

此外,由于水果还包含一些特定的逻辑,我们可以向它添加一个 Fruit 脚本组件,虽然目前看起来还没有什么用!

创建 Fruit 脚本组件与上面创建 Game 组件类似,然后选择刚才制作的 prefab 重新编辑,关联上 Fruit 用户脚本组件即可。

动态维护多种水果

整个游戏共 11 种水果(当然也可以添加或者改成其他的东西),如果每种水果都像上面去手动生成预制资源然后分别初始化,那也太繁琐了,我们需要解决动态渲染多种水果的方式。

我们需要获得每种水果的贴图信息,然后在实例化水果时选择对应贴图即可,最简单的方式就是维护一个配置表,每行的数据字段包括 id 和 iconSF。

  1. const FruitItem = cc.Class({ 
  2.     name'FruitItem'
  3.     properties: { 
  4.         id: 0, // 水果的类型 
  5.         iconSF: cc.SpriteFrame // 贴图资源 
  6.     } 
  7. }); 

然后为 Game 脚本组件新增一个 fruits 属性,用于保存每种水果的配置信息,其类型是数组,数组内元素类型为刚才创建的 FruitItem。

  1. // Game.js 
  2. properties: { 
  3.     fruits: { 
  4.         default: [], 
  5.         type: FruitItem 
  6.     }, 

回到编辑器,这时候可以发现 Game 组件的属性下面多了一个 Fruits 属性,将其长度修改为 11,然后依次编写每个水果的 id,同时将其贴图资源从资源编辑器贴过来(体力活)。

 

这样我们只需要传入想要制作的水果 id,就可以获取到对应的配置信息,并动态修改贴图了。

这种初始化的逻辑应该由水果自己维护,因此放在刚才创建的 Fruit 组件中,我们暴露一个 init 接口出来。

  1. // Fruit.js 
  2. properties: { 
  3.     id: 0, 
  4. }, 
  5. // 实例放在可以在其他组件中调用 
  6. init(data) { 
  7.     this.id = data.id 
  8.     // 根据传入的参数修改贴图资源 
  9.     const sp = this.node.getComponent(cc.Sprite) 
  10.     sp.spriteFrame = data.iconSF 
  11. }, 

然后修改一下上面的初始化水果的代码:

  1. // Game.js 
  2. createOneFruit(num) { 
  3.     let fruit = cc.instantiate(this.fruitPrefab); 
  4.     // 获取到配置信息 
  5.     const config = this.fruits[num - 1] 
  6.  
  7.     // 获取到节点的Fruit组件并调用实例方法 
  8.     fruit.getComponent('Fruit').init({ 
  9.         id: config.id, 
  10.         iconSF: config.iconSF 
  11.     }); 

这样就可以愉快的创建各种水果了。

监听点击事件

cocos 提供了各种事件监听,前端和客户端同学一定不会陌生。

整个游戏会在点击屏幕时创建一个水果,这只要监听一下全局点击事件即可,这个逻辑同样放在 Game 脚本组件中。

  1. onLoad() { 
  2.     // 监听点击事件 
  3.     this.node.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this) 
  4. }, 
  5. onTouchStart(){ 
  6.     this.createOneFruit(1) // 生成水果 

实际游戏中还需要处理随机生成水果、上一个水果在点击的 x 轴下落等细节逻辑,这里不再赘述。

物理系统:自由落体与刚体碰撞

上面处理了水果创建的逻辑,在整个游戏中,水果是可以产生下落及弹性碰撞等物理效果的,利用 cocos 内置的物理引擎,可以很方便的实现。

对 cocos 引擎不熟悉的同学可以先看看这个官方 demo,里面展示的比较详细(起码比文档要更容易理解)。

开启物理引擎与碰撞检测

首先是开启物理引擎,以及设置重力大小:

  1. const instance = cc.director.getPhysicsManager() 
  2. instance.enabled = true 
  3. // instance.debugDrawFlags = 4 
  4. instance.gravity = cc.v2(0, -960); 

然后需要开启碰撞检测,默认是关闭的:

  1. const collisionManager = cc.director.getCollisionManager(); 
  2. collisionManager.enabled = true 

然后设置四周的墙壁用于碰撞,这样水果就不会无限制往下面掉落了:

  1.  // 设置四周的碰撞区域 
  2. let width = this.node.width; 
  3. let height = this.node.height; 
  4.  
  5. let node = new cc.Node(); 
  6.  
  7. let body = node.addComponent(cc.RigidBody); 
  8. body.type = cc.RigidBodyType.Static
  9.  
  10. const _addBound = (node, x, y, width, height) => { 
  11.     let collider = node.addComponent(cc.PhysicsBoxCollider); 
  12.     collider.offset.x = x; 
  13.     collider.offset.y = y; 
  14.     collider.size.width = width; 
  15.     collider.size.height = height; 
  16.  
  17. _addBound(node, 0, -height / 2, width, 1); 
  18. _addBound(node, 0, height / 2, width, 1); 
  19. _addBound(node, -width / 2, 0, 1, height); 
  20. _addBound(node, width / 2, 0, 1, height); 
  21.  
  22. node.parent = this.node; 

现在我们就开启了游戏世界的物理引擎,然后还需要配置需要受引擎影响的节点,也就是我们的水果。

水果刚体组件与碰撞组件

回到 creator,找到我们的水果 prefab,然后添加物理组件。

首先是 Rigid Body(刚体)组件:

然后是物理碰撞组件,因为我们的水果全是圆形的,都选择 PhysicsCircleCollider 组件就可以了,如果有个香蕉之类不规则多边形边的话,工作量就会增加不少~

 

接下来可以看看整体效果,(记得把刚才的点击事件加上,然后控制一下随机生成水果类型):

 

完美!!

水果碰撞回调

添加完成之后,还需要开启刚体组件的碰撞属性 Enabled Contact Listener,这样可以接收到碰撞之后的回调。

 

这个碰撞回调同样写在 Fruit 脚本组件里面:

  1. // Fruit.js 
  2. onBeginContact(contact, self, other) { 
  3.     // 检测到是两个相同水果的碰撞 
  4.     if (self.node && other.node) { 
  5.         const s = self.node.getComponent('Fruit'
  6.         const o = other.node.getComponent('Fruit'
  7.         if (s && o && s.id === o.id) { 
  8.             self.node.emit('sameContact', {self, other}); 
  9.         } 
  10.     } 
  11. }, 

为了保证 Fruit 组件功能的单一性,在两个相同水果发生碰撞时,我们通过事件通知 Game.js,这样可以在初始化水果的时候注册 sameContact 自定义事件的处理方法。

  1. // Game.js 
  2. createOneFruit(num) { 
  3.     let fruit = cc.instantiate(this.fruitPrefab); 
  4.     // ...其他初始化逻辑 
  5.      fruit.on('sameContact', ({self, other}) => { 
  6.         // 两个node都会触发,临时处理,看看有没有其他方法只展示一次的 
  7.         other.node.off('sameContact')  
  8.         // 处理水果合并的逻辑,下面再处理 
  9.         this.onSameFruitContact({self, other}) 
  10.      }) 

这样当水果发生碰撞时,我们就能够监听并处理消除升级逻辑了。

消除水果动画

无动画版本

简单的消除逻辑就是将两个节点删除,然后在原水果位置生成高一级的水果即可,没有任何动画效果:

  1. self.node.removeFromParent(false
  2. other.node.removeFromParent(false
  3.  
  4. const {x, y} = other.node // 获取合并的水果位置 
  5. const id = other.getComponent('Fruit').id 
  6.  
  7. const nextId = id + 1 
  8. const newFruit = this.createFruitOnPos(x, y, nextId) // 在指定位置生成新的水果 

虽然看起来有点奇怪,但的确可以以玩了!

分析动画

打开源站,通过 Performance 面板分析一下动画效果(这里就不录 gif 了)。

 

可以看见合成的时候动画效果包括:

  • 碰撞水果向原水果中心移动
  • 果粒爆炸的粒子效果
  • 水珠爆炸的粒子效果
  • 一滩果汁的缩放动画

此外还有爆炸声和水声的音效。

管理爆炸素材资源

由于整个动画涉及到的素材较多,每种水果均包含 3 种颜色不同的贴图,与上面 FruitItem 类似,我们也采用 prefab 加动态资源的做法来管理对应素材和动画逻辑。

首先定义一个 JuiceItem,保存单种水果爆炸需要的素材:

  1. // Game.js 
  2. const JuiceItem = cc.Class({ 
  3.     name'JuiceItem'
  4.     properties: { 
  5.         particle: cc.SpriteFrame, // 果粒 
  6.         circle: cc.SpriteFrame, // 水珠 
  7.         slash: cc.SpriteFrame, // 果汁 
  8.     } 
  9. }); 

然后为 Game 组件新增一个 juices 属性:

  1. // Game.js 
  2. properties: { 
  3.     juices: { 
  4.         default: [], 
  5.         type: JuiceItem 
  6.     }, 
  7.     juicePrefab: { 
  8.         defaultnull
  9.         type: cc.Prefab 
  10.     }, 

接下来又是卖劳力的时候了,将贴图资源都拖放到 juices 属性下:

 

然后新增一个空的预制资源,主要是为了挂载脚本组件,也就是下面的 Juice 脚本,然后记得将该预制资源挂载到 Game 的 juicePrefab 上。

最后,新建 Juice 组件,用来实现爆炸的动画逻辑,同样需要暴露 init 接口:

  1. // Juice.js 
  2. cc.Class({ 
  3.     extends: cc.Component, 
  4.  
  5.     properties: { 
  6.         particle: { 
  7.             defaultnull
  8.             type: cc.SpriteFrame 
  9.         }, 
  10.         circle: { 
  11.             defaultnull
  12.             type: cc.SpriteFrame 
  13.         }, 
  14.         slash: { 
  15.             defaultnull
  16.             type: cc.SpriteFrame 
  17.         } 
  18.     }, 
  19.     // 同样暴露一个init接口 
  20.     init(data) { 
  21.         this.particle = data.particle 
  22.         this.circle = data.particle 
  23.         this.slash = data.slash 
  24.     }, 
  25.     // 动画效果 
  26.     showJuice(){ 
  27.  
  28.     } 

这样,在合并的时候,我们初始化一个 Juice 节点,同时展示爆炸效果即可。

  1. // Game.js 
  2. let juice = cc.instantiate(this.juicePrefab); 
  3. this.node.addChild(juice); 
  4.  
  5. const config = this.juices[id - 1] 
  6. const instance = juice.getComponent('Juice'
  7. instance.init(config) 
  8. instance.showJuice(pos, n) // 对应的爆炸逻辑 

爆炸粒子动画

关于粒子动画,网上能查到不少资料,如果感兴趣,也可以移步我之前整理的前端常见动画实现原理:

  1. https://www.shymean.com/article/%E5%89%8D%E7%AB%AF%E5%B8%B8%E8%A7%81%E5%8A%A8%E7%94%BB%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86 

粒子动画的主要的实现思路为:初始化 N 个粒子,控制他们的速度大小、方向和生命周期,然后控制每个粒子按照对应的参数执行动画,所有粒子汇集在一起的效果就组成了粒子动画。

话虽如此,要把动画效果调好还是挺麻烦的,需要控制各种随机参数。

  1. showJuice(pos, width) { 
  2.     // 果粒 
  3.     for (let i = 0; i < 10; ++i) { 
  4.         const node = new cc.Node('Sprite'); 
  5.         const sp = node.addComponent(cc.Sprite); 
  6.         sp.spriteFrame = this.particle; 
  7.         node.parent = this.node; 
  8.         // ... 一堆随机的参数 
  9.  
  10.         node.position = pos; 
  11.         node.runAction( 
  12.             cc.sequence
  13.                 // ...各种action对应的动画逻辑 
  14.                 cc.callFunc(function () { 
  15.                     // 动画结束后消除粒子 
  16.                     node.active = false 
  17.                 }, this)) 
  18.         ) 
  19.     } 
  20.  
  21.     // 水珠 
  22.     for (let f = 0; f < 20; f++) { 
  23.         // 同果粒,使用的spriteFrame切换成 this.circle 
  24.     } 
  25.  
  26.     // 果汁只有一张贴图,使用this.slash,展示常规的action缩放和透明动画即可 
  27. }, 

源项目的代码中使用 createFruitL 这个方法来处理爆炸动画,虽然经过了代码压缩,但依稀能看出对应的动画参数逻辑,如果不想调整动画参数,可以借鉴一下。 

 

这样,就完成了爆炸效果的展示,大概类似于这样,虽然有点丑。

 

音效

通过 cc.audioEngine 直接播放 AudioClip 资源来实现音效。

在 Game 组件下新增两个类型为 AudioClip 的资源,方便脚本组件访问:

  1. properties: { 
  2.     boomAudio: { 
  3.         defaultnull
  4.         type: cc.AudioClip 
  5.     }, 
  6.     waterAudio: { 
  7.         defaultnull
  8.         type: cc.AudioClip 
  9.     } 

同上,在属性检查器中将两个音频资源从资源管理器拖动到Game组件的属性下方:

  1. onSameFruitContact(){ 
  2.     cc.audioEngine.play(this.boomAudio, false, 1); 
  3.     cc.audioEngine.play(this.waterAudio, false, 1); 

这样就可以在碰撞的时候听到声音了。

构建打包

完成整个游戏的开发之后,可以选择构建发布,打包成 web-mobile 版本,然后部署在服务器上,就可以给其他人快乐地玩耍了。

 

小结

不知不就就写到了最后,貌似!!已经大工告成了!!

虽然还有很多细节没有实现,比如添加得分、合成西瓜之后的撒花等功能,感兴趣的同学可以自己克隆去尝试修改一下。

本文所有代码及素材都放在 Github 上面了,也可以通过在线预览地址体验:

  1. https://github.com/tangxiangmin/cocos-big-watermelon 
  2.  
  3. https://web-game-9gh6nrus14fec37e-1252170212.tcloudbaseapp.com/ 

完成这个游戏花了这周六下午+一个晚上的时间,由于对 cocos creator 并不是很熟悉,因此花了一些时间去看文档、查资料,甚至去 B 站上看了点教学视频。不过收获的成就感与满足感还是很大的,也算是正儿八经写了点游戏。

最后,尤其要感谢我媳妇,帮忙测试及提新需求。不说了,我还得再去加一个点击水果直接消除的功能!

作者:shymean

编辑:陶家龙

出处:www.shymean.com/article/使用cocos实现一个合成大西瓜

 

责任编辑:武晓燕 来源: shymean
相关推荐

2021-02-06 12:31:23

5G5G专网通信市场

2021-02-22 11:13:17

VS Code代码编程

2020-11-04 07:56:19

工具Linux 翻译

2021-02-04 12:20:46

5G5G专网通信市场

2021-04-27 07:52:19

StarterSpring Boot配置

2021-11-29 07:47:57

gRPCGUI客户端

2021-11-04 17:23:03

Java对象 immutable

2022-01-21 07:35:06

LRU缓存java

2020-05-22 10:35:07

CPU线程操作系统

2022-05-07 13:52:22

Feign 增强包K8s

2015-09-21 14:22:43

2021-07-29 09:29:12

AI游戏DeepMind

2020-09-10 06:58:34

C语言DBProxy

2021-02-03 10:17:03

5G5G专网通信市场

2021-12-12 18:18:15

代码元宇宙Python

2022-02-14 07:34:23

工具类GET、POST

2022-03-01 08:21:32

工具类代码封装网络请求

2022-07-20 09:38:31

Python语音合成代码

2021-05-14 13:30:17

Mybatis分表插件

2024-02-19 00:00:00

Redis分布式
点赞
收藏

51CTO技术栈公众号