intial implementation

This commit is contained in:
Joseph Aquino 2025-08-28 23:32:06 -04:00
parent 87e1f863be
commit e5e7028554
6 changed files with 627 additions and 64 deletions

80
include/Game.h Normal file
View File

@ -0,0 +1,80 @@
#pragma once
#include <vector>
#include <Util.h>
#include <SFML/Graphics.hpp>
#include <imgui-SFML.h>
#include <imgui.h>
#include <Random.h>
struct Ball
{
Ball(sf::Vector2f, sf::Vector2f);
sf::Vector2f pos;
sf::Vector2f previousPos{};
sf::Vector2f velocity;
Color color{sf::Color::Red};
bool alive{true};
};
struct Brick
{
Brick(sf::Vector2f);
sf::Vector2f pos;
Color color{sf::Color::White};
bool alive{true};
};
struct Player
{
Player();
sf::Vector2f pos;
sf::Vector2f previousPos{};
Color color{sf::Color::White};
float score{};
int lives{3};
bool left{false};
bool right{false};
};
class Game
{
public:
Game();
void run();
private:
void imgui();
void render();
void input();
void collision();
void movement();
void updateEntities();
void checkEndGame();
private:
sf::Clock clock;
sf::RenderWindow window;
Player player;
std::vector<Brick> bricks;
std::vector<Brick> specialBricks;
std::vector<Ball> balls;
std::vector<sf::Vector2f> ballsToAdd;
sf::CircleShape tempCircle;
sf::RectangleShape tempRect;
};

74
include/Random.h Normal file
View File

@ -0,0 +1,74 @@
#pragma once
#ifndef RANDOM_MT_H
#define RANDOM_MT_H
#include <chrono>
#include <random>
// This header-only Random namespace implements a self-seeding Mersenne Twister.
// Requires C++17 or newer.
// It can be #included into as many code files as needed (The inline keyword avoids ODR violations)
// Freely redistributable, courtesy of learncpp.com (https://www.learncpp.com/cpp-tutorial/global-random-numbers-random-h/)
namespace Random
{
// Returns a seeded Mersenne Twister
// Note: we'd prefer to return a std::seed_seq (to initialize a std::mt19937), but std::seed can't be copied, so it can't be returned by value.
// Instead, we'll create a std::mt19937, seed it, and then return the std::mt19937 (which can be copied).
inline std::mt19937 generate()
{
std::random_device rd{};
// Create seed_seq with clock and 7 random numbers from std::random_device
std::seed_seq ss{
static_cast<std::seed_seq::result_type>(std::chrono::steady_clock::now().time_since_epoch().count()),
rd(), rd(), rd(), rd(), rd(), rd(), rd() };
return std::mt19937{ ss };
}
// Here's our global std::mt19937 object.
// The inline keyword means we only have one global instance for our whole program.
inline std::mt19937 mt{ generate() }; // generates a seeded std::mt19937 and copies it into our global object
// Generate a random int between [min, max] (inclusive)
// * also handles cases where the two arguments have different types but can be converted to int
inline int get(int min, int max)
{
return std::uniform_int_distribution{min, max}(mt);
}
// The following function templates can be used to generate random numbers in other cases
// See https://www.learncpp.com/cpp-tutorial/function-template-instantiation/
// You can ignore these if you don't understand them
// Generate a random value between [min, max] (inclusive)
// * min and max must have the same type
// * return value has same type as min and max
// * Supported types:
// * short, int, long, long long
// * unsigned short, unsigned int, unsigned long, or unsigned long long
// Sample call: Random::get(1L, 6L); // returns long
// Sample call: Random::get(1u, 6u); // returns unsigned int
template <typename T>
T get(T min, T max)
{
return std::uniform_int_distribution<T>{min, max}(mt);
}
// Generate a random value between [min, max] (inclusive)
// * min and max can have different types
// * return type must be explicitly specified as a template argument
// * min and max will be converted to the return type
// Sample call: Random::get<std::size_t>(0, 6); // returns std::size_t
// Sample call: Random::get<std::size_t>(0, 6u); // returns std::size_t
// Sample call: Random::get<std::int>(0, 6u); // returns int
template <typename R, typename S, typename T>
R get(S min, T max)
{
return get<R>(static_cast<R>(min), static_cast<R>(max));
}
}
#endif

54
include/Util.h Normal file
View File

@ -0,0 +1,54 @@
#pragma once
#include <SFML/Graphics.hpp>
struct Color
{
Color() = default;
Color(float r_in, float g_in, float b_in, float a_in = 255)
: r(r_in)
, g(g_in)
, b(b_in)
, a(a_in)
{ }
Color(sf::Color color_in)
{
r = (float)color_in.r / 255.f;
g = (float)color_in.g / 255.f;
b = (float)color_in.b / 255.f;
a = (float)color_in.a / 255.f;
}
float r{};
float g{};
float b{};
float a{};
sf::Color sfml()
{
return sf::Color{uint8_t(r * 255), uint8_t(g * 255), uint8_t(b * 255), uint8_t(a * 255)};
}
float* imgui()
{
return &r;
}
};
namespace global
{
inline sf::Vector2f playerSize{500, 25};
inline sf::Vector2f playerHalfSize{playerSize / 2.f};
inline sf::Vector2f brickSize{50, 10};
inline sf::Vector2f brickHalfSize{brickSize / 2.f};
inline int totalSpecialBricks{3};
inline int rowSize{8};
inline int columnSize{5};
inline int totalBricks{rowSize * columnSize};
inline float ballRadius{5.0f};
inline float ballMaxSpeed{10};
inline float playerSpeed{10};
inline sf::Vector2f playerStartPos{700,700};
inline bool windowCollasped{false};
}

View File

@ -29,18 +29,20 @@ workspace "breakout"
cdialect "C17" cdialect "C17"
systemversion "latest" systemversion "latest"
kind "WindowedApp" kind "WindowedApp"
targetname "breakout"
files
{
"src/**.cpp",
"include/**.h",
"include/**.hpp",
"vendor/imgui/imgui.cpp",
"vendor/imgui/imgui_draw.cpp",
"vendor/imgui/imgui_tables.cpp",
"vendor/imgui/imgui_widgets.cpp",
"vendor/imgui/imgui-SFML.cpp"
}
files
{
"src/**.cpp",
"include/**.h",
"include/**.hpp",
"vendor/imgui/imgui.cpp",
"vendor/imgui/imgui_draw.cpp",
"vendor/imgui/imgui_tables.cpp",
"vendor/imgui/imgui_widgets.cpp",
"vendor/imgui/imgui-SFML.cpp"
}
--visual studio-- --visual studio--
filter {"action:vs*", "system:windows"} filter {"action:vs*", "system:windows"}
targetdir (vs_bindir) targetdir (vs_bindir)
@ -63,7 +65,7 @@ workspace "breakout"
{ {
"src", "src",
include_dir, include_dir,
vs_sfmldir .. "/include", sfmldir .. "/include",
imguidir imguidir
} }
@ -140,7 +142,6 @@ workspace "breakout"
defines {"LOG_ENABLE", "GAME_DEBUG"} defines {"LOG_ENABLE", "GAME_DEBUG"}
symbols "on" symbols "on"
runtime "Debug" runtime "Debug"
targetname "breakout"
filter "configurations:release" filter "configurations:release"
defines {"GAME_RELEASE"} defines {"GAME_RELEASE"}
@ -148,4 +149,3 @@ workspace "breakout"
inlining "Auto" inlining "Auto"
symbols "off" symbols "off"
runtime "Release" runtime "Release"
targetname "breakout"

400
src/Game.cpp Normal file
View File

@ -0,0 +1,400 @@
#include <Game.h>
#include <algorithm>
#include <iostream>
sf::Vector2f getOverlap(sf::FloatRect first, sf::FloatRect second)
{
sf::Vector2f result;
sf::Vector2f delta;
delta.x = std::abs(second.position.x - first.position.x);
delta.y = std::abs(second.position.y - first.position.y);
result.x = (first.size.x + second.size.x) - delta.x;
result.y = (first.size.y + second.size.y) - delta.y;
return result;
}
sf::Vector2f velocityInRandomDir(float speed_in)
{
bool upOrDown{(bool)Random::get(0,1)};
sf::Angle angle = sf::degrees((float)upOrDown ? Random::get(225, 315) : Random::get(45, 135));
return {speed_in * std::cos(angle.asRadians()), speed_in * std::sin(angle.asRadians())};
}
Ball::Ball(sf::Vector2f position_in, sf::Vector2f vel_in)
: velocity(vel_in)
, pos(position_in)
{ }
Brick::Brick(sf::Vector2f position_in)
: pos(position_in)
{ }
Player::Player()
: pos(global::playerStartPos)
{ }
Game::Game()
: player()
, window({sf::VideoMode({ 1920u, 1080u }), "breakout"})
{
window.setFramerateLimit(60);
if (!ImGui::SFML::Init(window))
return;
tempCircle.setOrigin({global::ballRadius, global::ballRadius});
bricks.reserve(global::totalBricks);
bricks.emplace_back(sf::Vector2f{200, 100});
specialBricks.reserve(global::totalSpecialBricks);
balls.reserve(10);
ballsToAdd.reserve(10);
ballsToAdd.emplace_back(global::playerStartPos.x, global::playerStartPos.y - 300);
}
void Game::input()
{
while (const std::optional event = window.pollEvent())
{
ImGui::SFML::ProcessEvent(window, *event);
if (event->is<sf::Event::Closed>())
{
window.close();
}
if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>())
{
switch (keyPressed->scancode)
{
case sf::Keyboard::Scan::Escape:
window.close();
break;
case sf::Keyboard::Scan::Left:
case sf::Keyboard::Scan::A:
player.left = true;
break;
case sf::Keyboard::Scan::Right:
case sf::Keyboard::Scan::D:
player.right = true;
break;
default:
break;
}
}
if (const auto* keyReleased = event->getIf<sf::Event::KeyReleased>())
{
switch (keyReleased->scancode)
{
case sf::Keyboard::Scan::Left:
case sf::Keyboard::Scan::A:
player.left = false;
break;
case sf::Keyboard::Scan::Right:
case sf::Keyboard::Scan::D:
player.right = false;
break;
default:
break;
}
}
}
}
void Game::collision()
{
// declaring at top so it can be used in and out of the ball collision loop
const sf:: FloatRect playerBounds = {player.pos, global::playerHalfSize};
const sf:: FloatRect previousPlayerBounds = {player.previousPos, global::playerHalfSize};
//ball collisions
for (auto& ball : balls)
{
const sf::FloatRect ballBounds = {ball.pos, {global::ballRadius, global::ballRadius}};
const sf::FloatRect previousBallBounds = {ball.previousPos, {global::ballRadius, global::ballRadius}};
for (auto& brick : bricks)
{
const sf::FloatRect brickBounds = {brick.pos, global::brickHalfSize};
auto intersect = getOverlap(ballBounds, brickBounds);
auto previousIntersect = getOverlap(previousBallBounds, brickBounds);
if (intersect.x > 0 && previousIntersect.x <= 0 && intersect.y > 0)
{
brick.alive = false;
ball.velocity.x *= -1;
if (ball.pos.x < player.pos.x)
{
ball.pos.x -= intersect.x;
}
else
{
ball.pos.x += intersect.x;
}
}
// coming from y direction
if (intersect.y > 0 && previousIntersect.y <= 0 && intersect.x > 0)
{
brick.alive = false;
ball.velocity.y *= -1;
if (ball.pos.y < player.pos.y)
{
ball.pos.y -= intersect.y;
}
else
{
ball.pos.y += intersect.y;
}
}
}
for (auto& brick : specialBricks)
{
const sf::FloatRect brickBounds = {brick.pos, global::brickHalfSize};
const auto intersect = getOverlap(ballBounds, brickBounds);
const auto previousIntersect = getOverlap(previousBallBounds, brickBounds);
bool spawnExtraBall{false};
// coming from x direction
if (intersect.x > 0 && previousIntersect.x <= 0 && intersect.y > 0)
{
brick.alive = false;
spawnExtraBall = true;
ball.velocity.x *= -1;
if (ball.pos.x < player.pos.x)
{
ball.pos.x -= intersect.x;
}
else
{
ball.pos.x += intersect.x;
}
}
// coming from y direction
if (intersect.y > 0 && previousIntersect.y <= 0 && intersect.x > 0)
{
brick.alive = false;
spawnExtraBall = true;
ball.velocity.y *= -1;
if (ball.pos.y < player.pos.y)
{
ball.pos.y -= intersect.y;
}
else
{
ball.pos.y += intersect.y;
}
}
if (spawnExtraBall)
{
ballsToAdd.emplace_back(ball.pos);
}
}
//wall collide
if (ballBounds.position.y + ballBounds.size.y > window.getSize().y)
{
ball.alive = false;
}
if (ball.pos.x - global::ballRadius <= 0)
{
ball.pos.x = global::ballRadius;
ball.velocity.x *= -1;
}
if (ball.pos.x + global::ballRadius >= window.getSize().x)
{
ball.pos.x = window.getSize().x - global::ballRadius;
ball.velocity.x *= -1;
}
if (ball.pos.y - global::ballRadius < 0)
{
ball.pos.y = global::ballRadius;
ball.velocity.y *= -1;
}
// player collide
auto intersect = getOverlap(ballBounds, playerBounds);
auto previousIntersect = getOverlap(previousBallBounds, previousPlayerBounds);
// coming from x direction
if (intersect.x > 0 && previousIntersect.x <= 0 && intersect.y > 0)
{
ball.velocity.x *= -1;
if (ball.pos.x < player.pos.x)
{
ball.pos.x -= intersect.x;
}
else
{
ball.pos.x += intersect.x;
}
}
// coming from y direction
if (intersect.y > 0 && previousIntersect.y <= 0 && intersect.x > 0)
{
ball.velocity.y *= -1;
if (ball.pos.y < player.pos.y)
{
ball.pos.y -= intersect.y;
}
else
{
ball.pos.y += intersect.y;
}
}
}
//player wall collide
if (playerBounds.position.x + playerBounds.size.x > window.getSize().x)
{
player.pos.x = window.getSize().x - global::playerHalfSize.x;
}
if (playerBounds.position.x < global::playerHalfSize.x)
{
player.pos.x = global::playerHalfSize.x;
}
}
void Game::updateEntities()
{
balls.erase(std::remove_if(balls.begin(), balls.end(), [](const Ball& ball){ return !ball.alive; }), balls.end());
bricks.erase(std::remove_if(bricks.begin(), bricks.end(), [](const Brick& brick){ return !brick.alive; }), bricks.end());
const auto limit = ballsToAdd.size();
for (int i = 0; i < limit; i++)
{
balls.emplace_back(ballsToAdd[i], velocityInRandomDir(global::ballMaxSpeed));
ballsToAdd[i] = {0,0};
}
ballsToAdd.clear();
}
void Game::checkEndGame()
{
if (balls.empty())
{
player.lives--;
ballsToAdd.emplace_back(sf::Vector2f{player.pos.x, player.pos.y - 500});
}
if (player.lives <= 0)
{
}
}
void Game::movement()
{
for (auto& ball : balls)
{
ball.previousPos = ball.pos;
ball.pos += ball.velocity;
}
int dir = player.right - player.left;
player.previousPos = player.pos;
player.pos.x += global::playerSpeed * dir;
}
void Game::imgui()
{
ImGui::SFML::Update(window, clock.restart());
ImGui::Begin("menu");
ImGui::SetWindowCollapsed(global::windowCollasped);
ImGui::Text("Controls:");
ImGui::Indent();
ImGui::Text("A/D or arrow keys: move left and right");
ImGui::Text("R: reset game");
ImGui::Text("F1: expand/collaspe this window");
ImGui::Unindent();
ImGui::End();
}
void Game::render()
{
window.clear();
tempRect.setSize(global::brickSize);
tempRect.setOrigin(global::brickHalfSize);
for (auto& brick : bricks)
{
tempRect.setPosition(brick.pos);
tempRect.setFillColor(brick.color.sfml());
window.draw(tempRect);
}
for (auto& brick : specialBricks)
{
tempRect.setPosition(brick.pos);
tempRect.setFillColor(brick.color.sfml());
window.draw(tempRect);
}
tempRect.setOrigin(global::playerHalfSize);
tempRect.setSize(global::playerSize);
tempRect.setPosition(player.pos);
tempRect.setFillColor(player.color.sfml());
window.draw(tempRect);
tempCircle.setRadius(global::ballRadius);
for (auto& ball : balls)
{
tempCircle.setPosition(ball.pos);
tempCircle.setFillColor(ball.color.sfml());
window.draw(tempCircle);
}
ImGui::SFML::Render(window);
window.display();
}
void Game::run()
{
while(window.isOpen())
{
input();
updateEntities();
movement();
collision();
checkEndGame();
imgui();
render();
}
ImGui::SFML::Shutdown();
}

View File

@ -1,52 +1,7 @@
#define DRAW_SCREEN 1 #include <Game.h>
#include <SFML/Graphics.hpp>
#include <imgui-SFML.h>
#include <imgui.h>
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
Game game;
game.run();
#if DRAW_SCREEN == 1
auto window = sf::RenderWindow(sf::VideoMode({ 1920u, 1080u }), "2d-platformer");
window.setFramerateLimit(60);
if (!ImGui::SFML::Init(window))
return -1;
sf::Clock clock;
while (window.isOpen())
{
while (const std::optional event = window.pollEvent())
{
ImGui::SFML::ProcessEvent(window, *event);
if (event->is<sf::Event::Closed>())
{
window.close();
}
if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>())
{
if (keyPressed->scancode == sf::Keyboard::Scan::Escape)
{
window.close();
}
}
}// end user input loop
ImGui::SFML::Update(window, clock.restart());
ImGui::Begin("sdfkjasbdf");
ImGui::End();
window.clear();
ImGui::SFML::Render(window);
window.display();
}
ImGui::SFML::Shutdown();
#endif
} }