Day 07 · 游戏也要管理状态:场景切换·资源加载·对象池实战
学习目标:掌握场景管理、动态资源加载(AssetBundle)、预制体和对象池优化
预计时间:3 小时
难度:⭐⭐⭐⭐☆
为什么需要资源管理?
随着游戏规模扩大,你会遇到:
- 场景切换时界面卡顿(资源未释放)
- 频繁创建/销毁节点导致 GC 卡顿
- 移动端内存不足崩溃
本章的三大武器:场景管理 + 动态加载 + 对象池,能解决这些问题。
1. 场景管理(director)
1.1 场景切换
typescript
import { _decorator, Component, director, Director } from 'cc';
const { ccclass } = _decorator;
@ccclass('SceneManager')
export class SceneManager extends Component {
// 场景名称需要在构建设置中添加才能打包
static SCENE_MENU = 'Menu';
static SCENE_GAME = 'Game';
static SCENE_RESULT = 'Result';
// 加载场景(默认:加载完后立即切换,旧场景销毁)
static loadGame() {
director.loadScene(SceneManager.SCENE_GAME);
}
// 带回调的场景切换
static loadGameWithCallback(onProgress?: (completedCount: number, totalCount: number) => void) {
director.loadScene(
SceneManager.SCENE_GAME,
(err) => {
if (err) {
console.error('场景加载失败:', err);
return;
}
console.log('场景加载成功');
}
);
}
// 预加载场景(提前加载到内存,切换时无等待)
static preloadScene(sceneName: string) {
director.preloadScene(sceneName, (completedCount, totalCount) => {
const progress = (completedCount / totalCount * 100).toFixed(0);
console.log(`预加载进度: ${progress}%`);
}, (err) => {
if (err) console.error('预加载失败:', err);
else console.log('预加载完成:', sceneName);
});
}
}
1.2 场景常驻节点
默认情况下,切换场景时旧场景的所有节点都会被销毁。如果某些节点需要跨场景保留(如音乐管理器、玩家数据):
typescript
import { _decorator, Component, director, game, Node } from 'cc';
const { ccclass } = _decorator;
@ccclass('PersistNode')
export class PersistNode extends Component {
onLoad() {
// 将此节点标记为常驻节点(不被场景切换销毁)
game.addPersistRootNode(this.node);
}
// 如需在某个场景中移除常驻
removePersist() {
game.removePersistRootNode(this.node);
}
}


1.3 场景生命周期与数据传递
typescript
// 场景间传递数据(通过单例管理器)
export class GameData {
private static _instance: GameData | null = null;
static get instance(): GameData {
if (!GameData._instance) {
GameData._instance = new GameData();
}
return GameData._instance;
}
// 游戏数据
score: number = 0;
level: number = 1;
playerName: string = '';
reset() {
this.score = 0;
this.level = 1;
}
}
// 在游戏场景中
GameData.instance.score = 1500;
director.loadScene('Result');
// 在结算场景中
const finalScore = GameData.instance.score; // 获取上一场景的得分
2. 动态资源加载
2.1 Resources 文件夹加载
将资源放在 assets/resources/ 文件夹下,可以动态加载:
typescript
import { _decorator, Component, resources, SpriteFrame, AudioClip, Prefab, JsonAsset, instantiate } from 'cc';
const { ccclass } = _decorator;
@ccclass('DynamicLoader')
export class DynamicLoader extends Component {
start() {
this.loadSprite();
this.loadAudio();
this.loadPrefab();
this.loadJSON();
}
// 加载图片(SpriteFrame)
loadSprite() {
resources.load('textures/hero/hero-idle/spriteFrame', SpriteFrame, (err, spriteFrame) => {
if (err) { console.error(err); return; }
// 使用 spriteFrame
// this.sprite.spriteFrame = spriteFrame;
});
}
// 加载音频
loadAudio() {
resources.load('audio/bgm', AudioClip, (err, clip) => {
if (err) { console.error(err); return; }
const audioSource = this.getComponent('AudioSource') as any;
audioSource.clip = clip;
audioSource.play();
});
}
// 加载预制体
loadPrefab() {
resources.load('prefabs/Enemy', Prefab, (err, prefab) => {
if (err) { console.error(err); return; }
const enemy = instantiate(prefab);
this.node.addChild(enemy);
});
}
// 加载 JSON 配置文件
loadJSON() {
resources.load('data/level-config', JsonAsset, (err, asset) => {
if (err) { console.error(err); return; }
const config = asset.json as { levels: any[] };
console.log('关卡数量:', config.levels.length);
});
}
// 批量加载(加载整个文件夹)
loadAllFrames() {
resources.loadDir('textures/hero', SpriteFrame, (err, frames) => {
if (err) { console.error(err); return; }
console.log('加载了', frames.length, '帧');
});
}
// 释放资源(不再使用时必须释放!)
releaseResources(path: string) {
resources.release(path);
// 或者按资源引用释放
// resources.release(myAsset);
}
}
2.2 AssetBundle(分包加载)
AssetBundle 允许将资源分包,按需下载:

typescript
import { _decorator, Component, assetManager, AssetManager, Prefab, instantiate } from 'cc';
const { ccclass } = _decorator;
@ccclass('BundleLoader')
export class BundleLoader extends Component {
start() {
this.loadLevel2Bundle();
}
loadLevel2Bundle() {
// 加载 Bundle(名称是在 Inspector 中配置的 Bundle 名)
assetManager.loadBundle('level2', (err, bundle) => {
if (err) { console.error('Bundle 加载失败:', err); return; }
console.log('Level2 Bundle 加载成功');
this.loadEnemyFromBundle(bundle);
});
}
loadEnemyFromBundle(bundle: AssetManager.Bundle) {
bundle.load('prefabs/BossEnemy', Prefab, (err, prefab) => {
if (err) { console.error(err); return; }
const boss = instantiate(prefab);
this.node.addChild(boss);
});
}
// 释放整个 Bundle
releaseBundle() {
assetManager.removeBundle(assetManager.getBundle('level2')!);
}
}
3. 预制体(Prefab)
预制体是可复用的节点模板,是 Cocos 游戏开发最核心的工作单元。
3.1 创建预制体
- 在场景中搭建好节点结构(例如:Enemy 节点 + Sprite + 碰撞体 + EnemyController 脚本)
- 将节点从层级管理器 拖拽到资源管理器 中 → 自动生成
.prefab文件 - 原节点变为预制体实例(蓝色标识)

3.2 代码实例化预制体
typescript
import { _decorator, Component, Prefab, Node, instantiate, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('EnemySpawner')
export class EnemySpawner extends Component {
@property(Prefab)
enemyPrefab: Prefab = null!;
@property(Node)
spawnParent: Node = null!; // 敌人的父节点(便于统一管理)
spawnEnemy(x: number, y: number) {
const enemy = instantiate(this.enemyPrefab);
this.spawnParent.addChild(enemy);
enemy.setPosition(x, y, 0);
return enemy;
}
spawnWave(count: number, spacing: number) {
for (let i = 0; i < count; i++) {
const x = (i - count / 2) * spacing;
this.scheduleOnce(() => {
this.spawnEnemy(x, 400);
}, i * 0.3); // 每隔0.3秒生成一个
}
}
}
4. 对象池(NodePool)
频繁创建/销毁节点会导致GC(垃圾回收)卡顿,在弹幕游戏、消消乐等需要大量临时节点的场景中,必须使用对象池。

typescript
import { _decorator, Component, Prefab, Node, instantiate, NodePool } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('BulletPool')
export class BulletPool extends Component {
@property(Prefab)
bulletPrefab: Prefab = null!;
// 子弹对象池
private _pool: NodePool = new NodePool();
// 预热(提前创建一批对象放入池中)
start() {
this.preheat(20); // 预先创建20个子弹
}
preheat(count: number) {
for (let i = 0; i < count; i++) {
const bullet = instantiate(this.bulletPrefab);
this._pool.put(bullet); // 放入池中(会自动禁用节点)
}
console.log(`对象池预热完成,池中有 ${this._pool.size()} 个对象`);
}
// 从池中获取子弹(复用 > 新建)
getBullet(x: number, y: number): Node {
let bullet: Node;
if (this._pool.size() > 0) {
bullet = this._pool.get()!; // 从池中取出(会自动激活节点)
} else {
// 池中没有可用对象时,新建一个
bullet = instantiate(this.bulletPrefab);
}
this.node.addChild(bullet);
bullet.setPosition(x, y, 0);
return bullet;
}
// 回收子弹到池中
recycleBullet(bullet: Node) {
this._pool.put(bullet); // 会自动从父节点移除并禁用
}
// 清空池(场景切换时调用)
onDestroy() {
this._pool.clear();
}
}
4.1 对象池配合 IPoolManager 接口
typescript
import { _decorator, Component, NodePool, IPoolManager } from 'cc';
const { ccclass, property } = _decorator;
// 子弹组件:实现 IPoolManager 接口
@ccclass('Bullet')
export class Bullet extends Component implements IPoolManager {
private _speed: number = 600;
private _pool: NodePool = null!;
// 被放入池中时调用(做清理工作)
unuse() {
// 重置状态
this._speed = 600;
this.node.setPosition(0, 0, 0);
// 取消所有 tween
this.node.stopAllActions();
}
// 从池中取出时调用(做初始化工作)
reuse(...args: any[]) {
const [speed] = args;
if (speed) this._speed = speed;
}
setPool(pool: NodePool) {
this._pool = pool;
}
update(deltaTime: number) {
// 向上移动
this.node.setPosition(
this.node.position.x,
this.node.position.y + this._speed * deltaTime,
0
);
// 超出屏幕时回收
if (this.node.position.y > 700) {
this._pool.put(this.node);
}
}
}
5. 加载界面实现
typescript
import { _decorator, Component, ProgressBar, Label, director } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('LoadingScene')
export class LoadingScene extends Component {
@property(ProgressBar)
progressBar: ProgressBar = null!;
@property(Label)
progressLabel: Label = null!;
@property({ displayName: '目标场景名' })
targetScene: string = 'Game';
start() {
this.loadTargetScene();
}
loadTargetScene() {
director.preloadScene(
this.targetScene,
(completedCount, totalCount) => {
const progress = completedCount / totalCount;
this.progressBar.progress = progress;
this.progressLabel.string = `${Math.floor(progress * 100)}%`;
},
(err) => {
if (err) {
console.error('加载失败:', err);
return;
}
// 加载完成后延迟 0.5 秒再切换(给用户看清进度)
this.scheduleOnce(() => {
director.loadScene(this.targetScene);
}, 0.5);
}
);
}
}

6. 今日总结
- ✅ 掌握场景切换、预加载和常驻节点
- ✅ 掌握 resources 和 AssetBundle 动态加载
- ✅ 掌握预制体的创建和实例化
- ✅ 掌握对象池(NodePool)的完整使用流程
- ✅ 实战:加载场景与进度显示
⚠️ 常见坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 场景切换后内存暴涨 | 动态加载的资源未释放 | 场景销毁时调用 resources.release() |
| 对象池节点位置错乱 | 取出后未重置位置 | 在 reuse() 中重置所有状态 |
| 预制体修改后不生效 | 场景中有旧的实例 | 在层级管理器中右键预制体实例 → "还原到预制体" |
| find() 跨场景找不到节点 | 常驻节点不在当前场景中 | 用单例管理器存引用,而非 find() |