阅读前请先下载项目源码,边读边看源码以加深理解和实操,
源码地址已放于文章末尾!
嘿,各位未来的游戏大神们,欢迎回到咱们的专栏!上一期咱们把“地基”打得牢牢的,不仅搞定了环境,还把项目结构看了个底朝天。今天,咱们就要开始给这个“毛坯房”注入灵魂,让它真正地“活”起来!

咱们要解决一个核心问题:游戏是怎么从一堆静态的资源,变成一个能动、能交互的动态世界的?这就不得不提游戏开发的“圣杯”——游戏循环(Game Loop)。
游戏世界的心跳:游戏循环
想象一下你看过的动画片,比如《猫和老鼠》,它其实是由一秒钟24张静态图片连续播放形成的。游戏世界也是一个道理,它本质上是在以极快的速度(通常是每秒60次)不断地清空画布 -> 计算新位置 -> 绘制新画面。这个“清空-计算-绘制”的循环,就是游戏的心跳,我们称之为“游戏循环”。
在Cocos Creator里,引擎已经帮我们把这个最底层的循环给封装好了。我们开发者,就像是这部动画大片的“分镜导演”,只需要在每一帧(每一次循环)告诉引擎,哪个角色该往前走一步,哪个子弹该往前飞一点就行。而我们下达这些指令的地方,通常就是在组件脚本的 update 方法里。
// 这是一个非常简化的示例
// Cocos Creator的组件脚本,需要继承自Component
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('YourScriptName')
export class YourScriptName extends Component {
// 每帧都会被引擎自动调用
update(deltaTime: number) {
// deltaTime 就是距离上一帧过去的时间,通常是 0.016秒左右(1/60)
// this.node 指的是挂载了这个脚本的节点
// 为了让移动看起来平滑且与帧率无关,我们会用速度乘以时间
this.node.position.x += 100 * deltaTime; // 让节点每秒向右移动100像素
}
}
理解了这一点,咱们再去看游戏的启动流程,就会豁然开朗。
游戏启动:一场精心策划的“舞台剧”
一个游戏从你点击图标到能玩,背后经历了一系列复杂的加载和初始化流程,就像一场舞台剧,幕后人员需要先把灯光、道具、演员都准备好,然后大幕才能拉开。
我画了一张流程图,让大家看看这场剧是怎么开演的:
sequenceDiagram
participant Player as 玩家
participant Init as Init.ts (开场演员)
participant GameDirector as GameDirector.ts (总导演)
participant LevelManager as LevelManager.ts (关卡管家)
participant RoleLayer as RoleLayer.ts (角色导演)
Player->>+Init: 点击游戏图标,启动!
Init->>Init: 系统初始化(initSystems),加载音频、UI、SDK等基础模块
Init->>GameDirector: 告诉总导演:“基础模块OK,可以进游戏了!” (InitLoadFinished事件)
Init->>Init: 显示主页UI (UISystem.showUI(UIEnum.HomeUI))
Note over Init: 此时玩家看到主界面
Player->>GameDirector: 点击“开始游戏”按钮
GameDirector->>+GameDirector: 开始加载核心资源 (loadSubBound)
Note over GameDirector: 加载 Game, Effect 等核心代码和资源
GameDirector->>GameDirector: 加载关卡预制体,并创建对象池 (loadGameAssets)
GameDirector->>-GameDirector: 资源加载完毕!
GameDirector->>LevelManager: 激活“关卡管家”,准备开工!
LevelManager->>+RoleLayer: 导演,该你上场了,把玩家角色给我创建出来!
RoleLayer->>RoleLayer: 根据存档创建玩家队伍 (setRoles)
LevelManager->>-RoleLayer: 角色就绪!
Note over LevelManager: 同时,道路、道具、碰撞系统等也已就绪
Note over Player: 游戏正式开始!
是不是感觉脉络清晰多了?咱们这就深入代码,看看这几位关键“演员”的“剧本”是怎么写的。
1. Init.ts (开场演员)
这是游戏的入口脚本,挂在我们的主场景 mainScene 的 Canvas 节点上。它的任务最纯粹,就是负责“开场”。
assets/Init/InitScripts/Init.ts 核心代码:
@ccclass('Init')
export class Init extends Component {
// 这个变量用来确保初始化只执行一次,防止重复操作
private isSysInitFish = false;
// Cocos生命周期函数,节点加载时执行
protected onLoad() {
// ... 省略了获取Canvas和Camera的代码 ...
this.uiLayer = this.node.getChildByName('UILayer'); // 找到UI的根节点
this.initSystems(); // 立刻开始初始化各个系统
}
/** 初始化各个系统 */
protected initSystems() {
StorageSystem.init(); // 存档系统
AudioSystem.init(); // 音频系统
SDKSystem.init(); // SDK系统(对接平台用的)
UISystem.init(this.uiLayer); // UI系统,告诉它UI应该放在哪
AdvertSystem.init(this.uiLayer); // 广告系统
}
// Cocos生命周期函数,每帧执行
protected update(dt) {
// 因为各个系统的初始化可能是异步的,所以这里每帧检查一下
this.checkSysInitState();
}
/** 检测各个系统是否加载完成 */
protected checkSysInitState() {
if (!this.isSysInitFish) {
let isFinish = true;
// 只要有一个系统没准备好,isFinish就是false
isFinish = isFinish && StorageSystem.isInitFinished;
isFinish = isFinish && AudioSystem.isInitFinished;
// ... 检查所有系统 ...
this.isSysInitFish = isFinish;
if (this.isSysInitFish) {
// 所有系统都说“我好了!”,那就正式进入游戏!
this.enterGame();
}
}
}
/** 进入游戏 */
protected enterGame() {
// 发送一个全局事件,告诉其他可能在监听的模块:“初始化搞定啦!”
EventManager.emit(EventTypes.GameEvents.InitLoadFinished);
clog.log('#进入游戏');
// 显示主界面UI
UISystem.showUI(UIEnum.HomeUI);
// 预加载一些核心资源包,让后续进入关卡时更快
this.preLoadBound();
}
}
代码解读:
Init.ts的核心职责就是调用一系列System的init()静态方法,启动游戏的基础服务。- 它通过一个
update里的轮询 (checkSysInitState) 来等待所有异步的初始化操作完成,这是一种简单有效的处理异步的方式。 - 当一切准备就绪,
enterGame方法会显示主界面,并预加载资源,把舞台的控制权交给了玩家。
2. GameDirector.ts (总导演)
这位是重量级角色,负责游戏核心流程的调度。它在 onLoad 时就通过 EventManager 监听了各种游戏事件,比如我们最关心的 GameStart。
assets/Init/InitScripts/GameDirector.ts 核心代码:
@ccclass('GameDirector')
export class GameDirector extends Component {
private _levelManager: Node = null; // 缓存一下关卡管理器的节点
protected onLoad() {
this.onEvents(); // 在加载时就开始监听事件
}
protected onEvents() {
// 监听“游戏开始”事件,一旦听到,就执行 onGameStart 方法
EventManager.on(EventTypes.GameEvents.GameStart, this.onGameStart, this);
// ... 监听其他事件,比如游戏结束、暂停等 ...
}
/** 开始游戏 */
protected onGameStart(cb?) {
// 显示加载界面
EventManager.emit(EventTypes.GameEvents.SetInitUIEnable, true);
// 这是一个回调地狱,但逻辑很清晰:
// 1. 先加载核心代码和资源分包
this.loadSubBound(() => {
// 2. 再加载关卡独有的额外资源(本项目没用到,但框架留了口子)
this.loadExtralAssets(() => {
// 3. 最后加载关卡场景里会用到的所有预制体(Prefab)
this.loadGameAssets(() => {
// 预先创建一些对象到对象池里,避免玩的时候突然卡一下
this.preLoadPrefabs();
// 激活关卡管理器,正式开始游戏!
this._levelManager.active = true;
// 隐藏加载界面
EventManager.emit(EventTypes.GameEvents.SetInitUIEnable, false);
// 如果有回调,就执行(比如UI需要知道加载完成了)
cb && cb();
})
});
})
}
/** 加载关卡必须的预制体 */
private loadGameAssets(cb) {
// ...
// 获取 levelManager 的预制体实例,并挂到场景中
this._levelManager = GlobalPool.get('LevelManager');
this._levelManager.parent = this.node;
// ...
// 加载自定义的Prefab,并为它们创建对象池
this.loadCustomPrefabs(() => {
// ...
cb && cb();
});
}
/** 加载自定义预制体并创建对象池 */
private loadCustomPrefabs(cb) {
// ... 遍历需要加载的Prefab路径 ...
Loader.loadBundleDir(bound, url, (prefabs: Prefab[]) => {
// 加载完成后,为每一个Prefab创建一个专用的对象池
for (let i = 0; i < prefabs.length; i++) {
const p = prefabs[i];
GlobalPool.createPool(p.data.name, p);
}
// ...
}, Prefab, false);
}
}
代码解读:
- 事件驱动:
GameDirector的核心是一个事件监听器。它平时静静地待着,听到事件广播才开始干活。 - 异步流程控制:通过回调函数,它将一系列异步的加载操作串联起来,保证了资源加载的正确顺序。
- 对象池的准备:它最重要的职责之一,就是在游戏开始前,调用
GlobalPool.createPool,把所有后面会频繁使用的对象(敌人、子弹、特效)的“模具”(Prefab)都准备好,建立了对象池。
3. LevelManager.ts (关卡管家)
当 GameDirector 激活它后,它就正式接管了游戏世界。
assets/Game/Script/Common/LevelManager.ts 核心代码:
@ccclass('LevelManager')
export class LevelManager extends Component {
// 在编辑器里拖入需要管理的逻辑层根节点
@property(Node)
private layerNodeArr: Node[] = [];
// 存放所有动态创建的游戏对象的根节点
@property(Node)
private perfabsLayer: Node = null;
// 存放所有自定义逻辑层的实例
private customLayers: BasicLayer[] = [];
// Cocos生命周期,当节点第一次被激活时(也就是游戏开始时)调用
protected onEnable() {
this.reset(); // 重置所有数据
this.setData(); // 设置新关卡的数据
}
/** 初始化自定义层级 */
protected initCustomLayers() {
// 按照游戏逻辑的依赖顺序,依次new出各个逻辑层
this.customLayers.push(new RoadLayer(this.perfabsLayer)); // 先铺路
this.customLayers.push(new PropsLayer(this.perfabsLayer)); // 再放道具
this.customLayers.push(new CollisionManager(this.perfabsLayer)); // 然后建立碰撞网格
this.customLayers.push(new EffectLayer(this.perfabsLayer)); // 特效层
this.customLayers.push(new MergeEffectLayer(this.perfabsLayer)); // 合批特效层(子弹)
this.customLayers.push(new RoleLayer(this.perfabsLayer)); // 最后创建角色
// ...
}
/** 设定层级数据 */
protected setCustomLayersData(d?) {
// 遍历所有逻辑层,调用它们的 setData,把关卡数据传下去
for (let i = 0; i < this.customLayers.length; i++) {
this.customLayers[i].setData(d);
}
}
// 每帧调用
protected update(dt) {
// ...
// 驱动所有逻辑层进行更新
this.updateCustomLayers(ft)
}
}
代码解读:
- 分层架构:
LevelManager体现了清晰的分层思想。它不关心具体的游戏逻辑,只负责管理RoadLayer、RoleLayer等具体的“施工队”。 - 统一的生命周期:它将所有逻辑层统一纳入自己的生命周期管理。当
LevelManager被激活(onEnable),它会调用所有层的setData;在自己的update里,它会调用所有层的customUpdate。这让整个关卡的逻辑更新井然有序。
今日总结
今天我们给游戏成功“注入了灵魂”!我们不仅理解了游戏循环这个核心概念,还通过深入分析 Init.ts、GameDirector.ts 和 LevelManager.ts 这三大“幕后功臣”的源码,彻底搞清楚了游戏从启动到运行的每一个细节。
我们学到了:
- 事件驱动和回调函数如何管理复杂的异步流程。
- 对象池的原理和重要性,以及它是在何时被创建的。
- 清晰的分层架构如何让我们的代码不至于乱成一团麻。
现在,舞台已经搭好,灯光音响全部就位。下一期,万众瞩目的主角就要登场了!我们将深入 RoleLayer.ts 和 Role.ts,亲手把咱们的英雄队伍创建出来,并让他们听从我们的指挥,在屏幕上左右横跳!
准备好了吗?下一期,让咱们的角色“活”起来!敬请期待!
游戏源码下载地址:
https://wwuj.lanzoul.com/i4N1n33e9dib
验证码hack
5077



被折叠的 条评论
为什么被折叠?



