From 6124a454535345de7a1e61cedeec02e3d0bf5455 Mon Sep 17 00:00:00 2001 From: Stephen Seo Date: Fri, 1 Dec 2017 14:00:49 +0900 Subject: [PATCH] Replace garbage-collection-like cleanup Entity IDs are now guaranteed to not change for a living entity. Cleanup is replaced by using a std::unordered_set to store deleted Entity IDs to be reclaimed on later calls to addEntity(). --- src/EC/Manager.hpp | 164 +++++++++++++------------------------------- src/test/ECTest.cpp | 88 ++---------------------- 2 files changed, 56 insertions(+), 196 deletions(-) diff --git a/src/EC/Manager.hpp b/src/EC/Manager.hpp index 93397fe..259d2b4 100644 --- a/src/EC/Manager.hpp +++ b/src/EC/Manager.hpp @@ -63,14 +63,15 @@ namespace EC }; using ComponentsStorage = typename EC::Meta::Morph >::type; - // Entity: isAlive, dataIndex, ComponentsTags Info - using EntitiesTupleType = std::tuple; + // Entity: isAlive, ComponentsTags Info + using EntitiesTupleType = std::tuple; using EntitiesType = std::vector; EntitiesType entities; ComponentsStorage componentsStorage; std::size_t currentCapacity = 0; std::size_t currentSize = 0; + std::unordered_set deletedSet; public: /*! @@ -100,7 +101,7 @@ namespace EC entities.resize(newCapacity); for(std::size_t i = currentCapacity; i < newCapacity; ++i) { - entities[i] = std::make_tuple(false, i, BitsetType{}); + entities[i] = std::make_tuple(false, BitsetType{}); } currentCapacity = newCapacity; @@ -110,41 +111,58 @@ namespace EC /*! \brief Adds an entity to the system, returning the ID of the entity. - WARNING: The ID of an entity may change after calls to cleanup(). - Usage of entity IDs should be safe during initialization. - Otherwise, only use the ID given during usage of - forMatchingSignature(). + Note: The ID of an entity is guaranteed to not change. */ std::size_t addEntity() { - if(currentSize == currentCapacity) + if(deletedSet.empty()) { - resize(currentCapacity + EC_GROW_SIZE_AMOUNT); + if(currentSize == currentCapacity) + { + resize(currentCapacity + EC_GROW_SIZE_AMOUNT); + } + + std::get(entities[currentSize]) = true; + std::get(entities[currentSize]).reset(); + + return currentSize++; + } + else + { + std::size_t id; + { + auto iter = deletedSet.begin(); + id = *iter; + deletedSet.erase(iter); + } + std::get(entities[id]) = true; + std::get(entities[id]).reset(); + return id; } - - std::get(entities[currentSize]) = true; - - return currentSize++; } /*! \brief Marks an entity for deletion. - A deleted Entity is not actually deleted until cleanup() is called. - While an Entity is "deleted" but still in the system, calls to - forMatchingSignature() will ignore the Entity. + A deleted Entity's id is stored to be reclaimed later when + addEntity is called. Thus calling addEntity may return an id of + a previously deleted Entity. */ void deleteEntity(const std::size_t& index) { - std::get(entities.at(index)) = false; + if(hasEntity(index)) + { + std::get(entities.at(index)) = false; + deletedSet.insert(index); + } } /*! \brief Checks if the Entity with the given ID is in the system. - Note that deleted Entities that haven't yet been cleaned up - (via cleanup()) are considered still in the system. + Note that deleted Entities are still considered in the system. + Consider using isAlive(). */ bool hasEntity(const std::size_t& index) const { @@ -166,12 +184,12 @@ namespace EC /*! \brief Returns the current size or number of entities in the system. - Note that this size includes entities that have been marked for - deletion but not yet cleaned up by the cleanup function. + Note this function will only count entities where isAlive() returns + true. */ std::size_t getCurrentSize() const { - return currentSize; + return currentSize - deletedSet.size(); } /* @@ -189,11 +207,10 @@ namespace EC /*! \brief Returns a const reference to an Entity's info. - An Entity's info is a std::tuple with a bool, std::size_t, and a + An Entity's info is a std::tuple with a bool, and a bitset. \n The bool determines if the Entity is alive. - \n The std::size_t is the ID to this Entity's data in the system. \n The bitset shows what Components and Tags belong to the Entity. */ const EntitiesTupleType& getEntityInfo(const std::size_t& index) const @@ -215,7 +232,7 @@ namespace EC Component& getEntityData(const std::size_t& index) { return std::get >(componentsStorage).at( - std::get(entities.at(index))); + index); } /*! @@ -250,7 +267,7 @@ namespace EC const Component& getEntityData(const std::size_t& index) const { return std::get >(componentsStorage).at( - std::get(entities.at(index))); + index); } /*! @@ -302,90 +319,6 @@ namespace EC entities.at(index)).template getTagBit(); } - /*! - \brief Does garbage collection on Entities. - - Does housekeeping on the vector containing Entities that will - result in entity IDs changing if some Entities were marked for - deletion. - - This function should be called periodically to correctly handle - deletion of entities. - - The vector returned by this function lists all entities that have - changed as a result of calling this function. - - The first parameter in the tuple (bool) is true if the entity has - been relocated (entity id has changed) and is false if the entity - has been deleted. - - The second parameter (size_t) is the original entity id of the - entity. If the previous parameter was false for deleted, then this - refers to the id of that deleted entity. If the previous parameter - was true for relocated, then this refers to the previous id of the - relocated entity - - The third parameter (size_t) is the new entity id of the entity if - the entity was relocated. If the entity was deleted then this - parameter is undefined as it is not used in that case. - */ - using CleanupReturnType = - std::vector >; - CleanupReturnType cleanup() - { - CleanupReturnType changedVector; - if(currentSize == 0) - { - return changedVector; - } - - std::size_t rhs = currentSize - 1; - std::size_t lhs = 0; - - while(lhs < rhs) - { - while(!std::get(entities[rhs])) - { - if(rhs == 0) - { - currentSize = 0; - return changedVector; - } - changedVector.push_back(std::make_tuple(false, rhs, 0)); - std::get(entities[rhs]).reset(); - --rhs; - } - if(lhs >= rhs) - { - break; - } - else if(!std::get(entities[lhs])) - { - // lhs is marked for deletion - - // store deleted and changed id - changedVector.push_back(std::make_tuple(false, lhs, 0)); - changedVector.push_back(std::make_tuple(true, rhs, lhs)); - - // swap lhs entity with rhs entity - std::swap(entities[lhs], entities.at(rhs)); - - // clear deleted bitset - std::get(entities[rhs]).reset(); - - // inc/dec pointers - ++lhs; --rhs; - } - else - { - ++lhs; - } - } - currentSize = rhs + 1; - - return changedVector; - } - /*! \brief Adds a component to the given Entity. @@ -415,7 +348,7 @@ namespace EC template void addComponent(const std::size_t& entityID, Args&&... args) { - if(!hasEntity(entityID) || !isAlive(entityID)) + if(!isAlive(entityID)) { return; } @@ -428,7 +361,7 @@ namespace EC std::get >( componentsStorage - )[std::get(entities[entityID])] = std::move(component); + )[entityID] = std::move(component); } /*! @@ -445,7 +378,7 @@ namespace EC template void removeComponent(const std::size_t& entityID) { - if(!hasEntity(entityID) || !isAlive(entityID)) + if(!isAlive(entityID)) { return; } @@ -466,7 +399,7 @@ namespace EC template void addTag(const std::size_t& entityID) { - if(!hasEntity(entityID) || !isAlive(entityID)) + if(!isAlive(entityID)) { return; } @@ -489,7 +422,7 @@ namespace EC template void removeTag(const std::size_t& entityID) { - if(!hasEntity(entityID) || !isAlive(entityID)) + if(!isAlive(entityID)) { return; } @@ -1558,6 +1491,7 @@ namespace EC currentSize = 0; currentCapacity = 0; + deletedSet.clear(); resize(EC_INIT_ENTITIES_SIZE); } }; diff --git a/src/test/ECTest.cpp b/src/test/ECTest.cpp index c2d042e..ad61c88 100644 --- a/src/test/ECTest.cpp +++ b/src/test/ECTest.cpp @@ -132,10 +132,6 @@ TEST(EC, Manager) } manager.deleteEntity(e0); - manager.cleanup(); - - std::size_t edata = std::get(manager.getEntityInfo(0)); - EXPECT_EQ(edata, 1); std::size_t e2 = manager.addEntity(); @@ -154,7 +150,6 @@ TEST(EC, Manager) manager.deleteEntity(e1); manager.deleteEntity(e2); - manager.cleanup(); } TEST(EC, MoveComponentWithUniquePtr) @@ -217,24 +212,13 @@ TEST(EC, DeletedEntities) { auto eid = manager.addEntity(); manager.addComponent(eid); - if(i >= 2) - { - manager.deleteEntity(eid); - } } - - manager.cleanup(); + manager.deleteEntity(2); + manager.deleteEntity(3); for(unsigned int i = 0; i < 4; ++i) { - if(i < 2) - { - EXPECT_TRUE(manager.hasComponent(i)); - } - else - { - EXPECT_FALSE(manager.hasComponent(i)); - } + EXPECT_TRUE(manager.hasComponent(i)); } for(unsigned int i = 0; i < 2; ++i) @@ -393,44 +377,6 @@ TEST(EC, FunctionStorage) } } -/* - This test demonstrates that while entity ids may change after cleanup, - pointers to their components do not. -*/ -TEST(EC, DataPointers) -{ - EC::Manager manager; - - auto first = manager.addEntity(); - auto second = manager.addEntity(); - - manager.addComponent(first, 5, 5); - manager.addComponent(second, 10, 10); - manager.addTag(second); - - auto* cptr = &manager.getEntityData(second); - - EXPECT_EQ(10, cptr->x); - EXPECT_EQ(10, cptr->y); - - manager.deleteEntity(first); - manager.cleanup(); - - auto newSecond = second; - manager.forMatchingSignature>( - [&newSecond] (auto eid) { - newSecond = eid; - }); - - EXPECT_NE(newSecond, second); - - auto* newcptr = &manager.getEntityData(newSecond); - EXPECT_EQ(newcptr, cptr); - - EXPECT_EQ(10, newcptr->x); - EXPECT_EQ(10, newcptr->y); -} - TEST(EC, DeletedEntityID) { EC::Manager manager; @@ -443,28 +389,10 @@ TEST(EC, DeletedEntityID) manager.deleteEntity(e0); manager.deleteEntity(e1); - auto changedVector = manager.cleanup(); - - for(const auto& t : changedVector) - { - if(std::get<1>(t) == 0) - { - EXPECT_FALSE(std::get<0>(t)); - } - else if(std::get<1>(t) == 1) - { - EXPECT_FALSE(std::get<0>(t)); - } - else - { - EXPECT_TRUE(std::get<0>(t)); - } - } - - EXPECT_FALSE(manager.hasEntity(2)); - EXPECT_FALSE(manager.hasEntity(3)); - EXPECT_TRUE(manager.hasEntity(0)); - EXPECT_TRUE(manager.hasEntity(1)); + EXPECT_FALSE(manager.isAlive(e0)); + EXPECT_FALSE(manager.isAlive(e1)); + EXPECT_TRUE(manager.isAlive(e2)); + EXPECT_TRUE(manager.isAlive(e3)); } TEST(EC, MultiThreaded) @@ -497,7 +425,6 @@ TEST(EC, MultiThreaded) { manager.deleteEntity(i); } - manager.cleanup(); for(unsigned int i = 0; i < 3; ++i) { @@ -563,7 +490,6 @@ TEST(EC, MultiThreaded) { manager.deleteEntity(i); } - manager.cleanup(); manager.callForMatchingFunction(f0, 8);