CocosCreator 入门篇(一)--从 0 到 1 的飞机大战
前言
因为工作需要的关系,最近开始接触 CocosCreator 框架,新手任务是要实现一个简单的飞机大战游戏,因为是过了一遍入门教程直接上手制作的,所以过程也是磕磕绊绊的,在此做一个记录,希望能够帮助有需要的同学.
PS:项目源码放在最后
准备工作
飞机大战的游戏简介
飞机大战的游戏规则是:用手控制主角飞机的移动,当飞机处于被控制状态时发射子弹,敌机不断的在屏幕上部出现,玩家要做的就是移动飞机主角打中飞机,同时躲避敌机和子弹. (PS:躲避敌机和子弹的碰撞我没做,因为我是无敌哒,玩了这么多年游戏,我早就想这么做了)
界面和流程
飞机大战的界面和流程比较简单,包括开始界面、战前准备(选择战机)、游戏主场景和游戏结束场景等界面,如图所示:

游戏模块的开发
在此,我们首先来实现一个最简单的功能:开始界面、选择主角形象界面、战斗场景. 其中战斗场景包括主角飞机的拖动发射子弹功能,随机产生的敌机模块,敌机发射子弹,以及敌机被击中后的销毁
开始游戏模块
开始游戏的界面十分简单,包含一个界面背景、上层的开始游戏按钮,另外一个"Control"是一个空节点,用来放置开始界面逻辑组件的空节点.

cc.Class({
extends:cc.Component,
properties:{
playBtn:{
default:null,
type:cc.Button
}
},
onLoad(){
//按钮点击事件监听,跳转到准备界面
this.playBtn.on('click',this.onPlay,this)
},
onPlay(){
//跳转到选择主角形象界面
cc.director.loadScene('PlaneReady')
}
})
游戏准备界面
游戏准备界面包括一个背景,一个弹板上面有两架示意飞机供选择,这里用的是按钮来完成的,“ConfirmBtn”是进入游戏的按钮.Control 是控制节点,负责装载整个界面的控制逻辑.

我们先看一下这个模块的主要代码
onLoad(){
//plane1监听
this.plane1.node.on('click', (event) => {
const index = window.PLAYER_1
this.selectPlane(index)
this.setSelectWrapper(index)
})
//plane2监听
this.plane2.node.on('click', (event) => {
const index = window.PLAYER_2
this.selectPlane(index)
this.setSelectWrapper(index)
})
//确定按钮点击事件监听
this.confirmBtn.node.on('click', this.onStart.bind(this))
},
//确认按钮点击事件
onStart() {
cc.director.loadScene('MainGame')
},
//设置选择事件
selectPlane(index) {
switch (index) {
case window.PLAYER_1:
window.Player = window.PLAYER_1
break;
case window.PLAYER_2:
window.Player = window.PLAYER_2
break;
default:
window.Player = window.PLAYER_1
break;
}
},
这个界面的场景比较简单,我们主要实现的是选择什么类型的飞机,然后给用户一个选择飞机的反馈,这里监听飞机的点击事件,用来修改选中的 index 的值. 同时,这个 index 的值需要在场景中进行传递,所以我们需要将其挂载到全局变量上
屏幕背景滚动脚本
因为游戏准备界面和游戏主界面中都用到了屏幕背景滚动的功能,所以将这部分的功能抽离出来做了个简单的封装. 背景无限滚动的原理其实很简单,大致思路就是使用两张图片同时移动,当一张图片彻底移动出屏幕之后,将其放到初始位置重新滚动 下面我们来看一下简单的代码
properties: {
//背景1
bg01: {
default: null,
type: cc.Sprite
},
//背景2
bg02: {
default: null,
type: cc.Sprite
},
//滚动速度
bg_speed: 2
},
// LIFE-CYCLE CALLBACKS:
bgMove: function (bgList, speed) {
//每次循环二张图片一起滚动
for (var index = 0; index < bgList.length; index++) {
bgList[index].y -= speed;
}
//y坐标减去自身的height得到这张背景刚好完全离开场景时的y值
if (bgList[0].y <= 0 - bgList[0].height) {
bgList[0].y = 640; //离开场景后将此背景图的y重新赋值,位于场景的上方
}
if (bgList[1].y <= 640 - 2 * bgList[1].height) {
bgList[1].y = 640;
}
},
update() {
this.bgMove([this.bg01.node, this.bg02.node], this.bg_speed);
},
游戏主界面显示模块
游戏主界面显示模块是整个游戏中最关键的部分,主要可以分为三个部分的逻辑:显示及地图的滚动(滚动功能主要是上面的脚本),主角和敌机的逻辑
游戏主界面的层级结构主要有:背景层控制背景滚动、Control 主要是挂载主界面的逻辑.另外,主角飞机和敌机部分以及子弹都我在这里使用了预制资源 Prefab.

游戏初始化
onLoad() {
let self = this
//加载我们的主角飞机的预设文件
cc.loader.loadRes(
window.Player == window.PLAYER_1 ? 'plane.prefab' : 'plane2.prefab',
(err, prefab) => {
//创建主角飞机的节点
self.heroPlane = cc.instantiate(prefab)
const res = self.heroPlane.getComponent('Plane')
.setControlNode(self.canvas)
//主角飞机的初始定位
self.heroPlane.setPosition(cc.p(0, -200))
//添加到当前canvas上
self.node.addChild(self.heroPlane)
}
)
//读取敌机的预设
cc.loader.loadResDir('enemy', (err, arr) => {
const res = arr.filter(item => item.name == 'explode_enemy' ? false : true)
//随机创建敌机
createEnemy(res, this)
})
},
start() {
//开启碰撞检测
const manager = cc.director.getCollisionManager()
manager.enabled = true
},
Control 上面挂载的逻辑很简单,主要实现了敌机和主角飞机的创建,同时开启了碰撞检测功能.
主角飞机的预设文件
主角飞机的预设文件主要实现了主角飞机拖动的监听,同时处理了子弹生成和子弹的飞行效果. 因为子弹的和销毁比较频繁,所以这里使用了对象池来处理子弹的生成和销毁.
我们先处理一下飞机的拖动监听事件
//注册飞机触摸事件函数
registerMoveEvent() {
let self = this
//移动位置
this.moveToPos = cc.p(0, 0)
this.isMoving = false
//开始触摸监听
this.node.on(cc.Node.EventType.TOUCH_START, event => {
let touches = event.getTouches()
let touchLoc = touches[0].getLocation()
self.isMoving = true
//转换到父节点空间坐标系
self.moveToPos = self.node.parent.convertToNodeSpaceAR(touchLoc)
//当触摸的时候发射子弹
this.launchBullet()
}, self.node)
//拖动监听
this.node.on(cc.Node.EventType.TOUCH_MOVE, event => {
let touches = event.getTouches()
//获取当前节点坐标
let touchLoc = touches[0].getLocation()
//转换为父节点的坐标
self.moveToPos = self.node.parent.convertToNodeSpaceAR(touchLoc)
}, self.node)
//注意,TOUCH_CANCEL和TOUCH_END的区别,要想控制飞机的拖动结束标记,只用END是不够的
this.node.on(cc.Node.EventType.TOUCH_CANCEL, event => {
self.isMoving = false
})
//触摸结束监听
self.node.on(cc.Node.EventType.TOUCH_END, event => {
self.isMoving = false
})
},
这里使用了 update 的生命周期钩子函数来处理飞机位置的更新
//更新飞机位置信息
updateHeroPos(dt) {
//如果飞机触摸事件结束,则停止飞机位置的更新
if (!this.isMoving) return
let oldPos = this.node.position
if (Math.abs(this.moveToPos.x - oldPos.x) < 3 && Math.abs(this.moveToPos.y - oldPos.y) < 3) return
if (Math.abs(this.moveToPos.x) >= 210) {
const padding = this.node.width / 2
this.moveToPos = { y: this.moveToPos.y, x: this.moveToPos.x > 0 ? 210 - padding : padding - 210 }
}
//这里使用的是cc.pNormalize返回一个长度为1的标准化过后的向量来控制移动的位置
let direction = cc.pNormalize(cc.pSub(this.moveToPos, oldPos))
this.node.setPosition(cc.pAdd(oldPos, cc.pMult(direction, 300 * dt)))
},
剩下的就是处理一下子弹的生成和位置的处理,因为主角飞机的子弹处理和敌机子弹处理都是大同小异的,所以这里只介绍一下主角子弹的处理
//初始化飞机子弹
initBullet() {
//首先创建子弹的对象池
this.bulletPool = new cc.NodePool()
let initCount = 10
for (let i = 0; i < initCount; i++) {
let bullet = cc.instantiate(this.bulletPrefab[Math.random() > 0.5 ? 0 : 1])
this.bulletPool.put(bullet.node)
}
const self = this
//控制子弹的生成
this.bulletCall = cc.callFunc(target => {
let bullet
//从对象池中获取子弹对象
if (this.bulletPool.size() < 0) {
bullet = this.bulletPool.get()
} else {
bullet = cc.instantiate(this.bulletPrefab[Math.random() > 0.5 ? 0 : 1])
}
//子弹的飞行动作
this.runBulletAction(this.node.width / 6 * Math.random() > 0.5 ? -1 : 1, this.node.height / 5, bullet)
//如果飞机触摸事件结束,则停止子弹的生成
if (!this.isMoving) return
//延迟0.3秒后生成子弹
this.node.runAction(cc.sequence(cc.delayTime(0.3), self.bulletCall))
})
},
//发射子弹
launchBullet(num = 2) {
const self = this
for (let i = 1; i <= num; ++i) {
this.node.runAction(cc.sequence(cc.delayTime(0.3 * i), self.bulletCall))
}
},
//子弹飞行动作
runBulletAction(x, y, bullet) {
//子弹的飞行动作
x = this.node.position.x + x,
y = this.node.position.y + y
bullet.parent = this.node.parent
let pos = this.node.getPosition()
bullet.setPosition({ x, y })
let finished = cc.callFunc(target => {
this.bulletPool.put(bullet)
}, this)
//子弹从飞机的位置开始,飞到屏幕之外
bullet.runAction(cc.sequence(cc.moveTo(1, x, 600), finished))
},
上面就是处理子弹的逻辑,和敌机子弹的逻辑都是相同的,大概就是利用对象池控制子弹的生成和回收,然后根据当前飞机的位置给定子弹初始位置,然后在一定时间之内飞出屏幕,同时回调函数中生成新的子弹.
敌机的处理
敌机的逻辑主要是处理了子弹的生成销毁和飞行逻辑,以及当敌机和主角子弹碰撞后的销毁逻辑 因为敌机子弹和主角子弹的处理都是大同小异的,所以这里就不做过多赘述了,这里需要详细讲的就是敌机的生成逻辑了. 先简单介绍一下敌机的生成逻辑,因为敌机不止一种,而且每种不止一个,所以我们处理敌机的对象池的时候要维护一个二维数组,外层是敌机的种类,内层是对应敌机的数量,同时我们要将飞机的的回收动作挂载到生成的敌机对象上,方便敌机在自己的碰撞逻辑中处理回收动作
//createNormalEnemy.js
//创建敌机
const createEnemy = function (_nodes, _thisArg) {
// thisArg = _thisArg
_thisArg ? thisArg = _thisArg : null
_nodes ? nodes = _nodes : null
// nodes = _nodes
const initCount = 5
//创建敌机对象池
for (var i = 1; i <= initCount; ++i) {
enemyPools[i] = new cc.NodePool()
for (let j = 1; j <= initCount; ++j) {
//因为现在我只有两种敌机(预期是五个),所以这里做了一个判断,只处理1、2两种敌机
const type = i <= 1 ? 1 : 2
let enemy = cc.instantiate(getEnemyNode(nodes, type - 1))
enemy.getComponent('NormalEnemy0' + type).setControlNode(thisArg.canvas, i)
enemyPools[i].put(enemy)
}
}
//初始生成三架敌机
thisArg.node.runAction(cc.sequence(cc.delayTime(1), EnemyCallback))
thisArg.node.runAction(cc.sequence(cc.delayTime(0.5), EnemyCallback))
thisArg.node.runAction(cc.sequence(cc.delayTime(1.5), EnemyCallback))
}
//敌机创建成功的回调,用来控制敌机的飞行与回收
let EnemyCallback = cc.callFunc(event => {
//敌机类型
// let enemyType = window.getRandomInt(1, 5)
let enemyType = window.getRandomInt(1, 2)
//创建敌机
let enemyPool = enemyPools[enemyType]
let enemy
if (enemyPool.size() > 0) {
enemy = enemyPool.get()
} else {
return
enemy = cc.instantiate(getEnemyNode(nodes, enemyType - 1))
enemy.getComponent('NormalEnemy02').setControlNode(thisArg.canvas)
}
//获取父级元素
enemy.parent = thisArg.node
//敌机横坐标
let enemyPosX = window.getRandomInt(-210 + enemy.width / 2, 210 - enemy.width / 2)
//设置敌机初始坐标,应该设置在屏幕外
let pos = cc.p(enemyPosX, 320 + enemy.height / 2)
enemy.setPosition(pos)
// enemy.runAction(cc.sequence(cc.moveTo(3, pos.x, -200), finished))
//敌机飞行结束后的回调
let finished = cc.callFunc(target => {
//飞行动作接收后生成新的敌机
thisArg.node.runAction(cc.sequence(cc.delayTime(Math.random()), EnemyCallback))
}, thisArg)
//将对象回收到对象池中
enemy.__des__ = () => {
thisArg.node.runAction(cc.sequence(cc.delayTime(Math.random()), EnemyCallback))
enemy.stopAction(enemy.__action__)
enemyPool.put(enemy)
}
enemy.__this__ = thisArg
//根据敌机的不同种类添加不同的飞行动作(暂时处理)
switch (enemyType) {
//此处的enemyAction为敌机的飞行动作(就是runAction),由于逻辑十分简单,就不多做赘述了
case 1:
enemyAction(3, pos.x, enemy, finished)
break
case 2:
enemyAction(5, pos.x, enemy, finished)
break
default:
enemyAction(1, pos.x, enemy, finished)
break;
}
}, thisArg)
上面介绍了一下飞机大战的一个简单实现,因为本人第一次使用 cocos Creator,也是第一次做类似游戏的小东西,所以很渣,本文全当是一个逻辑的梳理了.如果有看官有其他建议或者意见的话,本渣期待大神的指点