3

从0开始实现一个合成大西瓜

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=Mzg2NDAzMjE5NQ%3D%3D&%3Bmid=2247487977&%3Bidx=1&%3Bsn=f0824e466609b22033149c33cadef4cd
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

muUJ3a2.jpg!mobile

作者:橙红年代 ( https://juejin.cn/post/6923803717808422925 )

最近微博上曝出了很多瓜,"合成大西瓜"这个游戏也很火热,玩了一阵还挺有意思的。 研究了一下原理,发现目前流传的版本都是魔改编译后的版本,代码经过压缩不具备可读性,因此决定自己照着实现一个。

本项目主要用作cocos creator练手使用,所有美术素材和音频材料均来源于 http://www.wesane.com/game/654/。

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

本文所有代码及素材都放在github( https://github.com/tangxiangmin/cocos-big-watermelon )上面了,也可以通过在线预览地址( https://web-game-9gh6nrus14fec37e-1252170212.tcloudbaseapp.com/ )体验

1. 游戏逻辑

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

  • 在生成一个水果

  • 点击屏幕,水果移动到对应x轴位置并自由下落

  • 每个水果会与其他水果发生碰撞,两个相同的水果碰撞时会发生合并,升级成更高一级的水果

水果共有11种类型,

yuQfMrm.png!mobileimg

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

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

  • 生成水果

  • 水果下落与碰撞

  • 水果消除动画效果及升级逻辑

2. 预备工作

2.1. cocos creator基本概念

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

需要了解的基本概念

节点

2.2. 游戏素材

首先需要准备美术资源,本位所有美术素材和音频材料均来源于 http://www.wesane.com/game/654/。

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

ZJZzqeV.png!mobileimg

所需的图片资源包括

  • 11张水果贴图

  • 每种水果合成效果贴图,均包含

    • 一张果粒图片

    • 一张圆形水珠图片

    • 一张爆炸贴图

  • 两个西瓜合成时有灯光和撒花的效果,时间有限暂不实现

音频文件同理,可以在Filter栏选择 .mp3 后缀的请求快速筛选对应资源。

  • 水果消除时的爆炸声和水声

3. 创建游戏场景和背景

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

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

3.1. 创建scene和背景节点

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

yUvuYf.png!mobileimg

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

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

367zQfN.png!mobileimg

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

JBRrYvN.png!mobileimg

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

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

a6JrEnN.png!mobileimg

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

3.2. 场景脚本组件

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

yIjuAvf.png!mobileimg

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

eIjqeuV.png!mobileimg

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

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

// Game.js
cc.Class({
extends: cc.Component,

properties: {

},
onLoad(){

},
start(){ }
})

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

4. 创建水果

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

4.1. 生成单个水果预制资源

这种动态创建的节点可以通过预制资源 Prefab 来控制,

制作prefab最简单的方式就是将资源从资源管理器拖动到场景编辑器中,然后再将层级管理器中的节点拖回资源管理器。

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

BbUbQrb.png!mobileimgjqiMFr2.png!mobileimg

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

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

// Game.js
properties: {
fruitPrefab: {
default: null,
type: cc.Prefab
},
}

回到creator,。选择Canvas节点,可以在属性检查器中的 Game 组件栏目看见和修改该属性了。我们将刚才制作的prefab资源从资源管理器拖动到这里,在初始化的时候,有cocos负责初始化对应的属性数据

73Yje2M.png!mobileimg

4.2. 创建单个水果

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

// Game.js
onLoad(){
let fruit = cc.instantiate(this.fruitPrefab);
fruit.setPosition(cc.v2(0, 400));

this.node.addChild(fruit);
}

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

J7ZBRbN.png!mobileimg

nice,非常好的开始!

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

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

4.3. 动态维护多种水果

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

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

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

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

// Game.js
properties: {
fruits: {
default: [],
type: FruitItem
},
}

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

U7Brya7.png!mobileimg

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

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

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

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

// Game.js
createOneFruit(num) {
let fruit = cc.instantiate(this.fruitPrefab);
// 获取到配置信息
const config = this.fruits[num - 1]

// 获取到节点的Fruit组件并调用实例方法
fruit.getComponent('Fruit').init({
id: config.id,
iconSF: config.iconSF
});
}

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

4.4. 监听点击事件

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

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

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

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

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

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

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

5.1. 开启物理引擎与碰撞检测

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

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

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

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

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

 // 设置四周的碰撞区域
let width = this.node.width;
let height = this.node.height;

let node = new cc.Node();

let body = node.addComponent(cc.RigidBody);
body.type = cc.RigidBodyType.Static;

const _addBound = (node, x, y, width, height) => {
let collider = node.addComponent(cc.PhysicsBoxCollider);
collider.offset.x = x;
collider.offset.y = y;
collider.size.width = width;
collider.size.height = height;
}

_addBound(node, 0, -height / 2, width, 1);
_addBound(node, 0, height / 2, width, 1);
_addBound(node, -width / 2, 0, 1, height);
_addBound(node, width / 2, 0, 1, height);

node.parent = this.node;

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

5.2. 水果刚体组件与碰撞组件

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

首先是Rigid Body(刚体)组件 zYbYJf2.png!mobile

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

be6zMnM.png!mobileimg

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

BnumIvr.png!mobileimg

完美!!

5.3. 水果碰撞回调

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

7buaYn.png!mobileimg

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

// Fruit.js
onBeginContact(contact, self, other) {
// 检测到是两个相同水果的碰撞
if (self.node && other.node) {
const s = self.node.getComponent('Fruit')
const o = other.node.getComponent('Fruit')
if (s && o && s.id === o.id) {
self.node.emit('sameContact', {self, other});
}
}
},

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

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

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

6. 消除水果动画

6.1. 无动画版本

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

self.node.removeFromParent(false)
other.node.removeFromParent(false)

const {x, y} = other.node // 获取合并的水果位置
const id = other.getComponent('Fruit').id

const nextId = id + 1
const newFruit = this.createFruitOnPos(x, y, nextId) // 在指定位置生成新的水果

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

6.2. 分析动画

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

I367VfQ.png!mobileimg

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

  • 碰撞水果向原水果中心移动

  • 果粒爆炸的粒子效果

  • 水珠爆炸的粒子效果

  • 一滩果汁的缩放动画

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

6.3. 管理爆炸素材资源

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

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

// Game.js
const JuiceItem = cc.Class({
name: 'JuiceItem',
properties: {
particle: cc.SpriteFrame, // 果粒
circle: cc.SpriteFrame, // 水珠
slash: cc.SpriteFrame, // 果汁
}
});

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

// Game.js
properties: {
juices: {
default: [],
type: JuiceItem
},
juicePrefab: {
default: null,
type: cc.Prefab
},
}

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

RnQN7nn.png!mobileimg

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

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

// Juice.js
cc.Class({
extends: cc.Component,

properties: {
particle: {
default: null,
type: cc.SpriteFrame
},
circle: {
default: null,
type: cc.SpriteFrame
},
slash: {
default: null,
type: cc.SpriteFrame
}
},
// 同样暴露一个init接口
init(data) {
this.particle = data.particle
this.circle = data.particle
this.slash = data.slash
},
// 动画效果
showJuice(){

}
}

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

// Game.js
let juice = cc.instantiate(this.juicePrefab);
this.node.addChild(juice);

const config = this.juices[id - 1]
const instance = juice.getComponent('Juice')
instance.init(config)
instance.showJuice(pos, n) // 对应的爆炸逻辑

6.4. 爆炸粒子动画

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

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

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

showJuice(pos, width) {
// 果粒
for (let i = 0; i < 10; ++i) {
const node = new cc.Node('Sprite');
const sp = node.addComponent(cc.Sprite);
sp.spriteFrame = this.particle;
node.parent = this.node;
// ... 一堆随机的参数

node.position = pos;
node.runAction(
cc.sequence(
// ...各种action对应的动画逻辑
cc.callFunc(function () {
// 动画结束后消除粒子
node.active = false
}, this))
)
}

// 水珠
for (let f = 0; f < 20; f++) {
// 同果粒,使用的spriteFrame切换成 this.circle
}

// 果汁只有一张贴图,使用this.slash,展示常规的action缩放和透明动画即可
},

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

zMRNveu.png!mobileimg

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

Rb6bU3Q.png!mobileimg

6.5. 音效

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

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

properties: {
boomAudio: {
default: null,
type: cc.AudioClip
},
waterAudio: {
default: null,
type: cc.AudioClip
}
}

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

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

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

7. 构建打包

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

JFnAzef.png!mobileimg

8. 小结

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

虽然还有很多细节没有实现,比如添加得分、合成西瓜之后的撒花等功能,感兴趣的同学可以自己克隆去尝试修改一下。本文所有代码及素材都放在github上面了,也可以通过在线预览地址体验

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

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

你的点赞和在看是最大的支持 :heart:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK