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,官方下载地址链接
简单的使用方法如下:
- 打开图片目录,把所有的图片拖拽到 TexurePacker 中
- 保存
制作完 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 功能:
- 首先我们创建一个动画资源
- 将动画资源绑定到
点击屏幕开始游戏
所在的节点上 - 然后设置关键帧,给每个关键帧设定恰当的角度,比如说第一帧的角度为 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("点击了暂停按钮")
}
}
然后编写暂停页面:
- 首先添加一个暂停的背景图,设置透明度遮盖原有的游戏背景
- 添加三个按钮,设置好按钮的样式以及显示内容
- 为暂停背景设置一个 BlockInputEvents,屏蔽调下层节点
然后编写这三个页面的显隐关系,大概的逻辑是:
- 玩家进入游戏后显示一个静态的背景,当点击屏幕后隐藏调游戏准备界面,进入游戏开始界面
- 玩家可以在游戏界面点击暂停按钮,点击暂停按钮后由游戏开始界面进入游戏暂停界面
- 在游戏暂停界面有三个按钮,点击继续游戏会回到游戏开始界面,点击重新开始也会回到游戏开始界面,点击回到主页会回到游戏准备界面
以上的逻辑可以通过统一的按键点击方法进行处理:
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))
}
然后编写子弹消亡的逻辑,目前一共有三个场景可以回收子弹:
- 重新开始游戏
- 回到主页
- 子弹超出画布范围
// 回收单颗子弹
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. 添加敌机
添加敌机的逻辑和添加子弹的逻辑相似:
- 在节点树中创建一个敌机节点
- 创建敌机的脚本,并关联给敌机节点
- 把敌机节点制作成 PerFab
- 在主逻辑中编写敌机对象池、敌机创建、敌机销毁的方法
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))
//...
得到的效果是这样的:
好了,上集就先到这,努力更新下集中......