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