ECSY is an ECS framework for JavaScript, and this is one of their examples written in C++ with Magnum and EnTT.
Usage
git clone https://gist.github.com/a0a9ab4fb4cd808dfd89bc6f2ee3e1af.git
cd ecsy1
mkdir build
cd build
cmake ..
msbuild ecsy1ECSY is an ECS framework for JavaScript, and this is one of their examples written in C++ with Magnum and EnTT.
Usage
git clone https://gist.github.com/a0a9ab4fb4cd808dfd89bc6f2ee3e1af.git
cd ecsy1
mkdir build
cd build
cmake ..
msbuild ecsy1| cmake_minimum_required(VERSION 3.1) | |
| project(ECSY1) | |
| # Add module path in case this is project root | |
| if(PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) | |
| set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/../../modules/" ${CMAKE_MODULE_PATH}) | |
| endif() | |
| find_package(Magnum REQUIRED | |
| GL | |
| MeshTools | |
| Primitives | |
| Shaders | |
| Sdl2Application) | |
| set_directory_properties(PROPERTIES CORRADE_USE_PEDANTIC_FLAGS ON) | |
| add_executable(ecsy1 ECSY1.cpp) | |
| target_link_libraries(magnum-primitives PRIVATE | |
| Magnum::Application | |
| Magnum::GL | |
| Magnum::Magnum | |
| Magnum::MeshTools | |
| Magnum::Primitives | |
| Magnum::Shaders) | |
| install(TARGETS ecsy1 DESTINATION ${MAGNUM_BINARY_INSTALL_DIR}) |
| /** ECSY with Magnum and EnTT - Part I | |
| A reimplementation of the first ECSY example | |
| https://ecsy.io/docs/#/?id=usage | |
| The example is divided into two parts. | |
| 1. True to the original | |
| 2. Optimised | |
| This is Part I | |
| */ | |
| #include <Magnum/GL/DefaultFramebuffer.h> | |
| #include <Magnum/GL/Mesh.h> | |
| #include <Magnum/GL/Renderer.h> | |
| #include <Magnum/MeshTools/Compile.h> | |
| #include <Magnum/Platform/Sdl2Application.h> | |
| #include <Magnum/Primitives/Circle.h> | |
| #include <Magnum/Primitives/Square.h> | |
| #include <Magnum/Shaders/Flat.h> | |
| #include <Magnum/Trade/MeshData2D.h> | |
| #include <Magnum/Timeline.h> | |
| #include "externals/entt.hpp" | |
| using namespace Magnum; | |
| using namespace Math::Literals; // For _rgbf | |
| static entt::registry World; | |
| const unsigned int NUM_ELEMENTS = 200; | |
| const float SPEED_MULTIPLIER = 300; | |
| const float SHAPE_SIZE = 20; | |
| const float SHAPE_HALF_SIZE = SHAPE_SIZE / 2; | |
| const unsigned int CANVAS_WIDTH = 1200; | |
| const unsigned int CANVAS_HEIGHT = 600; | |
| /** | |
| * -------------------------------------------------------------- | |
| * | |
| * Components | |
| * | |
| * These encapsulates all data in an ECS architecture | |
| * | |
| * -------------------------------------------------------------- | |
| */ | |
| struct Velocity { | |
| float x { 0 }; | |
| float y { 0 }; | |
| }; | |
| struct Position { | |
| float x { 0 }; | |
| float y { 0 }; | |
| }; | |
| enum class Shape { | |
| Box, Circle | |
| }; | |
| struct Renderable {}; // A data-less component, a.k.a. "tag" | |
| // This will act as a filter for the render system, | |
| // to ensure that entities that do have e.g. a Position | |
| // but aren't renderable - like a camera - isn't included. | |
| /** | |
| * --------------------------------------------------------- | |
| * | |
| * Systems | |
| * | |
| * These operate on the aforementioned data. They typically | |
| * don't carry state and thus won't need a constructor or class. | |
| * They utilise a "view" which is ECS jargon for a subset of all data. | |
| * The interesting bit being that it doesn't matter what entity is | |
| * associated with the data; the system only knows about the data | |
| * | |
| * --------------------------------------------------------- | |
| */ | |
| static void MovableSystem(const float delta, const float time) { | |
| World.view<Velocity, Position>().each([=](auto& velocity, auto& position) { | |
| position.x += velocity.x * delta; | |
| position.y += velocity.y * delta; | |
| if (position.x > CANVAS_WIDTH + SHAPE_HALF_SIZE) position.x = -SHAPE_HALF_SIZE; | |
| if (position.x < -SHAPE_HALF_SIZE) position.x = CANVAS_WIDTH + SHAPE_HALF_SIZE; | |
| if (position.y > CANVAS_HEIGHT + SHAPE_HALF_SIZE) position.y = -SHAPE_HALF_SIZE; | |
| if (position.y < -SHAPE_HALF_SIZE) position.y = CANVAS_HEIGHT + SHAPE_HALF_SIZE; | |
| }); | |
| } | |
| static void RendererSystem(const float delta, const float time) { | |
| GL::Renderer::setClearColor(0xffffff_rgbf); | |
| GL::defaultFramebuffer.clear(GL::FramebufferClear::Color | GL::FramebufferClear::Depth); | |
| /** | |
| * # Exercise for the reader | |
| * | |
| * There's room for optimisation here. | |
| * | |
| * The next few lines creates and uploads shaders and meshes | |
| * to the GPU on each frame. This is true to the ECSY example | |
| * but the more effective route would be to create these *once* | |
| * and reuse them across frames. | |
| * | |
| */ | |
| auto shader = Shaders::Flat2D{}; | |
| auto boxFill = MeshTools::compile(Primitives::circle2DSolid(20)); | |
| auto boxStroke = MeshTools::compile(Primitives::circle2DWireframe(20)); | |
| auto circleFill = MeshTools::compile(Primitives::squareSolid()); | |
| auto circleStroke = MeshTools::compile(Primitives::squareWireframe()); | |
| auto projectionMatrix = Matrix3::projection({CANVAS_WIDTH, CANVAS_HEIGHT}); | |
| World.view<Shape, Position, Renderable>().each([&](const auto& shape, | |
| const auto& position, | |
| const auto& renderable) { | |
| auto transformationMatrix = ( | |
| // Convert from OpenGL to a Canvas coordinate space. | |
| // OpenGL treats (0, 0) as the center of the screen, | |
| // but the maths from ECSY is based on HTML Canvas, | |
| // where (0, 0) is the lower left corner. | |
| Matrix3::translation(Vector2{CANVAS_WIDTH / -2.0f, CANVAS_HEIGHT / -2.0f}) * | |
| Matrix3::translation(Vector2{position.x, position.y}) * | |
| Matrix3::scaling(Vector2{SHAPE_HALF_SIZE, SHAPE_HALF_SIZE}) | |
| ); | |
| shader.setTransformationProjectionMatrix(projectionMatrix * transformationMatrix); | |
| if (shape == Shape::Box) { | |
| shader.setColor(0xe2736e_rgbf); | |
| boxFill.draw(shader); | |
| shader.setColor(0xb74843_rgbf); | |
| boxStroke.draw(shader); | |
| } | |
| else if (shape == Shape::Circle) { | |
| shader.setColor(0x39c495_rgbf); | |
| circleFill.draw(shader); | |
| shader.setColor(0x0b845b_rgbf); | |
| circleStroke.draw(shader); | |
| } | |
| }); | |
| } | |
| /** | |
| * ------------------------------------------------------- | |
| * | |
| * Helper functions | |
| * | |
| * ------------------------------------------------------- | |
| */ | |
| auto getRandom01() -> float { | |
| return static_cast<float>((double)rand() / (RAND_MAX + 1.0)); | |
| } | |
| auto getRandomVelocity() -> Velocity { | |
| return { | |
| SPEED_MULTIPLIER * (2 * getRandom01() - 1), | |
| SPEED_MULTIPLIER * (2 * getRandom01() - 1) | |
| }; | |
| } | |
| auto getRandomPosition() -> Position { | |
| return { | |
| getRandom01() * CANVAS_WIDTH, | |
| getRandom01() * CANVAS_HEIGHT | |
| }; | |
| } | |
| auto getRandomShape() -> Shape { | |
| return getRandom01() >= 0.5 ? Shape::Box : Shape::Circle; | |
| } | |
| /** | |
| * --------------------------------------------------------- | |
| * | |
| * Application | |
| * | |
| * --------------------------------------------------------- | |
| */ | |
| class BoxesAndCircles : public Platform::Application { | |
| public: | |
| explicit BoxesAndCircles(const Arguments& arguments); | |
| private: | |
| void drawEvent() override; | |
| // Use this to keep track of time and delta time | |
| Timeline _timeline; | |
| unsigned int _count { 0 }; | |
| }; | |
| BoxesAndCircles::BoxesAndCircles(const Arguments& arguments) : | |
| Platform::Application{ | |
| arguments, | |
| Configuration{}.setTitle("Boxes and Circles") | |
| .setSize({CANVAS_WIDTH, CANVAS_HEIGHT}) | |
| } | |
| { | |
| // Spawn NUM_ELEMENTS number of entities, with a random color | |
| for (int ii = 0; ii < NUM_ELEMENTS; ii++) { | |
| auto entity = World.create(); | |
| World.assign<Velocity>(entity, getRandomVelocity()); | |
| World.assign<Shape>(entity, getRandomShape()); | |
| World.assign<Position>(entity, getRandomPosition()); | |
| World.assign<Renderable>(entity); | |
| } | |
| _timeline.start(); | |
| } | |
| void BoxesAndCircles::drawEvent() { | |
| auto delta = _timeline.previousFrameDuration(); | |
| auto time = _timeline.previousFrameTime(); | |
| MovableSystem(delta, time); | |
| RendererSystem(delta, time); | |
| swapBuffers(); | |
| _timeline.nextFrame(); | |
| _count += 1; | |
| // Log current FPS, once every 60th event | |
| if (_count % 60 == 0) Debug() << 1.0f / delta << "fps"; | |
| redraw(); | |
| } | |
| int main(int argc, char** argv) { | |
| BoxesAndCircles app({argc, argv}); | |
| return app.exec(); | |
| } |