0

我不太确定我是否需要一个对象池,但它似乎是最可行的解决方案,但有一些与之相关的不需要的缺点。我正在制作一个游戏,其中实体存储在对象池中。这些实体不是直接用 new 分配的,而是std::deque为它们处理内存。

这是我的对象池或多或少的样子:

struct Pool
{  
    Pool()
        : _pool(DEFAULT_SIZE)
    {}

    Entity* create()
    {
        if(!_destroyedEntitiesIndicies.empty())
        {
            _nextIndex = _destroyedEntitiesIndicies.front();
            _destroyedEntitiesIndicies.pop();
        }

        Entity* entity = &_pool[_nextIndex];
        entity->id = _nextIndex;
        return entity;
    }

    void destroy(Entity* x)
    {
        _destroyedEntitiesIndicies.emplace(x->id);
        x->id = 0;
    }

private:

    std::deque<Entity> _pool;
    std::queue<int> _destroyedEntitiesIndicies;
    int _nextIndex = 0;
};

如果我销毁一个实体,它的 ID 将被添加到_destroyedEntitiesIndicies队列中,这将使 ID 被重新使用,最后它的 ID 将设置为 0。现在唯一的陷阱是,如果我销毁一个实体,然后立即创建一个新实体,之前销毁的实体将更新为刚刚创建的同一实体。

IE

Entity* object1 = pool.create(); // create an object
pool.destroy(object1); // destroy it

Entity* object2 = pool.create(); // create another object
// now object1 will be the same as object2

std::cout << (object1 == object2) << '\n'; // this will print out 1

这对我来说似乎不正确。我该如何避免这种情况?显然上述情况可能不会发生(因为我会将对象销毁延迟到下一帧)。但这可能会在将实体状态保存到文件或类似的东西时引起一些干扰。

编辑:

假设我做了 NULL 实体来销毁它们。如果我能够从池中获取实体,或者存储指向实际实体的指针副本怎么办?销毁时如何使所有其他重复实体为空?

IE

Pool pool;
Entity* entity = pool.create();

Entity* theSameEntity = pool.get(entity->getId());

pool.destroy(entity);

// now entity == nullptr, but theSameEntity still points to the original entity
4

2 回答 2

1

这个问题似乎有不同的部分。让我们来看看:

(...) 如果我销毁一个实体,然后立即创建一个新实体,则先前销毁的实体将更新为刚刚创建的同一实体。这对我来说似乎不正确。我该如何避免这种情况?

您可以修改此方法:

void destroy(Entity* x)
    {
        _destroyedEntitiesIndicies.emplace(x->id);
        x->id = 0;
    }

成为:

void destroy(Entity *&x)
    {
        _destroyedEntitiesIndicies.emplace(x->id);
        x->id = 0;
        x = NULL;
    }

这样,您将避免遇到的特定问题。但是,它不能解决整个问题,您始终可以拥有不会更新为 NULL 的副本。

另一种方法是使用auto_ptr<>(在 C++'98 中,unique_ptr<>在 C++-11 中),它保证它们的内部指针在释放时将设置为 NULL。如果将此与 Entity 类中运算符 new 和 delete 的重载相结合(见下文),您可以拥有一个非常强大的机制。有一些变体,例如shared_ptr<>,在标准的新版本 C++-11 中,它们也可能对您有用。你的具体例子:

auto_ptr<Entity> object1( new Entity ); // calls pool.create()
object1.release();                      // calls pool.destroy, if needed

auto_ptr<Entity> object2( new Entity ); // create another object

// now object1 will NOT be the same as object2
std::cout << (object1.get() == object2.get()) << '\n'; // this will print out 0

您有各种可能的信息来源,例如cplusplus.comwikipediaHerb Shutter的一篇非常有趣的文章。

对象池的替代品?

创建对象池是为了避免在已知最大对象数的情况下进行昂贵的连续内存操作。对于您的情况,我无法想到对象池的替代方案,我认为您正在尝试正确的设计。但是,如果您有很多创建和销毁,那么最好的方法可能不是对象池。没有实验和测量时间是不可能说的。

关于实施,有多种选择。

首先,不清楚您是否通过避免内存分配来体验性能优势,因为您正在使用 _destroyedEntitiesIndicies(无论如何,您每次销毁对象时都可能分配内存)。如果与普通分配相比,这可以为您提供足够的性能提升,您将不得不对您的代码进行试验。您可以尝试完全删除 _destroyedEntitiesIndicies,并仅在用完它们时尝试查找空槽(_nextIndice >= DEFAULT_SIZE)。另一件要尝试的事情是丢弃那些空闲槽中浪费的内存并分配另一个块(DEFAULT_SIZE)。

同样,这一切都取决于您所体验的实际用途。找出答案的唯一方法是实验和测量。

最后,请记住,您可以修改类 Entity 以透明地支持或不支持对象池。这样做的一个好处是您可以试验它是否是一种更好的方法。

class Entity {
public:
    // more things...
    void * operator new(size_t size)
    {
        return pool.create();
    }

    void operator delete(void * entity)
    {
    }

private:
    Pool pool;
};

希望这可以帮助。

于 2013-06-30T10:28:05.067 回答
1

如果您只想Entity通过 访问实例create,则必须隐藏该get函数(无论如何,该函数在您的原始代码中并不存在:))。

我认为在你的游戏中添加这种安全性有点过头了,但如果你真的需要一种机制来控制对内存中某些部分的访问,我会考虑返回类似 ahandle或 aweak pointer而不是原始指针的东西。这weak pointer将包含向量/映射上的索引(您存储在除那个之外的任何东西都无法访问的地方weak pointer),该索引又包含实际Entity指针和一个小的散列值,指示弱指针是否仍然有效。

这里有一些代码,所以你明白我的意思:

struct WeakEntityPtr; // Forward declaration.
struct WeakRefIndex { unsigned int m_index; unsigned int m_hash; }; // Small helper struct.
class Entity {
    friend struct WeakEntityPtr;
private:
    static std::vector< Entity* > s_weakTable( 100 );
    static std::vector< char > s_hashTable( 100 );
    static WeakRefIndex findFreeWeakRefIndex(); // find next free index and change the hash value in the hashTable at that index
struct WeakEntityPtr {
private:
    WeakRefIndex m_refIndex;
public:
    inline Entity* get() {
        Entity* result = nullptr;

        // Check if the weak pointer is still valid by comparing the hash values.
        if ( m_refIndex.m_hash == Entity::s_hashTable[ m_refIndex.m_index ] )
        {
            result = WeakReferenced< T >::s_weakTable[ m_refIndex.m_index ];
        }

        return result;
    }
}

虽然这不是一个完整的例子(你必须照顾正确的(复制)构造函数、赋值操作等......)但它应该让你明白我在说什么。

但是,我想强调的是,我仍然认为一个简单的池足以满足您在这种情况下尝试执行的操作。您必须使其余代码与实体很好地配合使用,这样它们就不会重用它们不应该重用的对象,但我认为这比整个handle/weak pointer故事更容易完成并且可以更清晰地维护以上。

于 2013-06-30T11:00:11.613 回答