微信飞机大战小游戏编写分享(上)

1. 下载游戏引擎

游戏引擎我选择的是 cocos creator,官网地址:链接

您可以直接点击下载免费获得这款引擎软件,建议下载最新的稳定版本,写这篇文章的时候,适合写 2D 小游戏的最新版本貌似是:V2.4.8

一般我们会先下载一个 DASHBOARD,可以理解成一个 cocos 的盒子,用于放各种游戏工具。

然后从 DASHBOARD 中下载相应版本的 cocos creator。

2. 创建游戏项目

安装完毕后,我们进入大盘界面:

点击 new 创建一个新的 empty 游戏工程:

看到上面这个界面后,就意味着咱们的开发环境已经搭建好了,可以开始开发小游戏咯!

3. 创建游戏画布

首先我们把游戏资源导入到我们的游戏工程中,游戏资源地址:

然后查看背景图片的大小,资源里的背景图大小是:640 x 1136

于是我们设置画布的大小为 640 x 1136

4. 设置游戏背景

为了减少加载图片所带来的性能损耗,我们把所有的资源合到一张图片中(cocos 能够自动识别这些图片),合图工具用的是:TexturePacker,官方下载地址链接

简单的使用方法如下:

  1. 打开图片目录,把所有的图片拖拽到 TexurePacker 中

  1. 保存

制作完 plist 文件后,我们新建一个叫 plist 的文件夹存放 plist 文件,后续我们使用图片都通过 plist 获取。

然后我们把背景图片 bg 拖拽到节点树上:

为了制作一个轮播的背景,我们需要两张背景图,假设为 A 和 B(想让背景图片动起来,我们必须保证画布中永远有背景)。详细解释参见:链接

首先我们创建一个控制游戏逻辑的 TypeScript 脚本:

然后把脚本文件关联到画布中:

关联到画布后,咱们编写脚本,控制背景图片轮播,首先需要定义两个节点(我们只使用背景图片的坐标属性,因此定义为节点类型就足够了):

@property(cc.Node)
bg1: cc.Node = null;

@property(cc.Node)
bg2: cc.Node = null;

定义完成后记得到画布中进行绑定:

绑定后我们编写轮播的代码:

// 在游戏加载的时候定义背景图片的位置
protected onLoad() {
    this.bg1.y = 0
    this.bg2.y = this.bg1.y + this.bg2.height
}

update (dt) {
    // 背景图片的移动速度
    this.bg1.y -= 10;
    this.bg2.y -= 10;
    // 背景图片轮播逻辑(没明白可以看视频哦)
    if(this.bg1.y <= -this.bg1.height){
        // 当一张背景移动到屏幕外面后,立马补到另一张背景图片的后面
        this.bg1.y = this.bg2.y + this.bg1.height
    }
    if(this.bg2.y <= -this.bg2.height){
        this.bg2.y = this.bg1.y + this.bg2.height
    }
}

编写完成后,咱们就有了轮播的背景图。

5. 添加开始游戏标语

首先我们把游戏标语添加到节点树中,选择一个合适的位置:

为了提示玩家点击屏幕开始游戏,我们在这个节点下添加一个 label 节点用于显示文字:

为了模仿微信飞机大战的展示效果,我们给这行 点击屏幕开始游戏 的文字添加一个上下晃动的动画效果,这里用到了 cocos 的 animation 功能:

  1. 首先我们创建一个动画资源
  2. 将动画资源绑定到 点击屏幕开始游戏 所在的节点上
  3. 然后设置关键帧,给每个关键帧设定恰当的角度,比如说第一帧的角度为 0,第二帧的角度为 15,第三帧的角度为 0,第四帧为 -15,第五帧为 0。

连起来播放后大概就成了这样:

6. 编写游戏准备、游戏中、游戏暂停三个状态

虽然我们编写好了一个轮播的背景,有了动态的效果,但我们希望在游戏刚开始的时候背景是不动的,等玩家进入游戏后背景再动起来,让小飞机有飞翔的效果。

首先我们定义一个判断背景图片是否在动的变量

isBgMove = false

然后把移动背景的代码封装到一个方法里,在 update 方法中通过判断 isBgMove 变量控制背景是否移动:

update (dt) {
    if(this.isBgMove){
        this.moveBg()
    }
}

moveBg(){
    // 让背景图片动起来
    this.bg1.y -= 10;
    this.bg2.y -= 10;
    if(this.bg1.y <= -this.bg1.height){
        this.bg1.y = this.bg2.y + this.bg1.height
    }
    if(this.bg2.y <= -this.bg2.height){
        this.bg2.y = this.bg1.y + this.bg2.height
    }
}

有了控制背景轮播的开关后,我们编写游戏准备、游戏中、游戏暂停三个状态。

首先我们把上面定义的 shoot_copyright 节点改个名字,改成游戏准备节点 status_ready;

然后依次创建两个空节点 status_playing 和 status_pause:

然后在游戏开始页面创建一个暂停按钮:

为暂停按钮创建一个点击事件,我们编写一个通用处理点击事件的方法,通过控制台进行调试:

clickButton(sender, str){
    if(str == "pause"){
        console.log("点击了暂停按钮")
    }
}

然后编写暂停页面:

  1. 首先添加一个暂停的背景图,设置透明度遮盖原有的游戏背景
  2. 添加三个按钮,设置好按钮的样式以及显示内容
  3. 为暂停背景设置一个 BlockInputEvents,屏蔽调下层节点

然后编写这三个页面的显隐关系,大概的逻辑是:

  1. 玩家进入游戏后显示一个静态的背景,当点击屏幕后隐藏调游戏准备界面,进入游戏开始界面
  2. 玩家可以在游戏界面点击暂停按钮,点击暂停按钮后由游戏开始界面进入游戏暂停界面
  3. 在游戏暂停界面有三个按钮,点击继续游戏会回到游戏开始界面,点击重新开始也会回到游戏开始界面,点击回到主页会回到游戏准备界面

以上的逻辑可以通过统一的按键点击方法进行处理:

clickButton(sender, str){
    if(str == "pause"){
        // 点击暂停后显示暂停页面
        this.pause.active = true
    }else if(str == "continue"){
        // 点击继续游戏后隐藏暂停页面
        this.pause.active = false
    }else if(str == "restart"){
        // 点击重新开始后隐藏暂停页面
        this.pause.active = false
    }else if(str == "backHome"){
        // 点击回到主页隐藏暂停界面,停止游戏,停止背景移动
        this.pause.active = false
        this.playing.active = false
        this.isBgMove = false
        this.ready.active = true
    }
}

实现效果大概是这样的:

7. 编写游戏主角我方飞机

编写好了游戏场景切换功能后,我们开始编写我们的游戏主角。

首先添加一个节点,为它设置一张图片(Sprite 属性),然后制作一个小动画:

然后编写飞机跟随手指(鼠标)移动的逻辑,简单来说就是要注册一个触摸移动的监听事件:

setTouch() {
    // ....
    this.node.on("touchmove", (event) => {
        // 获取飞机的位置
        let hero_pos = this.hero.getPosition()
        // 获取手指(鼠标)距离上一次事件移动相对于左下角的距离对象
        let move_pos = event.getDelta()
        // 飞机的位置加上移动的相对位置得到飞机的最新位置
        this.hero.setPosition(cc.v2(hero_pos.x + move_pos.x, hero_pos.y + move_pos.y))
    }, this);
    //...
}

大概的效果是酱紫的:

8. 让飞机可以发射子弹

由于子弹是会重复利用的资源,我们这里采用预制体资源,首先我们在节点树中创建一个子弹节点,然后给子弹配一个脚本:

在每一帧中改变子弹的 y 值,让子弹有发射的效果。

update(dt) {
    this.node.y += 10
}

配置好脚本后,我们把子弹节点拖拽到资源管理器中,使其变成一个预制体,然后编写主逻辑脚本,先定义一个预制体:

// 子弹
@property(cc.Prefab)
pre_bullet: cc.Prefab

然后尝试在每次鼠标点击结束(触摸手指离开屏幕)的时候生成一颗子弹:

setTouch() {
    this.node.on("touchend", (event) => {
        //......
        // 生成一颗子弹
        let bullet = cc.instantiate(this.pre_bullet)
        // 把子弹挂在到节点树上
        bullet.parent = this.node
        // 获取飞机主角的位置
        let pos = this.hero.getPosition()
        // 设置子弹的初始位置为飞机头
        bullet.setPosition(cc.v2(pos.x, pos.y + this.hero.height / 2))
    }, this);
}

这样一来飞机就可以发射子弹了:

9. 对象池与单例

现在虽然能不停地发射子弹了,但是一直创建子弹实例不进行删除可不行,如果游戏时间久了游戏会越来越卡。

我们使用 cocos 提供的对象池对子弹进行缓存,先编写一个生成子弹的方法:

createBullet() {
    // 创建子弹的方法
    let bullet = null
    // 生成子弹的时候先到对象池中取
    if (this.bulletPool.size() > 0) {
        // 如果对象池中有子弹对象则直接使用
        bullet = this.bulletPool.get()
    } else {
        // 如果对象池没有子弹了,就创建一颗新的子弹
        bullet = cc.instantiate(this.pre_bullet)
    }
    // 获取子弹后挂在跟节点下
    bullet.parent = this.node
    // 获取飞机的位置
    let pos = this.hero.getPosition()
    // 设置子弹的初始位置
    bullet.setPosition(cc.v2(pos.x, pos.y + this.hero.height / 2))
}

然后编写子弹消亡的逻辑,目前一共有三个场景可以回收子弹:

  1. 重新开始游戏
  2. 回到主页
  3. 子弹超出画布范围
// 回收单颗子弹
bulletKilled(bullet) {
    // 回收子弹的方法
    bullet.setPosition(cc.v2(0, 0))
    this.bulletPool.put(bullet)
}

// 回收全部子弹
removeBullets() {
    let children = this.node.children
    for (let i = children.length - 1; i >= 0; i--) {
        let bullet = children[i].getComponent("bullet")
        if (bullet) {
            this.bulletKilled(children[i])
        }
    }
}

阿菌在开发的时候比较困扰的问题是,我给子弹单独创建一个脚本后,怎么在子弹脚本中引用主逻辑类中的方法呢?

通过在 cocos 论坛搜索,大佬给出的答案是使用单例,单例的简单使用模版如下:

@ccclass
export default class Singleton extends cc.Component {

    // 单例
    public static instance: Singleton = null

    onLoad() {
        // 初始化单例
        if (Singleton.instance == null) {
            Singleton.instance = this
        } else {
            this.destroy()
            return
        }

通过上面的代码,把主逻辑对象导出,在子弹脚本中可以这么使用:

const {ccclass, property} = cc._decorator;
// 导入主逻辑类
import Singleton from "./main";

@ccclass
export default class NewClass extends cc.Component {

    update(dt) {
        this.node.y += 15
        if(this.node.y > 590){
            // 使用主逻辑单例对象 
            Singleton.instance.bulletKilled(this.node)
        }
    }
}

10. 添加敌机

添加敌机的逻辑和添加子弹的逻辑相似:

  1. 在节点树中创建一个敌机节点
  2. 创建敌机的脚本,并关联给敌机节点
  3. 把敌机节点制作成 PerFab
  4. 在主逻辑中编写敌机对象池、敌机创建、敌机销毁的方法
createEnemy1() {
    // 创建敌机1的方法
    let enemy1 = null
    if (this.enemy1Pool.size() > 0) {
        enemy1 = this.enemy1Pool.get()
    } else {
        enemy1 = cc.instantiate(this.pre_enemy_1)
    }
    enemy1.parent = this.node
    enemy1.setPosition(cc.v2(0, 590))
}

enemy1Killed(enemy1){
    this.enemy1Pool.put(enemy1)
}

removeEnemy1s() {
    let children = this.node.children
    for (let i = children.length - 1; i >= 0; i--) {
        let enemy1 = children[i].getComponent("enemy1")
        if (enemy1) {
            this.bulletKilled(children[i])
        }
    }
}

在敌机脚本中设置敌机移动后,得到的效果大概是这样子的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jujgCh49-1647963420444)(https://s21.aconvert.com/convert/p3r68-cdx67/6d7lo-6fcx8.gif)]

11. 子弹与敌机碰撞

有了敌机之后,我们让我方飞机发射的子弹可以击中敌机。

首先我们给敌机和子弹添加碰撞组件(记得给子弹和敌机添加分组):

然后到项目设置中设置敌机和子弹可以碰撞:

接下来编辑敌机死亡的帧动画(还要编辑一个敌机正常状态的动画):

然后在主逻辑中开启碰撞:

// 开启碰撞检测系统,未开启时无法检测
cc.director.getCollisionManager().enabled = true;

开启碰撞后,给子弹编写处理碰撞的方法:

onCollisionEnter(other, self) {
    if (self.tag == 1) {
        // 普通子弹命中了普通敌机
        Singleton.instance.bulletKilled(this.node)
    }
    if (other.tag == 2){
        // 击中的是普通敌机
        let enemy = other.getComponent("enemy_1")
        if(enemy && !enemy.isDie){
            enemy.hit()
        }
    }
}

给敌机添加被击中后的处理逻辑:

hit(){
    // 击中后状态设置为死亡
    this.isDie = true
    // 播放帧动画
    let anim = this.getComponent(cc.Animation)
    anim.play('enemy_1_die')
}

over(){
    // 帧动画播放完后把敌机放回对象池中,等待下一次出现
    Singleton.instance.enemy1Killed(this.node)
}

记得给敌机的出生坐标设置一个随机值

//...
enemy1.parent = this.node
let randomX = 295 - 590 * Math.random()
enemy1.setPosition(cc.v2(randomX, 590))
//...

得到的效果是这样的:

好了,上集就先到这,努力更新下集中……

游戏资源地址:
链接:https://pan.baidu.com/s/1rL82cUYMnxgZQ3xkff5RGw
提取码:r10d

学习参考:链接

页面下部广告