/** * This is an example of using EntityX. * * It is an SFML2 application that spawns 100 random circles on a 2D plane * moving in random directions. If two circles collide they will explode and * emit particles. * * This illustrates a bunch of EC/EntityX concepts: * * - Separation of data via components. * - Separation of logic via systems. * - Use of events (colliding bodies trigger a CollisionEvent). * * Compile with: * * c++ -I.. -O3 -std=c++11 -Wall -lsfml-system -lsfml-window -lsfml-graphics -lentityx example.cc -o example */ #include #include #include #include #include #include #include #include #include #include #include using std::cerr; using std::cout; using std::endl; namespace ex = entityx; float r(int a, float b = 0) { return static_cast(std::rand() % (a * 1000) + b * 1000) / 1000.0; } struct Body { Body(const sf::Vector2f &position, const sf::Vector2f &direction, float rotationd = 0.0) : position(position), direction(direction), rotationd(rotationd) {} sf::Vector2f position; sf::Vector2f direction; float rotation = 0.0, rotationd; }; using Renderable = std::shared_ptr; struct Particle { explicit Particle(sf::Color colour, float radius, float duration) : colour(colour), radius(radius), alpha(colour.a), d(colour.a / duration) {} sf::Color colour; float radius, alpha, d; }; struct Collideable { explicit Collideable(float radius) : radius(radius) {} float radius; }; // Emitted when two entities collide. struct CollisionEvent { CollisionEvent(ex::Entity left, ex::Entity right) : left(left), right(right) {} ex::Entity left, right; }; class SpawnSystem : public ex::System { public: explicit SpawnSystem(sf::RenderTarget &target, int count) : size(target.getSize()), count(count) {} void update(ex::EntityManager &es, ex::EventManager &events, ex::TimeDelta dt) override { int c = 0; ex::ComponentHandle collideable; es.each([&](ex::Entity entity, Collideable&) { ++c; }); for (int i = 0; i < count - c; i++) { ex::Entity entity = es.create(); // Mark as collideable (explosion particles will not be collideable). collideable = entity.assign(r(10, 5)); // "Physical" attributes. entity.assign( sf::Vector2f(r(size.x), r(size.y)), sf::Vector2f(r(100, -50), r(100, -50))); // Shape to apply to entity. Renderable shape(new sf::CircleShape(collideable->radius)); shape->setFillColor(sf::Color(r(128, 127), r(128, 127), r(128, 127))); shape->setOrigin(collideable->radius, collideable->radius); entity.assign(shape); } } private: sf::Vector2u size; int count; }; // Updates a body's position and rotation. struct BodySystem : public ex::System { void update(ex::EntityManager &es, ex::EventManager &events, ex::TimeDelta dt) override { es.each([dt](ex::Entity entity, Body &body) { body.position += body.direction * static_cast(dt); body.rotation += body.rotationd * dt; }); }; }; // Bounce bodies off the edge of the screen. class BounceSystem : public ex::System { public: explicit BounceSystem(sf::RenderTarget &target) : size(target.getSize()) {} void update(ex::EntityManager &es, ex::EventManager &events, ex::TimeDelta dt) override { es.each([this](ex::Entity entity, Body &body) { if (body.position.x + body.direction.x < 0 || body.position.x + body.direction.x >= size.x) body.direction.x = -body.direction.x; if (body.position.y + body.direction.y < 0 || body.position.y + body.direction.y >= size.y) body.direction.y = -body.direction.y; }); } private: sf::Vector2u size; }; // Determines if two Collideable bodies have collided. If they have it emits a // CollisionEvent. This is used by ExplosionSystem to create explosion // particles, but it could be used by a SoundSystem to play an explosion // sound, etc.. // // Uses a fairly rudimentary 2D partition system, but performs reasonably well. class CollisionSystem : public ex::System { static const int PARTITIONS = 200; struct Candidate { sf::Vector2f position; float radius; ex::Entity entity; }; public: explicit CollisionSystem(sf::RenderTarget &target) : size(target.getSize()) { size.x = size.x / PARTITIONS + 1; size.y = size.y / PARTITIONS + 1; } void update(ex::EntityManager &es, ex::EventManager &events, ex::TimeDelta dt) override { reset(); collect(es); collide(events); }; private: std::vector> grid; sf::Vector2u size; void reset() { grid.clear(); grid.resize(size.x * size.y); } void collect(ex::EntityManager &entities) { entities.each([this](ex::Entity entity, Body &body, Collideable &collideable) { unsigned int left = static_cast(body.position.x - collideable.radius) / PARTITIONS, top = static_cast(body.position.y - collideable.radius) / PARTITIONS, right = static_cast(body.position.x + collideable.radius) / PARTITIONS, bottom = static_cast(body.position.y + collideable.radius) / PARTITIONS; Candidate candidate {body.position, collideable.radius, entity}; unsigned int slots[4] = { left + top * size.x, right + top * size.x, left + bottom * size.x, right + bottom * size.x, }; grid[slots[0]].push_back(candidate); if (slots[0] != slots[1]) grid[slots[1]].push_back(candidate); if (slots[1] != slots[2]) grid[slots[2]].push_back(candidate); if (slots[2] != slots[3]) grid[slots[3]].push_back(candidate); }); } void collide(ex::EventManager &events) { for (const std::vector &candidates : grid) { for (const Candidate &left : candidates) { for (const Candidate &right : candidates) { if (left.entity == right.entity) continue; if (collided(left, right)) events.emit(left.entity, right.entity); } } } } float length(const sf::Vector2f &v) { return std::sqrt(v.x * v.x + v.y * v.y); } bool collided(const Candidate &left, const Candidate &right) { return length(left.position - right.position) < left.radius + right.radius; } }; class ParticleSystem : public ex::System { public: void update(ex::EntityManager &es, ex::EventManager &events, ex::TimeDelta dt) override { es.each([dt](ex::Entity entity, Particle &particle) { particle.alpha -= particle.d * dt; if (particle.alpha <= 0) { entity.destroy(); } else { particle.colour.a = particle.alpha; } }); } }; class ParticleRenderSystem : public ex::System { public: explicit ParticleRenderSystem(sf::RenderTarget &target) : target(target) {} void update(ex::EntityManager &es, ex::EventManager &events, ex::TimeDelta dt) override { sf::VertexArray vertices(sf::Quads); es.each([&vertices](ex::Entity entity, Particle &particle, Body &body) { const float r = particle.radius; vertices.append(sf::Vertex(body.position + sf::Vector2f(-r, -r), particle.colour)); vertices.append(sf::Vertex(body.position + sf::Vector2f(r, -r), particle.colour)); vertices.append(sf::Vertex(body.position + sf::Vector2f(r, r), particle.colour)); vertices.append(sf::Vertex(body.position + sf::Vector2f(-r, r), particle.colour)); }); target.draw(vertices); } private: sf::RenderTarget ⌖ }; // For any two colliding bodies, destroys the bodies and emits a bunch of bodgy explosion particles. class ExplosionSystem : public ex::System, public ex::Receiver { public: void configure(ex::EventManager &events) override { events.subscribe(*this); } void update(ex::EntityManager &es, ex::EventManager &events, ex::TimeDelta dt) override { for (ex::Entity entity : collided) { emit_particles(es, entity); entity.destroy(); } collided.clear(); } void emit_particles(ex::EntityManager &es, ex::Entity entity) { ex::ComponentHandle body = entity.component(); ex::ComponentHandle renderable = entity.component(); ex::ComponentHandle collideable = entity.component(); sf::Color colour = (*renderable)->getFillColor(); colour.a = 200; float area = (M_PI * collideable->radius * collideable->radius) / 3.0; for (int i = 0; i < area; i++) { ex::Entity particle = es.create(); float rotationd = r(720, 180); if (std::rand() % 2 == 0) rotationd = -rotationd; float offset = r(collideable->radius, 1); float angle = r(360) * M_PI / 180.0; particle.assign( body->position + sf::Vector2f(offset * cos(angle), offset * sin(angle)), body->direction + sf::Vector2f(offset * 2 * cos(angle), offset * 2 * sin(angle)), rotationd); float radius = r(3, 1); particle.assign(colour, radius, radius / 2); } } void receive(const CollisionEvent &collision) { // Events are immutable, so we can't destroy the entities here. We defer // the work until the update loop. collided.insert(collision.left); collided.insert(collision.right); } private: std::unordered_set collided; }; // Render all Renderable entities and draw some informational text. class RenderSystem :public ex::System { public: explicit RenderSystem(sf::RenderTarget &target, sf::Font &font) : target(target) { text.setFont(font); text.setPosition(sf::Vector2f(2, 2)); text.setCharacterSize(18); text.setColor(sf::Color::White); } void update(ex::EntityManager &es, ex::EventManager &events, ex::TimeDelta dt) override { es.each([this](ex::Entity entity, Body &body, Renderable &renderable) { renderable->setPosition(body.position); renderable->setRotation(body.rotation); target.draw(*renderable.get()); }); last_update += dt; frame_count++; if (last_update >= 0.5) { std::ostringstream out; const double fps = frame_count / last_update; out << es.size() << " entities (" << static_cast(fps) << " fps)"; text.setString(out.str()); last_update = 0.0; frame_count = 0.0; } target.draw(text); } private: double last_update = 0.0; double frame_count = 0.0; sf::RenderTarget ⌖ sf::Text text; }; class Application : public ex::EntityX { public: explicit Application(sf::RenderTarget &target, sf::Font &font) { systems.add(target, 500); systems.add(); systems.add(target); systems.add(target); systems.add(); systems.add(); systems.add(target, font); systems.add(target); systems.configure(); } void update(ex::TimeDelta dt) { systems.update_all(dt); } }; int main() { std::srand(std::time(nullptr)); sf::RenderWindow window(sf::VideoMode::getDesktopMode(), "EntityX Example", sf::Style::Fullscreen); sf::Font font; if (!font.loadFromFile("LiberationSans-Regular.ttf")) { cerr << "error: failed to load LiberationSans-Regular.ttf" << endl; return 1; } Application app(window, font); sf::Clock clock; while (window.isOpen()) { sf::Event event; while (window.pollEvent(event)) { switch (event.type) { case sf::Event::Closed: case sf::Event::KeyPressed: window.close(); break; default: break; } } window.clear(); sf::Time elapsed = clock.restart(); app.update(elapsed.asSeconds()); window.display(); } }