diff options
author | Alec Thomas <alec@swapoff.org> | 2012-09-20 21:24:08 -0400 |
---|---|---|
committer | Alec Thomas <alec@swapoff.org> | 2012-09-20 21:24:08 -0400 |
commit | d98de9aeb12ff50d49b833439bd78402e85d1991 (patch) | |
tree | 4e8289ca39d2fd5265e092ba0f8e771d5998d529 |
Initial import.
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | Makefile | 21 | ||||
-rw-r--r-- | Makefile.conf | 9 | ||||
-rw-r--r-- | entityx/Components.cc | 11 | ||||
-rw-r--r-- | entityx/Components.h | 66 | ||||
-rw-r--r-- | entityx/Components_test.cc | 54 | ||||
-rw-r--r-- | entityx/Entity.cc | 18 | ||||
-rw-r--r-- | entityx/Entity.h | 419 | ||||
-rw-r--r-- | entityx/Entity_test.cc | 207 | ||||
-rw-r--r-- | entityx/Event.cc | 17 | ||||
-rw-r--r-- | entityx/Event.h | 153 | ||||
-rw-r--r-- | entityx/Event_test.cc | 40 | ||||
-rw-r--r-- | entityx/Makefile | 66 | ||||
-rw-r--r-- | entityx/System.cc | 24 | ||||
-rw-r--r-- | entityx/System.h | 128 | ||||
-rw-r--r-- | entityx/System_test.cc | 117 | ||||
-rw-r--r-- | entityx/World.cc | 35 | ||||
-rw-r--r-- | entityx/World.h | 59 |
18 files changed, 1448 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc667ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.*.dep +*.a +*.so +*.o diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4a8e416 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +PREFIX=/usr/local + +all: + @make -C entityx all + +test: + @make -C entityx test + +clean: + @make -C entityx clean + +cleantests: + @make -C entityx cleantests + +distclean: + @make -C entityx distclean + +install: all + install -m755 ./entityx/libentity.so $(PREFIX)/lib + install -m644 ./entityx/libentity.a $(PREFIX)/lib + install -m644 ./entityx/*.h $(PREFIX)/include diff --git a/Makefile.conf b/Makefile.conf new file mode 100644 index 0000000..49ba7e3 --- /dev/null +++ b/Makefile.conf @@ -0,0 +1,9 @@ +CXX=c++ +DEBUG=-g +CXXFLAGS=-ansi -pedantic -Werror -Wall -Wextra -Wno-unused-parameter -Wno-error=unused-variable $(DEBUG) -std=c++11 -I. +LDFLAGS=-lboost_signals-mt -lglog $(DEBUG) +# Optimisations +#CXXFLAGS += -O3 -fomit-frame-pointer -ffast-math -fstrict-aliasing -msse2 + +# XXX Need all dependencies compiled with libc++...unlikely. +#-stdlib=libc++ diff --git a/entityx/Components.cc b/entityx/Components.cc new file mode 100644 index 0000000..0103144 --- /dev/null +++ b/entityx/Components.cc @@ -0,0 +1,11 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#include "Components.h" diff --git a/entityx/Components.h b/entityx/Components.h new file mode 100644 index 0000000..8c96fa7 --- /dev/null +++ b/entityx/Components.h @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#pragma once + +#include <string> +#include <boost/unordered_set.hpp> +#include "entityx/Entity.h" + +namespace entity { + +/** + * Allow entities to be tagged with strings. + */ +class TagsComponent : public Component<TagsComponent> { + struct TagsPredicate { + TagsPredicate(const std::string &tag) : tag(tag) {} + + bool operator () (EntityManager &manager, Entity id) { + auto tags = manager.component<TagsComponent>(id); + return tags != nullptr && tags->tags.find(tag) != tags->tags.end(); + } + + std::string tag; + }; + + public: + /** + * Construct a new TagsComponent with the given tags. + * + * eg. TagsComponent tags("a", "b", "c"); + */ + template <typename ... Args> + TagsComponent(const std::string &tag, const Args & ... tags) { + set_tags(tag, tags ...); + } + + /** + * Filter the provided view to only those entities with the given tag. + */ + static EntityManager::View view(const EntityManager::View &view, const std::string &tag) { + return EntityManager::View(view, TagsPredicate(tag)); + } + + boost::unordered_set<std::string> tags; + + private: + template <typename ... Args> + void set_tags(const std::string &tag1, const std::string &tag2, const Args & ... tags) { + this->tags.insert(tag1); + set_tags(tag2, tags ...); + } + + void set_tags(const std::string &tag) { + tags.insert(tag); + } +}; + +} diff --git a/entityx/Components_test.cc b/entityx/Components_test.cc new file mode 100644 index 0000000..e32fb46 --- /dev/null +++ b/entityx/Components_test.cc @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#include <gtest/gtest.h> +#include "entityx/Components.h" + +using namespace std; +using namespace boost; +using namespace entity; + + +struct Position : public Component<Position> {}; + + +template <typename T> +int size(const T &t) { + int n = 0; + for (auto i : t) { + ++n; + } + return n; +} + + +TEST(TagsComponentTest, TestVariadicConstruction) { + auto tags = TagsComponent("player", "indestructible"); + unordered_set<string> expected; + expected.insert("player"); + expected.insert("indestructible"); + ASSERT_TRUE(expected == tags.tags); +} + +TEST(TagsComponentTest, TestEntitiesWithTag) { + EventManager ev; + EntityManager en(ev); + Entity a = en.create(); + en.assign<Position>(a); + for (int i = 0; i < 99; ++i) { + auto e = en.create(); + en.assign<Position>(e); + en.assign<TagsComponent>(e, "positionable"); + } + en.assign<TagsComponent>(a, "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 new file mode 100644 index 0000000..fefc1cd --- /dev/null +++ b/entityx/Entity.cc @@ -0,0 +1,18 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#include <algorithm> +#include "entityx/Entity.h" + +namespace entity { + +BaseComponent::Family BaseComponent::family_counter_ = 0; + +} diff --git a/entityx/Entity.h b/entityx/Entity.h new file mode 100644 index 0000000..e832c15 --- /dev/null +++ b/entityx/Entity.h @@ -0,0 +1,419 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#pragma once + + +#include <inttypes.h> +#include <algorithm> +#include <bitset> +#include <cassert> +#include <iterator> +#include <set> +#include <string> +#include <utility> +#include <vector> +#include <boost/unordered_set.hpp> +#include <boost/shared_ptr.hpp> +#include <glog/logging.h> +#include "entityx/Event.h" + +namespace entity { + +/** + * Entity handle. + */ +typedef uint64_t Entity; + + +class EntityManager; + + +/** + * Base component class, only used for insertion into collections. + * + * Family is used for registration. + */ +struct BaseComponent { + public: + typedef uint64_t Family; + + protected: + static Family family_counter_; +}; + + +/** + * Component implementations should inherit from this. + * + * Components MUST provide a no-argument constructor. + * Components SHOULD provide convenience constructors for initializing on assignment to an Entity. + * + * This is a struct to imply that components should be data-only. + * + * Usage: + * + * struct Position : public Component<Position> { + * Position(float x = 0.0f, float y = 0.0f) : x(x), y(y) {} + * + * float x, y; + * }; + * + * family() is used for registration. + */ +template <typename Derived> +struct Component : public BaseComponent { + public: + /** + * Emitted when a component of this type is added to an entity. + */ + class AddEvent : public Event<AddEvent> { + public: + AddEvent(Entity entity, boost::shared_ptr<Derived> component) : + entity(entity), component(component) {} + + Entity entity; + boost::shared_ptr<Derived> component; + }; + + + /// Used internally for registration. + static Family family() { + static Family family = family_counter_++; + // The 64-bit bitmask supports a maximum of 64 components. + assert(family < 64); + return family; + } +}; + + +/** + * Emitted when an entity is added to the system. + */ +struct EntityCreatedEvent : public Event<EntityCreatedEvent> { + EntityCreatedEvent(EntityManager &manager, Entity entity) : + manager(manager), entity(entity) {} + + EntityManager &manager; + Entity entity; +}; + + +struct EntityDestroyedEvent : public Event<EntityDestroyedEvent> { + EntityDestroyedEvent(EntityManager &manager, Entity entity) : + manager(manager), entity(entity) {} + + EntityManager &manager; + Entity entity; +}; + + +/** + * Emitted when any component is added to an entity. + */ +struct ComponentAddedEvent : public Event<ComponentAddedEvent> { + ComponentAddedEvent(EntityManager &manager, Entity entity, boost::shared_ptr<BaseComponent> component) : + manager(manager), entity(entity), component(component) {} + + EntityManager &manager; + Entity entity; + boost::shared_ptr<BaseComponent> component; +}; + + +/** + * Manages Entity creation and component assignment. + * + * eg. + * EntityManager e; + * + * Entity player = e.create(); + * + * e.assign<Movable>(player); + * e.assign<Physical>(player); + * e.assign<Scriptable>(player); + * Controllable *controllable = e.assign<Controllable>(player); + */ +class EntityManager : boost::noncopyable { + private: + typedef std::vector<boost::shared_ptr<BaseComponent>> EntitiesComponent; + + public: + EntityManager(EventManager &event_manager) : event_manager_(event_manager) {} + + class View { + private: + public: + typedef boost::function<bool (EntityManager &, Entity)> Predicate; + + /// A predicate that excludes entities that don't match the given component mask. + class ComponentMaskPredicate { + public: + ComponentMaskPredicate(const std::vector<uint64_t> &entity_bits, uint64_t mask) : entity_bits_(entity_bits), mask_(mask) {} + + bool operator () (EntityManager &, Entity entity) { + return (entity_bits_.at(entity) & mask_) == mask_; + } + + private: + const std::vector<uint64_t> &entity_bits_; + uint64_t mask_; + }; + + /// An iterator over a view of the entities in an EntityManager. + class Iterator : public std::iterator<std::input_iterator_tag, Entity> { + public: + Iterator &operator ++ () { + ++i_; + next(); + return *this; + } + bool operator == (const Iterator& rhs) const { return i_ == rhs.i_; } + bool operator != (const Iterator& rhs) const { return i_ != rhs.i_; } + Entity & operator * () { return i_; } + const Entity & operator * () const { return i_; } + + private: + friend class View; + + Iterator(EntityManager &manager, const std::vector<Predicate> &predicates, Entity entity) : manager_(manager), predicates_(predicates), i_(entity) { + next(); + } + + void next() { + while (i_ < manager_.size() && !predicate()) { + ++i_; + } + } + + bool predicate() { + for (auto &p : predicates_) { + if (!p(manager_, i_)) { + return false; + } + } + return true; + } + + EntityManager &manager_; + const std::vector<Predicate> predicates_; + Entity i_; + }; + + // Create a sub-view with an additional predicate. + View(const View &view, Predicate predicate) : manager_(view.manager_), predicates_(view.predicates_) { + predicates_.push_back(predicate); + } + + Iterator begin() { return Iterator(manager_, predicates_, 0); } + Iterator end() { return Iterator(manager_, predicates_, manager_.size()); } + const Iterator begin() const { return Iterator(manager_, predicates_, 0); } + const Iterator end() const { return Iterator(manager_, predicates_, manager_.size()); } + + private: + friend class EntityManager; + + View(EntityManager &manager, Predicate predicate) : manager_(manager) { + predicates_.push_back(predicate); + } + + EntityManager &manager_; + std::vector<Predicate> predicates_; + }; + + /** + * Number of managed entities. + */ + size_t size() const { return entity_component_mask_.size(); } + + /** + * Create a new Entity. + * + * Emits EntityCreatedEvent. + */ + Entity create() { + Entity id; + if (free_list_.empty()) { + id = id_counter_++; + accomodate_entity(id); + } else { + id = *free_list_.erase(free_list_.begin()); + } + event_manager_.emit<EntityCreatedEvent>(*this, id); + return id; + } + + /** + * Destroy an existing Entity and its associated Components. + * + * Emits EntityDestroyedEvent. + */ + void destroy(Entity entity) { + CHECK(entity < entity_component_mask_.size()) << "Entity ID outside entity vector range"; + event_manager_.emit<EntityDestroyedEvent>(*this, entity); + for (auto &components : entity_components_) { + components.at(entity).reset(); + } + entity_component_mask_.at(entity) = 0; + free_list_.insert(entity); + } + + /** + * Check if an Entity is registered. + */ + bool exists(Entity entity) { + if (entity_component_mask_.empty() || entity >= id_counter_) { + return false; + } + return free_list_.find(entity) == free_list_.end(); + } + + /** + * Assigns a previously constructed Component to an Entity. + * + * @returns component + */ + template <typename C> + boost::shared_ptr<C> assign(Entity entity, boost::shared_ptr<C> component) { + boost::shared_ptr<BaseComponent> base = boost::static_pointer_cast<BaseComponent>(component); + accomodate_component(C::family()); + entity_components_.at(C::family()).at(entity) = base; + entity_component_mask_.at(entity) |= uint64_t(1) << C::family(); + + // TODO(alec): Figure out why this doesn't compile...gets an odd error about AddEvent not being a value. + //event_manager_.emit<C::AddEvent>(entity, component); + event_manager_.emit<ComponentAddedEvent>(*this, entity, base); + return component; + } + + /** + * Assign a Component to an Entity, optionally passing through Component constructor arguments. + * + * Position *position = em.assign<Position>(e, x, y); + * + * @returns Newly created component. + */ + template <typename C, typename ... Args> + boost::shared_ptr<C> assign(Entity entity, Args && ... args) { + return assign<C>(entity, boost::make_shared<C>(args ...)); + } + + /** + * Retrieve a Component assigned to an Entity. + * + * @returns Component instance, or empty shared_ptr<> if the Entity does not have that Component. + */ + template <typename C> + boost::shared_ptr<C> component(Entity id) { + // We don't bother checking the component mask, as we return a nullptr anyway. + if (C::family() >= entity_components_.size()) { + return boost::shared_ptr<C>(); + } + auto &c = entity_components_.at(C::family()).at(id); + return boost::static_pointer_cast<C>(c); + } + + /** + * Get all entities with the given component. + */ + template <typename C> + View entities_with_components() { + auto mask = component_mask<C>(); + return View(*this, View::ComponentMaskPredicate(entity_component_mask_, mask)); + } + + /** + * Find Entities that have all of the specified Components. + */ + template <typename C1, typename C2, typename ... Components> + View entities_with_components() { + auto mask = component_mask<C1, C2, Components ...>(); + return View(*this, View::ComponentMaskPredicate(entity_component_mask_, mask)); + } + + /** + * Unpack components directly into pointers. + * + * Components missing from the entity will be set to nullptr. + * + * Useful for fast bulk iterations. + * + * Position *p; + * Direction *d; + * unpack<Position, Direction>(e, p, d); + * + * Ideally this process would be more like: + * + * for (auto components : em.unpack_entities<Position, Direction>()) { + * } + */ + template <typename A> + void unpack(Entity id, A *&a) { + a = component<A>(id).get(); + } + + /** + * Unpack components directly into pointers. + * + * Components missing from the entity will be set to nullptr. + * + * Useful for fast bulk iterations. + * + * Position *p; + * Direction *d; + * unpack<Position, Direction>(e, p, d); + */ + template <typename A, typename B, typename ... Args> + void unpack(Entity id, A *&a, B *&b, Args *& ... args) { + a = component<A>(id).get(); + unpack<B, Args ...>(id, b, args ...); + } + + private: + template <typename C> + uint64_t component_mask() { + return uint64_t(1) << C::family(); + } + + template <typename C1, typename C2, typename ... Components> + uint64_t component_mask() { + return component_mask<C1>() | component_mask<C2, Components ...>(); + } + + inline void accomodate_entity(Entity entity) { + if (entity_component_mask_.size() <= entity) { + entity_component_mask_.resize(entity + 1); + for (auto &components : entity_components_) { + components.resize(entity + 1); + } + } + } + + inline void accomodate_component(BaseComponent::Family family) { + if (entity_components_.size() <= family) { + entity_components_.resize(family + 1); + for (auto &components : entity_components_) { + components.resize(id_counter_); + } + } + } + + Entity id_counter_ = 0; + + EventManager &event_manager_; + // A nested array of: components = entity_components_[family][entity] + std::vector<EntitiesComponent> entity_components_; + // Bitmask of components associated with each entity. Index into the vector is the Entity. + std::vector<uint64_t> entity_component_mask_; + // List of available Entity IDs. + boost::unordered_set<Entity> free_list_; +}; + +} diff --git a/entityx/Entity_test.cc b/entityx/Entity_test.cc new file mode 100644 index 0000000..8feb91a --- /dev/null +++ b/entityx/Entity_test.cc @@ -0,0 +1,207 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#include <iterator> +#include <string> +#include <vector> +#include <boost/ref.hpp> +#include <glog/logging.h> +#include <gtest/gtest.h> +#include "entityx/Entity.h" + +using namespace std; +using namespace boost; +using namespace entity; + + +template <typename T> +int size(const T &t) { + int n = 0; + for (auto i : t) { + ++n; + } + return n; +} + +struct Position : Component<Position> { + Position(float x = 0.0f, float y = 0.0f) : x(x), y(y) {} + + float x, y; +}; + +struct Direction : Component<Direction> { + Direction(float x = 0.0f, float y = 0.0f) : x(x), y(y) {} + + float x, y; +}; + + +class EntityManagerTest : public ::testing::Test { + protected: + EntityManagerTest() : em(ev) {} + + EventManager ev; + EntityManager em; + + virtual void SetUp() { + } +}; + + +TEST_F(EntityManagerTest, TestCreateEntity) { + Entity e = em.create(); + ASSERT_TRUE(em.exists(e)); +} + +TEST_F(EntityManagerTest, TestComponentConstruction) { + auto e = em.create(); + auto p = em.assign<Position>(e, 1, 2); + auto cp = em.component<Position>(e); + ASSERT_EQ(p, cp); + ASSERT_FLOAT_EQ(1.0, cp->x); + ASSERT_FLOAT_EQ(2.0, cp->y); +} + +TEST_F(EntityManagerTest, TestComponentCreationWithObject) { + auto e = em.create(); + auto p = em.assign(e, make_shared<Position>(1.0, 2.0)); + auto cp = em.component<Position>(e); + ASSERT_EQ(p, cp); + ASSERT_FLOAT_EQ(1.0, cp->x); + ASSERT_FLOAT_EQ(2.0, cp->y); +} + +TEST_F(EntityManagerTest, TestDestroyEntity) { + Entity e = em.create(); + Entity f = em.create(); + auto ep = em.assign<Position>(e); + em.assign<Position>(f); + em.assign<Direction>(e); + em.assign<Direction>(f); + + ASSERT_EQ(2, ep.use_count()); + ASSERT_TRUE(em.exists(e)); + ASSERT_TRUE(em.exists(f)); + ASSERT_TRUE(em.component<Position>(e)); + ASSERT_TRUE(em.component<Direction>(e)); + ASSERT_TRUE(em.component<Position>(f)); + ASSERT_TRUE(em.component<Direction>(f)); + + em.destroy(e); + + ASSERT_FALSE(em.exists(e)); + ASSERT_TRUE(em.exists(f)); + ASSERT_FALSE(em.component<Position>(e)); + ASSERT_FALSE(em.component<Direction>(e)); + ASSERT_TRUE(em.component<Position>(f)); + ASSERT_TRUE(em.component<Direction>(f)); + ASSERT_EQ(1, ep.use_count()); +} + +TEST_F(EntityManagerTest, TestGetEntitiesWithComponent) { + Entity e = em.create(); + Entity f = em.create(); + Entity g = em.create(); + em.assign<Position>(e); + em.assign<Direction>(e); + em.assign<Position>(f); + em.assign<Position>(g); + ASSERT_EQ(3, size(em.entities_with_components<Position>())); + ASSERT_EQ(1, size(em.entities_with_components<Direction>())); +} + +TEST_F(EntityManagerTest, TestGetEntitiesWithIntersectionOfComponents) { + vector<Entity> entities; + for (int i = 0; i < 150; ++i) { + Entity e = em.create(); + entities.push_back(e); + if (i % 2 == 0) + em.assign<Position>(e); + if (i % 3 == 0) + em.assign<Direction>(e); + + } + ASSERT_EQ(50, size(em.entities_with_components<Direction>())); + ASSERT_EQ(75, size(em.entities_with_components<Position>())); + ASSERT_EQ(25, size(em.entities_with_components<Direction, Position>())); +} + +TEST_F(EntityManagerTest, TestUnpack) { + Entity e = em.create(); + auto p = em.assign<Position>(e); + auto d = em.assign<Direction>(e); + + Position *up; + Direction *ud; + em.unpack<Position, Direction>(e, up, ud); + ASSERT_EQ(p.get(), up); + ASSERT_EQ(d.get(), ud); +} + +TEST_F(EntityManagerTest, TestUnpackNullMissing) { + Entity e = em.create(); + auto p = em.assign<Position>(e); + + Position *up = reinterpret_cast<Position*>(0Xdeadbeef); + Direction *ud = reinterpret_cast<Direction*>(0Xdeadbeef); + ASSERT_EQ(reinterpret_cast<Direction*>(0xdeadbeef), ud); + em.unpack<Position, Direction>(e, up, ud); + ASSERT_EQ(p.get(), up); + ASSERT_EQ(nullptr, ud); +} + +TEST_F(EntityManagerTest, TestComponentIdsDiffer) { + ASSERT_NE(Position::family(), Direction::family()); +} + + +TEST_F(EntityManagerTest, TestEntityCreatedEvent) { + struct EntityCreatedEventReceiver : public Receiver<EntityCreatedEventReceiver> { + void receive(const EntityCreatedEvent &event) { + created.push_back(event.entity); + } + + vector<Entity> created; + }; + + EntityCreatedEventReceiver receiver; + ev.subscribe<EntityCreatedEvent>(receiver); + + ASSERT_EQ(0, receiver.created.size()); + for (int i = 0; i < 10; ++i) { + em.create(); + } + ASSERT_EQ(10, receiver.created.size()); +}; + + +TEST_F(EntityManagerTest, TestEntityDestroyedEvent) { + struct EntityDestroyedEventReceiver : public Receiver<EntityDestroyedEventReceiver> { + void receive(const EntityDestroyedEvent &event) { + destroyed.push_back(event.entity); + } + + vector<Entity> destroyed; + }; + + EntityDestroyedEventReceiver receiver; + ev.subscribe<EntityDestroyedEvent>(receiver); + + ASSERT_EQ(0, receiver.destroyed.size()); + 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); + } + ASSERT_TRUE(entities == receiver.destroyed); +}; diff --git a/entityx/Event.cc b/entityx/Event.cc new file mode 100644 index 0000000..484a17c --- /dev/null +++ b/entityx/Event.cc @@ -0,0 +1,17 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#include "entityx/Event.h" + +namespace entity { + +BaseEvent::Family BaseEvent::family_counter_ = 0; + +} diff --git a/entityx/Event.h b/entityx/Event.h new file mode 100644 index 0000000..510da8f --- /dev/null +++ b/entityx/Event.h @@ -0,0 +1,153 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#pragma once + +#include <inttypes.h> +#include <boost/shared_ptr.hpp> +#include <boost/bind.hpp> +#include <boost/function.hpp> +#include <boost/noncopyable.hpp> +#include <boost/signal.hpp> +#include <boost/unordered_map.hpp> + + +namespace entity { + + +/// Used internally by the EventManager. +class BaseEvent { + public: + typedef boost::shared_ptr<BaseEvent> Ptr; + typedef uint64_t Family; + + virtual ~BaseEvent() {} + + protected: + static Family family_counter_; +}; + + +/** + * Event types should subclass from this. + * + * struct Explosion : public Event<Explosion> { + * Explosion(int damage) : damage(damage) {} + * int damage; + * }; + */ +template <typename Derived> +class Event : public BaseEvent { + public: + typedef boost::shared_ptr<Event<Derived>> Ptr; + + /// Used internally for registration. + static Family family() { + static Family family = family_counter_++; + return family; + } +}; + + +class BaseReceiver { + public: + virtual ~BaseReceiver() { + for (auto connection : connections_) { + connection.disconnect(); + } + } + + private: + friend class EventManager; + std::list<boost::signals::connection> connections_; +}; + +template <typename Derived> +class Receiver : public BaseReceiver { + public: + virtual ~Receiver() {} +}; + + +/** + * Handles event subscription and delivery. + * + * Subscriptions are automatically removed when receivers are destroyed.. + */ +class EventManager : boost::noncopyable { + public: + + /** + * Subscribe an object to receive events of type E. + * + * Receivers must be subclasses of Receiver and must implement a receive() method accepting the given event type. + * + * eg. + * + * struct ExplosionReceiver : public Receiver<ExplosionReceiver> { + * void receive(const Explosion &explosion) { + * } + * }; + * + * ExplosionReceiver receiver; + * em.subscribe<Explosion>(receiver); + */ + template <typename E, typename Receiver> + void subscribe(Receiver &receiver) { + void (Receiver::*receive) (const E &) = &Receiver::receive; + auto sig = signal_for(E::family()); + auto wrapper = EventCallbackWrapper<E>(boost::bind(receive, &receiver, _1)); + static_cast<BaseReceiver&>(receiver).connections_.push_back(sig->connect(wrapper)); + } + + /** + * Emit an event to receivers. + * + * This method constructs a new event object of type E with the provided arguments, then delivers it to all receivers. + * + * eg. + * + * EntityManager em; + * em.emit<Explosion>(10); + * + */ + template <typename E, typename ... Args> + void emit(Args && ... args) { + E event(args ...); + auto sig = signal_for(E::family()); + (*sig)(static_cast<BaseEvent*>(&event)); + } + + private: + typedef boost::signal<void (const BaseEvent*)> EventSignal; + typedef boost::shared_ptr<EventSignal> EventSignalPtr; + + EventSignalPtr signal_for(int id) { + auto it = handlers_.find(id); + if (it == handlers_.end()) { + EventSignalPtr sig(boost::make_shared<EventSignal>()); + handlers_.insert(make_pair(id, sig)); + return sig; + } + return it->second; + } + + // Functor used as a event signal callback that casts to E. + template <typename E> + struct EventCallbackWrapper { + EventCallbackWrapper(boost::function<void (const E &)> callback) : callback(callback) {} + void operator () (const BaseEvent* event) { callback(*(static_cast<const E*>(event))); } + boost::function<void (const E &)> callback; + }; + + boost::unordered_map<int, EventSignalPtr> handlers_; +}; + +} diff --git a/entityx/Event_test.cc b/entityx/Event_test.cc new file mode 100644 index 0000000..63b0529 --- /dev/null +++ b/entityx/Event_test.cc @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#include <string> +#include <vector> +#include <glog/logging.h> +#include <gtest/gtest.h> +#include "entityx/Event.h" + +using namespace entity; +using namespace boost; + +struct Explosion : public Event<Explosion> { + Explosion(int damage) : damage(damage) {} + int damage; +}; + +struct ExplosionSystem : public Receiver<ExplosionSystem> { + void receive(const Explosion &explosion) { + damage_received += explosion.damage; + } + + int damage_received = 0; +}; + +TEST(EventManagerTest, TestEmitReceive) { + EventManager em; + ExplosionSystem explosion_system; + em.subscribe<Explosion>(explosion_system); + ASSERT_EQ(0, explosion_system.damage_received); + em.emit<Explosion>(10); + ASSERT_EQ(10, explosion_system.damage_received); +} diff --git a/entityx/Makefile b/entityx/Makefile new file mode 100644 index 0000000..039fce0 --- /dev/null +++ b/entityx/Makefile @@ -0,0 +1,66 @@ +include ../Makefile.conf + +S=Event.cc Entity.cc System.cc World.cc Components.cc +L=libentity.a +SL=libentity.so +O=$(S:.cc=.o) + +TESTS=Entity_test.cc System_test.cc Event_test.cc Components_test.cc +TESTS_O=$(TESTS:.cc=.o) +TESTS_BIN=$(TESTS:.cc=) +ALL_TESTS_BIN=.all_tests + +ALL_SOURCES=$(S) $(TESTS) +ALL_O=$(ALL_SOURCES:.cc=.o) + +ALL_DEPS=$(patsubst %.cc,.%.cc.dep,$(ALL_SOURCES)) + +LDFLAGS+=-L. -lentity +CXXFLAGS+=-I.. + +.PHONY: all clean distclean test cleantests + +all: $(L) $(SL) + +# Consolidated test binary +test: $(ALL_TESTS_BIN) + @./$(ALL_TESTS_BIN) + +$(ALL_TESTS_BIN): $(TESTS_O) $(L) + $(CXX) $(LDFLAGS) -lgtest -lgtest_main $(TESTS_O) -o $@ + +# Individual test binaries +$(TESTS_BIN): $(TESTS_O) $(L) + $(CXX) $(LDFLAGS) -lgtest -lgtest_main $@.o -o $@ + +$(L): $(O) + ar rc $(L) $(O) + +$(SL): $(O) + $(CXX) $(LDFLAGS) -fPIC -shared -o $@ $(O) + +%.o: %.cc + $(CXX) $(CXXFLAGS) -c $< -o $@ + +# Command targets + +clean: cleantests + rm -f $(SL) $(O) $(L) + +cleantests: + rm -f $(TESTS_O) $(TESTS_BIN) $(ALL_TESTS_BIN) + +distclean: clean + rm -f $(ALL_DEPS) + +-include $(ALL_DEPS) + +.%.cc.dep: %.cc %.h + @touch $@ + @makedepend -f$@ -I.. $^ > /dev/null 2>&1 + @rm -f $@.bak + +.%.cc.dep: %.cc + @touch $@ + @makedepend -f$@ -I.. $< > /dev/null 2>&1 + @rm -f $@.bak diff --git a/entityx/System.cc b/entityx/System.cc new file mode 100644 index 0000000..4c05f2b --- /dev/null +++ b/entityx/System.cc @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#include "entityx/System.h" + +namespace entity { + +BaseSystem::Family BaseSystem::family_counter_; + +void SystemManager::configure() { + for (auto pair : systems_) { + pair.second->configure(events_); + } + initialized_ = true; +} + +} diff --git a/entityx/System.h b/entityx/System.h new file mode 100644 index 0000000..ad6a9ba --- /dev/null +++ b/entityx/System.h @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#pragma once + + +#include <inttypes.h> +#include <boost/noncopyable.hpp> +#include <boost/shared_ptr.hpp> +#include <boost/unordered_map.hpp> +#include <glog/logging.h> +#include "entityx/Entity.h" +#include "entityx/Event.h" + + +namespace entity { + +/** + * Base System class. Generally should not be directly used, instead see System<Derived>. + */ +class BaseSystem : boost::noncopyable { + public: + typedef uint64_t Family; + + virtual ~BaseSystem() {} + + /** + * Called once all Systems have been added to the SystemManager. + * + * Typically used to set up event handlers. + */ + virtual void configure(EventManager &events) {} + + /** + * Apply System behavior. + * + * Called every game step. + */ + virtual void update(EntityManager &entities, EventManager &events, double dt) = 0; + + static Family family_counter_; + + protected: + boost::shared_ptr<EntityManager> entities; +}; + + +/** + * Use this class when implementing Systems. + * + * struct MovementSystem : public System<MovementSystem> { + * void update(EntityManager &entities, EventManager &events, double dt) { + * // Do stuff to/with entities... + * } + * } + */ +template <typename Derived> +class System : public BaseSystem { + public: + virtual ~System() {} + + static Family family() { + static Family family = family_counter_++; + return family; + } +}; + + +class SystemManager : boost::noncopyable { + public: + SystemManager(EntityManager &entities, EventManager &events) : entities_(entities), events_(events) {} + + /** + * Add a System to the SystemManager. + * + * Must be called before Systems can be used. + * + * eg. + * auto movement = system.add<MovementSystem>() + */ + template <typename S, typename ... Args> + boost::shared_ptr<S> add(Args && ... args) { + boost::shared_ptr<S> s(new S(args ...)); + systems_.insert(std::make_pair(S::family(), s)); + return s; + } + + template <typename S> + boost::shared_ptr<S> system() { + auto it = systems_.find(S::family()); + CHECK(it != systems_.end()); + return it == systems_.end() + ? boost::shared_ptr<S>() + : boost::static_pointer_cast<S>(it->second); + } + + /** + * Call the System::update() method for a registered system. + */ + template <typename S> + void update(double dt) { + CHECK(initialized_) << "SystemManager::configure() not called"; + boost::shared_ptr<S> s = system<S>(); + s->update(entities_, events_, dt); + } + + /** + * Configure the system. Call after adding all Systems. + * + * This is typically used to set up event handlers. + */ + void configure(); + + private: + bool initialized_ = false; + EntityManager &entities_; + EventManager &events_; + boost::unordered_map<BaseSystem::Family, boost::shared_ptr<BaseSystem>> systems_; +}; + +} diff --git a/entityx/System_test.cc b/entityx/System_test.cc new file mode 100644 index 0000000..6133eb0 --- /dev/null +++ b/entityx/System_test.cc @@ -0,0 +1,117 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#include <string> +#include <vector> +#include <glog/logging.h> +#include <gtest/gtest.h> +#include "entityx/World.h" +#include "entityx/System.h" + + +using namespace std; +using namespace boost; +using namespace entity; + +struct Position : Component<Position> { + Position(float x = 0.0f, float y = 0.0f) : x(x), y(y) {} + + float x, y; +}; + +struct Direction : Component<Direction> { + Direction(float x = 0.0f, float y = 0.0f) : x(x), y(y) {} + + float x, y; +}; + + +class MovementSystem : public System<MovementSystem> { + public: + MovementSystem(string label = "") : label(label) {} + + void update(EntityManager &es, EventManager &events, double) override { + auto entities = es.entities_with_components<Position, Direction>(); + Position *position; + Direction *direction; + for (auto entity : entities) { + es.unpack<Position, Direction>(entity, position, direction); + position->x += direction->x; + position->y += direction->y; + } + } + + string label; +}; + + +class TestWorld : public entity::World { + public: + std::vector<Entity> entities; + + SystemManager &sm() { return system_manager; } + EntityManager &em() { return entity_manager; } + + protected: + void configure() override { + } + + void initialize() override { + for (int i = 0; i < 150; ++i) { + Entity e = entity_manager.create(); + entities.push_back(e); + if (i % 2 == 0) + entity_manager.assign<Position>(e, 1, 2); + if (i % 3 == 0) + entity_manager.assign<Direction>(e, 1, 1); + } + } + + void update(double dt) override { + } +}; + + +class SystemManagerTest : public ::testing::Test { + protected: + TestWorld world; + + virtual void SetUp() override { + world.start(); + } +}; + + +TEST_F(SystemManagerTest, TestConstructSystemWithArgs) { + world.sm().add<MovementSystem>("movement"); + world.sm().configure(); + + ASSERT_EQ("movement", world.sm().system<MovementSystem>()->label); +} + + +TEST_F(SystemManagerTest, TestApplySystem) { + world.sm().add<MovementSystem>(); + world.sm().configure(); + + world.sm().update<MovementSystem>(0.0); + Position *position; + Direction *direction; + for (auto entity : world.entities) { + world.em().unpack<Position, Direction>(entity, position, direction); + if (position && direction) { + ASSERT_FLOAT_EQ(2.0, position->x); + ASSERT_FLOAT_EQ(3.0, position->y); + } else if (position) { + ASSERT_FLOAT_EQ(1.0, position->x); + ASSERT_FLOAT_EQ(2.0, position->y); + } + } +} diff --git a/entityx/World.cc b/entityx/World.cc new file mode 100644 index 0000000..5e8d8db --- /dev/null +++ b/entityx/World.cc @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#include "World.h" + +namespace entity { + +void World::start() { + configure(); + system_manager.configure(); + initialize(); +} + +void World::run() { + running_ = true; + double dt; + while (running_) { + dt = timer_.elapsed(); + timer_.restart(); + update(dt); + } +} + +void World::stop() { + running_ = false; +} + +} diff --git a/entityx/World.h b/entityx/World.h new file mode 100644 index 0000000..0cca1e2 --- /dev/null +++ b/entityx/World.h @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2012 Alec Thomas <alec@swapoff.org> + * All rights reserved. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. + * + * Author: Alec Thomas <alec@swapoff.org> + */ + +#pragma once + +#include <boost/timer.hpp> +#include "entityx/Entity.h" +#include "entityx/Event.h" +#include "entityx/System.h" + +namespace entity { + +class World { + public: + World() : entity_manager(event_manager), system_manager(entity_manager, event_manager) {} + virtual ~World() {} + + void start(); + void run(); + void stop(); + + protected: + /** + * Configure the world. + * + * This is called once on World initialization. It is typically used to add Systems to the world, load permanent + * resources, global configuration, etc. + */ + virtual void configure() = 0; + + /** + * Initialize the entities and events in the world. + * + * Typically used when + */ + virtual void initialize() = 0; + + /** + * Update the world. + */ + virtual void update(double dt) = 0; + + EventManager event_manager; + EntityManager entity_manager; + SystemManager system_manager; + + private: + boost::timer timer_; + bool running_ = false; +}; + +} |