CocosCreator 入门篇(一)--从 0 到 1 的飞机大战

2,041 阅读9分钟

CocosCreator 入门篇(一)--从 0 到 1 的飞机大战

前言

因为工作需要的关系,最近开始接触 CocosCreator 框架,新手任务是要实现一个简单的飞机大战游戏,因为是过了一遍入门教程直接上手制作的,所以过程也是磕磕绊绊的,在此做一个记录,希望能够帮助有需要的同学.

PS:项目源码放在最后

官方入门教程 官方 API 文档

准备工作

飞机大战的游戏简介

飞机大战的游戏规则是:用手控制主角飞机的移动,当飞机处于被控制状态时发射子弹,敌机不断的在屏幕上部出现,玩家要做的就是移动飞机主角打中飞机,同时躲避敌机和子弹. (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,也是第一次做类似游戏的小东西,所以很渣,本文全当是一个逻辑的梳理了.如果有看官有其他建议或者意见的话,本渣期待大神的指点

完整代码在这里