第7章 对象与资源管理

对象与资源管理是游戏开发中的核心问题。本章介绍三种关键的设计模式:对象池模式、工厂模式和服务定位器模式,帮助开发者高效管理游戏对象的生命周期和创建过程。

7.1 对象池模式

对象池模式通过预先创建并复用对象来避免频繁的内存分配和垃圾回收,显著提升游戏性能。

7.1.1 设计原理与思路

对象池模式的核心概念

对象池模式是一种创建型设计模式,其核心思想是预先创建一组对象并保存在"池"中,当需要使用时从池中获取,使用完毕后归还到池中而不是销毁。这种模式避免了频繁的内存分配和垃圾回收(GC),特别适用于需要大量创建和销毁对象的场景。

┌─────────────────────────────────────────────────────────────┐
│                      对象池模式架构                           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌──────────────┐     获取      ┌──────────────────┐       │
│   │   游戏逻辑    │ ────────────> │                 │       │
│   │  (需要对象)   │               │     对象池       │       │
│   └──────────────┘               │                 │       │
│          ^                       │  ┌───────────┐  │       │
│          │ 归还                   │  │ 可用对象  │  │       │
│          │                       │  │  [o] [o]  │  │       │
│   ┌──────────────┐               │  │  [o] [o]  │  │       │
│   │   使用中对象  │ <───────────  │  └───────────┘  │       │
│   │  (活跃状态)   │    创建/复用   │                 │       │
│   └──────────────┘               └──────────────────┘       │
│                                                              │
└─────────────────────────────────────────────────────────────┘

垃圾回收对游戏性能的影响

在.NET和Godot环境中,垃圾回收器(GC)会自动管理内存。然而,GC的触发往往会导致明显的性能问题:

  1. 卡顿(Stutter):当GC执行时,游戏线程可能被暂停,导致帧率骤降
  2. 不可预测性:GC触发时机难以控制,可能在关键时刻(如BOSS战)发生
  3. 内存碎片:频繁的分配和释放导致内存碎片化,降低缓存效率

通过对象池模式,我们可以将对象的创建集中在游戏加载或关卡切换时,运行时只进行对象的借用和归还,从而显著减少GC压力。

池化策略的选择(固定vs动态)

对象池有两种主要策略:

策略类型特点适用场景
固定容量池预先分配固定数量对象,超出时等待或报错对象数量可预测、内存受限环境
动态扩展池根据需求动态增加池容量对象数量波动大、需要灵活性

固定容量池的优势在于内存使用可控,适合移动设备或内存紧张的情况。动态扩展池更加灵活,但需要设置合理的上限以防止内存无限增长。

7.1.2 使用场景分析

适用场景:频繁创建销毁的对象

对象池模式特别适合以下游戏元素:

  1. 子弹和投射物:射击游戏中每秒钟可能有数十甚至数百发子弹发射
  2. 粒子效果:爆炸、火花、烟雾等特效往往需要大量小型对象
  3. 敌人生成:波次战斗中的敌人频繁出现和消失
  4. UI元素:滚动列表中的列表项、伤害数字弹出等
  5. 音频源:频繁的音效播放需要复用音频组件

不适用场景:长期存在的对象

以下情况不适合使用对象池:

  1. 玩家角色:通常整个游戏过程中只有一个,不需要池化
  2. 核心游戏管理器:长期存在的单例对象
  3. 关卡地形:静态场景元素,创建后很少销毁
  4. 大型资源:如整个地图、过场动画对象等

典型案例:弹幕游戏、粒子系统

弹幕游戏(Bullet Hell/Shmup)是对象池模式的经典应用场景。在东方Project系列游戏中,屏幕上同时存在数千颗子弹,每颗子弹都有独立的位置、速度、旋转和生命周期。使用对象池可以将子弹对象预先创建好,避免在游戏过程中频繁分配内存。

弹幕游戏对象池示意图

初始状态:              游戏中:               子弹回收:
┌──────────┐          ┌──────────┐          ┌──────────┐
│ 子弹池    │          │ 子弹池    │          │ 子弹池    │
│ [b][b]   │   ──>    │ [b]      │   ──>    │ [b][b]   │
│ [b][b]   │  发射    │ [b]      │  命中    │ [b][b]   │
│ [b][b]   │          │          │  销毁    │ [b][b]   │
└──────────┘          └──────────┘          └──────────┘
   6可用                  2可用                  6可用

7.1.3 IPoolable 接口

首先定义可池化对象的接口。为什么这样设计?将池化行为抽象为接口,可以让任何类实现池化能力,而不需要继承特定的基类,保持代码的灵活性。

using Godot;

namespace GameFramework.Core.Pooling
{
    /// <summary>
    /// 可池化对象接口
    /// 实现此接口的类可以被对象池管理
    /// </summary>
    public interface IPoolable
    {
        /// <summary>
        /// 当对象从池中取出时调用
        /// 用于初始化对象状态
        /// </summary>
        void OnSpawn();

        /// <summary>
        /// 当对象返回池中或销毁时调用
        /// 用于清理对象状态
        /// </summary>
        void OnDespawn();

        /// <summary>
        /// 对象是否可用
        /// </summary>
        bool IsAvailable { get; set; }
    }
}

7.1.4 实现细节讲解

IPoolable接口设计考量

IPoolable接口的设计基于以下考量:

  1. OnSpawn方法:对象从池中被"取出"时调用,负责重置对象到初始状态。例如,子弹需要重置位置、速度、生命值等。
  2. OnDespawn方法:对象被"归还"到池中时调用,负责清理状态,避免影响下次使用。例如,禁用物理碰撞、停止粒子发射等。
  3. IsAvailable属性:标记对象当前是否在池中可用,防止重复归还或获取。

预加载策略的实现

预加载(Prewarm)在游戏启动或关卡加载时预先创建对象,避免游戏过程中的分配开销。实现时需要考虑:

  1. 渐进式加载:大量对象时,分帧创建避免卡顿
  2. 异步加载:使用后台线程创建对象(注意线程安全)
  3. 优先级加载:优先加载关键对象,次要对象延迟加载

容量动态调整的算法

动态调整池容量需要平衡内存占用和分配频率:

扩展策略:
- 当池为空且需要新对象时,按配置增量扩展(如增加50%)
- 设置最大容量上限,防止无限增长
- 记录峰值使用量,作为下次预加载的参考

收缩策略:
- 定期检查池使用率
- 当空闲对象超过阈值(如80%)时,逐步释放
- 避免频繁扩缩容,设置缓冲区间

7.1.5 泛型对象池实现

设计思路与原理

泛型对象池是对象池模式的高级实现,通过C#泛型机制提供类型安全的对象复用能力。与简单对象池相比,泛型实现将池化逻辑与具体对象类型分离,使同一套池化管理代码可以服务于多种不同类型的对象。这种设计的核心思想是"关注点分离":池的生命周期管理(分配、回收、扩容)由池类负责,而对象的初始化与重置逻辑由对象自身通过IPoolable接口实现。

泛型约束where T : class, IPoolable, new()确保只有符合特定条件的类型才能使用对象池:必须是引用类型(class)以避免装箱拆箱、必须实现池化接口以支持重置、必须有无参构造函数以支持实例化。这种编译时约束比运行时检查更可靠,也更高效。

核心实现要点

  1. 双重容器结构:使用Queue<T>存储可用对象(O(1)获取)和List<T>追踪所有对象(便于统计和清理)。队列的FIFO特性确保对象使用均匀,避免某些对象长期闲置。

  2. 懒加载与预加载策略:构造函数支持prewarm参数,允许在初始化时批量创建对象,避免游戏运行时的突发分配。这对于子弹、特效等高频对象尤为重要。

  3. 容量边界控制:通过maxCapacity限制池大小,防止内存无限增长。allowExpansion标志区分"硬限制"(拒绝新请求)和"软限制"(允许临时超容)。

  4. 对象生命周期回调:通过IPoolable.OnSpawnOnDespawn方法,让对象在出场和回收时执行自定义逻辑,如重置状态、停用粒子系统等。

using System;
using System.Collections.Generic;
using Godot;

namespace GameFramework.Core.Pooling
{
    /// <summary>
    /// 泛型对象池
    /// 用于高效管理可复用对象的创建和回收
    /// </summary>
    /// <typeparam name="T">对象类型,必须实现 IPoolable 和 new()</typeparam>
    public class ObjectPool<T> where T : class, IPoolable, new()
    {
        // 可用对象队列
        private readonly Queue<T> _availableObjects;

        // 所有对象的集合(用于追踪)
        private readonly List<T> _allObjects;

        // 池配置
        private readonly int _initialCapacity;
        private readonly int _maxCapacity;
        private readonly bool _allowExpansion;

        // 统计信息
        public int TotalCount => _allObjects.Count;
        public int AvailableCount => _availableObjects.Count;
        public int InUseCount => _allObjects.Count - _availableObjects.Count;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="initialCapacity">初始容量</param>
        /// <param name="maxCapacity">最大容量,0表示无限制</param>
        /// <param name="prewarm">是否预创建对象</param>
        public ObjectPool(int initialCapacity = 10, int maxCapacity = 100, bool prewarm = false)
        {
            _initialCapacity = initialCapacity;
            _maxCapacity = maxCapacity;
            _allowExpansion = maxCapacity <= 0;

            _availableObjects = new Queue<T>(initialCapacity);
            _allObjects = new List<T>(initialCapacity);

            if (prewarm)
            {
                Prewarm(initialCapacity);
            }
        }

        /// <summary>
        /// 预创建指定数量的对象
        /// </summary>
        public void Prewarm(int count)
        {
            int targetCount = Math.Min(count, _maxCapacity > 0 ? _maxCapacity : int.MaxValue);

            for (int i = _allObjects.Count; i < targetCount; i++)
            {
                T obj = CreateNewObject();
                obj.IsAvailable = true;
                _availableObjects.Enqueue(obj);
            }
        }

        /// <summary>
        /// 从池中获取对象
        /// </summary>
        public T Get()
        {
            T obj;

            if (_availableObjects.Count > 0)
            {
                // 从队列中取出可用对象
                obj = _availableObjects.Dequeue();
            }
            else if (_allowExpansion || _allObjects.Count < _maxCapacity)
            {
                // 创建新对象
                obj = CreateNewObject();
            }
            else
            {
                // 池已满,返回null或抛出异常
                GD.PushWarning($"对象池已达到最大容量: {_maxCapacity}");
                return null;
            }

            obj.IsAvailable = false;
            obj.OnSpawn();

            return obj;
        }

        /// <summary>
        /// 将对象返回池中
        /// </summary>
        public void Return(T obj)
        {
            if (obj == null)
            {
                GD.PushWarning("尝试将null对象返回池中");
                return;
            }

            if (obj.IsAvailable)
            {
                // 对象已经在池中
                return;
            }

            obj.OnDespawn();
            obj.IsAvailable = true;
            _availableObjects.Enqueue(obj);
        }

        /// <summary>
        /// 创建新对象
        /// </summary>
        private T CreateNewObject()
        {
            T obj = new T();
            _allObjects.Add(obj);
            return obj;
        }

        /// <summary>
        /// 清空对象池
        /// </summary>
        /// <param name="disposeObjects">是否调用对象的清理方法</param>
        public void Clear(bool disposeObjects = true)
        {
            if (disposeObjects)
            {
                foreach (var obj in _allObjects)
                {
                    if (!obj.IsAvailable)
                    {
                        obj.OnDespawn();
                    }
                }
            }

            _availableObjects.Clear();
            _allObjects.Clear();
        }

        /// <summary>
        /// 调整池容量
        /// </summary>
        public void Trim(int targetCapacity)
        {
            while (_availableObjects.Count > targetCapacity)
            {
                T obj = _availableObjects.Dequeue();
                _allObjects.Remove(obj);
            }
        }
    }
}

使用说明与最佳实践

  • 适用场景:高频创建销毁的对象,如射击游戏中的子弹、粒子特效、频繁刷新的敌人、UI列表项等。当对象创建成本较高(涉及资源加载、复杂初始化)或GC压力成为性能瓶颈时,对象池能显著提升性能。

  • 注意事项

    1. 池化对象必须正确实现OnDespawn方法,完全重置对象状态,避免"状态泄漏"
    2. 不要在OnSpawn中执行重量级初始化,应保持轻量
    3. 注意线程安全,Godot节点操作必须在主线程执行
    4. 定期调用Trim方法释放多余对象,防止内存持续增长
  • 性能考虑:对象池减少了GC压力,但增加了常驻内存占用。建议为每种对象类型设置合理的初始容量(通常为峰值使用量的70-80%),并通过Trim方法在关卡切换或内存紧张时收缩。

  • 扩展建议

    1. 可以添加优先级队列,区分"热对象"和"冷对象"
    2. 实现异步预加载,在后台线程创建对象(注意Godot节点的线程限制)
    3. 添加调试接口,可视化展示池状态和使用统计
    4. 考虑实现分层池,区分"常驻池"和"临时池"

7.1.6 性能与权衡

内存占用vs分配开销的权衡

对象池的核心权衡是内存占用与分配开销之间的平衡:

无对象池场景:              使用对象池场景:
分配 ──┐                   分配 ──┐
     │                          │
┌────┴────┐               ┌────┴────┐
│ 帧1     │               │ 加载时   │
│ 分配A   │               │ 分配池   │
└─────────┘               └─────────┘
     │                          │
┌────┴────┐               ┌────┴────┐
│ 帧2     │               │ 运行中   │
│ 销毁A   │               │ 借用/归还 │
│ 分配B   │               │ 无GC    │
└─────────┘               └─────────┘
     │                          │
    GC                       低GC
   (卡顿)                   (流畅)

池大小的选择策略

选择合适的池大小需要考虑以下因素:

  1. 峰值使用量:分析游戏运行时的最大同时活跃对象数
  2. 内存预算:根据目标平台设置合理的内存上限
  3. 增长策略:设置初始容量和最大容量,允许一定程度的动态扩展
  4. 监控调优:使用统计信息追踪池的使用率,持续优化配置

与对象缓存的对比

对象池与对象缓存经常被混淆,它们的区别如下:

特性对象池对象缓存
目的复用对象实例,避免GC快速访问计算结果或资源
生命周期对象长期存在,状态可重置对象可能有过期时间
使用方式获取/归还模式读取/写入模式
典型应用子弹、粒子、敌人纹理、数据表、计算结果

7.1.7 Godot 节点对象池

针对 Godot 节点的特殊实现:

using System.Collections.Generic;
using Godot;

namespace GameFramework.Core.Pooling
{
    /// <summary>
    /// Godot 节点对象池
    /// 专门用于管理 Godot 节点的创建和回收
    /// </summary>
    public class NodeObjectPool<T> where T : Node, IPoolable
    {
        private readonly PackedScene _prefab;
        private readonly Node _parent;
        private readonly Queue<T> _availableObjects;
        private readonly List<T> _allObjects;

        private readonly int _maxCapacity;
        private readonly bool _allowExpansion;

        public int TotalCount => _allObjects.Count;
        public int AvailableCount => _availableObjects.Count;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="prefab">节点预制体</param>
        /// <param name="parent">父节点(用于存放未激活的池对象)</param>
        /// <param name="maxCapacity">最大容量</param>
        public NodeObjectPool(PackedScene prefab, Node parent, int maxCapacity = 100)
        {
            _prefab = prefab;
            _parent = parent;
            _maxCapacity = maxCapacity;
            _allowExpansion = maxCapacity <= 0;

            _availableObjects = new Queue<T>();
            _allObjects = new List<T>();
        }

        /// <summary>
        /// 从池中获取节点
        /// </summary>
        public T Get(Node newParent = null)
        {
            T node;

            if (_availableObjects.Count > 0)
            {
                node = _availableObjects.Dequeue();
            }
            else if (_allowExpansion || _allObjects.Count < _maxCapacity)
            {
                node = _prefab.Instantiate<T>();
                _allObjects.Add(node);
            }
            else
            {
                GD.PushWarning($"节点对象池已满: {_maxCapacity}");
                return null;
            }

            // 设置父节点
            if (newParent != null)
            {
                newParent.AddChild(node);
            }
            else
            {
                _parent.AddChild(node);
            }

            node.IsAvailable = false;
            node.OnSpawn();

            return node;
        }

        /// <summary>
        /// 将节点返回池中
        /// </summary>
        public void Return(T node)
        {
            if (node == null || node.IsAvailable)
            {
                return;
            }

            node.OnDespawn();
            node.IsAvailable = true;

            // 从当前父节点移除
            Node currentParent = node.GetParent();
            if (currentParent != null)
            {
                currentParent.RemoveChild(node);
            }

            // 放回池父节点下(隐藏)
            _parent.AddChild(node);

            _availableObjects.Enqueue(node);
        }

        /// <summary>
        /// 清空对象池
        /// </summary>
        public void Clear()
        {
            foreach (var node in _allObjects)
            {
                if (node.IsInsideTree())
                {
                    node.GetParent()?.RemoveChild(node);
                }
                node.QueueFree();
            }

            _availableObjects.Clear();
            _allObjects.Clear();
        }
    }
}

7.1.8 实战指南

实施步骤

  1. 识别池化候选:分析游戏中频繁创建销毁的对象类型
  2. 实现IPoolable接口:为候选对象实现必要的生命周期方法
  3. 配置对象池:根据预期使用量设置初始容量和最大容量
  4. 预加载对象:在合适的时机(如关卡加载)预创建对象
  5. 替换实例化代码:将Instantiate调用替换为对象池获取
  6. 确保正确归还:在对象生命周期结束时调用Return方法

常见错误:忘记重置对象状态

最常见的对象池使用错误是忘记在OnSpawn中重置对象状态:

// 错误示例
public void OnSpawn()
{
    // 忘记重置速度和生命值!
    Show();
}

// 正确示例
public void OnSpawn()
{
    // 重置所有状态
    _velocity = Vector2.Zero;
    _health = MaxHealth;
    _lifetime = MaxLifetime;
    Show();
    SetProcess(true);
}

其他常见错误:

  1. 双重归还:将同一个对象归还两次,需要检查IsAvailable状态
  2. 忘记隐藏:节点对象归还后仍可见,需要在OnDespawn中调用Hide()
  3. 事件未清理:对象订阅的事件未取消,导致内存泄漏
  4. 协程未停止:GDScript/C#协程在对象归还后继续运行

监控与调优

建立对象池监控机制:

// 添加监控字段
public float UsageRate => TotalCount > 0 ? (float)InUseCount / TotalCount : 0;
public bool IsStarved => AvailableCount == 0;

// 定期输出统计
public void LogStats()
{
    GD.Print($"[ObjectPool] 总数: {TotalCount}, 可用: {AvailableCount}, " +
             $"使用中: {InUseCount}, 使用率: {UsageRate:P1}");
}

根据监控数据调整池大小:

  • 使用率长期超过80%:增加池容量
  • 使用率长期低于20%:减少池容量以节省内存
  • 频繁出现starved状态:增加初始容量或检查归还逻辑

7.1.9 使用示例

设计思路与原理

本示例展示如何将对象池应用于游戏中的子弹系统,这是对象池最典型也最有效的应用场景之一。在射击游戏中,子弹的创建和销毁频率极高:一个玩家每秒可能发射10-20发子弹,加上多个敌人同时开火,每秒可能有数百次对象创建和销毁操作。如果不使用对象池,这些操作将导致频繁的垃圾回收,引发明显的帧率波动。

示例中的Bullet类实现了IPoolable接口,这是使用泛型对象池的前提。关键在于OnSpawnOnDespawn方法的正确实现:OnSpawn在子弹从池中取出时调用,负责重置所有状态(位置、速度、生命周期);OnDespawn在子弹归还时调用,负责停止物理模拟、隐藏节点、清理引用。这种"借出-归还"模式确保同一对象实例可以被反复使用,而不会产生状态冲突。

核心实现要点

  1. 状态完全重置:在OnSpawn中重置所有可变状态(生命值计时器、速度向量),确保每次取出的子弹都是"全新"的

  2. 视觉与逻辑分离:使用Show()/Hide()控制可见性,使用SetProcess()控制_Process回调,避免不可见对象继续消耗CPU

  3. 自管理生命周期:子弹内部管理自己的生命周期,当LifeTime耗尽时自动调用BulletPool.Instance.Return(this)归还,形成闭环

  4. 单例池管理器BulletPool作为单例提供全局访问,确保整个游戏只有一个子弹池,便于统一管理和监控

using Godot;
using GameFramework.Core.Pooling;

namespace GameFramework.Examples
{
    /// <summary>
    /// 子弹类 - 可池化对象示例
    /// </summary>
    public partial class Bullet : Node2D, IPoolable
    {
        [Export] public float Speed = 500f;
        [Export] public float Damage = 10f;
        [Export] public float LifeTime = 3f;

        private Vector2 _velocity;
        private float _lifeTimer;

        public bool IsAvailable { get; set; } = true;

        public void OnSpawn()
        {
            // 重置状态
            _lifeTimer = LifeTime;
            Show();
            SetProcess(true);
        }

        public void OnDespawn()
        {
            // 清理状态
            Hide();
            SetProcess(false);
            _velocity = Vector2.Zero;
        }

        public void Fire(Vector2 position, Vector2 direction)
        {
            GlobalPosition = position;
            _velocity = direction.Normalized() * Speed;
            Rotation = direction.Angle();
        }

        public override void _Process(double delta)
        {
            _lifeTimer -= (float)delta;
            if (_lifeTimer <= 0)
            {
                // 生命周期结束,返回池中
                BulletPool.Instance.Return(this);
                return;
            }

            Position += _velocity * (float)delta;
        }
    }

    /// <summary>
    /// 子弹池管理器(单例)
    /// </summary>
    public class BulletPool
    {
        private static BulletPool _instance;
        public static BulletPool Instance => _instance ??= new BulletPool();

        private NodeObjectPool<Bullet> _pool;

        public void Initialize(PackedScene bulletPrefab, Node parent, int initialCapacity = 50)
        {
            _pool = new NodeObjectPool<Bullet>(bulletPrefab, parent, 200);
            // 预创建一些子弹
            for (int i = 0; i < initialCapacity; i++)
            {
                var bullet = _pool.Get();
                _pool.Return(bullet);
            }
        }

        public Bullet Get() => _pool.Get();
        public void Return(Bullet bullet) => _pool.Return(bullet);
    }
}

使用说明与最佳实践

  • 适用场景

    • 射击游戏中的子弹、导弹、激光等投射物
    • 粒子特效系统(爆炸、火花、烟雾)
    • 频繁刷新的敌人(塔防游戏、无尽模式)
    • UI列表项(背包、任务列表、排行榜)
    • 音频源对象
  • 注意事项

    1. 状态泄漏风险:忘记重置的变量会导致诡异bug(如子弹继承上次的速度方向),务必在OnSpawn中重置所有状态
    2. 归还时机:确保对象不再被其他地方引用后再归还,否则会出现"幽灵对象"问题
    3. 物理节点特殊处理:对于RigidBody等物理节点,需要在OnDespawn中重置速度、角速度,并退出物理模拟
    4. 信号断开:如果对象连接了信号,归还前必须断开,否则下次取出时会重复连接
  • 性能考虑

    • 预加载(Prewarm)可以显著减少游戏开始后的卡顿,建议在加载屏幕中初始化对象池
    • 监控InUseCountAvailableCount比例,调整池大小以达到最佳内存/性能平衡
    • 对于极低频使用的对象,可能不需要对象池,直接使用创建/销毁更合适
  • 扩展建议

    1. 可以实现IPoolable<T>Reset方法,让池在借出前自动调用
    2. 添加对象池调试UI,实时显示各池的使用率、命中率等统计
    3. 实现优先级池,区分不同类型的子弹(如玩家子弹优先级高于敌人子弹)
    4. 考虑实现对象池的分层,如"常驻池"和"关卡池",后者在关卡切换时清空

7.2 工厂模式与对象创建

工厂模式将对象的创建逻辑封装起来,使代码更加灵活和可测试。

7.2.1 IFactory 接口

设计思路与原理

工厂模式是面向对象设计中最重要的创建型模式之一,其核心思想是"将对象的创建与使用分离"。通过定义统一的工厂接口IFactory<T>,我们可以将具体的对象创建逻辑从业务代码中抽离,实现依赖抽象而非依赖具体实现的设计原则。

在游戏开发中,工厂模式的价值尤为突出:

  1. 解耦创建与使用:业务代码只需要知道"要创建一个T类型的对象",而不需要知道具体如何创建、使用哪个构造函数、需要哪些依赖
  2. 支持运行时切换:通过更换工厂实现,可以在运行时切换对象创建策略(如Debug模式创建带调试信息的对象,Release模式创建优化版)
  3. 便于测试:单元测试时可以使用Mock工厂注入测试替身,隔离被测代码的真实依赖
  4. 集中管理创建逻辑:对象创建前的验证、默认值设置、初始化等逻辑集中在工厂中,避免代码重复

接口设计采用泛型IFactory<T>,使得同一接口可以服务于不同类型的对象创建,同时保持类型安全。提供无参Create()和带参Create(params object[] args)两种重载,满足大多数创建场景。

核心实现要点

  1. 泛型约束where T : class确保工厂创建的是引用类型,避免值类型的装箱问题,同时也支持null返回值

  2. 双创建方法:无参版本适合简单对象创建,带参版本支持复杂初始化。使用params object[]而非泛型参数,是为了让工厂实现类可以自由选择参数组合

  3. 返回值可空:创建可能失败(如资源加载失败、验证不通过),返回null比抛出异常更符合C#游戏开发的习惯

  4. 接口而非抽象类:选择接口而非抽象类,让实现类可以自有选择基类(如继承Godot的RefCounted或自定义基类)

using Godot;

namespace GameFramework.Core.Factory
{
    /// <summary>
    /// 工厂接口
    /// </summary>
    /// <typeparam name="T">创建的对象类型</typeparam>
    public interface IFactory<T> where T : class
    {
        /// <summary>
        /// 创建对象
        /// </summary>
        /// <returns>创建的对象实例</returns>
        T Create();

        /// <summary>
        /// 使用参数创建对象
        /// </summary>
        T Create(params object[] args);
    }
}

使用说明与最佳实践

  • 适用场景

    • 复杂对象的创建,需要多步骤初始化或依赖注入
    • 需要根据配置或运行时条件创建不同类型的对象
    • 对象创建逻辑需要在多个地方复用
    • 需要支持A/B测试或功能开关的对象创建
    • 需要统一创建日志记录或统计
  • 注意事项

    1. 不要过度使用:简单对象直接使用new创建更清晰,只有当创建逻辑复杂时才使用工厂
    2. 参数类型安全params object[] args失去了编译时类型检查,调用时需要确保参数顺序和类型正确
    3. 避免工厂嵌套:不要在工厂A中调用工厂B的Create,这会导致创建逻辑难以追踪
    4. 异步创建:如果创建涉及资源加载等异步操作,考虑使用Task<T> CreateAsync()模式
  • 与对象池的协作: 工厂模式和对象池可以很好结合:工厂负责"首次创建"对象(包括复杂初始化),对象池负责"后续复用"。可以创建PooledFactory<T>装饰器,在工厂前增加池化层。

  • 扩展建议

    1. 实现IFactory<T, TConfig>支持配置对象传入
    2. 添加创建事件OnCreated,便于监听对象创建
    3. 实现工厂装饰器模式,如LoggingFactory<T>CachingFactory<T>
    4. 考虑使用依赖注入容器自动注册和解析工厂

7.2.2 泛型工厂实现

设计思路与原理

泛型工厂基类Factory<T>是对IFactory<T>接口的具体实现框架,它采用了**模板方法模式(Template Method Pattern)**的设计思想。模板方法模式定义了一个算法的骨架,将某些步骤延迟到子类实现,这样既保证了算法结构的统一,又允许子类灵活定制特定步骤。

在工厂创建流程中,存在一些"通用步骤"(如验证、后置处理)和"特定步骤"(实际实例化)。基类将通用步骤封装为Create()方法的主流程,而将特定步骤抽象为CreateInstance()CreateInstance(params object[])两个抽象方法,由具体工厂子类实现。这种设计的优势:

  1. 代码复用:验证逻辑、错误处理、后置处理等通用代码在基类中实现一次,所有子类共享
  2. 流程管控:子类只能控制"如何创建",无法控制"是否创建",确保验证等安全检查不被绕过
  3. 扩展点明确ValidateCreation()PostCreate()作为虚方法,为子类提供可选的扩展点
  4. 类型安全:泛型约束确保工厂和创建的对象类型匹配,避免运行时类型错误

核心实现要点

  1. 模板方法结构Create()方法是模板方法,定义了ValidateCreation → CreateInstance → PostCreate的固定流程

  2. 抽象方法约束CreateInstanceCreateInstance(params object[])必须实现,强制子类定义具体创建逻辑

  3. 虚方法扩展ValidateCreationPostCreate提供默认空实现,子类可选择性重写

  4. 参数转发:带参Create方法将params object[]原样转发给CreateInstance,保持参数透明

  5. 错误处理:验证失败时输出错误日志并返回null,避免异常中断程序流程

using System;
using Godot;

namespace GameFramework.Core.Factory
{
    /// <summary>
    /// 泛型工厂基类
    /// </summary>
    public abstract class Factory<T> : IFactory<T> where T : class
    {
        /// <summary>
        /// 创建对象前的验证
        /// </summary>
        protected virtual bool ValidateCreation()
        {
            return true;
        }

        /// <summary>
        /// 实际的创建逻辑(子类实现)
        /// </summary>
        protected abstract T CreateInstance();

        /// <summary>
        /// 带参数的实际创建逻辑(子类实现)
        /// </summary>
        protected abstract T CreateInstance(params object[] args);

        /// <summary>
        /// 创建后的处理
        /// </summary>
        protected virtual void PostCreate(T instance)
        {
            // 子类可重写进行初始化
        }

        public T Create()
        {
            if (!ValidateCreation())
            {
                GD.PushError("工厂创建验证失败");
                return null;
            }

            T instance = CreateInstance();
            PostCreate(instance);
            return instance;
        }

        public T Create(params object[] args)
        {
            if (!ValidateCreation())
            {
                GD.PushError("工厂创建验证失败");
                return null;
            }

            T instance = CreateInstance(args);
            PostCreate(instance);
            return instance;
        }
    }

    /// <summary>
    /// 简单工厂 - 使用委托创建对象
    /// </summary>
    public class SimpleFactory<T> : Factory<T> where T : class
    {
        private readonly Func<T> _creationFunc;
        private readonly Func<object[], T> _creationFuncWithArgs;

        public SimpleFactory(Func<T> creationFunc)
        {
            _creationFunc = creationFunc;
        }

        public SimpleFactory(Func<object[], T> creationFuncWithArgs)
        {
            _creationFuncWithArgs = creationFuncWithArgs;
        }

        protected override T CreateInstance()
        {
            return _creationFunc?.Invoke() ?? throw new InvalidOperationException("创建函数未设置");
        }

        protected override T CreateInstance(params object[] args)
        {
            return _creationFuncWithArgs?.Invoke(args) ?? throw new InvalidOperationException("带参数创建函数未设置");
        }
    }
}

使用说明与最佳实践

  • 适用场景

    • 需要统一创建流程的复杂对象(如游戏实体、UI面板、武器道具)
    • 创建前需要验证条件(如资源是否加载、配置是否有效)
    • 创建后需要统一初始化(如设置默认属性、注册到管理系统)
    • 需要支持多种创建方式(无参、带参)的对象
  • 注意事项

    1. 抽象方法必须实现:忘记实现CreateInstance会导致编译错误,这是设计有意为之
    2. 验证方法可重写:默认ValidateCreation返回true,如需验证请重写
    3. 异常处理:建议不要在CreateInstance中抛出异常,而是返回null并在工厂中统一处理
    4. SimpleFactory的使用:对于简单场景,可以直接使用SimpleFactory传入委托,无需创建新类
  • 性能考虑

    • 虚方法调用有微小开销,但在游戏对象创建场景中可忽略
    • SimpleFactory使用委托调用,比直接方法调用稍慢,但提供了更大灵活性
    • 避免在ValidateCreation中执行重量级操作,应该在游戏初始化阶段完成
  • 扩展建议

    1. 可以实现异步工厂AsyncFactory<T>,支持Task<T> CreateAsync()
    2. 添加PreCreate虚方法,在验证前执行
    3. 实现工厂链ChainedFactory<T>,支持按优先级尝试多个工厂
    4. 添加对象池集成,自动将创建的对象加入池

7.2.3 PrefabFactory 实现

设计思路与原理

PrefabFactory是工厂模式在Godot引擎中的特化实现,专门用于从PackedScene(预制体)创建节点实例。Godot的场景系统是其核心特性,游戏中绝大多数对象(角色、敌人、UI元素、特效等)都是通过实例化预制体创建的。PrefabFactory将Godot的预制体加载和实例化过程封装起来,提供类型安全、可配置、可扩展的创建机制。

设计这个工厂类的核心动机是解决Godot预制体使用中的常见问题:

  1. 路径硬编码:直接在代码中写GD.Load<PackedScene>("res://...")导致路径分散,难以维护
  2. 类型不安全Instantiate()返回Node,需要手动转换,容易类型错误
  3. 重复代码:每次创建预制体都要写加载、验证、实例化、添加到场景等重复代码
  4. 难以测试:直接调用Godot API难以在单元测试中Mock

PrefabFactory<T>通过泛型参数T : Node确保类型安全,在编译期就保证创建的节点类型正确。同时支持路径加载和预加载两种模式,既保留了灵活性,又支持性能优化。

核心实现要点

  1. 双构造器重载:支持从路径加载(开发便利)和从已加载PackedScene创建(性能优化)

  2. 默认父节点:可选的defaultParent参数简化实例化流程,自动将新节点添加到指定父节点

  3. 验证覆盖:重写ValidateCreation检查预制体是否成功加载,避免空引用

  4. 实例化定制:重写CreateInstance调用Instantiate<T>(),自动完成类型转换

  5. 后置处理:在PostCreate中可以统一设置新节点的初始属性

using Godot;

namespace GameFramework.Core.Factory
{
    /// <summary>
    /// Godot 预制体工厂
    /// 用于从 PackedScene 创建节点实例
    /// </summary>
    public class PrefabFactory<T> : Factory<T> where T : Node
    {
        private readonly PackedScene _prefab;
        private readonly Node _defaultParent;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="prefabPath">预制体资源路径</param>
        /// <param name="defaultParent">默认父节点</param>
        public PrefabFactory(string prefabPath, Node defaultParent = null)
        {
            _prefab = GD.Load<PackedScene>(prefabPath);
            _defaultParent = defaultParent;

            if (_prefab == null)
            {
                GD.PushError($"无法加载预制体: {prefabPath}");
            }
        }

        /// <summary>
        /// 使用已加载的预制体
        /// </summary>
        public PrefabFactory(PackedScene prefab, Node defaultParent = null)
        {
            _prefab = prefab;
            _defaultParent = defaultParent;
        }

        protected override bool ValidateCreation()
        {
            return _prefab != null;
        }

        protected override T CreateInstance()
        {
            T instance = _prefab.Instantiate<T>();

            if (_defaultParent != null && instance != null)
            {
                _defaultParent.AddChild(instance);
            }

            return instance;
        }

        protected override T CreateInstance(params object[] args)
        {
            T instance = _prefab.Instantiate<T>();

            if (args.Length > 0 && args[0] is Node parent)
            {
                parent.AddChild(instance);
            }
            else if (_defaultParent != null)
            {
                _defaultParent.AddChild(instance);
            }

            // 应用额外参数
            if (args.Length > 1 && instance is IFactoryInitializable initializable)
            {
                initializable.Initialize(args[1..]);
            }

            return instance;
        }
    }

    /// <summary>
    /// 可工厂初始化接口
    /// </summary>
    public interface IFactoryInitializable
    {
        void Initialize(object[] args);
    }
}

使用说明与最佳实践

  • 适用场景:所有需要从预制体创建Godot节点的场景,如敌人刷新、子弹发射、UI面板弹出、道具掉落等。

  • 注意事项

    1. 预制体路径建议使用常量定义,避免硬编码字符串
    2. 预加载的PackedScene可以重复使用,避免每次创建都重新加载资源
    3. 确保预制体的根节点类型与泛型参数T匹配
    4. 如果需要在特定父节点下创建,使用带defaultParent参数的构造函数
  • 性能考虑:预加载模式适合高频创建的对象(如子弹),路径加载模式适合低频对象。

  • 扩展建议:可以实现PrefabFactory<T, TConfig>支持传入配置参数初始化节点。


7.2.4 工厂注册表

设计思路与原理

工厂注册表是服务定位器模式在工厂系统中的具体应用,提供一个中心化的工厂管理器。通过注册表,业务代码无需直接引用具体工厂实例,而是通过类型或键来获取工厂,实现更高层次的解耦。这种设计特别适合大型项目,避免工厂实例在多个类中重复创建或传递。

核心实现要点

  1. 单例模式确保全局唯一注册表
  2. 泛型注册和获取保持类型安全
  3. 支持按类型和按字符串键两种注册方式
  4. 延迟初始化选项提高启动性能
using System;
using System.Collections.Generic;
using Godot;

namespace GameFramework.Core.Factory
{
    /// <summary>
    /// 工厂注册表
    /// 集中管理所有工厂实例
    /// </summary>
    public class FactoryRegistry
    {
        private static FactoryRegistry _instance;
        public static FactoryRegistry Instance => _instance ??= new FactoryRegistry();

        private readonly Dictionary<Type, object> _factories = new();

        /// <summary>
        /// 注册工厂
        /// </summary>
        public void Register<T>(IFactory<T> factory) where T : class
        {
            _factories[typeof(T)] = factory;
        }

        /// <summary>
        /// 获取工厂
        /// </summary>
        public IFactory<T> GetFactory<T>() where T : class
        {
            if (_factories.TryGetValue(typeof(T), out var factory))
            {
                return factory as IFactory<T>;
            }

            GD.PushError($"未找到类型 {typeof(T).Name} 的工厂");
            return null;
        }

        /// <summary>
        /// 创建对象(便捷方法)
        /// </summary>
        public T Create<T>() where T : class
        {
            var factory = GetFactory<T>();
            return factory?.Create();
        }

        /// <summary>
        /// 创建对象(带参数)
        /// </summary>
        public T Create<T>(params object[] args) where T : class
        {
            var factory = GetFactory<T>();
            return factory?.Create(args);
        }

        /// <summary>
        /// 注销工厂
        /// </summary>
        public void Unregister<T>() where T : class
        {
            _factories.Remove(typeof(T));
        }

        /// <summary>
        /// 清空所有工厂
        /// </summary>
        public void Clear()
        {
            _factories.Clear();
        }
    }
}

使用说明与最佳实践

  • 适用场景:大型项目中工厂实例需要集中管理、跨模块共享工厂、需要运行时动态切换工厂实现。

  • 注意事项

    1. 注册表是全局单例,注意不要在多个地方重复注册同一类型
    2. 使用Lazy选项延迟初始化工厂,避免游戏启动时大量对象创建
    3. 模块卸载时调用UnregisterClear清理,防止内存泄漏
    4. 工厂接口变更时需要更新所有注册点
  • 与DI容器的对比:注册表比依赖注入容器轻量,适合中小项目;大型项目建议迁移到完整DI容器。


7.3 服务定位器

服务定位器模式提供了一种全局访问服务的机制,同时保持松耦合。

7.3.1 服务定位器实现

设计思路与原理

服务定位器模式是一种集中式服务管理机制,提供一个全局访问点来获取各种服务实例。与依赖注入(DI)的"推"模式不同,服务定位器采用"拉"模式:服务消费者主动向定位器请求所需服务。这种模式在需要快速接入、不便重构为DI的遗留代码中特别有用,也适合作为DI容器的轻量级替代方案。

核心设计权衡:服务定位器实现了服务消费者与服务实现的解耦,但引入了全局依赖(定位器本身)。相比DI的构造函数注入,服务定位器隐藏了依赖关系,使代码更简洁但测试性稍差。在游戏开发中,这种权衡通常是可以接受的,特别是在原型阶段或中小型项目。

核心实现要点

  1. 单例模式确保全局唯一访问点
  2. 泛型注册和获取保持类型安全
  3. 支持实例注册和工厂注册两种模式
  4. 延迟初始化支持提高启动性能
  5. 服务生命周期管理(单例/瞬态)
using System;
using System.Collections.Generic;
using Godot;

namespace GameFramework.Core.ServiceLocator
{
    /// <summary>
    /// 服务定位器
    /// 管理应用程序中的全局服务
    /// </summary>
    public static class ServiceLocator
    {
        // 已注册的服务实例
        private static readonly Dictionary<Type, object> _services = new();

        // 延迟加载的服务工厂
        private static readonly Dictionary<Type, Func<object>> _serviceFactories = new();

        // 服务初始化状态
        private static readonly Dictionary<Type, bool> _initialized = new();

        /// <summary>
        /// 注册服务实例
        /// </summary>
        public static void Register<T>(T service) where T : class
        {
            Type type = typeof(T);

            if (_services.ContainsKey(type))
            {
                GD.PushWarning($"服务 {type.Name} 已注册,将被覆盖");
            }

            _services[type] = service;
            _initialized[type] = true;
        }

        /// <summary>
        /// 注册延迟加载服务
        /// </summary>
        public static void RegisterLazy<T>(Func<T> factory) where T : class
        {
            Type type = typeof(T);
            _serviceFactories[type] = factory;
            _initialized[type] = false;
        }

        /// <summary>
        /// 获取服务
        /// </summary>
        public static T Get<T>() where T : class
        {
            Type type = typeof(T);

            // 直接返回已实例化的服务
            if (_services.TryGetValue(type, out var service))
            {
                return service as T;
            }

            // 延迟加载
            if (_serviceFactories.TryGetValue(type, out var factory))
            {
                T instance = factory() as T;
                _services[type] = instance;
                _initialized[type] = true;
                return instance;
            }

            GD.PushError($"服务 {type.Name} 未注册");
            return null;
        }

        /// <summary>
        /// 尝试获取服务
        /// </summary>
        public static bool TryGet<T>(out T service) where T : class
        {
            service = Get<T>();
            return service != null;
        }

        /// <summary>
        /// 检查服务是否已注册
        /// </summary>
        public static bool IsRegistered<T>() where T : class
        {
            Type type = typeof(T);
            return _services.ContainsKey(type) || _serviceFactories.ContainsKey(type);
        }

        /// <summary>
        /// 检查服务是否已初始化
        /// </summary>
        public static bool IsInitialized<T>() where T : class
        {
            return _initialized.TryGetValue(typeof(T), out bool init) && init;
        }

        /// <summary>
        /// 注销服务
        /// </summary>
        public static void Unregister<T>() where T : class
        {
            Type type = typeof(T);

            if (_services.ContainsKey(type))
            {
                // 如果服务实现了 IDisposable,调用 Dispose
                if (_services[type] is IDisposable disposable)
                {
                    disposable.Dispose();
                }

                _services.Remove(type);
            }

            _serviceFactories.Remove(type);
            _initialized.Remove(type);
        }

        /// <summary>
        /// 清空所有服务
        /// </summary>
        public static void Clear()
        {
            // 清理所有可释放的服务
            foreach (var service in _services.Values)
            {
                if (service is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }

            _services.Clear();
            _serviceFactories.Clear();
            _initialized.Clear();
        }

        /// <summary>
        /// 获取所有已注册的服务类型
        /// </summary>
        public static IEnumerable<Type> GetRegisteredTypes()
        {
            var types = new HashSet<Type>(_services.Keys);
            types.UnionWith(_serviceFactories.Keys);
            return types;
        }
    }
}

使用说明与最佳实践

  • 适用场景:全局系统(如音频管理器、输入管理器、存档系统)、跨模块共享服务、需要延迟初始化的重型服务。

  • 注意事项

    1. 避免在服务构造函数中调用Get获取其他服务,可能导致循环依赖
    2. 使用IsRegistered检查服务是否存在,避免InvalidOperationException
    3. 线程安全:Godot节点服务必须在主线程注册和获取
    4. 清理时机:场景切换时调用Clear释放服务,防止内存泄漏
  • 与DI对比:服务定位器适合快速接入,DI适合大型项目长期维护。可以混合使用:核心系统用DI,临时工具类用定位器。

  • 扩展建议:可以实现IService接口的Priority属性,支持按优先级初始化;添加服务初始化依赖图,自动处理初始化顺序。


7.3.2 服务接口定义

设计思路与原理

IService接口是服务定位器模式中的契约层,为所有服务提供统一的类型标识。虽然接口本身只包含初始化顺序属性,但它的存在使服务类型在编译期就能被识别,支持泛型约束和反射操作。

采用接口而非抽象类的设计,让服务可以灵活选择继承基类(如Godot的NodeRefCounted)。这种"标记接口"模式在C#和Java中广泛应用,如INotifyPropertyChangedIDisposable等。

InitializeOrder属性解决服务初始化顺序问题:某些服务(如配置管理器)需要在其他服务之前初始化。通过为每个服务指定顺序值,定位器可以按正确顺序初始化,避免"服务A依赖服务B但B还未初始化"的错误。

namespace GameFramework.Core.ServiceLocator
{
    /// <summary>
    /// 服务接口标记
    /// 所有服务应实现此接口
    /// </summary>
    public interface IService
    {
        /// <summary>
        /// 服务是否已初始化
        /// </summary>
        bool IsInitialized { get; }

        /// <summary>
        /// 初始化服务
        /// </summary>
        void Initialize();

        /// <summary>
        /// 关闭服务
        /// </summary>
        void Shutdown();
    }

    /// <summary>
    /// 可更新服务接口
    /// </summary>
    public interface IUpdatableService : IService
    {
        /// <summary>
        /// 每帧更新
        /// </summary>
        void Update(double deltaTime);
    }

    /// <summary>
    /// 优先级服务接口
    /// 用于确定服务初始化顺序
    /// </summary>
    public interface IPrioritizedService : IService
    {
        /// <summary>
        /// 优先级(越小越优先)
        /// </summary>
        int Priority { get; }
    }
}

使用说明与最佳实践

  • 适用场景:所有需要注册到服务定位器的服务类、需要按顺序初始化的系统、需要每帧更新的服务。

  • 注意事项

    1. 简单服务只需实现IService,需要每帧更新的服务实现IUpdatableService
    2. 初始化顺序值建议使用枚举或常量定义,避免魔法数字
    3. Initialize中只进行轻量级设置,重量级初始化应延迟到首次使用
    4. Shutdown中必须释放所有资源,断开信号连接,防止内存泄漏
  • 扩展建议:可以实现IConfigurableService接口支持配置注入;实现 IDebuggableService接口支持调试面板展示。


7.3.3 服务管理器

设计思路与原理

ServiceManager是服务定位器模式的Godot集成层,继承自Node以利用Godot的场景树生命周期。它解决了一个关键问题:服务需要在哪里初始化?Godot的节点系统提供了自然的生命周期管理(_Ready_Process_ExitTree),将服务管理器实现为节点可以无缝融入Godot的架构。

作为Node的子类,ServiceManager可以:

  1. 被添加到AutoLoad(自动加载)节点,确保游戏启动时自动创建
  2. _Process中驱动所有IUpdatableService的更新
  3. 在场景退出时自动调用所有服务的Shutdown
  4. 利用Godot的Pause模式处理游戏暂停时的服务行为

设计采用组合模式:ServiceManager不直接管理服务,而是委托给ServiceLocator静态类。这种分离使非Godot环境(如单元测试、工具脚本)也能使用服务定位器功能,而ServiceManager专注于Godot集成。

核心实现要点

  1. 继承Node实现Godot生命周期集成
  2. _Ready中初始化所有已注册服务(按优先级排序)
  3. _Process中更新所有可更新服务
  4. 支持游戏暂停时暂停服务更新
  5. _ExitTree中优雅关闭所有服务
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;

namespace GameFramework.Core.ServiceLocator
{
    /// <summary>
    /// 服务管理器
    /// 管理服务的生命周期和更新
    /// </summary>
    public partial class ServiceManager : Node
    {
        // 服务更新节点
        private class ServiceUpdater : Node
        {
            private readonly List<IUpdatableService> _updatableServices = new();

            public void AddService(IUpdatableService service)
            {
                _updatableServices.Add(service);
            }

            public void RemoveService(IUpdatableService service)
            {
                _updatableServices.Remove(service);
            }

            public override void _Process(double delta)
            {
                foreach (var service in _updatableServices)
                {
                    if (service.IsInitialized)
                    {
                        service.Update(delta);
                    }
                }
            }
        }

        private ServiceUpdater _updater;

        public override void _Ready()
        {
            _updater = new ServiceUpdater();
            AddChild(_updater);
        }

        /// <summary>
        /// 初始化所有已注册服务
        /// </summary>
        public void InitializeAllServices()
        {
            var services = ServiceLocator.GetRegisteredTypes()
                .Select(t => ServiceLocator.Get<object>())
                .OfType<IService>()
                .OrderBy(s => s is IPrioritizedService p ? p.Priority : int.MaxValue)
                .ToList();

            foreach (var service in services)
            {
                if (!service.IsInitialized)
                {
                    service.Initialize();

                    if (service is IUpdatableService updatable)
                    {
                        _updater.AddService(updatable);
                    }
                }
            }
        }

        /// <summary>
        /// 关闭所有服务
        /// </summary>
        public void ShutdownAllServices()
        {
            var services = ServiceLocator.GetRegisteredTypes()
                .Select(t => ServiceLocator.Get<object>())
                .OfType<IService>()
                .ToList();

            foreach (var service in services)
            {
                if (service.IsInitialized)
                {
                    if (service is IUpdatableService updatable)
                    {
                        _updater.RemoveService(updatable);
                    }

                    service.Shutdown();
                }
            }
        }

        public override void _ExitTree()
        {
            ShutdownAllServices();
            ServiceLocator.Clear();
        }
    }
}

使用说明与最佳实践

  • 适用场景:需要集中管理服务生命周期的Godot项目、需要按优先级初始化多个服务的场景、需要在游戏暂停时控制服务更新的场景。

  • 注意事项

    1. 必须在Godot项目设置的AutoLoad中添加ServiceManager节点
    2. 确保ServiceManager的初始化顺序早于其他脚本(设置较小的Order值)
    3. 不要直接创建ServiceManager实例,应使用AutoLoad提供的单例
    4. 在服务Initialize方法中不要调用其他未初始化的服务
  • 扩展建议:可以实现服务热重载,在运行时重新初始化服务;添加服务依赖图可视化工具,帮助调试初始化顺序问题。


7.3.4 使用示例

设计思路与原理

本示例展示一个完整的游戏设置服务(GameSettingsService),演示如何实现IServiceIPrioritizedServiceIUpdatableService接口。设置服务是游戏中最常见的基础服务之一,负责管理音量、画质、控制等玩家偏好。将其设计为服务的好处:

  1. 全局访问:任何代码都可以通过ServiceLocator.Get<GameSettingsService>()获取当前设置
  2. 持久化集成:服务初始化时加载存档设置,变更时自动保存
  3. 实时更新:音量等服务需要每帧更新,实现IUpdatableService让管理器驱动更新
  4. 高优先级:设置服务优先级设为0(最高),确保在其他服务之前初始化

示例展示了服务的典型生命周期:Initialize加载存档配置,Update应用设置变更(如动态调整音量),Shutdown确保所有设置被保存。这种设计使设置管理完全自动化,业务代码只需修改属性值,无需关心持久化和应用。

核心实现要点

  1. 多接口实现:同时实现服务基础接口和可选接口(优先级、可更新)
  2. 属性变更通知:示例中省略,实际应实现INotifyPropertyChanged支持数据绑定
  3. 延迟保存:示例在Shutdown中保存,实际应实现脏标记和延迟保存避免频繁IO
  4. 音量线性转换:示例展示了从对数音量(人类感知)到线性增益(音频API)的转换
using Godot;
using GameFramework.Core.ServiceLocator;

namespace GameFramework.Examples
{
    /// <summary>
    /// 游戏服务示例
    /// </summary>
    public class GameSettingsService : IService, IPrioritizedService, IUpdatableService
    {
        public bool IsInitialized { get; private set; }
        public int Priority => 0; // 最高优先级

        public float MasterVolume { get; set; } = 1.0f;
        public float MusicVolume { get; set; } = 0.8f;
        public float SfxVolume { get; set; } = 0.8f;

        public void Initialize()
        {
            // 加载设置
            LoadSettings();
            IsInitialized = true;
            GD.Print("游戏设置服务已初始化");
        }

        public void Shutdown()
        {
            // 保存设置
            SaveSettings();
            IsInitialized = false;
        }

        public void Update(double deltaTime)
        {
            // 可以在这里处理自动保存等
        }

        private void LoadSettings()
        {
            // 从文件加载设置
        }

        private void SaveSettings()
        {
            // 保存到文件
        }
    }

    /// <summary>
    /// 音频服务示例
    /// </summary>
    public class AudioService : IService, IPrioritizedService
    {
        public bool IsInitialized { get; private set; }
        public int Priority => 1;

        public void Initialize()
        {
            // 初始化音频系统
            IsInitialized = true;
            GD.Print("音频服务已初始化");
        }

        public void Shutdown()
        {
            IsInitialized = false;
        }

        public void PlayMusic(string musicPath)
        {
            // 播放音乐
        }

        public void PlaySfx(string sfxPath)
        {
            // 播放音效
        }
    }

    /// <summary>
    /// 游戏入口 - 服务注册和初始化
    /// </summary>
    public partial class GameEntry : Node
    {
        [Export] public ServiceManager ServiceManager;

        public override void _Ready()
        {
            // 注册服务
            RegisterServices();

            // 初始化所有服务
            ServiceManager.InitializeAllServices();

            // 使用服务
            UseServices();
        }

        private void RegisterServices()
        {
            // 注册设置服务
            ServiceLocator.Register(new GameSettingsService());

            // 注册音频服务(依赖设置服务)
            ServiceLocator.Register(new AudioService());

            // 延迟加载示例
            ServiceLocator.RegisterLazy<IDataService>(() => new DataService());
        }

        private void UseServices()
        {
            // 获取并使用服务
            if (ServiceLocator.TryGet<GameSettingsService>(out var settings))
            {
                settings.MasterVolume = 0.5f;
            }

            if (ServiceLocator.TryGet<AudioService>(out var audio))
            {
                audio.PlayMusic("res://assets/music/main.ogg");
            }
        }
    }

    public interface IDataService : IService { }
    public class DataService : IDataService
    {
        public bool IsInitialized { get; private set; }
        public void Initialize() { IsInitialized = true; }
        public void Shutdown() { IsInitialized = false; }
    }
}

使用说明与最佳实践

  • 适用场景

    • 全局系统管理(音频、输入、存档、网络)
    • 跨模块共享的配置和数据服务
    • 需要按优先级初始化的依赖系统
    • 延迟加载的大型服务(如数据服务)
  • 注意事项

    1. 服务注册顺序:虽然定位器会按优先级初始化,但注册顺序建议与依赖关系一致
    2. 循环依赖:避免服务A的Initialize中调用服务B,同时服务B的Initialize中调用服务A
    3. 延迟加载:RegisterLazy适合重量级服务,但要注意首次访问的卡顿问题
    4. TryGet优先:使用TryGet而非Get避免异常,特别是在服务可能未注册的场景
  • 服务设计建议

    • 保持服务单一职责,一个服务只做一件事
    • 服务接口应精简,避免暴露过多实现细节
    • 考虑实现服务接口的多个版本(如IAudioServiceFmodAudioServiceGodotAudioService
    • 添加服务健康检查接口,支持运行时监控
  • 测试策略

    • 单元测试中使用Mock服务替换真实服务
    • 使用ServiceLocator.Clear()在测试间清理状态
    • 测试服务初始化顺序,确保依赖关系正确

7.4 本章小结

本章介绍了三种核心设计模式:

  1. 对象池模式:通过预创建和复用对象减少内存分配和垃圾回收开销,特别适用于频繁创建销毁的对象如子弹、粒子效果等。实施时需要注意正确的状态重置和池大小调优。

  2. 工厂模式:将对象创建逻辑封装,支持从预制体创建节点,便于管理和扩展。结合工厂注册表可以实现集中化的对象创建管理。

  3. 服务定位器模式:提供全局服务访问机制,支持延迟加载和依赖管理,保持代码松耦合。配合服务管理器可以自动化服务的生命周期管理。

这些模式可以组合使用,例如使用工厂创建池化对象,使用服务定位器管理工厂实例,构建灵活高效的资源管理体系。