diff options
-rw-r--r-- | CMakeLists.txt | 6 | ||||
-rw-r--r-- | README.md | 37 | ||||
-rw-r--r-- | entityx/Components_test.cc | 10 | ||||
-rw-r--r-- | entityx/Entity.cc | 11 | ||||
-rw-r--r-- | entityx/Entity.h | 89 | ||||
-rw-r--r-- | entityx/Entity_test.cc | 78 | ||||
-rw-r--r-- | entityx/System_test.cc | 12 |
7 files changed, 133 insertions, 110 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index c4547c2..7ee2745 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ macro(create_test TARGET_NAME SOURCE) entityx glog ${Boost_LIBRARIES} + ${Boost_SIGNALS_LIBRARY} ${GTEST_BOTH_LIBRARIES} ) add_test(${TARGET_NAME} ${TARGET_NAME}) @@ -60,6 +61,11 @@ set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g") set(sources entityx/Components.cc entityx/System.cc entityx/Event.cc entityx/Entity.cc entityx/Manager.cc) add_library(entityx STATIC ${sources}) add_library(entityx_shared SHARED ${sources}) +target_link_libraries( + entityx_shared + ${Boost_SIGNALS_LIBRARY} + glog +) set_target_properties(entityx_shared PROPERTIES OUTPUT_NAME entityx) include_directories( @@ -29,13 +29,15 @@ Entity entity = entities.create(); And destroying an entity is done with: ```c++ -entities.destroy(entity); +entity.destroy(); ``` -#### Testing for destroyed entities +#### Implementation details -The Entity type can be seen as a safe pointer. -Test if it is valid with `entity.exists()`. +- Each Entity is a convenience class wrapping an Entity::Id. +- An Entity handle can be invalidated with `invalidate()`. This does not affect the underlying entity. +- When an entity is destroyed the manager adds its ID to a free list and invalidates the Entity handle. +- When an entity is created IDs are recycled from the free list before allocating new ones. ### Components (entity data) @@ -43,11 +45,6 @@ The idea with ECS is to not have any functionality in the component. All logic s To that end Components are typically [POD types](http://en.wikipedia.org/wiki/Plain_Old_Data_Structures) containing self-contained sets of related data. Implementations are [curiously recurring template pattern](http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern) (CRTP) subclasses of `Component<T>`. -Implementation notes: - -- Components must provide a no-argument constructor. -- The current implementation can handle up to 64 components in total. - #### Creating components As an example, position and direction information might be represented as: @@ -103,6 +100,11 @@ if (position) { } ``` +#### Implementation notes + +- Components must provide a no-argument constructor. +- The current implementation can handle up to 64 components in total. This can be extended with little effort. + ### Systems (implementing behavior) Systems implement behavior using one or more components. Implementations are subclasses of `System<T>` and *must* implement the `update()` method, as shown below. @@ -187,7 +189,22 @@ DebugCollisions debug_collisions; events.subscribe<Collision>(debug_collisions); ``` -There can be more than one subscriber for an event; each one will be called. +#### Builtin events + +Several events are emitted by EntityX itself: + +- `EntityCreatedEvent` - emitted when a new Entity has been created. + - `Entity entity` - Newly created Entity. +- `EntityDestroyedEvent` - emitted when an Entity is *about to be* destroyed. + - `Entity entity` - Entity about to be destroyed. +- `ComponentAddedEvent<T>` - emitted when a new component is added to an entity. + - `Entity entity` - Entity that component was added to. + - `boost::shared_ptr<T> component` - The component added. + +#### Implementation notes + +- There can be more than one subscriber for an event; each one will be called. +- Event objects are destroyed after delivery, so references should not be retained. ### Manager (tying it all together) diff --git a/entityx/Components_test.cc b/entityx/Components_test.cc index 8c4d210..d9a0ecd 100644 --- a/entityx/Components_test.cc +++ b/entityx/Components_test.cc @@ -41,14 +41,14 @@ TEST(TagsComponentTest, TestVariadicConstruction) { TEST(TagsComponentTest, TestEntitiesWithTag) { EventManager ev; EntityManager en(ev); - Entity::Id a = en.create(); - en.assign<Position>(a); + Entity a = en.create(); + a.assign<Position>(); for (int i = 0; i < 99; ++i) { auto e = en.create(); - en.assign<Position>(e); - en.assign<TagsComponent>(e, "positionable"); + e.assign<Position>(); + e.assign<TagsComponent>("positionable"); } - en.assign<TagsComponent>(a, "player", "indestructible"); + a.assign<TagsComponent>("player", "indestructible"); auto entities = en.entities_with_components<Position>(); ASSERT_EQ(100, size(entities)); ASSERT_EQ(1, size(TagsComponent::view(entities, "player"))); diff --git a/entityx/Entity.cc b/entityx/Entity.cc index a17813f..039cc4a 100644 --- a/entityx/Entity.cc +++ b/entityx/Entity.cc @@ -17,13 +17,14 @@ const Entity::Id Entity::INVALID = Entity::Id(-1); BaseComponent::Family BaseComponent::family_counter_ = 0; -bool Entity::exists() const { - return entities_ != nullptr && entities_->exists(id_); +void Entity::invalidate() { + id_ = INVALID; + manager_ = nullptr; } -void Entity::detach() { - id_ = INVALID; - entities_ = nullptr; +void Entity::destroy() { + manager_->destroy(id_); + invalidate(); } } diff --git a/entityx/Entity.h b/entityx/Entity.h index f5028b0..e9c5d3f 100644 --- a/entityx/Entity.h +++ b/entityx/Entity.h @@ -34,6 +34,12 @@ class EntityManager; /** * A convenience handle around an Entity::Id. + * + * NOTE: Because Entity IDs are recycled by the EntityManager, there is no way + * to determine if an Entity is still associated with the same logical entity. + * + * The only way to robustly track an Entity through its lifecycle is to + * subscribe to EntityDestroyedEvent. */ class Entity { public: @@ -47,30 +53,39 @@ class Entity { Entity() {} /** - * Alias for exists(). + * Check if Entity handle is invalid. */ bool operator ! () const { - return !exists(); + return !valid(); } bool operator == (const Entity &other) const { - return other.entities_ == entities_ && other.id_ == id_; + return other.manager_ == manager_ && other.id_ == id_; } bool operator != (const Entity &other) const { - return other.entities_ != entities_ || other.id_ != id_; + return other.manager_ != manager_ || other.id_ != id_; } /** - * Detach Entity handle from the EntityManager. + * Is this Entity handle valid? + * + * *NOTE:* This does *not* imply that the originally associated entity ID is + * the same, or is valid in any way. */ - void detach(); - - Id id() const { return id_; } + bool valid() const { + return manager_ != nullptr && id_ != INVALID; + } - operator Id () { return id_; } + /** + * Invalidate Entity handle, disassociating it from an EntityManager and invalidating its ID. + * + * Note that this does *not* affect the underlying entity and its components. + */ + void invalidate(); - bool exists() const; + Id id() const { return id_; } + EntityManager &manager() { return *manager_; } template <typename C> boost::shared_ptr<C> assign(boost::shared_ptr<C> component); @@ -85,12 +100,17 @@ class Entity { template <typename A, typename B, typename ... Args> void unpack(boost::shared_ptr<A> &a, boost::shared_ptr<B> &b, Args && ... args); + /** + * Destroy and invalidate this Entity. + */ + void destroy(); + private: friend class EntityManager; - Entity(EntityManager *entities, Entity::Id id) : entities_(entities), id_(id) {} + Entity(EntityManager *entities, Entity::Id id) : manager_(entities), id_(id) {} - EntityManager *entities_ = nullptr; + EntityManager *manager_ = nullptr; Entity::Id id_ = INVALID; }; @@ -150,20 +170,16 @@ struct Component : public BaseComponent { * Emitted when an entity is added to the system. */ struct EntityCreatedEvent : public Event<EntityCreatedEvent> { - EntityCreatedEvent(EntityManager &manager, Entity::Id entity) : - manager(manager), entity(entity) {} + EntityCreatedEvent(Entity entity) : entity(entity) {} - EntityManager &manager; - Entity::Id entity; + Entity entity; }; struct EntityDestroyedEvent : public Event<EntityDestroyedEvent> { - EntityDestroyedEvent(EntityManager &manager, Entity::Id entity) : - manager(manager), entity(entity) {} + EntityDestroyedEvent(Entity entity) : entity(entity) {} - EntityManager &manager; - Entity::Id entity; + Entity entity; }; @@ -172,11 +188,10 @@ struct EntityDestroyedEvent : public Event<EntityDestroyedEvent> { */ template <typename T> struct ComponentAddedEvent : public Event<ComponentAddedEvent<T>> { - ComponentAddedEvent(EntityManager &manager, Entity::Id entity, boost::shared_ptr<T> component) : - manager(manager), entity(entity), component(component) {} + ComponentAddedEvent(Entity entity, boost::shared_ptr<T> component) : + entity(entity), component(component) {} - EntityManager &manager; - Entity::Id entity; + Entity entity; boost::shared_ptr<T> component; }; @@ -339,7 +354,7 @@ class EntityManager : boost::noncopyable { id = *it; free_list_.erase(it); } - event_manager_.emit<EntityCreatedEvent>(*this, id); + event_manager_.emit<EntityCreatedEvent>(Entity(this, id)); return Entity(this, id); } @@ -350,7 +365,7 @@ class EntityManager : boost::noncopyable { */ void destroy(Entity::Id entity) { CHECK(entity < entity_component_mask_.size()) << "Entity::Id ID outside entity vector range"; - event_manager_.emit<EntityDestroyedEvent>(*this, entity); + event_manager_.emit<EntityDestroyedEvent>(Entity(this, entity)); for (auto &components : entity_components_) { components.at(entity).reset(); } @@ -358,16 +373,6 @@ class EntityManager : boost::noncopyable { free_list_.insert(entity); } - /** - * Check if an Entity::Id is registered. - */ - bool exists(Entity::Id entity) { - if (entity_component_mask_.empty() || entity >= id_counter_) { - return false; - } - return free_list_.find(entity) == free_list_.end(); - } - Entity get(Entity::Id id) { return Entity(this, id); } @@ -384,7 +389,7 @@ class EntityManager : boost::noncopyable { entity_components_.at(C::family()).at(entity) = base; entity_component_mask_.at(entity) |= uint64_t(1) << C::family(); - event_manager_.emit<ComponentAddedEvent<C>>(*this, entity, component); + event_manager_.emit<ComponentAddedEvent<C>>(Entity(this, entity), component); return component; } @@ -520,27 +525,27 @@ class EntityManager : boost::noncopyable { template <typename C> boost::shared_ptr<C> Entity::assign(boost::shared_ptr<C> component) { - return entities_->assign<C>(id_, component); + return manager_->assign<C>(id_, component); } template <typename C, typename ... Args> boost::shared_ptr<C> Entity::assign(Args && ... args) { - return entities_->assign<C>(id_, args ...); + return manager_->assign<C>(id_, args ...); } template <typename C> boost::shared_ptr<C> Entity::component() { - return entities_->component<C>(id_); + return manager_->component<C>(id_); } template <typename A> void Entity::unpack(boost::shared_ptr<A> &a) { - entities_->unpack(id_, a); + manager_->unpack(id_, a); } template <typename A, typename B, typename ... Args> void Entity::unpack(boost::shared_ptr<A> &a, boost::shared_ptr<B> &b, Args && ... args) { - entities_->unpack(id_, a, b, args ...); + manager_->unpack(id_, a, b, args ...); } } diff --git a/entityx/Entity_test.cc b/entityx/Entity_test.cc index 36a607b..828ae17 100644 --- a/entityx/Entity_test.cc +++ b/entityx/Entity_test.cc @@ -79,27 +79,24 @@ TEST_F(EntityManagerTest, TestCreateEntity) { ASSERT_TRUE(em.size() == 0); Entity e2; - ASSERT_FALSE(e2.exists()); - ASSERT_FALSE(em.exists(e2)); + ASSERT_FALSE(e2.valid()); Entity e = em.create(); - ASSERT_TRUE(e.exists()); - ASSERT_TRUE(em.exists(e)); + ASSERT_TRUE(e.valid()); ASSERT_TRUE(em.size() == 1); e2 = e; - ASSERT_TRUE(e2.exists()); - ASSERT_TRUE(em.exists(e2)); + ASSERT_TRUE(e2.valid()); } TEST_F(EntityManagerTest, TestEntityAsBoolean) { ASSERT_TRUE(em.size() == 0); Entity e = em.create(); - ASSERT_TRUE(em.exists(e)); + ASSERT_TRUE(e.valid()); ASSERT_TRUE(em.size() == 1); ASSERT_FALSE(!e); - em.destroy(e); + e.destroy(); ASSERT_TRUE(!e); @@ -110,8 +107,7 @@ TEST_F(EntityManagerTest, TestEntityAsBoolean) { TEST_F(EntityManagerTest, TestEntityReuse) { Entity e1 = em.create(); auto id = e1.id(); - em.destroy(e1); - ASSERT_TRUE(!em.exists(e1)); + e1.destroy(); Entity e2 = em.create(); // It is assumed that the allocation will reuse the same entity id. ASSERT_EQ(e2.id(), id); @@ -141,23 +137,21 @@ TEST_F(EntityManagerTest, TestDestroyEntity) { Entity f = em.create(); auto ep = e.assign<Position>(); f.assign<Position>(); - e.assign<Direction>(e); + e.assign<Direction>(); f.assign<Direction>(); ASSERT_EQ(2, ep.use_count()); - ASSERT_TRUE(em.exists(e)); - ASSERT_TRUE(em.exists(f)); + ASSERT_TRUE(e.valid()); + ASSERT_TRUE(f.valid()); ASSERT_TRUE(e.component<Position>()); ASSERT_TRUE(e.component<Direction>()); ASSERT_TRUE(f.component<Position>()); ASSERT_TRUE(f.component<Direction>()); - em.destroy(e); + e.destroy(); - ASSERT_FALSE(em.exists(e)); - ASSERT_TRUE(em.exists(f)); - ASSERT_FALSE(e.component<Position>()); - ASSERT_FALSE(e.component<Direction>()); + ASSERT_FALSE(e.valid()); + ASSERT_TRUE(f.valid()); ASSERT_TRUE(f.component<Position>()); ASSERT_TRUE(f.component<Direction>()); ASSERT_EQ(1, ep.use_count()); @@ -176,7 +170,7 @@ TEST_F(EntityManagerTest, TestGetEntitiesWithComponent) { } TEST_F(EntityManagerTest, TestGetEntitiesWithIntersectionOfComponents) { - vector<Entity::Id> entities; + vector<Entity> entities; for (int i = 0; i < 150; ++i) { Entity e = em.create(); entities.push_back(e); @@ -193,17 +187,17 @@ TEST_F(EntityManagerTest, TestGetEntitiesWithIntersectionOfComponents) { TEST_F(EntityManagerTest, TestGetEntitiesWithComponentAndUnpacking) { vector<Entity::Id> entities; - Entity::Id e = em.create(); - Entity::Id f = em.create(); - Entity::Id g = em.create(); + Entity e = em.create(); + Entity f = em.create(); + Entity g = em.create(); std::vector<std::pair<shared_ptr<Position>, shared_ptr<Direction>>> position_directions; position_directions.push_back(std::make_pair( - em.assign<Position>(e, 1.0f, 2.0f), - em.assign<Direction>(e, 3.0f, 4.0f))); + e.assign<Position>(1.0f, 2.0f), + e.assign<Direction>(3.0f, 4.0f))); position_directions.push_back(std::make_pair( - em.assign<Position>(f, 7.0f, 8.0f), - em.assign<Direction>(f, 9.0f, 10.0f))); - em.assign<Position>(g, 5.0f, 6.0f); + f.assign<Position>(7.0f, 8.0f), + f.assign<Direction>(9.0f, 10.0f))); + g.assign<Position>(5.0f, 6.0f); int i = 0; shared_ptr<Position> position; @@ -221,13 +215,13 @@ TEST_F(EntityManagerTest, TestGetEntitiesWithComponentAndUnpacking) { } TEST_F(EntityManagerTest, TestUnpack) { - Entity::Id e = em.create(); - auto p = em.assign<Position>(e); - auto d = em.assign<Direction>(e); + Entity e = em.create(); + auto p = e.assign<Position>(); + auto d = e.assign<Direction>(); shared_ptr<Position> up; shared_ptr<Direction> ud; - em.unpack<Position, Direction>(e, up, ud); + e.unpack<Position, Direction>(up, ud); ASSERT_EQ(p, up); ASSERT_EQ(d, ud); } @@ -236,12 +230,12 @@ TEST_F(EntityManagerTest, TestUnpack) { struct NullDeleter {template<typename T> void operator()(T*) {} }; TEST_F(EntityManagerTest, TestUnpackNullMissing) { - Entity::Id e = em.create(); - auto p = em.assign<Position>(e); + Entity e = em.create(); + auto p = e.assign<Position>(); shared_ptr<Position> up(reinterpret_cast<Position*>(0Xdeadbeef), NullDeleter()); shared_ptr<Direction> ud(reinterpret_cast<Direction*>(0Xdeadbeef), NullDeleter()); - em.unpack<Position, Direction>(e, up, ud); + e.unpack<Position, Direction>(up, ud); ASSERT_EQ(p, up); ASSERT_EQ(shared_ptr<Direction>(), ud); } @@ -256,7 +250,7 @@ TEST_F(EntityManagerTest, TestEntityCreatedEvent) { created.push_back(event.entity); } - vector<Entity::Id> created; + vector<Entity> created; }; EntityCreatedEventReceiver receiver; @@ -275,20 +269,20 @@ TEST_F(EntityManagerTest, TestEntityDestroyedEvent) { destroyed.push_back(event.entity); } - vector<Entity::Id> destroyed; + vector<Entity> destroyed; }; EntityDestroyedEventReceiver receiver; ev.subscribe<EntityDestroyedEvent>(receiver); ASSERT_EQ(0, receiver.destroyed.size()); - vector<Entity::Id> entities; + vector<Entity> entities; for (int i = 0; i < 10; ++i) { entities.push_back(em.create()); } ASSERT_EQ(0, receiver.destroyed.size()); for (auto e : entities) { - em.destroy(e); + e.destroy(); } ASSERT_TRUE(entities == receiver.destroyed); } @@ -325,9 +319,9 @@ TEST_F(EntityManagerTest, TestComponentAddedEvent) { ASSERT_EQ(0, receiver.position_events); ASSERT_EQ(0, receiver.direction_events); for (int i = 0; i < 10; ++i) { - Entity::Id e = em.create(); - em.assign<Position>(e, float(i), float(i)); - em.assign<Direction>(e, float(-i), float(-i)); + Entity e = em.create(); + e.assign<Position>(float(i), float(i)); + e.assign<Direction>(float(-i), float(-i)); } ASSERT_EQ(10, receiver.position_events); ASSERT_EQ(10, receiver.direction_events); @@ -339,6 +333,6 @@ TEST_F(EntityManagerTest, TestEntityAssignment) { ASSERT_NE(a, b); b = a; ASSERT_EQ(a, b); - a.detach(); + a.invalidate(); ASSERT_NE(a, b); } diff --git a/entityx/System_test.cc b/entityx/System_test.cc index c529bd1..85b08da 100644 --- a/entityx/System_test.cc +++ b/entityx/System_test.cc @@ -43,7 +43,7 @@ class MovementSystem : public System<MovementSystem> { shared_ptr<Position> position; shared_ptr<Direction> direction; for (auto entity : entities) { - es.unpack<Position, Direction>(entity, position, direction); + entity.unpack<Position, Direction>(position, direction); position->x += direction->x; position->y += direction->y; } @@ -55,7 +55,7 @@ class MovementSystem : public System<MovementSystem> { class TestManager : public entityx::Manager { public: - std::vector<Entity::Id> entities; + std::vector<Entity> entities; SystemManager &sm() { return system_manager; } EntityManager &em() { return entity_manager; } @@ -66,12 +66,12 @@ class TestManager : public entityx::Manager { void initialize() override { for (int i = 0; i < 150; ++i) { - Entity::Id e = entity_manager.create(); + Entity e = entity_manager.create(); entities.push_back(e); if (i % 2 == 0) - entity_manager.assign<Position>(e, 1, 2); + e.assign<Position>(1, 2); if (i % 3 == 0) - entity_manager.assign<Direction>(e, 1, 1); + e.assign<Direction>(1, 1); } } @@ -106,7 +106,7 @@ TEST_F(SystemManagerTest, TestApplySystem) { shared_ptr<Position> position; shared_ptr<Direction> direction; for (auto entity : manager.entities) { - manager.em().unpack<Position, Direction>(entity, position, direction); + entity.unpack<Position, Direction>(position, direction); if (position && direction) { ASSERT_FLOAT_EQ(2.0, position->x); ASSERT_FLOAT_EQ(3.0, position->y); |