本文针对Cocos开源小项目FlappyBird游戏源码进行讲解
需要先下好cocos软件然后获取游戏源码包-网盘获取
🆗让我们开始吧
1 导入源码包
项目是用Cocos Creator 3.8.2版本创建的(从package.json中可以看出来),关于版本兼容性有两点需要注意:一个是向上兼容,可以用更高版本的编辑器打开(比如3.8.3、3.8.4),但建议只用比3.8.2高一点的版本,比如3.8.x系列;第二是向下不兼容,不能用低版本编辑器打开(比如3.7.x或3.6.x),这样会导致项目无法正常运行或报错。
安装好正确的编辑器后,我们把解压的源码文件夹导入我们的项目,首先点击导航栏的“项目”:
进入后页面是这样的,你可能会疑惑,怎么工作台没有游戏界面,因为导入的源码还在资源管理器中,没有搬到层级管理器中,相当于游戏还在仓库里没有放到台面上。
双击scenes文件夹里的Game场景文件把游戏场景打开,这样就能看到游戏的界面了。场景文件就是一个保存了游戏画面布局的数据文件,打开它时,Cocos会自动把这些数据转换成层级管理器中的实际游戏物件。我们可以先来运行玩一下。
我们先来了解一下游戏的流程,游戏开始时,小鸟在屏幕中间悬停。玩家点击屏幕,小鸟就会向上跳跃一下,不点击的话小鸟会自然下落。游戏中会不断有上下的管道从右向左移动,玩家需要通过点击屏幕控制小鸟穿过管道之间的缝隙,碰到管道或者掉到地上game over,显示分数。每成功通过一对管道就得一分,目标就是尽可能得高分。
这个FlappyBird游戏的代码结构很清晰,GameManager管理整体游戏状态(准备、游戏中、结束),Bird类控制小鸟的跳跃和碰撞,MoveBg负责背景的无限滚动,再加上分数统计和音效系统,所有这些组件协同工作,就构成了一个完整的游戏循环。
2 项目结构
接着我们来了解一下FlappyBird游戏的代码结构,可以分为两大部分:
一、场景结构(上半部分):
1.Game是主场景,下面的Canvas包含了所有游戏元素
2.Camera负责游戏画面
3.Bg(背景)、PipeSpawn(管道生成)、Land(地面)是游戏场景元素
4.GameManager和Bird是核心游戏对象
5.三个UI界面(Ready准备/Gaming游戏中/GameOver结束)负责不同状态的显示
6.audioMgr管理所有音效
二、资源结构(下半部分):
1.animations存放动画文件
2.audios存放音效文件
3.prefabs存放预制体(可重用的游戏对象)
4.scenes存放场景文件
5.scripts存放所有代码脚本
6.textures存放图片资源
游戏采用了典型的Cocos游戏架构:场景层级(Hierarchy)包含了游戏运行时所需的所有节点组件,从相机、UI界面到游戏对象都以树状结构组织;资源系统(Assets)则存放了游戏开发所需的各类资源文件,包括动画、音频、预制体、场景、脚本和贴图等,形成了一个完整的游戏开发工程结构。
3 核心代码
游戏用的TypeScript(简称TS)语言编写,这也是Cocos Creator推荐的开发语言。TypeScript是JavaScript的超集,它在JavaScript的基础上增加了类型系统,让代码更容易维护和调试,从文件扩展名.ts就可以看出来。TypeScript的优势是可以在编写代码时就发现可能的错误,而不是等到运行时才发现,这对游戏开发来说特别重要。
下面列出FlappyBird的核心的脚本代码,都放在assets/scripts文件夹里面:
1.GameManager.ts:游戏的大脑,控制游戏状态(准备/游戏中/结束)、管理游戏进程和分数
2.Bird.ts:小鸟的控制器,处理点击跳跃、控制小鸟旋转,还有碰撞检测
3.MoveBg.ts:背景移动系统,控制背景和地面的无限滚动、实现视差效果
4.PipeSpawner.ts:管道生成器,生成和控制管道、设置管道间距和速度
5.UI文件夹下的:GameReadyUI.ts是准备界面,GameOverUI.ts是结束界面
6.AudioMgr.ts:音频管理器,控制游戏音效和背景音乐
上面这些脚本构成了游戏的核心逻辑系统,每个脚本都负责特定的功能下面我挑几个代码(讲解注释)
A:GameManager.ts
主要逻辑是通过管理游戏状态来控制小鸟、背景、管道、UI显示和音效,同时实时更新分数。
import { _decorator, AudioClip, Component, Game, Label, Node } from 'cc';
import { Bird } from './Bird';
import { MoveBg } from './MoveBg';
import { PipeSpawner } from './PipeSpawner';
import { GameReadyUI } from './UI/GameReadyUI';
import { GameData } from './GameData';
import { GameOverUI } from './UI/GameOverUI';
import { AudioMgr } from './AudioMgr';
const { ccclass, property } = _decorator;
// 定义游戏状态枚举(状态分为:准备中、游戏中、游戏结束)
enum GameState {
Ready, // 准备中
Gaming, // 游戏中
GameOver // 游戏结束
}
@ccclass('GameManager')
export class GameManager extends Component {
// 单例模式,让其他地方可以直接通过GameManager.inst()访问
private static _inst: GameManager = null;
public static inst() {
return this._inst;
}
// 游戏的移动速度(比如背景滚动的速度)
@property
moveSpeed: number = 100;
// 小鸟的控制组件
@property(Bird)
bird: Bird = null;
// 背景和地面的移动组件
@property(MoveBg)
bgMoving: MoveBg = null;
@property(MoveBg)
landMoving: MoveBg = null;
// 管道生成器
@property(PipeSpawner)
pipeSpawner: PipeSpawner = null;
// 各种UI:游戏开始前的准备UI、游戏中的UI、游戏结束UI
@property(GameReadyUI)
gameReadyUI: GameReadyUI = null;
@property(Node)
gamingUI: Node = null;
@property(GameOverUI)
gameOverUI: GameOverUI = null;
// 显示分数的标签组件
@property(Label)
scoreLabel: Label = null;
// 背景音乐和游戏结束音效
@property(AudioClip)
bgAudio: AudioClip = null;
@property(AudioClip)
gameOverAudio: AudioClip = null;
// 当前游戏状态,初始是准备状态
curGS: GameState = GameState.Ready;
// 当组件加载时会调用,初始化GameManager实例
onLoad() {
GameManager._inst = this; // 把当前实例赋值给静态变量,供其他地方调用
}
// 游戏开始时会调用的逻辑
protected start(): void {
this.transitionToReadyState(); // 切换到准备状态
AudioMgr.inst.play(this.bgAudio, 0.1); // 播放背景音乐,音量是0.1
}
// 切换到“准备状态”的逻辑
transitionToReadyState() {
this.curGS = GameState.Ready; // 设置当前状态为准备中
this.bird.disableControl(); // 禁用小鸟的控制,防止玩家操作
this.bgMoving.disableMoving(); // 停止背景滚动
this.landMoving.disableMoving(); // 停止地面滚动
this.pipeSpawner.pause(); // 暂停生成管道
this.gamingUI.active = false; // 隐藏游戏中的UI
this.gameOverUI.hide(); // 隐藏游戏结束的UI
this.gameReadyUI.node.active = true; // 显示准备状态的UI
}
// 切换到“游戏进行状态”的逻辑
transitionToGamingState() {
this.curGS = GameState.Gaming; // 设置当前状态为游戏中
this.bird.enableControl(); // 启用小鸟的控制,让玩家可以操作
this.bgMoving.enableMoving(); // 开始背景滚动
this.landMoving.enableMoving(); // 开始地面滚动
this.pipeSpawner.start(); // 开始生成管道
this.gameReadyUI.node.active = false; // 隐藏准备状态的UI
this.gamingUI.active = true; // 显示游戏中的UI
}
// 切换到“游戏结束状态”的逻辑
transitionToGameOverState() {
if (this.curGS == GameState.GameOver) return; // 如果已经是游戏结束状态,直接返回
this.curGS = GameState.GameOver; // 设置当前状态为游戏结束
this.bird.disableControlNotRGD(); // 禁用小鸟的控制,但保留重力等效果
this.bgMoving.disableMoving(); // 停止背景滚动
this.landMoving.disableMoving(); // 停止地面滚动
this.pipeSpawner.pause(); // 暂停生成管道
this.gamingUI.active = false; // 隐藏游戏中的UI
// 显示游戏结束UI,并更新分数
this.gameOverUI.show(GameData.getScore(), GameData.getBestScore());
GameData.saveScore(); // 保存当前分数
AudioMgr.inst.stop(); // 停止背景音乐
AudioMgr.inst.playOneShot(this.gameOverAudio); // 播放游戏结束音效
}
// 增加分数的逻辑
addScore(count: number = 1) {
GameData.addScore(); // 更新分数
this.scoreLabel.string = GameData.getScore().toString(); // 把新的分数显示在屏幕上
}
}
B:Bird.ts
通过触摸屏幕控制小鸟飞起,管理小鸟的旋转与物理运动,碰撞管道或地面触发游戏结束,穿越管道得分。
import { _decorator, Animation, animation, AudioClip, Collider2D, Component, Contact2DType, Input, input, IPhysics2DContact, Node, RigidBody2D, SkelAnimDataHub, Vec2, Vec3 } from 'cc';
import { Tags } from './Tags';
import { GameManager } from './GameManager';
import { AudioMgr } from './AudioMgr';
const { ccclass, property } = _decorator;
@ccclass('Bird')
export class Bird extends Component {
private rgd2D: RigidBody2D = null; // 小鸟的物理刚体组件
@property
rotateSpeed: number = 30; // 小鸟旋转的速度
@property(AudioClip)
clickAudio: AudioClip = null; // 小鸟点击时播放的音效
private _canControl: boolean = false; // 是否可以控制小鸟的状态
// 在组件加载时执行
onLoad() {
// 监听触摸事件,当触摸屏幕时调用 onTouchStart 方法
input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
// 获取并注册碰撞事件的回调
let collider = this.getComponent(Collider2D);
if (collider) {
// 开始接触时调用 onBeginContact
collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
// 结束接触时调用 onEndContact
collider.on(Contact2DType.END_CONTACT, this.onEndContact, this);
}
// 获取小鸟的物理刚体组件
this.rgd2D = this.getComponent(RigidBody2D);
}
// 在组件销毁时执行,移除事件监听
onDestroy() {
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); // 移除触摸事件监听
let collider = this.getComponent(Collider2D);
if (collider) {
// 移除碰撞事件的回调
collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
collider.off(Contact2DType.END_CONTACT, this.onEndContact, this);
}
}
// 当触摸屏幕时调用,控制小鸟飞起来
onTouchStart() {
if (this._canControl == false) return; // 如果不能控制小鸟,直接返回
this.rgd2D.linearVelocity = new Vec2(0, 10); // 给小鸟一个向上的速度
this.node.angle = 30; // 设置小鸟的角度
AudioMgr.inst.playOneShot(this.clickAudio); // 播放点击音效
}
// 每一帧都会调用,用来更新小鸟的角度(旋转)
protected update(dt: number): void {
if (this._canControl == false) return; // 如果不能控制小鸟,直接返回
this.node.angle -= this.rotateSpeed * dt; // 根据旋转速度更新小鸟角度
if (this.node.angle < -60) {
this.node.angle = -60; // 限制小鸟最大旋转角度为-60度
}
}
// 启用小鸟的控制
public enableControl() {
this.getComponent(Animation).enabled = true; // 启动动画
this.rgd2D.enabled = true; // 启用物理刚体
this._canControl = true; // 设置可以控制小鸟
}
// 禁用小鸟的控制
public disableControl() {
this.getComponent(Animation).enabled = false; // 禁用动画
this.rgd2D.enabled = false; // 禁用物理刚体
this._canControl = false; // 设置不能控制小鸟
}
// 禁用小鸟的控制,但保留物理刚体(主要用于游戏结束时)
public disableControlNotRGD() {
this.getComponent(Animation).enabled = false; // 禁用动画
this._canControl = false; // 设置不能控制小鸟
}
// 碰撞开始时的回调函数
onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
console.log(otherCollider.tag); // 打印碰撞物体的标签
// 如果碰到地面或者管道,游戏结束
if (otherCollider.tag === Tags.LAND || otherCollider.tag === Tags.PIPE) {
GameManager.inst().transitionToGameOverState(); // 切换到游戏结束状态
}
}
// 碰撞结束时的回调函数
onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
// 如果小鸟通过了管道的中间区域,得分
if (otherCollider.tag === Tags.PIPE_MIDDLE) {
GameManager.inst().addScore(); // 增加分数
}
}
}
C:MoveBg.ts
主要用来实现游戏中的背景平滑滚动效果。通过两个背景交替移动,避免了背景结束后出现空白
import { _decorator, Component, Node } from 'cc';
import { GameManager } from './GameManager';
const { ccclass, property } = _decorator;
@ccclass('MoveBg')
export class MoveBg extends Component {
@property(Node)
target1ToMove: Node = null; // 第一个需要移动的目标节点(背景)
@property(Node)
target2ToMove: Node = null; // 第二个需要移动的目标节点(背景)
private moveSpeed: number = 100; // 背景移动的速度
private _canMoving: boolean = false; // 控制背景是否可以移动
// 在游戏开始时设置背景移动的速度
start() {
this.moveSpeed = GameManager.inst().moveSpeed; // 获取游戏管理器中的移动速度
}
// 每一帧都执行,用来更新背景的位置
update(deltaTime: number) {
// 如果不能移动背景,就直接返回
if (this._canMoving == false) return;
const moveDistance = this.moveSpeed * deltaTime; // 计算背景每帧应该移动的距离
// 获取第一个背景的当前位置并更新位置
let p1 = this.target1ToMove.getPosition();
this.target1ToMove.setPosition(p1.x - moveDistance, p1.y);
// 获取第二个背景的当前位置并更新位置
let p2 = this.target2ToMove.getPosition();
this.target2ToMove.setPosition(p2.x - moveDistance, p2.y);
// 如果第一个背景移动出屏幕(x小于-730),则将其位置重新设置到第二个背景的右边
if (p1.x < -730) {
p2 = this.target2ToMove.getPosition();
this.target1ToMove.setPosition(p2.x + 728, p2.y); // 让第一个背景出现在第二个背景的右边
}
// 如果第二个背景移动出屏幕(x小于-730),则将其位置重新设置到第一个背景的右边
if (p2.x < -730) {
p1 = this.target1ToMove.getPosition();
this.target2ToMove.setPosition(p1.x + 728, p1.y); // 让第二个背景出现在第一个背景的右边
}
}
// 启动背景的移动
public enableMoving() {
this._canMoving = true; // 允许背景移动
}
// 停止背景的移动
public disableMoving() {
this._canMoving = false; // 禁止背景移动
}
}
D:PipeSpawner.ts
定时生成管道并随机设置位置,用于创建游戏中的障碍物,增强游戏的挑战性和动态性。
import { _decorator, Component, instantiate, math, Node, Prefab } from 'cc';
import { Pipe } from './Pipe';
const { ccclass, property } = _decorator;
@ccclass('PipeSpawner')
export class PipeSpawner extends Component {
@property(Prefab)
pipePrefab: Prefab = null; // 管道的预设模板
@property
spawnRate: number = 0.5; // 生成管道的间隔时间,单位是秒
private timer: number = 0; // 计时器,用来累积时间
private _isSpawning: boolean = false; // 判断是否正在生成管道
// 每一帧都执行
update(deltaTime: number) {
if (this._isSpawning == false) return; // 如果没有开启生成管道,就什么都不做
this.timer += deltaTime; // 每帧增加经过的时间
if (this.timer > this.spawnRate) { // 如果时间超过了设定的间隔
this.timer = 0; // 重置计时器
const pipeInst = instantiate(this.pipePrefab); // 创建一个新的管道实例
this.node.addChild(pipeInst); // 将管道添加到当前场景中
const p = this.node.getWorldPosition(); // 获取当前节点的位置
pipeInst.setWorldPosition(p); // 将管道放到相同的位置
const y = math.randomRangeInt(-100, 200); // 随机生成一个Y坐标,范围从-100到200
const pLoca = pipeInst.getPosition(); // 获取管道当前的坐标
pipeInst.setPosition(pLoca.x, y); // 更新管道的位置,只改变Y坐标
}
}
// 暂停生成管道
public pause() {
this._isSpawning = false; // 停止生成管道
// 遍历所有生成的管道,并禁用它们
const nodeArray = this.node.children;
for (let i = 0; i < nodeArray.length; i++) {
const pipe = nodeArray[i].getComponent(Pipe);
if (pipe) {
pipe.enabled = false; // 禁用管道的行为
}
}
}
// 开始生成管道
public start() {
this._isSpawning = true; // 开启管道生成
}
}
E:GameReadyUI.ts
import { _decorator, Component, director, Label, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('GameOverUI')
export class GameOverUI extends Component {
@property(Label)
curScoreLabel: Label = null; // 当前分数的标签
@property(Label)
bestScoreLabel: Label = null; // 最佳分数的标签
@property(Node)
newSprite: Node = null; // 新纪录图标(显示新纪录时才显示)
@property([Node])
medalArray: Node[] = []; // 奖牌数组,用来显示不同等级的奖牌
// 显示游戏结束时的UI
public show(curScore: number, bestScrore: number) {
this.node.active = true; // 激活游戏结束UI
this.curScoreLabel.string = curScore.toString(); // 显示当前分数
this.bestScoreLabel.string = bestScrore.toString(); // 显示最佳分数
// 如果当前分数超过最佳分数,显示新纪录图标
if (curScore > bestScrore) {
this.newSprite.active = true; // 显示新纪录图标
} else {
this.newSprite.active = false; // 不显示新纪录图标
}
// 根据当前分数决定显示哪一个奖牌
const index = curScore / 10; // 分数除以10
let indexInt = Math.floor(index); // 取整数部分
if (indexInt > 3) {
indexInt = 3; // 最多显示3个等级奖牌
}
this.medalArray[indexInt].active = true; // 显示对应等级的奖牌
}
// 隐藏游戏结束UI
public hide() {
this.node.active = false; // 隐藏游戏结束UI
}
// 按下“重新开始”按钮时,重新加载当前场景
onPlayButtonClick() {
director.loadScene(director.getScene().name); // 重新加载当前场景
}
}
具体完整可看源码中内容,本文仅供参考学习。