Skip to content

Instantly share code, notes, and snippets.

@alanjfs
Created October 5, 2019 18:47
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save alanjfs/a0a9ab4fb4cd808dfd89bc6f2ee3e1af to your computer and use it in GitHub Desktop.
Save alanjfs/a0a9ab4fb4cd808dfd89bc6f2ee3e1af to your computer and use it in GitHub Desktop.
ECSY's documentation example, implemented with Magnum and EnTT
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();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment