diff --git a/include/Game.h b/include/Game.h new file mode 100644 index 0000000..a56de26 --- /dev/null +++ b/include/Game.h @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include + +#include +#include +#include + +#include + +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 bricks; + std::vector specialBricks; + std::vector balls; + std::vector ballsToAdd; + sf::CircleShape tempCircle; + sf::RectangleShape tempRect; +}; \ No newline at end of file diff --git a/include/Random.h b/include/Random.h new file mode 100644 index 0000000..28c2beb --- /dev/null +++ b/include/Random.h @@ -0,0 +1,74 @@ +#pragma once + +#ifndef RANDOM_MT_H +#define RANDOM_MT_H + +#include +#include + +// 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::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 + T get(T min, T max) + { + return std::uniform_int_distribution{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(0, 6); // returns std::size_t + // Sample call: Random::get(0, 6u); // returns std::size_t + // Sample call: Random::get(0, 6u); // returns int + template + R get(S min, T max) + { + return get(static_cast(min), static_cast(max)); + } +} + +#endif \ No newline at end of file diff --git a/include/Util.h b/include/Util.h new file mode 100644 index 0000000..369c3af --- /dev/null +++ b/include/Util.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +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}; +} \ No newline at end of file diff --git a/premake5.lua b/premake5.lua index ba6ca80..3f3d9ca 100644 --- a/premake5.lua +++ b/premake5.lua @@ -29,18 +29,20 @@ workspace "breakout" cdialect "C17" systemversion "latest" 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-- filter {"action:vs*", "system:windows"} targetdir (vs_bindir) @@ -63,7 +65,7 @@ workspace "breakout" { "src", include_dir, - vs_sfmldir .. "/include", + sfmldir .. "/include", imguidir } @@ -140,12 +142,10 @@ workspace "breakout" defines {"LOG_ENABLE", "GAME_DEBUG"} symbols "on" runtime "Debug" - targetname "breakout" - + filter "configurations:release" defines {"GAME_RELEASE"} optimize "Speed" inlining "Auto" symbols "off" - runtime "Release" - targetname "breakout" \ No newline at end of file + runtime "Release" \ No newline at end of file diff --git a/src/Game.cpp b/src/Game.cpp new file mode 100644 index 0000000..8462bf4 --- /dev/null +++ b/src/Game.cpp @@ -0,0 +1,400 @@ +#include +#include +#include + +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()) + { + window.close(); + } + + if (const auto* keyPressed = event->getIf()) + { + 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()) + { + 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(); +} diff --git a/src/main.cpp b/src/main.cpp index 2d3c045..b015c18 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,52 +1,7 @@ -#define DRAW_SCREEN 1 - -#include -#include -#include +#include int main(int argc, char* argv[]) { - - -#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()) - { - window.close(); - } - - if (const auto* keyPressed = event->getIf()) - { - 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 + Game game; + game.run(); } \ No newline at end of file