文章

浅析游戏编程中对象池的概念与实现

简单介绍了对象池的应用场景,配合例程介绍其代码实现时需考虑的问题,并提供在个人项目中使用对象池重构原本对象管理的过程作为参考

浅析游戏编程中对象池的概念与实现

参考文章:http://gameprogrammingpatterns.com/object-pool.html

一、对象池设计思路

1.1 应用场景

  • 游戏开发常常会遇到需要频繁创建和销毁大量相同对象的场景(例如MC的粒子效果、塔防游戏的子弹、RTS的兵种单位),若不做任何特殊处理,其主要会导致以下性能问题
    • 频繁创建与销毁对象的性能开销较大
    • 会导致出现大量内存碎片,累积之后可能导致游戏最终无法运行
      • 例如在连续空间上开辟1KB、2KB、4KB、3KB、1KB的五块空间
      • 然后将2KB和3KB的内存释放,此时获得了总空闲空间5KB,但二者并不相邻
      • 若此时想一次性开辟5KB空间,先前释放所得的5KB无法被使用,因为不连续
    • 同时创建巨量低价值的相同对象(如过多的粒子)可能导致瞬间消耗完所有可用内存,而导致阻塞其它重要的执行需求

1.2 粗略设计

  • 对象池(Object Pool)核心设计思想如下
    • 创建对象时将对象储存在数组或栈等内存连续的数据结构中,这避免了内存碎片
    • 对象每次用完后放回池中循环利用而非直接销毁,这避免了内存的频繁分配销毁
    • 对象池地容量具有上限,这避免了意外地创建过量对象阻塞程序

1.3 细节设计

1.3.1 容量问题

  • 对象池的容量上限需根据游戏需求进行设置,更大的池子能提供更强的应对能力和时间效率,更小的池子能节省更多的内存空间
  • 容量存在上限意味着同时只能激活固定数量的对象,当所有对象均处于繁忙状态时,我们就无法调出新的对象来使用了,对此我们有以下解决办法
    • 设置足够大的固定容量:确保不会出现所有对象繁忙的状况,但这缺乏灵活性
    • 直接忽略新的请求:例如对象池管理的粒子系统,若所有粒子都正在被使用,那么屏幕已经充满了闪动的图形,玩家并不会注意到下一个爆炸的粒子效果比不上现在这个的璀璨
    • 强制干掉一个使用中的对象:例如对象池管理的音效播放系统,假设在音效池满时需播放新的声音效果,由于玩家通常对声音很敏感(我最近在玩堕落之主时就遇到过好几次打怪的时候突然没声儿、一会儿突然又有声儿了的情况),我们最好不应该直接忽略新的音效,更好的解决方法是用新声音覆盖掉正在播放中最低音量的声音,这样既不会引人注意了
    • 对原对象池进行扩容:在游戏总体设计允许的内存灵活范围内,我们可以选择在运行时增加池子的大小,并考虑当增加的内存不再需要时,池子是否需要缩回原来的大小
    • 或者创建新的溢出池:同理需考虑新池子是否应当在不再被需要时进行销毁

1.3.2 池槽位尺寸

  • 如果对象池中仅存储同一类型的对象,那么没有问题,但若在定义容器时指出存储的对象类型是某种基类,这就意味着容器中可能存入尺寸不一致的不同派生类对象
    • 所以我们在设计池子的槽位时,需确保每个槽位尺寸都能存入最大的可能对象,否则超过预期大小的对象将会占据下一个对象的内存空间,导致内存崩坏(定量的食物,有人少吃无妨,但有人多吃了就可能导致他人吃不饱)
  • 如果派生类对象间的尺寸差异较大,那么每放进去一个小对象都是在浪费内存(好比租了一辆搬家用的大卡车来搬家,却只用它运了一箱矿泉水)
    • 在该情况下,可以考虑为每个派生类设立专门的对象池(好比大盘盛大菜、小盘装小菜)

1.3.3 查找耗时

  • 我们在查找空闲对象的时候,一般需要遍历整个对象列表找到非繁忙状态对象,这比较耗时,详细解决方法参照后文的代码实现案例

1.3.4 与GC的冲突

  • 由于内存管理系统通常会处理不常用的对象内存碎片,所以在支持垃圾回收的系统中设计对象池时,应当注意这个潜在的冲突问题
    • 当对象池不应被整体销毁时:应确保对象池阻止对象在暂时不需被使用时被GC真正地析构
    • 当对象池应当被整体销毁时:应及时清除对象池内的对象间可能存在的相互引用,因为这会阻止垃圾回收器真正回收这些对象

1.4 概念区别

1.4.1 享元模式

  • 对象池优化与享元模式不同
    • 对象池旨在复用创建成本高、生命周期短的无状态(或每次取出会重置状态)对象
    • 享元模式旨在存储需跨多个上下文共享的不可变对象,以减少共享资源的重复加载(如游戏纹理、音效、3D模型等)

1.4.2 内存池

  • 内存池(参考这篇文章)的目的是减少频繁调用new/deletemalloc/free带来的开销并避免内存碎片,其通常预先分配一大块内存后根据需要从中分配小块内存
    • 管理原始内存块
    • 不关心内存中存储的对象类型
    • 通常只负责内存的分配和释放
  • 内存池(处理原始内存)比对象池(处理具体对象)更底层,对象池可以基于内存池实现,后者提供底层的内存管理,前者在后者之上管理对象的生命周期

二、对象池代码实现

2.1 简化粒子系统示例

2.1.1 未优化的粗暴实现

  • 此处以简化的粒子系统为例(仿真的粒子系统通常应用重力、风力、摩擦等物理效果,而此处不搞那么复杂,粒子仅在直线上移动几帧后从屏幕中清除即可),以下是粒子类的实现示意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Particle
{
private:
    //表示粒子可移动的次数,正整数表示处于使用状态,<=0表示处于闲置
    int framesLeft;

    //粒子的坐标模拟
    double x, y;
    //粒子每次移动时坐标各分量上的增量
    double xVel, yVel;

public:
    //默认构造函数,初始化粒子为闲置状态,应当使用专门的函数进行实际的初始化
    Particle() :framesLeft(0) {}

    //使用专门的初始化方法,而非重载构造函数,因为对象复用时无法触发构造函数
    void Init(double _x, double _y, double _xVel, double _yVel, int _lifetime)
    {
        framesLeft = _lifetime;
        x = _x; y = _y;
        xVel = _xVel; yVel = _yVel;
    }

    //模拟粒子对象的实际功能
    void Animate()
    {
        //繁忙则返回
        if (!InUse()) return;

        //可移动次数递减
        framesLeft--;
        //以增量进行移动模拟
        x += xVel; y += yVel;
    }

    //返回是否处于使用状态
    bool InUse() const
    {
        return framesLeft > 0;
    }
};
  • 以下是粒子的对象池类实现示意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ParticlePool
{
private:
    //对象池的容量上限
    static const int POOL_SIZE = 100;

    //实际存储对象的连续内存容器,此处使用原生数组
    //此处为方便硬编码了数组容量,也可在堆区开辟动态数组(或使用模板参数)
    Particle particles[POOL_SIZE];

public:
    //提供粒子的调用方法
    void Create(double _x, double _y, double _xVel, double _yVel, int _lifetime)
    {
        //数组初始化时就由Particle的默认构造对象填满,此处只需从中找到一个闲置粒子即可
        for (int i = 0; i < POOL_SIZE; i++)
        {
            if (!particles[i].InUse())
            {
                //重新初始化其成员属性
                particles[i].Init(_x, _y, _xVel, _yVel, _lifetime);
                return;
            }
        }
        //在这个实现中,若没找到任何可用的粒子,就不创建新的粒子了
    }

    //统一更新所有对象的功能模拟
    void Animate()
    {
        for (int i = 0; i < POOL_SIZE; i++)
            particles[i].Animate();
    }
};
  • 如果对象池只专门存储一类对象(而不是存储无继承关系的不同类的通用对象池)
    • 可进一步加强类的封装,将构造函数、专门初始化方法、查询活跃状态方法等均封装为私有
    • 类为对应的对象池类声明友元权限(对象知道自己被什么池子所持有),缺点是会导致二者相互耦合,好处是杜绝了外界别处的意外访问

2.1.2 优化对象查找时间

  • 在前文的实现中,对象池的ParticlePool::Create方法在调出新粒子时可能需遍历整个数组来找到一个空闲槽,耗费$O(n)$时间复杂度,这在大池中会较慢,我们可以用空间换时间
    • 可以维护一个和存储对象的数组等大的额外栈容器存储指向空闲对象的指针,在请求调出新对象时直接从该栈顶弹出即可,仅耗费$O(1)$时间复杂度
    • 如果我们既不想开辟额外的空间,又想实现常量级的查询时间,那可以直接征用闲置对象自身的空间,代价是需要改变粒子类的定义(增大了实现复杂度)
      • 将除framesLeft以外的所有成员变量转移到live结构中存储,后者连同粒子指针next一并存储在联合体union的对象state中(联合体允许在同一块内存中存储不同数据类型,但同时只能使用其中一种类型的值)
        • 当粒子正在被使用时,该粒子对象使用的是union对象state中的live结构中的数据
        • 当粒子处于闲置状态且正被试图唤醒重用时,state的另一部分next成员被使用,其保留了指向当前粒子后方的一个可用粒子对象的指针,若无则是nullptr
      • 然后提供外界(对象池)读写私有成员next的方法函数,用于在每个粒子被征用和进入闲置时维护好next指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Particle
{
private:
    //表示粒子可移动的次数,正整数表示处于使用状态,<=0表示处于闲置
    int framesLeft = 0;

    // //粒子的坐标模拟
    // double x, y;
    // //粒子每次移动时坐标各分量上的增量
    // double xVel, yVel;

    //联合体对象state可以在结构体对象live和粒子指针next二者间切换
    union
    {
        //使用时的状态
        struct
        {
            double x = 0, y = 0;
            double xVel = 0, yVel = 0;
        } live;

        //闲置而可被重用时的状态
        Particle* next = nullptr;
    } state;

public:
    //默认构造函数,初始化粒子为闲置状态,应当使用专门的函数进行实际的初始化
    Particle() :framesLeft(0) {}

    //使用专门的初始化方法,而非重载构造函数,因为对象复用时无法触发构造函数
    void Init(double _x, double _y, double _xVel, double _yVel, int _lifetime)
    {
        framesLeft = _lifetime;
        state.live.x = _x;
        state.live.y = _y;
        state.live.xVel = _xVel;
        state.live.yVel = _yVel;
    }

    //模拟粒子对象的实际功能
    void Animate()
    {
        //繁忙则返回
        if (!InUse()) return;

        //可移动次数递减
        framesLeft--;
        //以增量进行移动模拟
        state.live.x += state.live.xVel;
        state.live.y += state.live.yVel;
    }

    //设置下一个闲置粒子对象指针
    void SetNext(Particle* _next)
    {
        state.next = _next;
    }

    //获取下一个闲置粒子对象指针
    Particle* GetNext() const
    {
        return state.next;
    }

    //返回是否处于使用状态
    bool InUse() const
    {
        return framesLeft > 0;
    }
};
  • 然后我们就可以(在对象池的类定义中)使用这些指针构建一个闲置粒子对象的链表,相当于征用了死亡粒子本身的内存,这种优化空间占用的方式称为FreeList
    • 维护闲置粒子对象链表的头节点
    • 在对象池构造函数中初始化链表
    • 修改Create函数查找闲置对象的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class ParticlePool
{
private:
    //对象池的容量上限
    static const int POOL_SIZE = 100;

    //实际存储对象的连续内存容器,此处使用原生数组
    //此处为方便硬编码了数组容量,也可在堆区开辟动态数组(或使用模板参数)
    Particle particles[POOL_SIZE];

    //维护闲置对象链表的头节点
    Particle* freeHead = nullptr;

public:
    ParticlePool()
    {
        //指向第一个可用的粒子
        freeHead = &particles[0];

        //每个粒子对象指向下一个
        for (int i = 0; i < POOL_SIZE - 1; i++)
            particles[i].SetNext(&particles[i + 1]);

        //末尾粒子对象指向nullptr
        particles[POOL_SIZE - 1].SetNext(nullptr);
    }
    
    //提供粒子的调用方法
    void Create(double _x, double _y, double _xVel, double _yVel, int _lifetime)
    {
        // //数组初始化时就由Particle的默认构造对象填满,此处只需从中找到一个闲置粒子即可
        // for (int i = 0; i < POOL_SIZE; i++)
        // {
        //     if (!particles[i].InUse())
        //     {
        //         //重新初始化其成员属性
        //         particles[i].Init(_x, _y, _xVel, _yVel, _lifetime);
        //         return;
        //     }
        // }

        //使用链表查找闲置对象,先保证池没有满
        if (freeHead != nullptr)
        {
            //取用当前头节点并将其从闲置链表移除,转移freeHead到下一位
            Particle* _newParticle = freeHead;
            freeHead = _newParticle->GetNext();
            //使用这个调出的新节点
            _newParticle->Init(_x, _y, _xVel, _yVel, _lifetime);
        }
        else
        {
            //池子满了,你可以按照需求选择应对策略,此处啥也不干
        }
    }

    //统一更新所有对象的功能模拟,并维护闲置粒子对象链表
    void Animate()
    {
        // for (int i = 0; i < POOL_SIZE; i++)
        //     particles[i].Animate();

        for (int i = 0; i < POOL_SIZE; i++)
        {
            //查找闲置粒子
            if (!particles[i].InUse())
            {
                //将粒子加到链表前部
                particles[i].SetNext(freeHead);
                freeHead = &particles[i];
            }
            //否则正常更新
            else
                particles[i].Animate();
        }
    }
};

2.2 通用模板对象池

2.2.1 基础对象池

  • 以下是我自己实现的一个简陋的对象池
    • 以两个数组分别存储闲置和繁忙的对象,省去查找闲置对象的时间
    • 支持扩容(调用AcquireObject可能触发)与缩容(调用ReleaseObject可能触发)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#ifndef _OBJECT_POOL_HPP_
#define _OBJECT_POOL_HPP_

#include <vector>
#include <chrono>
#include <algorithm>
#include <iostream>

template <typename T>
class ObjectPool
{
private:
    std::vector<T*> busyObjects;                          //存储活跃对象
    std::vector<T*> freeObjects;                          //存储闲置对象

    size_t initialCapacity = 100;                         //对象池的初始容量大小
    size_t maximumCapacity = 1000;                        //对象池扩容的极限容量
    size_t expandAmount = 100;                            //对象池每次扩容的幅度

    std::chrono::steady_clock::time_point lastShrinkTime; //上次缩容时间
    int shrinkCheckDuration = 60;                         //每隔多少秒检测一次缩容

public:
    ObjectPool();
    ObjectPool(size_t, size_t, size_t);                   //初始容量、极限容量、扩容幅度
    ~ObjectPool();

    T* AcquireObject();                                   //从池中获取一个闲置对象
    void ReleaseObject(T*);                               //释放一个闲置对象回池中

    const std::vector<T*>& GetBusyObjects() const;        //获取活跃对象列表

private:
    bool Expand(size_t);                                  //池子扩容
    void Shrink(size_t);                                  //池子缩容
};

template<typename T>
ObjectPool<T>::ObjectPool()
{
    //使用默认值,开辟一堆闲置对象
    for (size_t i = 0; i < initialCapacity; i++)
        freeObjects.emplace_back(new T());

    //初始化时间点
    lastShrinkTime = std::chrono::steady_clock::now();
}

template<typename T>
ObjectPool<T>::ObjectPool(size_t _initialCapacity, size_t _maximumCapacity, size_t _expandAmount)
    :initialCapacity(_initialCapacity), maximumCapacity(_maximumCapacity), expandAmount(_expandAmount)
{
    //开辟一堆闲置对象
    for (size_t i = 0; i < initialCapacity; i++)
        freeObjects.emplace_back(new T());

    //初始化时间点
    lastShrinkTime = std::chrono::steady_clock::now();
}

template<typename T>
ObjectPool<T>::~ObjectPool()
{
    for (T* _obj : busyObjects)
        delete _obj;
    for (T* _obj : freeObjects)
        delete _obj;
}

template<typename T>
T* ObjectPool<T>::AcquireObject()
{
    //检测扩容
    if (freeObjects.size() == 0)
    {
        //如果扩容失败则抛出异常(或可返回空指针)
        if (!Expand(expandAmount))
            throw std::runtime_error("Object Pool Expand Failed");
    }

    //转移对象到活跃列表
    T* _target = freeObjects.back();
    freeObjects.pop_back();
    busyObjects.emplace_back(_target);
    return _target;
}

template<typename T>
void ObjectPool<T>::ReleaseObject(T* _target)
{
    //如果找到目标
    auto _it = std::find(busyObjects.begin(), busyObjects.end(), _target);
    if (_it != busyObjects.end())
    {
        //清除对象的状态,要求T实现了Reset()签名的函数
        (*_it)->Reset();
        //将其转移到闲置列表
        freeObjects.emplace_back(*_it);
        busyObjects.erase(_it);
        std::cout << "Successfully Release The Object Into Free Pool\n";
    }

    //检查是否需要缩容
    auto _now = std::chrono::steady_clock::now();
    auto _durationSinceLastShrink = std::chrono::duration_cast<std::chrono::seconds>(_now - lastShrinkTime).count();
    if (_durationSinceLastShrink >= shrinkCheckDuration)
    {
        //如果空闲对象占总对象数的50%以上,则缩容
        if ((freeObjects.size() + busyObjects.size()) / 2 <= freeObjects.size())
        {
            //计算应当缩容的数量,此处是削减空闲列表的75%
            Shrink((int)(freeObjects.size() * 0.75));
            lastShrinkTime = _now;
        }
    }
}

template<typename T>
const std::vector<T*>& ObjectPool<T>::GetBusyObjects() const
{
    return busyObjects;
}

template<typename T>
bool ObjectPool<T>::Expand(size_t _expandAmount)
{
    //按照幅度扩容闲置对象列表,不超过极限容量
    size_t _currentCapacity = freeObjects.size() + busyObjects.size();
    size_t _actualExpandAmount = std::min(_expandAmount, maximumCapacity - _currentCapacity);
    //无法继续扩容,返回扩容失败
    if (_actualExpandAmount <= 0) return false;
    //扩容成功
    for (size_t i = 0; i < _actualExpandAmount; i++)
        freeObjects.emplace_back(new T());
    std::cout << "Successfully Expand ObjectPool By " << _actualExpandAmount << " Objects\n";
    return true;
}

template<typename T>
void ObjectPool<T>::Shrink(size_t _shrinkAmount)
{
    //按照幅度缩减闲置对象列表,不低于初始容量
    size_t _currentCapacity = freeObjects.size() + busyObjects.size();
    size_t _actualShrinkAmount = std::min(_shrinkAmount, _currentCapacity - initialCapacity);
    //直接从末尾删除,不用担心迭代器失效
    for (size_t i = freeObjects.size() - _actualShrinkAmount; i < freeObjects.size(); i++)
        delete freeObjects[i];
    freeObjects.erase(freeObjects.end() - _actualShrinkAmount, freeObjects.end());
    std::cout << "Successfully Shrink ObjectPool By " << _actualShrinkAmount << " Objects\n";
}

#endif
  • 以下是测试用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "ObjectPool.hpp"

class Object
{
private:
    int no;

public:
    void SetNo(int _no)
    {
        no = _no;
    }

    void OnUpdate()
    {
        std::cout << "Object[" << no << "] Updates\n";
    }
};

int main()
{
    //初始化对象池与对象元素
    ObjectPool<Object> pool(5, 10, 2);
    for (int i = 0; i < 10; i++)
        pool.AcquireObject()->SetNo(i);

    //遍历一次
    for (Object* _obj : pool.GetBusyObjects())
        _obj->OnUpdate();

    //释放一些元素
    pool.ReleaseObject(pool.GetBusyObjects()[0]);
    pool.ReleaseObject(pool.GetBusyObjects()[0]);
    pool.ReleaseObject(pool.GetBusyObjects()[0]);

    //遍历一次
    for (Object* _obj : pool.GetBusyObjects())
        _obj->OnUpdate();
}
//Successfully Expand ObjectPool By 2 Objects
//Successfully Expand ObjectPool By 2 Objects
//Successfully Expand ObjectPool By 1 Objects
//Object[0] Updates
//Object[1] Updates
//Object[2] Updates
//Object[3] Updates
//Object[4] Updates
//Object[5] Updates
//Object[6] Updates
//Object[7] Updates
//Object[8] Updates
//Object[9] Updates
//Successfully Release The Object Into Free Pool
//Successfully Release The Object Into Free Pool
//Successfully Release The Object Into Free Pool
//Object[3] Updates
//Object[4] Updates
//Object[5] Updates
//Object[6] Updates
//Object[7] Updates
//Object[8] Updates
//Object[9] Updates

2.2.2 复合对象池

  • 我将上述对象池移植到了我的塔防游戏中的敌人上(具体Diff参考此条Commit记录),敌人基类派生了若干子类,所以我为了统一操作,封装了一个EnemyPool如下,该实现事实上看起来并不优雅(新增派生类时我们需要较多地修改EnemyPool的代码),但我暂未想到更好的实现,待我后续经验丰富些再来想想
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#ifndef _ENEMY_POOL_H_
#define _ENEMY_POOL_H_

#include <SDL.h>

#include "ObjectPool.hpp"
#include "../../Enemy/Concrete/Slime.h"
#include "../../Enemy/Concrete/SlimeKing.h"
#include "../../Enemy/Concrete/Skeleton.h"
#include "../../Enemy/Concrete/Goblin.h"
#include "../../Enemy/Concrete/GoblinPriest.h"

//对象池的Enemy*迭代器,用于遍历敌人池中的所有敌人
class EnemyPoolIterator
{
private:
    std::vector<Enemy*> enemies;
    size_t index = 0;

public:
    EnemyPoolIterator(const std::vector<Enemy*>& _enemies, size_t _index = 0)
        : enemies(_enemies), index(_index) {}

    Enemy* operator*() const { return enemies[index]; }
    EnemyPoolIterator& operator++() { ++index; return *this; }
    bool operator!=(const EnemyPoolIterator& _other) const { return index != _other.index; }
};

//管理Enemy的派生类的大对象池
class EnemyPool
{
private:

    ObjectPool<Slime> slimePool;
    ObjectPool<SlimeKing> slimeKingPool;
    ObjectPool<Skeleton> skeletonPool;
    ObjectPool<Goblin> goblinPool;
    ObjectPool<GoblinPriest> goblinPriestPool;

public:
    void OnUpdate(double);
    void OnRender(SDL_Renderer*);

    //二者使得EnemyPool可被范围for循环遍历
    EnemyPoolIterator begin();                   //迭代器begin语义
    EnemyPoolIterator end();                     //迭代器end语义

    Enemy* Acquire(EnemyType);                   //获取一个特定类型的敌人对象

    bool NoBusyEnemy() const;                    //判断场上是否还有活跃的敌人

private:
    void RemoveDeadEnemies();                    //释放死亡敌人回到池中
};

#endif
  • 函数实现如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#include "../../../Header/Infra/ObjectPool/EnemyPool.h"

void EnemyPool::OnUpdate(double _delta)
{
    //更新对象池内的活跃对象
    RemoveDeadEnemies();

    for (Slime* _enemy : slimePool.GetBusyObjects())
        _enemy->OnUpdate(_delta);
    for (SlimeKing* _enemy : slimeKingPool.GetBusyObjects())
        _enemy->OnUpdate(_delta);
    for (Skeleton* _enemy : skeletonPool.GetBusyObjects())
        _enemy->OnUpdate(_delta);
    for (Goblin* _enemy : goblinPool.GetBusyObjects())
        _enemy->OnUpdate(_delta);
    for (GoblinPriest* _enemy : goblinPriestPool.GetBusyObjects())
        _enemy->OnUpdate(_delta);
}

void EnemyPool::OnRender(SDL_Renderer* _renderer)
{
    for (Slime* _enemy : slimePool.GetBusyObjects())
        _enemy->OnRender(_renderer);
    for (SlimeKing* _enemy : slimeKingPool.GetBusyObjects())
        _enemy->OnRender(_renderer);
    for (Skeleton* _enemy : skeletonPool.GetBusyObjects())
        _enemy->OnRender(_renderer);
    for (Goblin* _enemy : goblinPool.GetBusyObjects())
        _enemy->OnRender(_renderer);
    for (GoblinPriest* _enemy : goblinPriestPool.GetBusyObjects())
        _enemy->OnRender(_renderer);
}

EnemyPoolIterator EnemyPool::begin()
{
    std::vector<Enemy*> _allEnemies;

    //将所有忙对象添加进一个std::vector
    for (Slime* _e : slimePool.GetBusyObjects()) _allEnemies.emplace_back(_e);
    for (SlimeKing* _e : slimeKingPool.GetBusyObjects()) _allEnemies.emplace_back(_e);
    for (Skeleton* _e : skeletonPool.GetBusyObjects()) _allEnemies.emplace_back(_e);
    for (Goblin* _e : goblinPool.GetBusyObjects()) _allEnemies.emplace_back(_e);
    for (GoblinPriest* _e : goblinPriestPool.GetBusyObjects()) _allEnemies.emplace_back(_e);

    return EnemyPoolIterator(_allEnemies, 0);
}

EnemyPoolIterator EnemyPool::end()
{
    std::vector<Enemy*> _allEnemies;

    //和begin相同
    for (Slime* _e : slimePool.GetBusyObjects()) _allEnemies.emplace_back(_e);
    for (SlimeKing* _e : slimeKingPool.GetBusyObjects()) _allEnemies.emplace_back(_e);
    for (Skeleton* _e : skeletonPool.GetBusyObjects()) _allEnemies.emplace_back(_e);
    for (Goblin* _e : goblinPool.GetBusyObjects()) _allEnemies.emplace_back(_e);
    for (GoblinPriest* _e : goblinPriestPool.GetBusyObjects()) _allEnemies.emplace_back(_e);

    //注意这里是构造end迭代器,所以size即index
    return EnemyPoolIterator(_allEnemies, _allEnemies.size());
}

Enemy* EnemyPool::Acquire(EnemyType _type)
{
    switch (_type)
    {
    case EnemyType::None:
        return nullptr;
        break;
    case EnemyType::Slime:
        return slimePool.AcquireObject();
        break;
    case EnemyType::SlimeKing:
        return slimeKingPool.AcquireObject();
        break;
    case EnemyType::Skeleton:
        return skeletonPool.AcquireObject();
        break;
    case EnemyType::Goblin:
        return goblinPool.AcquireObject();
        break;
    case EnemyType::GoblinPriest:
        return goblinPriestPool.AcquireObject();
        break;
    default:
        return nullptr;
        break;
    }
}

bool EnemyPool::NoBusyEnemy() const
{
    if (slimePool.GetBusyObjects().empty()
        && slimeKingPool.GetBusyObjects().empty()
        && skeletonPool.GetBusyObjects().empty()
        && goblinPool.GetBusyObjects().empty()
        && goblinPriestPool.GetBusyObjects().empty())
        return true;
    return false;
}

void EnemyPool::RemoveDeadEnemies()
{
    //遍历所有敌人池,释放死亡的敌人回到池中
    for (Slime* _enemy : slimePool.GetBusyObjects())
    {
        if (!_enemy->IsAlive())
            slimePool.ReleaseObject(_enemy);
    }
    for (SlimeKing* _enemy : slimeKingPool.GetBusyObjects())
    {
        if (!_enemy->IsAlive())
            slimeKingPool.ReleaseObject(_enemy);
    }
    for (Skeleton* _enemy : skeletonPool.GetBusyObjects())
    {
        if (!_enemy->IsAlive())
            skeletonPool.ReleaseObject(_enemy);
    }
    for (Goblin* _enemy : goblinPool.GetBusyObjects())
    {
        if (!_enemy->IsAlive())
            goblinPool.ReleaseObject(_enemy);
    }
    for (GoblinPriest* _enemy : goblinPriestPool.GetBusyObjects())
    {
        if (!_enemy->IsAlive())
            goblinPriestPool.ReleaseObject(_enemy);
    }
}

2.2.3 重置池对象

  • 我最初将上述EnemyPool应用到项目中时,ObjectPool<T>::ReleaseObject的实现中并未在释放对象的时候调用对象的Reset函数,即把对象释放回自由列表中时并未清除对象身上的状态,这就导致当时出现了一个Bug
    • 只要我杀死了某类敌人的一个实例,那么往后生成的所有该类型的敌人都会在生成瞬间就被清除,重新释放回池中
    • 这是因为EnemyPool::RemoveDeadEnemies中通过检测对象的isAlive布尔字段,判断是否应当释放该对象,而由于我们最初的ObjectPool<T>::ReleaseObject并未清除对象状态,就导致了第一个该类型的死亡敌人会被保留isAlivefalse的状态被放回闲置列表,而当生成新的该类型对象时,就会重用这个”已死”的对象,导致新敌人无法生成
  • 所以ObjectPool<T>::ReleaseObject中才会需要在真正释放对象时调用对象的重置函数,具体修改参考此条Commit记录
    1
    2
    3
    4
    5
    6
    7
    
    void Enemy::Reset()
    {
      //重置怪物的状态
      isAlive = true;
      //重置其它状态
      //...
    }
    

2.3 开源库中的实现

2.4 C++26的std::hive

本文由作者按照 CC BY-NC-SA 4.0 进行授权