Skip to content

Instantly share code, notes, and snippets.

@NekitoSP
Forked from skypjack/README.md
Created May 25, 2020 03:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NekitoSP/9dd0d79b17d2b35b7669408fd8ebf883 to your computer and use it in GitHub Desktop.
Save NekitoSP/9dd0d79b17d2b35b7669408fd8ebf883 to your computer and use it in GitHub Desktop.
Pong with EnTT and Magnum

Pong

With Magnum and EnTT. An adaptation of ecs_pong which is example material for Flecs, another ECS framework, developed as a learning exercise of both Magnum, EnTT and ECS in general. Runs at a solid 60 fps, can you believe it?

pong2

There are NOTE markings left in the code to highlight various differences and things I struggled with and would have liked to have done.


Usage

The one external dependency is Magnum, which you can find here.

git clone https://gist.github.com/a0a9ab4fb4cd808dfd89bc6f2ee3e1af.git
cd pong
mkdir build
cd build
cmake ..
msbuild pong.sln
/* Pong
--------------------------
______
|______|
o
______
|______|
--------------------------
A re-implementation of https://github.com/SanderMertens/ecs_pong
To some extent. There are pieces missing in that example, coming from the
underlying framework. Such as rigid bodies, collision generation and rendering.
This things were implemented "my way", using Magnum.
# Remainder
- I wasn't able to figure out how to generate collisions
containing a normal and "depth"; the amount of intersection
happening. That is used to push the ball out from the paddle,
which isn't happening here. The result is a ball intersecting
the paddle somewhat.
*/
#include <algorithm>
#include <string>
#include <chrono>
#include <thread>
#include <Magnum/Platform/Sdl2Application.h>
#include <Magnum/GL/DefaultFramebuffer.h>
#include <Magnum/GL/Mesh.h>
#include <Magnum/GL/Renderer.h>
#include <Magnum/MeshTools/Compile.h>
#include <Magnum/MeshTools/Transform.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;
#define BALL_RADIUS 10
#define PLAYER_HEIGHT 8
#define PLAYER_WIDTH 60
#define PLAYER_SPEED 400
#define PADDLE_AIM_C 1.5
#define BALL_SPEED 430.0
#define BALL_SERVE_SPEED (BALL_SPEED / 3.0)
#define BALL_BOOST 0.5
#define COURT_WIDTH 200
#define COURT_HEIGHT 300
// TODO: There's already a Key enum in Magnum;
// use that and get rid of this.
struct Key {
enum {
None = 0,
A,
D,
Left,
Right,
Count
};
};
using Entity = entt::registry::entity_type;
// Global references to entities
//
// TODO: This isn't very nice. E.g. introducing a second player would
// mean refactoring most of the code, due to the player itself
// being hardcoded and not generalised.
//
Entity ball;
Entity player;
Entity ai;
/**
* --------------------------------------------------------------
*
* Components
*
* These encapsulates all data in an ECS architecture
*
* --------------------------------------------------------------
*/
typedef std::string Name;
struct Circle {
int radius;
};
struct Rectangle {
int width, height;
};
struct Collision {
double nx, ny;
// struct { double x, y; } normal;
double depth;
Entity entity_1, entity_2;
};
struct Position {
double x, y;
};
struct TargetPosition : public Position {
// Called simply "target" in the original example
// But really, it's the target position, before physics
};
struct Velocity {
double x, y;
};
struct Input {
int32_t x, y, z;
uint8_t keys[Key::Count];
Input() : x(0), y(0), z(0) {
for (uint32_t ii = 0; ii < Key::Count; ++ii) {
keys[ii] = Key::None;
}
}
};
/**
* ---------------------------------------------------------
*
* 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 PlayerInput() {
World.view<Input, TargetPosition>().each([=](auto& input, auto& target) {
// Move paddle using A/D or Left/Right
if (input.keys[Key::A] || input.keys[Key::Left]) {
target.x = -PLAYER_SPEED;
}
else if (input.keys[Key::D] || input.keys[Key::Right]) {
target.x = PLAYER_SPEED;
}
else {
target.x = 0;
}
});
}
static void AiThink() {
auto& ball_pos = World.get<Position>(ball);
auto& player_pos = World.get<Position>(player);
auto& ai_pos = World.get<Position>(ai);
double target_x = ball_pos.x + (player_pos.x > 0
? static_cast<double>(PLAYER_WIDTH) / 2.5 + BALL_RADIUS
: -static_cast<double>(PLAYER_WIDTH) / 2.5 + BALL_RADIUS
);
auto& target = World.get<TargetPosition>(ai);
target.x = target_x - ai_pos.x;
}
static void MovePaddle(double delta_time) {
World.view<Position, TargetPosition>().each([=](auto& pos, const auto& target) {
double abs_target = fabs(target.x);
double dir = abs_target / target.x;
double movement = (abs_target > PLAYER_WIDTH * delta_time)
? PLAYER_SPEED * dir
: target.x;
pos.x += movement * delta_time;
// Keep paddle in the court
auto court = static_cast<double>(COURT_WIDTH);
pos.x = std::clamp(pos.x, -court + PLAYER_WIDTH, court - PLAYER_WIDTH);
});
}
static void MoveBall(double delta_time) {
World.view<Position, Velocity>().each([=](auto& pos, const auto& velocity) {
pos.x += velocity.x * delta_time;
pos.y += velocity.y * delta_time;
});
}
static void ComputeCollisions() {
auto& ball_pos = World.get<Position>(ball);
auto& ball_vel = World.get<Velocity>(ball);
/*
o If the ball is within the surface area
o of the paddle, then we've got a hit.
______o_____
|____________| ___
/ \
______________________/_____\_
| \ ^ / |
| normal / \_|_/ | depth
| + |
| |
|______________________________|
*/
World.view<Position, Rectangle>().each([=](auto entity, auto& pos, auto& rectangle) {
auto horizontal = std::clamp(ball_pos.x, pos.x - rectangle.width, pos.x + rectangle.width) == ball_pos.x;
auto vertical = std::clamp(ball_pos.y, pos.y - rectangle.height, pos.y + rectangle.height) == ball_pos.y;
if (vertical && horizontal) {
// TODO: Compute these
// I struggled here; to figure out the normal and depth of an intersection.
// Looks simple! But I'm at a loss, and moved on. :(
auto nx = 0.0;
auto ny = 0.0;
auto depth = 0.0;
World.assign<Collision>(entity, nx, ny, depth, ball, entity);
}
});
}
static void HandleCollisions() {
auto& ball_pos = World.get<Position>(ball);
auto& ball_vel = World.get<Velocity>(ball);
World.view<Collision>().each([&](auto entity, const auto& col) {
/* Move the ball out of the paddle */
ball_pos.y -= col.ny * col.depth;
/* Use the paddle position to determine where the ball hit */
const auto& paddle_pos = World.get<Position>(col.entity_2);
double angle = PADDLE_AIM_C * (ball_pos.x - paddle_pos.x) / PLAYER_WIDTH;
double abs_angle = fabs(angle);
ball_vel.x = sin(angle) * BALL_SPEED;
ball_vel.y = cos(angle) * BALL_SPEED;
if (abs_angle > 0.6) {
ball_vel.x *= (1 + abs_angle * BALL_BOOST);
ball_vel.y *= (1 + abs_angle * BALL_BOOST);
}
if (ball_pos.y < paddle_pos.y) {
ball_vel.y *= -1;
}
World.remove<Collision>(entity);
});
}
static void BounceWalls() {
World.view<Position, Velocity>().each([](auto& pos, auto& vel) {
// NOTE: The only entity with a velocity is the ball,
// so this loops iterates only once for that one entity.
auto courtWidth = static_cast<double>(COURT_WIDTH);
auto courtHeight = static_cast<double>(COURT_HEIGHT);
if (std::clamp(pos.x, -courtWidth + BALL_RADIUS, courtWidth - BALL_RADIUS) != pos.x) {
vel.x *= -1.0; // Reverse x velocity if ball hits a vertical wall
}
/* If ball hits horizontal wall, reset the game */
int ceiling = std::clamp(pos.y, -courtHeight, courtHeight) != pos.y;
if (ceiling) {
pos = Position{0.0, 0.0};
vel = Velocity{0.0, BALL_SPEED * -ceiling};
}
});
}
static void RendererSystem() {
GL::Renderer::setClearColor(0x333333_rgbf);
GL::defaultFramebuffer.clear(GL::FramebufferClear::Color | GL::FramebufferClear::Depth);
GL::defaultFramebuffer.setViewport({{}, {COURT_WIDTH * 2, COURT_HEIGHT * 2}});
auto shader = Shaders::Flat2D{};
shader.setColor(0xffffff_rgbf);
auto paddle = MeshTools::compile(Primitives::squareSolid());
auto ball = MeshTools::compile(Primitives::circle2DSolid(10));
auto projection = Matrix3::projection({COURT_WIDTH * 2, COURT_HEIGHT * 2});
// Paddles
World.view<Rectangle, Position>().each([&](const auto& rectangle,
const auto& position) {
auto model = Matrix3::translation(Vector2(position.x, position.y))
* Matrix3::scaling(Vector2(rectangle.width, rectangle.height));
shader.setTransformationProjectionMatrix(projection * model);
paddle.draw(shader);
});
// Ball
World.view<Circle, Position>().each([&](const auto& circle,
const auto& position) {
auto model = Matrix3::translation(Vector2(position.x, position.y))
* Matrix3::scaling(Vector2(circle.radius / 2.0));
shader.setTransformationProjectionMatrix(projection * model);
ball.draw(shader);
});
}
static void LogInput() {
World.view<Input, TargetPosition>().each([](const auto& input, const auto& target) {
Debug() << "TargetPosition" << target.x;
});
World.view<Name, Position, TargetPosition>().each([](const auto& name, const auto& position, const auto& target) {
Debug() << name << target.x << position.x;
});
}
/**
* ---------------------------------------------------------
*
* Application
*
* ---------------------------------------------------------
*/
class Pong : public Platform::Application {
public:
explicit Pong(const Arguments& arguments);
private:
void drawEvent() override;
void keyPressEvent(KeyEvent& event) override;
void keyReleaseEvent(KeyEvent& event) override;
// Use this to keep track of time and delta time
Timeline _timeline;
unsigned int _count { 0 };
};
Pong::Pong(const Arguments& arguments) :
Platform::Application{
arguments,
Configuration{}.setTitle("Pong")
.setSize({COURT_WIDTH * 2, COURT_HEIGHT * 2})
}
{
this->setSwapInterval(1);
// Defined globally
ball = World.create();
player = World.create();
ai = World.create();
World.assign<Position>(ball, 0.0, 0.0);
World.assign<Velocity>(ball, 100.0, BALL_SERVE_SPEED * 2);
World.assign<Circle>(ball, BALL_RADIUS);
World.assign<Name>(player, "Player");
World.assign<Position>(player, 0.0, COURT_HEIGHT - PLAYER_HEIGHT - 20.0);
World.assign<Input>(player);
World.assign<TargetPosition>(player, 0.0, 0.0);
World.assign<Rectangle>(player, PLAYER_WIDTH, PLAYER_HEIGHT);
World.assign<Name>(ai, "AI");
World.assign<Position>(ai, 0.0, -COURT_HEIGHT + PLAYER_HEIGHT + 20.0);
World.assign<TargetPosition>(ai, 0.0, 0.0);
World.assign<Rectangle>(ai, PLAYER_WIDTH, PLAYER_HEIGHT);
_timeline.start();
}
void Pong::keyPressEvent(KeyEvent& event) {
World.view<Input>().each([&](auto& input) {
if (event.key() == KeyEvent::Key::D) input.keys[Key::D] = 1;
if (event.key() == KeyEvent::Key::A) input.keys[Key::A] = 1;
if (event.key() == KeyEvent::Key::Left) input.keys[Key::Left] = 1;
if (event.key() == KeyEvent::Key::Right) input.keys[Key::Right] = 1;
});
}
void Pong::keyReleaseEvent(KeyEvent& event) {
World.view<Input>().each([&](auto& input) {
if (event.key() == KeyEvent::Key::D) input.keys[Key::D] = 0;
if (event.key() == KeyEvent::Key::A) input.keys[Key::A] = 0;
if (event.key() == KeyEvent::Key::Left) input.keys[Key::Left] = 0;
if (event.key() == KeyEvent::Key::Right) input.keys[Key::Right] = 0;
});
}
void Pong::drawEvent() {
auto delta_time = _timeline.previousFrameDuration();
auto time = _timeline.previousFrameTime();
PlayerInput();
AiThink();
MovePaddle(delta_time);
MoveBall(delta_time);
ComputeCollisions();
HandleCollisions();
BounceWalls();
RendererSystem();
// LogInput();
swapBuffers();
_timeline.nextFrame();
_count += 1;
// Log current FPS, once every 60th event
if (_count % 60 == 0) Debug() << 1.0f / delta_time << "fps";
redraw();
}
int main(int argc, char** argv) {
Pong app({argc, argv});
return app.exec();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment