breakout/src/Game.cpp

702 lines
15 KiB
C++

#include <Game.h>
#include <vector>
#include <Util.h>
#include <SFML/Graphics.hpp>
#include <imgui-SFML.h>
#include <imgui.h>
#include <algorithm>
#include <fstream>
#include <iostream>
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<float>(window.getSize().y - 40)});
score.setPosition({10, static_cast<float>(window.getSize().y - 80)});
currentMult.setPosition({10, static_cast<float>(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<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;
case sf::Keyboard::Scan::R:
resetGame();
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;
}
}
// if (const auto* MouseButtonPressed = event->getIf<sf::Event::MouseButtonPressed>())
// {
// 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<int>(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();
}