游戏引擎我选择的是 cocos creator,官网地址:链接
您可以直接点击下载免费获得这款引擎软件,建议下载最新的稳定版本,写这篇文章的时候,适合写 2D 小游戏的最新版本貌似是:V2.4.8
一般我们会先下载一个 DASHBOARD,可以理解成一个 cocos 的盒子,用于放各种游戏工具。
然后从 DASHBOARD 中下载相应版本的 cocos creator。
安装完毕后,我们进入大盘界面:

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

看到上面这个界面后,就意味着咱们的开发环境已经搭建好了,可以开始开发小游戏咯!
首先我们把游戏资源导入到我们的游戏工程中,游戏资源地址:
然后查看背景图片的大小,资源里的背景图大小是:640 x 1136

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

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


制作完 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 }}编写完成后,咱们就有了轮播的背景图。

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

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

为了模仿微信飞机大战的展示效果,我们给这行 点击屏幕开始游戏 的文字添加一个上下晃动的动画效果,这里用到了 cocos 的 animation 功能:
点击屏幕开始游戏 所在的节点上
连起来播放后大概就成了这样:

虽然我们编写好了一个轮播的背景,有了动态的效果,但我们希望在游戏刚开始的时候背景是不动的,等玩家进入游戏后背景再动起来,让小飞机有飞翔的效果。
首先我们定义一个判断背景图片是否在动的变量
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("点击了暂停按钮") }}然后编写暂停页面:

然后编写这三个页面的显隐关系,大概的逻辑是:
以上的逻辑可以通过统一的按键点击方法进行处理:
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 }}实现效果大概是这样的:

编写好了游戏场景切换功能后,我们开始编写我们的游戏主角。
首先添加一个节点,为它设置一张图片(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); //...}大概的效果是酱紫的:

由于子弹是会重复利用的资源,我们这里采用预制体资源,首先我们在节点树中创建一个子弹节点,然后给子弹配一个脚本:
在每一帧中改变子弹的 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);}这样一来飞机就可以发射子弹了:

现在虽然能不停地发射子弹了,但是一直创建子弹实例不进行删除可不行,如果游戏时间久了游戏会越来越卡。
我们使用 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))}然后编写子弹消亡的逻辑,目前一共有三个场景可以回收子弹:
// 回收单颗子弹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 论坛搜索,大佬给出的答案是使用单例,单例的简单使用模版如下:
@ccclassexport 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";@ccclassexport default class NewClass extends cc.Component { update(dt) { this.node.y += 15 if(this.node.y > 590){ // 使用主逻辑单例对象 Singleton.instance.bulletKilled(this.node) } }}添加敌机的逻辑和添加子弹的逻辑相似:
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)]
有了敌机之后,我们让我方飞机发射的子弹可以击中敌机。
首先我们给敌机和子弹添加碰撞组件(记得给子弹和敌机添加分组):

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

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

然后在主逻辑中开启碰撞:
// 开启碰撞检测系统,未开启时无法检测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.nodelet randomX = 295 - 590 * Math.random()enemy1.setPosition(cc.v2(randomX, 590))//...得到的效果是这样的:

好了,上集就先到这,努力更新下集中......
游戏资源地址:
链接:https://pan.baidu.com/s/1rL82cUYMnxgZQ3xkff5RGw
提取码:r10d
学习参考:链接