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 ecsy1
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 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(); | |
} |