Day 07 · 游戏也要管理状态:场景切换·资源加载·对象池实战

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 创建预制体

  1. 在场景中搭建好节点结构(例如:Enemy 节点 + Sprite + 碰撞体 + EnemyController 脚本)
  2. 将节点从层级管理器 拖拽到资源管理器 中 → 自动生成 .prefab 文件
  3. 原节点变为预制体实例(蓝色标识)

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()

← Day 06 | 系列目录 | Day 08 →

相关推荐
aidenxian2 小时前
iOS App 真实包大小:你以为的大小为什么是错的
前端
donecoding2 小时前
遗嘱、水管与抢救室:TS 切入 Go 的流程控制、接口与并发
javascript·typescript·go
天才熊猫君2 小时前
📄 第三篇:Vue 3 命令式弹窗 Provide 污染与关闭动画修复
前端·javascript·vue.js
lxh01132 小时前
2024春招美团前端
前端
漫游的渔夫2 小时前
从 Demo 到生产:为什么你的 AI 功能一上线就成了不可控的“黑盒”?
前端·人工智能
天才熊猫君2 小时前
📄 第一篇:Vue 3 命令式弹窗使用指南
前端·javascript·vue.js
天才熊猫君2 小时前
📄 第二篇:Vue 3 命令式弹窗 provide/inject 机制解析
前端·javascript·vue.js
iReachers2 小时前
HTML打包EXE工具数据加密功能详解 - 加密保护HTML/JS/CSS资源
javascript·css·html·html加密·html转exe·html一键打包exe·exe打包