GameManager 单例模式
将 GameManager 做成单例模式,在 Cocos Creator 这类游戏开发中,是管理全局唯一状态和提供公共服务的最实用、最普遍的架构选择。它的好处和最佳实践可以用下图清晰地概括:
下面我们来详细解读每个核心优势。
📝 核心优势详解
1. 唯一数据源,杜绝状态混乱这是单例模式解决的最根本问题。游戏中有很多数据必须是全局唯一且统一的,例如:
- 玩家分数、金币数
- 游戏状态:正在运行、已暂停、已结束
- 当前关卡、游戏配置
如果这些数据分散在不同的脚本或节点中,很容易出现不一致(例如 UI 显示的分数和实际存储的分数不同)。单例
GameManager确保了整个游戏都从它这里读取和更新这些核心数据。
2. 全局访问点,替代低效查找没有单例时,其他脚本可能需要这样获取全局数据:
// ❌ 低效且脆弱的方式 let managerNode = find(‘Canvas/GameManager’); let manager = managerNode.getComponent(‘GameManager’); let score = manager.score;
使用单例后,任何地方都可以直接、安全地访问:
// ✅ 优雅且高效的方式 let score = GameManager.instance.score; GameManager.instance.addCoin(10);
这完全避免了 find 这种耗时的字符串查找,也无需担心节点路径改变导致代码失效。
3. 持久化生命周期,跨越场景在 Cocos Creator 中,切换场景时默认会销毁所有节点。如果将 GameManager 挂载在场景内的一个普通节点上,它也会被销毁,导致所有游戏进度数据丢失。单例模式配合 DontDestroyOnLoad(或 Cocos Creator 中的 PersistRootNode)标志,可以轻松让 GameManager 节点在场景切换时不被销毁,持久地存在于整个游戏生命周期中。
4. 核心通信枢纽,解耦系统模块单例 GameManager 是作为事件中心或服务提供者的理想位置。
- 模块解耦:玩家脚本不需要知道 UI 脚本的存在,它只需要触发一个事件: UI 脚本监听这个事件并更新显示: 这样,玩家和UI完全独立,修改任何一方都不会影响另一方。
- 提供服务:它可以提供音频播放、对象池、配置表加载等全局服务,让其他模块直接调用,无需各自实现。
🛠️ 在 Cocos Creator 中的实现方式
通常有两种主流实现方式:
方式一:挂载在持久化节点上的组件(推荐)这是最符合 Cocos 设计模式、最直观的方式。
// GameManager.ts
import { _decorator, Component, Node } from 'cc';
@ccclass(‘GameManager’)
export class GameManager extends Component {
// 1. 静态私有实例引用
private static _instance: GameManager = null;
// 2. 公共的静态获取方法
public static get instance(): GameManager {
return GameManager._instance;
}
// 3. 实例变量(你的游戏数据)
public score: number = 0;
public isPaused: boolean = false;
onLoad() {
// 4. 实现单例逻辑
if (GameManager._instance && GameManager._instance !== this) {
// 如果已存在实例,则销毁新创建的(重复的)这个组件
this.destroy();
return;
}
GameManager._instance = this;
// 5. 【关键】让节点跨场景持久化
Node.setPersistRootNode(this.node, true);
}
// 6. 业务方法
public addScore(value: number) {
this.score += value;
// 可以在这里触发事件
this.node.emit(‘score-updated’, this.score);
}
}
使用方法:在场景中创建一个名为“GameManager”的空节点,挂载此脚本。运行后,该节点将持久存在。
方式二:纯 TypeScript 类(无节点依赖)如果你不需要依赖引擎的组件生命周期和事件系统,或者想用于逻辑层。
// GameManager.ts
export class GameManager {
private static _instance: GameManager;
public score: number = 0;
private constructor() {} // 私有构造函数,防止外部new
public static get instance(): GameManager {
if (!GameManager._instance) {
GameManager._instance = new GameManager();
}
return GameManager._instance;
}
}
// 在其他脚本中使用
import { GameManager } from ‘./GameManager’;
let score = GameManager.instance.score;
⚠️ 关键注意事项与最佳实践
单例是一把强大的“双刃剑”,必须谨慎使用。
- 避免成为“上帝对象”:这是最常见的反模式。不要因为方便就把所有游戏逻辑都塞进 GameManager。它应该只负责协调和管理全局状态,而不是执行具体操作。✅ 好的职责:管理游戏状态、保存玩家存档、分发全局事件、提供配置数据。❌ 坏的职责:直接控制玩家移动、计算敌人AI、播放某个角色的动画。这些应该交给 PlayerController、EnemyAI、AnimationController 等具体的组件。
- 注意场景切换:如果你使用方法一(节点组件),务必确保挂载 GameManager 的节点在第一个场景就被创建并持久化,且后续场景不会重复创建它。
- 处理好销毁:在游戏真正退出或重启时,你需要手动重置单例中的数据,或销毁持久化节点,否则数据会残留。
- 考虑使用依赖注入:对于大型项目,过度依赖单例仍会导致模块间隐式耦合。可以考虑引入简单的依赖注入容器来管理这些全局服务,使依赖关系更清晰、更易于测试。
💎 总结
将 GameManager 做成单例模式,本质是为你的游戏世界创建一个可靠、唯一、易于访问的“指挥中心”和“数据中心”。它极大地提升了代码的整洁性、数据的一致性和模块的独立性。
核心决策点:如果你的游戏有需要全局访问、贯穿始终的数据或服务,那么使用单例模式的 GameManager 是一个明智而实用的选择。只要牢记“职责单一”原则,防止其膨胀失控,它就能成为你项目最坚实的架构基础之一。
查看1道真题和解析