#include #include #include #include #include #include #include #include #include Ball::Ball(sf::Vector2f position_in, sf::Vector2f vel_in) : pos(position_in) , velocity(vel_in) { } Brick::Brick(sf::Vector2f position_in) : pos(position_in) { } Player::Player(sf::Vector2f position_in) : pos(position_in) { } Game::Game() : window({sf::VideoMode({ 1920u, 1080u }), "project-breakout"}) , font("assets/fonts/ChakraPetch-Regular.ttf") , lives(font) , score(font) , currentMult(font) , collideSoundBuffer("assets/sounds/hit2.wav") , brickBreakSoundBuffer("assets/sounds/hit1.wav") , failSoundBuffer("assets/sounds/fail.wav") , collideSound(collideSoundBuffer) , brickBreakSound(brickBreakSoundBuffer) , failSound(failSoundBuffer) { if (!ImGui::SFML::Init(window)) return; if (!parseConfigFile()) return; lives.setPosition({10, static_cast(window.getSize().y - 40)}); score.setPosition({10, static_cast(window.getSize().y - 80)}); currentMult.setPosition({10, static_cast(window.getSize().y - 120)}); window.setFramerateLimit(framerate); collideSound.setVolume(volume); brickBreakSound.setVolume(volume); failSound.setVolume(volume); player.pos = playerStartPos; playerHalfSize = playerSize / 2.f; brickHalfSize = brickSize / 2.f; tempCircle.setOrigin({ballRadius, ballRadius}); ballMaxSpeed /= numPhysicsUpdates; bricks.reserve(totalBricks); specialBricks.reserve(totalSpecialBricks); balls.reserve(10); ballsToAdd.reserve(10); setupLevel(); ballsToAdd.emplace_back(playerStartPos.x, playerStartPos.y - 50); } void Game::setupLevel() { const int specialBrickInterval = totalBricks / totalSpecialBricks; constexpr int xSpacing = 20; constexpr int ySpacing = 20; const int xOffset = (int)brickSize.x + xSpacing; const int yOffset = (int)brickSize.y + ySpacing; const int numRows = window.getSize().x / xOffset; const int numColumns = (window.getSize().y - 300)/ yOffset; int row{}; int column{}; for (int i = 0; i < totalBricks; i++) { if (row >= numRows) { row = 0; column++; } if (column >= numColumns) break; if (i % specialBrickInterval == 0) { specialBricks.emplace_back(sf::Vector2f{(row * xOffset) + xSpacing + brickHalfSize.x, (column * yOffset) + ySpacing + brickHalfSize.y}); } else { bricks.emplace_back(sf::Vector2f{(row * xOffset) + xSpacing + brickHalfSize.x, (column * yOffset) + ySpacing + brickHalfSize.y}); } row++; } } bool Game::parseConfigFile() { std::ifstream configFile{"Config/game-config.txt"}; if(!configFile) { std::cerr << "ERROR: CANNOT OPEN 'game-config.txt'\n"; return false; } std::string inputBuff; while(configFile >> inputBuff) { if (inputBuff == "framerate") configFile >> framerate; if (inputBuff == "ballRadius") configFile >> ballRadius; if (inputBuff == "playerSpeed") configFile >> playerSpeed; if (inputBuff == "brickCount") configFile >> totalBricks; if (inputBuff == "specialBrickCount") configFile >> totalSpecialBricks; if (inputBuff == "brickSize") configFile >> brickSize.x >> brickSize.y; if (inputBuff == "playerSize") configFile >> playerSize.x >> playerSize.y; if (inputBuff == "ballColor") configFile >> ballColor.r >> ballColor.g >> ballColor.b; if (inputBuff == "playerStartPos") configFile >> playerStartPos.x >> playerStartPos.y; if (inputBuff == "brickColor") configFile >> brickColor.r >> brickColor.g >> brickColor.b; if (inputBuff == "playerColor") configFile >> playerColor.r >> playerColor.g >> playerColor.b; if (inputBuff == "specialBrickColor") configFile >> specialBrickColor.r >> specialBrickColor.g >> specialBrickColor.b; if (inputBuff == "ballSpeed") configFile >> ballMaxSpeed; } if (totalSpecialBricks > totalBricks) totalSpecialBricks = totalBricks; return true; } 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; case sf::Keyboard::Scan::R: resetGame(); 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; } } // if (const auto* MouseButtonPressed = event->getIf()) // { // switch (MouseButtonPressed->button) // { // case sf::Mouse::Button::Left: // //specialBricks.emplace_back((sf::Vector2f)MouseButtonPressed->position); // break; // default: // break; // } // } } } void Game::collision() { checkBallCollision(); //player wall collide const sf:: FloatRect playerBounds = {player.pos, playerHalfSize}; if (playerBounds.position.x + playerBounds.size.x > window.getSize().x) { player.pos.x = window.getSize().x - playerHalfSize.x; } if (playerBounds.position.x < playerHalfSize.x) { player.pos.x = playerHalfSize.x; } } void Game::checkBallCollision() { for (auto& ball : balls) { ball.collided = false; const sf::FloatRect ballBounds = {ball.pos, {ballRadius, ballRadius}}; const sf::FloatRect previousBallBounds = {ball.previousPos, {ballRadius, ballRadius}}; for (auto& brick : bricks) { const sf::FloatRect brickBounds = {brick.pos, brickHalfSize}; const auto intersect = getOverlap(ballBounds, brickBounds); const auto previousIntersect = getOverlap(previousBallBounds, brickBounds); // coming from x directon if (intersect.x > 0 && previousIntersect.x <= 0 && intersect.y > 0) { ball.collided = true; brick.alive = false; ball.velocity.x *= -1; if (ball.pos.x < brick.pos.x) {// from the left ball.pos.x -= intersect.x; } else {// from the right ball.pos.x += intersect.x; } } // coming from y direction if (intersect.y > 0 && previousIntersect.y <= 0 && intersect.x > 0) { ball.collided = true; brick.alive = false; ball.velocity.y *= -1; if (ball.pos.y < brick.pos.y) {// from the top ball.pos.y -= intersect.y; } else {// from the bottom ball.pos.y += intersect.y; } } } for (auto& brick : specialBricks) { const sf::FloatRect brickBounds = {brick.pos, 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) { ball.collided = true; brick.alive = false; spawnExtraBall = true; ball.velocity.x *= -1; if (ball.pos.x < brick.pos.x) {// from the left ball.pos.x -= intersect.x; } else {// from the right ball.pos.x += intersect.x; } } // coming from y direction if (intersect.y > 0 && previousIntersect.y <= 0 && intersect.x > 0) { ball.collided = true; brick.alive = false; spawnExtraBall = true; ball.velocity.y *= -1; if (ball.pos.y < brick.pos.y) {// from the top ball.pos.y -= intersect.y; } else {// from the bottom 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 - ballRadius <= 0) { ball.collided = true; ball.pos.x = ballRadius; ball.velocity.x *= -1; } if (ball.pos.x + ballRadius >= window.getSize().x) { ball.collided = true; ball.pos.x = window.getSize().x - ballRadius; ball.velocity.x *= -1; } if (ball.pos.y - ballRadius < 0) { ball.collided = true; ball.pos.y = ballRadius; ball.velocity.y *= -1; } // player collide const sf:: FloatRect playerBounds = {player.pos, playerHalfSize}; const sf:: FloatRect previousPlayerBounds = {player.previousPos, playerHalfSize}; const auto intersect = getOverlap(ballBounds, playerBounds); const auto previousIntersect = getOverlap(previousBallBounds, previousPlayerBounds); // coming from x direction if (intersect.x > 0 && previousIntersect.x <= 0 && intersect.y > 0) { ball.collided = true; ball.velocity.x *= -1; if (ball.pos.x < player.pos.x) {// from the left ball.pos.x -= intersect.x; } else {// from the right ball.pos.x += intersect.x; } } // coming from y direction if (intersect.y > 0 && previousIntersect.y <= 0 && intersect.x > 0) { ball.collided = true; ball.velocity.y *= -1; if (ball.pos.y < player.pos.y) {// from the top ball.pos.y -= intersect.y; } else if (ball.pos.y > player.pos.y) {// from the bottom ball.pos.y += intersect.y; } } } } 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()); specialBricks.erase(std::remove_if(specialBricks.begin(), specialBricks.end(), [](const Brick& brick){ return !brick.alive; }), specialBricks.end()); const auto limit = ballsToAdd.size(); for (size_t i = 0; i < limit; i++) { balls.emplace_back(ballsToAdd[i], velocityInRandomDir(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 - 50}); } if (player.lives <= 0) { resetGame(); } } void Game::movement() { for (auto& ball : balls) { ball.previousPos = ball.pos; ball.pos += ball.velocity; } const int dir = player.right - player.left; player.previousPos = player.pos; player.pos.x += (playerSpeed / numPhysicsUpdates) * dir; } void Game::imgui() { ImGui::SetNextWindowCollapsed(false, ImGuiCond_Once); ImGui::SetNextWindowSize({490,375}, ImGuiCond_Once); ImGui::SetNextWindowPos({26,29}, ImGuiCond_Once); if(!ImGui::Begin("menu")) { ImGui::End(); return; } ImGui::Text("Controls:"); ImGui::Indent(); ImGui::Text("A/D or arrow keys: move left and right"); ImGui::Text("R: reset game"); ImGui::Unindent(); if (ImGui::SliderFloat("Game Volume", &volume, 0, 100, "%.1f")) { collideSound.setVolume(volume); brickBreakSound.setVolume(volume); failSound.setVolume(volume); } ImGui::Text("Level config:"); if(ImGui::InputInt("Total bricks", &totalBricks)) { if (totalBricks < 0) totalBricks = 0; if (totalSpecialBricks > totalBricks) totalSpecialBricks = totalBricks; } if(ImGui::InputInt("Special bricks", &totalSpecialBricks)) { if (totalSpecialBricks < 0) totalSpecialBricks = 0; if (totalSpecialBricks > totalBricks) totalSpecialBricks = totalBricks; } if(ImGui::Button("Reload Level")) { resetGame(); } ImGui::Text("Game config:"); ImGui::SliderFloat("Player Speed", &playerSpeed, 1.f, 50.f); ImGui::SliderFloat("Ball Speed", &ballMaxSpeed, 1.f, 50.f); if (ImGui::SliderFloat("Player Length", &playerSize.x, 1.f, 600.f)) { playerHalfSize.x = playerSize.x / 2; } ImGui::ColorEdit3("Player Color", playerColor.imgui()); ImGui::ColorEdit3("Ball Color", ballColor.imgui()); ImGui::ColorEdit3("Brick Color", brickColor.imgui()); ImGui::ColorEdit3("Special Brick Color", specialBrickColor.imgui()); ImGui::End(); } void Game::render() { window.clear(); lives.setString("Lives: " + std::to_string(player.lives)); score.setString("Score: " + std::to_string(player.score)); currentMult.setString("Mult: x" + std::to_string(player.mult)); window.draw(lives); window.draw(score); window.draw(currentMult); tempRect.setOrigin(brickHalfSize); tempRect.setSize(brickSize); for (auto& brick : bricks) { tempRect.setPosition(brick.pos); tempRect.setFillColor(brickColor.sfml()); window.draw(tempRect); } for (auto& brick : specialBricks) { tempRect.setPosition(brick.pos); tempRect.setFillColor(specialBrickColor.sfml()); window.draw(tempRect); } tempRect.setOrigin(playerHalfSize); tempRect.setSize(playerSize); tempRect.setPosition(player.pos); tempRect.setFillColor(playerColor.sfml()); window.draw(tempRect); tempCircle.setRadius(ballRadius); for (auto& ball : balls) { tempCircle.setPosition(ball.pos); tempCircle.setFillColor(ballColor.sfml()); window.draw(tempCircle); } ImGui::SFML::Render(window); window.display(); } void Game::soundSystem() { for (auto& ball : balls) { if (ball.collided) { collideSound.play(); } if (!ball.alive) { failSound.play(); } } for (auto& brick : bricks) { if (!brick.alive) { brickBreakSound.play(); } } for (auto& brick : specialBricks) { if (!brick.alive) { brickBreakSound.play(); } } } void Game::scoreSystem() { player.mult = Math::max(balls.size(), 1); for (auto& brick : bricks) { if (!brick.alive) { player.score += 100 * player.mult; } } for (auto& brick : specialBricks) { if (!brick.alive) { player.score += 100 * player.mult; } } } void Game::resetGame() { bricks.clear(); specialBricks.clear(); balls.clear(); ballsToAdd.clear(); setupLevel(); player.lives = 3; player.score = 0; ballsToAdd.emplace_back(sf::Vector2f{player.pos.x, player.pos.y - 50}); } void Game::run() { while(window.isOpen()) { ImGui::SFML::Update(window, clock.restart()); input(); for (size_t i = 0; i <= numPhysicsUpdates; i++) { updateEntities(); movement(); collision(); soundSystem(); scoreSystem(); } checkEndGame(); imgui(); render(); } ImGui::SFML::Shutdown(); } void Game::runNoImgui() { while(window.isOpen()) { ImGui::SFML::Update(window, clock.restart()); input(); for (size_t i = 0; i <= numPhysicsUpdates; i++) { updateEntities(); movement(); collision(); soundSystem(); scoreSystem(); } checkEndGame(); render(); } ImGui::SFML::Shutdown(); }