为了女朋友!熬夜撸了一个“合成大西瓜”!(附源码) |
您所在的位置:网站首页 › 合成大西瓜官方网站 › 为了女朋友!熬夜撸了一个“合成大西瓜”!(附源码) |
本项目主要用作 cocos creator 练手使用,所有美术素材和音频材料均来源于 游戏逻辑 整个游戏逻辑比较简单,结合了俄罗斯方块与消除游戏的核心玩法 在生成一个水果 点击屏幕,水果移动到对应x轴位置并自由下落 每个水果会与其他水果发生碰撞,两个相同的水果碰撞时会发生合并,升级成更高一级的水果 水果共有 11 种类型, 游戏目标是合成最高级的水果:大西瓜!当堆积的水果超过顶部红线时则游戏结束 整理出需要实现的核心逻辑 生成水果 水果下落与碰撞 水果消除动画效果及升级逻辑 预备工作 cocos creator基本概念 整个项目使用cocos creator v2.4.3实现,建议初次了解的同学可以先过一下官方文档,本文不会过多介绍creator的使用(主要是我也不太熟练hah) 官方文档链接: 游戏素材 首先需要准备美术资源,本位所有美术素材和音频材料均来源于 www.wesane.com/game/654/。 首先访问游戏网站,打开network面板,可以看见游戏依赖的所有美术资源,我们下载自己所需的文件即可 所需的图片资源包括 11张水果贴图 每种水果合成效果贴图,均包含 一张果粒图片 一张圆形水珠图片 一张爆炸贴图 两个西瓜合成时有灯光和撒花的效果,时间有限暂不实现 音频文件同理,可以在Filter栏选择.mp3后缀的请求快速筛选对应资源。 水果消除时的爆炸声和水声 创建游戏场景和背景 打开cocos creator,新建一个项目(也可以直接导入从github下载的项目源码)。 然后记得将刚才下载的素材资源拖拽到右下角的资源管理器中。 创建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.js cc.Class({ extends: cc.Component, properties: { }, onLoad(){ }, start(){ } }) 我们需要在这里维护整个游戏的逻辑,后面逐步添加代码内容。 创建水果 水果是整个游戏的核心元素,在游戏中被频繁创建和销毁。 生成单个水果预制资源 这种动态创建的节点可以通过预制资源Prefab来控制, 制作prefab最简单的方式就是将资源从资源管理器拖动到场景编辑器中,然后再将层级管理器中的节点拖回资源管理器。 这里以等级最低的水果“葡萄”为例 然后将层级管理器中的节点删除,这样我们就得到了一个fruit的预制资源,在脚本组件中,就可以使用代码通过预制资源动态生成节点了。 修改Game.js,添加一个属性fruitPrefab,其类型为cc.Prefab, // Game.js properties: { fruitPrefab: { default: null, type: cc.Prefab }, } 回到creator,。选择Canvas节点,可以在属性检查器中的Game组件栏目看见和修改该属性了。我们将刚才制作的prefab资源从资源管理器拖动到这里,在初始化的时候,有cocos负责初始化对应的属性数据 创建单个水果 回到Game.js,开始编写真正的逻辑:创建一个葡萄 // Game.js onLoad(){ let fruit = cc.instantiate(this.fruitPrefab); fruit.setPosition(cc.v2(0,400)); this.node.addChild(fruit); } 预览模式下就可以看见屏幕正上方有一个葡萄了 nice,非常好的开始! 此外,由于水果还包含一些特定的逻辑,我们可以向它添加一个Fruit脚本组件,虽然目前看起来还没有什么用 创建Fruit脚本组件与上面创建Game组件类似,然后选择刚才制作的prefab重新编辑,关联上Fruit用户脚本组件即可。 动态维护多种水果 整个游戏共11种水果(当然也可以添加或者改成其他的东西),如果每种水果都像上面去手动生成预制资源然后分别初始化,那也太繁琐了,我们需要解决动态渲染多种水果的方式。 我们需要获得每种水果的贴图信息,然后在实例化水果时选择对应贴图即可,最简单的方式就是维护一个配置表,每行的数据字段包括id和iconSF constFruitItem = cc.Class({ name:'FruitItem', properties: { id:0,// 水果的类型 iconSF: cc.SpriteFrame// 贴图资源 } }); 然后为Game脚本组件新增一个fruits属性,用于保存每种水果的配置信息,其类型是数组,数组内元素类型为刚才创建的FruitItem // Game.js properties: { fruits: { default: [], type: FruitItem }, } 回到编辑器,这时候可以发现Game组件的属性下面多了一个Fruits属性,将其长度修改为11,然后依次编写每个水果的id,同时将其贴图资源从资源编辑器贴过来(体力活) 这样我们只需要传入想要制作的水果id,就可以获取到对应的配置信息,并动态修改贴图了 这种初始化的逻辑应该由水果自己维护,因此放在刚才创建的Fruit组件中,我们暴露一个init接口出来 // Fruit.js properties: { id:0, }, // 实例放在可以在其他组件中调用 init(data) { this.id = data.id // 根据传入的参数修改贴图资源 constsp = this.node.getComponent(cc.Sprite) sp.spriteFrame = data.iconSF }, 然后修改一下上面的初始化水果的代码 // Game.js createOneFruit(num) { let fruit = cc.instantiate(this.fruitPrefab); // 获取到配置信息 constconfig = this.fruits[num -1] // 获取到节点的Fruit组件并调用实例方法 fruit.getComponent('Fruit').init({ id: config.id, iconSF: config.iconSF }); } 这样就可以愉快的创建各种水果了 监听点击事件 cocos提供了各种事件监听,前端和客户端同学一定不会陌生。 整个游戏会在点击屏幕时创建一个水果,这只要监听一下全局点击事件即可,这个逻辑同样放在Game脚本组件中 onLoad() { // 监听点击事件 this.node.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this) }, onTouchStart(){ this.createOneFruit(1)// 生成水果 } 实际游戏中还需要处理随机生成水果、上一个水果在点击的x轴下落等细节逻辑,这里不再赘述。 物理系统:自由落体与刚体碰撞 上面处理了水果创建的逻辑,在整个游戏中,水果是可以产生下落及弹性碰撞等物理效果的,利用cocos内置的物理引擎,可以很方便的实现 对cocos引擎不熟悉的同学可以先看看这个官方demo,里面展示的比较详细(起码比文档要更容易理解) 开启物理引擎与碰撞检测 首先是开启物理引擎,以及设置重力大小 constinstance = cc.director.getPhysicsManager() instance.enabled =true // instance.debugDrawFlags = 4 instance.gravity = cc.v2(0,-960); 然后需要开启碰撞检测,默认是关闭的 constcollisionManager = cc.director.getCollisionManager(); collisionManager.enabled =true 然后设置四周的墙壁用于碰撞,这样水果就不会无限制往下面掉落了 // 设置四周的碰撞区域 let width = this.node.width; let height = this.node.height; let node =newcc.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; 现在我们就开启了游戏世界的物理引擎,然后还需要配置需要受引擎影响的节点,也就是我们的水果。 水果刚体组件与碰撞组件 回到creator,找到我们的水果prefab,然后添加物理组件 首先是Rigid Body(刚体)组件 然后是物理碰撞组件,因为我们的水果全是圆形的,都选择PhysicsCircleCollider组件就可以了,如果有个香蕉之类不规则多边形边的话,工作量就会增加不少~ 接下来可以看看整体效果,(记得把刚才的点击事件加上,然后控制一下随机生成水果类型) 完美!! 水果碰撞回调 添加完成之后,还需要开启刚体组件的碰撞属性Enabled Contact Listener,这样可以接收到碰撞之后的回调 这个碰撞回调同样写在Fruit脚本组件里面, // Fruit.js onBeginContact(contact, self, other) { // 检测到是两个相同水果的碰撞 if(self.node && other.node) { consts = self.node.getComponent('Fruit') consto = 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}) }) } 这样当水果发生碰撞时,我们就能够监听并处理消除升级逻辑了。 消除水果动画 无动画版本 简单的消除逻辑就是将两个节点删除,然后在原水果位置生成高一级的水果即可,没有任何动画效果 self.node.removeFromParent(false) other.node.removeFromParent(false) const{x, y} = other.node// 获取合并的水果位置 constid = other.getComponent('Fruit').id constnextId = id +1 constnewFruit = this.createFruitOnPos(x, y, nextId)// 在指定位置生成新的水果 虽然看起来有点奇怪,但的确可以以玩了! 分析动画 打开源站,通过Performance面板分析一下动画效果(这里就不录gif了) 可以看见合成的时候动画效果包括 碰撞水果向原水果中心移动 果粒爆炸的粒子效果 水珠爆炸的粒子效果 一滩果汁的缩放动画 此外还有爆炸声和水声的音效 管理爆炸素材资源 由于整个动画涉及到的素材较多,每种水果均包含3种颜色不同的贴图,与上面FruitItem类似,我们也采用prefab加动态资源的做法来管理对应素材和动画逻辑。 首先定义一个JuiceItem,保存单种水果爆炸需要的素材 // Game.js constJuiceItem = 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属性下 然后新增一个空的预制资源,主要是为了挂载脚本组件,也就是下面的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); constconfig = this.juices[id -1] constinstance = juice.getComponent('Juice') instance.init(config) instance.showJuice(pos, n)// 对应的爆炸逻辑 爆炸粒子动画 关于粒子动画,网上能查到不少资料,如果感兴趣,也可以移步我之前整理的前端常见动画实现原理。 粒子动画的主要的实现思路为:初始化N个粒子,控制他们的速度大小、方向和生命周期,然后控制每个粒子按照对应的参数执行动画,所有粒子汇集在一起的效果就组成了粒子动画。 话虽如此,要把动画效果调好还是挺麻烦的,需要控制各种随机参数。 showJuice(pos, width) { // 果粒 for(let i =0; i |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |