From d98de9aeb12ff50d49b833439bd78402e85d1991 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 20 Sep 2012 21:24:08 -0400 Subject: Initial import. --- .gitignore | 4 + Makefile | 21 +++ Makefile.conf | 9 + entityx/Components.cc | 11 ++ entityx/Components.h | 66 +++++++ entityx/Components_test.cc | 54 ++++++ entityx/Entity.cc | 18 ++ entityx/Entity.h | 419 +++++++++++++++++++++++++++++++++++++++++++++ entityx/Entity_test.cc | 207 ++++++++++++++++++++++ entityx/Event.cc | 17 ++ entityx/Event.h | 153 +++++++++++++++++ entityx/Event_test.cc | 40 +++++ entityx/Makefile | 66 +++++++ entityx/System.cc | 24 +++ entityx/System.h | 128 ++++++++++++++ entityx/System_test.cc | 117 +++++++++++++ entityx/World.cc | 35 ++++ entityx/World.h | 59 +++++++ 18 files changed, 1448 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 Makefile.conf create mode 100644 entityx/Components.cc create mode 100644 entityx/Components.h create mode 100644 entityx/Components_test.cc create mode 100644 entityx/Entity.cc create mode 100644 entityx/Entity.h create mode 100644 entityx/Entity_test.cc create mode 100644 entityx/Event.cc create mode 100644 entityx/Event.h create mode 100644 entityx/Event_test.cc create mode 100644 entityx/Makefile create mode 100644 entityx/System.cc create mode 100644 entityx/System.h create mode 100644 entityx/System_test.cc create mode 100644 entityx/World.cc create mode 100644 entityx/World.h 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 + * 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 + */ + +#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 + * 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 + */ + +#pragma once + +#include +#include +#include "entityx/Entity.h" + +namespace entity { + +/** + * Allow entities to be tagged with strings. + */ +class TagsComponent : public Component { + struct TagsPredicate { + TagsPredicate(const std::string &tag) : tag(tag) {} + + bool operator () (EntityManager &manager, Entity id) { + auto tags = manager.component(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 + 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 tags; + + private: + template + 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 + * 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 + */ + +#include +#include "entityx/Components.h" + +using namespace std; +using namespace boost; +using namespace entity; + + +struct Position : public Component {}; + + +template +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 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(a); + for (int i = 0; i < 99; ++i) { + auto e = en.create(); + en.assign(e); + en.assign(e, "positionable"); + } + en.assign(a, "player", "indestructible"); + auto entities = en.entities_with_components(); + 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 + * 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 + */ + +#include +#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 + * 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 + */ + +#pragma once + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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(float x = 0.0f, float y = 0.0f) : x(x), y(y) {} + * + * float x, y; + * }; + * + * family() is used for registration. + */ +template +struct Component : public BaseComponent { + public: + /** + * Emitted when a component of this type is added to an entity. + */ + class AddEvent : public Event { + public: + AddEvent(Entity entity, boost::shared_ptr component) : + entity(entity), component(component) {} + + Entity entity; + boost::shared_ptr 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(EntityManager &manager, Entity entity) : + manager(manager), entity(entity) {} + + EntityManager &manager; + Entity entity; +}; + + +struct EntityDestroyedEvent : public Event { + 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(EntityManager &manager, Entity entity, boost::shared_ptr component) : + manager(manager), entity(entity), component(component) {} + + EntityManager &manager; + Entity entity; + boost::shared_ptr component; +}; + + +/** + * Manages Entity creation and component assignment. + * + * eg. + * EntityManager e; + * + * Entity player = e.create(); + * + * e.assign(player); + * e.assign(player); + * e.assign(player); + * Controllable *controllable = e.assign(player); + */ +class EntityManager : boost::noncopyable { + private: + typedef std::vector> EntitiesComponent; + + public: + EntityManager(EventManager &event_manager) : event_manager_(event_manager) {} + + class View { + private: + public: + typedef boost::function Predicate; + + /// A predicate that excludes entities that don't match the given component mask. + class ComponentMaskPredicate { + public: + ComponentMaskPredicate(const std::vector &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 &entity_bits_; + uint64_t mask_; + }; + + /// An iterator over a view of the entities in an EntityManager. + class Iterator : public std::iterator { + 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 &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 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 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(*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(*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 + boost::shared_ptr assign(Entity entity, boost::shared_ptr component) { + boost::shared_ptr base = boost::static_pointer_cast(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(entity, component); + event_manager_.emit(*this, entity, base); + return component; + } + + /** + * Assign a Component to an Entity, optionally passing through Component constructor arguments. + * + * Position *position = em.assign(e, x, y); + * + * @returns Newly created component. + */ + template + boost::shared_ptr assign(Entity entity, Args && ... args) { + return assign(entity, boost::make_shared(args ...)); + } + + /** + * Retrieve a Component assigned to an Entity. + * + * @returns Component instance, or empty shared_ptr<> if the Entity does not have that Component. + */ + template + boost::shared_ptr 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(); + } + auto &c = entity_components_.at(C::family()).at(id); + return boost::static_pointer_cast(c); + } + + /** + * Get all entities with the given component. + */ + template + View entities_with_components() { + auto mask = component_mask(); + return View(*this, View::ComponentMaskPredicate(entity_component_mask_, mask)); + } + + /** + * Find Entities that have all of the specified Components. + */ + template + View entities_with_components() { + auto mask = component_mask(); + 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(e, p, d); + * + * Ideally this process would be more like: + * + * for (auto components : em.unpack_entities()) { + * } + */ + template + void unpack(Entity id, A *&a) { + a = component(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(e, p, d); + */ + template + void unpack(Entity id, A *&a, B *&b, Args *& ... args) { + a = component(id).get(); + unpack(id, b, args ...); + } + + private: + template + uint64_t component_mask() { + return uint64_t(1) << C::family(); + } + + template + uint64_t component_mask() { + return component_mask() | component_mask(); + } + + 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 entity_components_; + // Bitmask of components associated with each entity. Index into the vector is the Entity. + std::vector entity_component_mask_; + // List of available Entity IDs. + boost::unordered_set 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 + * 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 + */ + +#include +#include +#include +#include +#include +#include +#include "entityx/Entity.h" + +using namespace std; +using namespace boost; +using namespace entity; + + +template +int size(const T &t) { + int n = 0; + for (auto i : t) { + ++n; + } + return n; +} + +struct Position : Component { + Position(float x = 0.0f, float y = 0.0f) : x(x), y(y) {} + + float x, y; +}; + +struct Direction : Component { + 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(e, 1, 2); + auto cp = em.component(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(1.0, 2.0)); + auto cp = em.component(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(e); + em.assign(f); + em.assign(e); + em.assign(f); + + ASSERT_EQ(2, ep.use_count()); + ASSERT_TRUE(em.exists(e)); + ASSERT_TRUE(em.exists(f)); + ASSERT_TRUE(em.component(e)); + ASSERT_TRUE(em.component(e)); + ASSERT_TRUE(em.component(f)); + ASSERT_TRUE(em.component(f)); + + em.destroy(e); + + ASSERT_FALSE(em.exists(e)); + ASSERT_TRUE(em.exists(f)); + ASSERT_FALSE(em.component(e)); + ASSERT_FALSE(em.component(e)); + ASSERT_TRUE(em.component(f)); + ASSERT_TRUE(em.component(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(e); + em.assign(e); + em.assign(f); + em.assign(g); + ASSERT_EQ(3, size(em.entities_with_components())); + ASSERT_EQ(1, size(em.entities_with_components())); +} + +TEST_F(EntityManagerTest, TestGetEntitiesWithIntersectionOfComponents) { + vector entities; + for (int i = 0; i < 150; ++i) { + Entity e = em.create(); + entities.push_back(e); + if (i % 2 == 0) + em.assign(e); + if (i % 3 == 0) + em.assign(e); + + } + ASSERT_EQ(50, size(em.entities_with_components())); + ASSERT_EQ(75, size(em.entities_with_components())); + ASSERT_EQ(25, size(em.entities_with_components())); +} + +TEST_F(EntityManagerTest, TestUnpack) { + Entity e = em.create(); + auto p = em.assign(e); + auto d = em.assign(e); + + Position *up; + Direction *ud; + em.unpack(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(e); + + Position *up = reinterpret_cast(0Xdeadbeef); + Direction *ud = reinterpret_cast(0Xdeadbeef); + ASSERT_EQ(reinterpret_cast(0xdeadbeef), ud); + em.unpack(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 { + void receive(const EntityCreatedEvent &event) { + created.push_back(event.entity); + } + + vector created; + }; + + EntityCreatedEventReceiver receiver; + ev.subscribe(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 { + void receive(const EntityDestroyedEvent &event) { + destroyed.push_back(event.entity); + } + + vector destroyed; + }; + + EntityDestroyedEventReceiver receiver; + ev.subscribe(receiver); + + ASSERT_EQ(0, receiver.destroyed.size()); + vector 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 + * 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 + */ + +#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 + * 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 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + + +namespace entity { + + +/// Used internally by the EventManager. +class BaseEvent { + public: + typedef boost::shared_ptr Ptr; + typedef uint64_t Family; + + virtual ~BaseEvent() {} + + protected: + static Family family_counter_; +}; + + +/** + * Event types should subclass from this. + * + * struct Explosion : public Event { + * Explosion(int damage) : damage(damage) {} + * int damage; + * }; + */ +template +class Event : public BaseEvent { + public: + typedef boost::shared_ptr> 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 connections_; +}; + +template +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 { + * void receive(const Explosion &explosion) { + * } + * }; + * + * ExplosionReceiver receiver; + * em.subscribe(receiver); + */ + template + void subscribe(Receiver &receiver) { + void (Receiver::*receive) (const E &) = &Receiver::receive; + auto sig = signal_for(E::family()); + auto wrapper = EventCallbackWrapper(boost::bind(receive, &receiver, _1)); + static_cast(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(10); + * + */ + template + void emit(Args && ... args) { + E event(args ...); + auto sig = signal_for(E::family()); + (*sig)(static_cast(&event)); + } + + private: + typedef boost::signal EventSignal; + typedef boost::shared_ptr EventSignalPtr; + + EventSignalPtr signal_for(int id) { + auto it = handlers_.find(id); + if (it == handlers_.end()) { + EventSignalPtr sig(boost::make_shared()); + handlers_.insert(make_pair(id, sig)); + return sig; + } + return it->second; + } + + // Functor used as a event signal callback that casts to E. + template + struct EventCallbackWrapper { + EventCallbackWrapper(boost::function callback) : callback(callback) {} + void operator () (const BaseEvent* event) { callback(*(static_cast(event))); } + boost::function callback; + }; + + boost::unordered_map 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 + * 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 + */ + +#include +#include +#include +#include +#include "entityx/Event.h" + +using namespace entity; +using namespace boost; + +struct Explosion : public Event { + Explosion(int damage) : damage(damage) {} + int damage; +}; + +struct ExplosionSystem : public Receiver { + void receive(const Explosion &explosion) { + damage_received += explosion.damage; + } + + int damage_received = 0; +}; + +TEST(EventManagerTest, TestEmitReceive) { + EventManager em; + ExplosionSystem explosion_system; + em.subscribe(explosion_system); + ASSERT_EQ(0, explosion_system.damage_received); + em.emit(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 + * 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 + */ + +#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 + * 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 + */ + +#pragma once + + +#include +#include +#include +#include +#include +#include "entityx/Entity.h" +#include "entityx/Event.h" + + +namespace entity { + +/** + * Base System class. Generally should not be directly used, instead see System. + */ +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 entities; +}; + + +/** + * Use this class when implementing Systems. + * + * struct MovementSystem : public System { + * void update(EntityManager &entities, EventManager &events, double dt) { + * // Do stuff to/with entities... + * } + * } + */ +template +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() + */ + template + boost::shared_ptr add(Args && ... args) { + boost::shared_ptr s(new S(args ...)); + systems_.insert(std::make_pair(S::family(), s)); + return s; + } + + template + boost::shared_ptr system() { + auto it = systems_.find(S::family()); + CHECK(it != systems_.end()); + return it == systems_.end() + ? boost::shared_ptr() + : boost::static_pointer_cast(it->second); + } + + /** + * Call the System::update() method for a registered system. + */ + template + void update(double dt) { + CHECK(initialized_) << "SystemManager::configure() not called"; + boost::shared_ptr s = system(); + 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> 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 + * 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 + */ + +#include +#include +#include +#include +#include "entityx/World.h" +#include "entityx/System.h" + + +using namespace std; +using namespace boost; +using namespace entity; + +struct Position : Component { + Position(float x = 0.0f, float y = 0.0f) : x(x), y(y) {} + + float x, y; +}; + +struct Direction : Component { + Direction(float x = 0.0f, float y = 0.0f) : x(x), y(y) {} + + float x, y; +}; + + +class MovementSystem : public System { + public: + MovementSystem(string label = "") : label(label) {} + + void update(EntityManager &es, EventManager &events, double) override { + auto entities = es.entities_with_components(); + Position *position; + Direction *direction; + for (auto entity : entities) { + es.unpack(entity, position, direction); + position->x += direction->x; + position->y += direction->y; + } + } + + string label; +}; + + +class TestWorld : public entity::World { + public: + std::vector 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(e, 1, 2); + if (i % 3 == 0) + entity_manager.assign(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("movement"); + world.sm().configure(); + + ASSERT_EQ("movement", world.sm().system()->label); +} + + +TEST_F(SystemManagerTest, TestApplySystem) { + world.sm().add(); + world.sm().configure(); + + world.sm().update(0.0); + Position *position; + Direction *direction; + for (auto entity : world.entities) { + world.em().unpack(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 + * 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 + */ + +#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 + * 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 + */ + +#pragma once + +#include +#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; +}; + +} -- cgit v1.2.3