diff --git a/Block.cpp b/Block.cpp new file mode 100644 index 0000000..eb0b1ab --- /dev/null +++ b/Block.cpp @@ -0,0 +1,70 @@ +#include "Block.h" + +Block::Block(float x, float y, BlockType t, int h, bool hb, BonusType bt): + type(t), health(h), maxHealth(h), hasBonus(hb), bonusType(bt), destroyed(false) +{ + shape.setPosition(x, y); + shape.setSize({ BLOCK_WIDTH, BLOCK_HEIGHT }); + shape.setOrigin(BLOCK_WIDTH / 2, BLOCK_HEIGHT / 2); + updateColor(); +} + +void Block::updateColor() { + switch (type) { + case BlockType::UNBREAKABLE: + shape.setFillColor(sf::Color(150, 150, 150)); + break; + case BlockType::REGULAR: { + float ratio = static_cast(health) / maxHealth; + sf::Uint8 green = static_cast(255 * ratio); + shape.setFillColor(sf::Color(0, green, 0)); + break; + } + case BlockType::SPEED_CHANGE: + shape.setFillColor(sf::Color(255, 255, 0)); + break; + case BlockType::WITH_BONUS: + shape.setFillColor(sf::Color(255, 0, 255)); + break; + } +} + +void Block::hit() { + if (type != BlockType::UNBREAKABLE) { + health--; + updateColor(); + if (health <= 0) { + destroyed = true; + } + } +} + +bool Block::isDestroyed() const { + return destroyed; +} + +sf::FloatRect Block::getBounds() const { + return shape.getGlobalBounds(); +} + +BlockType Block::getType() const { + return type; +} + +bool Block::hasBonusInside() const { + return hasBonus; +} + +BonusType Block::getBonusType() const { + return bonusType; +} + +sf::Vector2f Block::getPosition() const { + return shape.getPosition(); +} + +void Block::draw(sf::RenderWindow& window) const { + if (!destroyed) { + window.draw(shape); + } +} \ No newline at end of file diff --git a/Block.h b/Block.h new file mode 100644 index 0000000..ced66b4 --- /dev/null +++ b/Block.h @@ -0,0 +1,27 @@ +#pragma once +#include "Types.h" +#include "Constants.h" + +class Block { +public: + Block(float x, float y, BlockType t, int h = 1, bool hb = false, BonusType bt = BonusType::BOTTOM_SHIELD); + + void updateColor(); + void hit(); + bool isDestroyed() const; + sf::FloatRect getBounds() const; + BlockType getType() const; + bool hasBonusInside() const; + BonusType getBonusType() const; + sf::Vector2f getPosition() const; + void draw(sf::RenderWindow& window) const; + +private: + sf::RectangleShape shape; + BlockType type; + int health; + int maxHealth; + bool hasBonus; + BonusType bonusType; + bool destroyed; +}; \ No newline at end of file diff --git a/Bonus.cpp b/Bonus.cpp new file mode 100644 index 0000000..cd8784d --- /dev/null +++ b/Bonus.cpp @@ -0,0 +1,72 @@ +#include "Bonus.h" + +Bonus::Bonus(float x, float y, BonusType t) + : type(t), active(true), used(false) { + shape.setPosition(x, y); + shape.setSize({ BONUS_WIDTH, BONUS_HEIGHT }); + shape.setOrigin(BONUS_WIDTH / 2, BONUS_HEIGHT / 2); + + switch (type) { + case BonusType::PADDLE_SIZE_INCREASE: + shape.setFillColor(sf::Color(0, 255, 0)); + break; + case BonusType::PADDLE_SIZE_DECREASE: + shape.setFillColor(sf::Color(255, 0, 0)); + break; + case BonusType::BALL_SPEED_INCREASE: + shape.setFillColor(sf::Color(255, 165, 0)); + break; + case BonusType::BALL_SPEED_DECREASE: + shape.setFillColor(sf::Color(0, 255, 255)); + break; + case BonusType::STICKY_PADDLE: + shape.setFillColor(sf::Color(255, 190, 200)); + break; + case BonusType::BOTTOM_SHIELD: + shape.setFillColor(sf::Color(255, 255, 255)); + break; + case BonusType::RANDOM_TRAJECTORY_CHANGE: + shape.setFillColor(sf::Color(128, 0, 128)); + break; + } +} + +void Bonus::update(float deltaTime) { + if (isActive()) { + shape.move(0, BONUS_SPEED * deltaTime); + } +} + +void Bonus::draw(sf::RenderWindow& window) const { + if (isActive()) { + window.draw(shape); + } +} + +void Bonus::activate() { + active = true; +} + +void Bonus::deactivate() { + active = false; +} + +void Bonus::use() { + used = true; +} + +bool Bonus::isActive() const { + return active && !used; +} + +sf::FloatRect Bonus::getBounds() const { + return shape.getGlobalBounds(); +} + +BonusType Bonus::getType() const { + return type; +} + +sf::Vector2f Bonus::getPosition() const { + return shape.getPosition(); +} \ No newline at end of file diff --git a/Bonus.h b/Bonus.h new file mode 100644 index 0000000..8df2bc8 --- /dev/null +++ b/Bonus.h @@ -0,0 +1,23 @@ +#pragma once +#include "Types.h" +#include "Constants.h" + +class Bonus { +public: + Bonus(float x, float y, BonusType t); + void update(float deltaTime); + void draw(sf::RenderWindow& window) const; + void activate(); + void deactivate(); + void use(); + bool isActive() const; + sf::FloatRect getBounds() const; + BonusType getType() const; + sf::Vector2f getPosition() const; + +private: + sf::RectangleShape shape; + BonusType type; + bool active; + bool used; +}; \ No newline at end of file diff --git a/Constants.h b/Constants.h new file mode 100644 index 0000000..a53e3a5 --- /dev/null +++ b/Constants.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +#define M_PI 3.1415926535897932384f + +const unsigned int WINDOW_WIDTH = 800; +const unsigned int WINDOW_HEIGHT = 600; +const float INITIAL_PADDLE_WIDTH = 100.0; +const float PADDLE_HEIGHT = 20.0; +const float BALL_RADIUS = 10.0; +const float BLOCK_WIDTH = 80.0; +const float BLOCK_HEIGHT = 30.0; +const float BONUS_WIDTH = 30.0; +const float BONUS_HEIGHT = 20.0; +const float INITIAL_BALL_SPEED = 300.0; +const float PADDLE_SPEED = 500.0; +const float BONUS_SPEED = 150.0; +const int LOSE_PENALTY = 10; +const int WIN_SCORE = 50; +const unsigned int FONT_SIZE = 24; +const sf::Color TEXT_COLOR = sf::Color::White; +const int INITIAL_LIVES = 3; \ No newline at end of file diff --git a/Game.cpp b/Game.cpp new file mode 100644 index 0000000..dbf126b --- /dev/null +++ b/Game.cpp @@ -0,0 +1,406 @@ +#include "Game.h" +#include + +Game::Game() + : window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), "Arkanoid"), + ballSpeed(INITIAL_BALL_SPEED), + paddleWidth(INITIAL_PADDLE_WIDTH), + score(0), lives(INITIAL_LIVES), + gameOver(false), + gameWon(false), + stickyAvailable(false), + ballStuck(false), + bottomShieldActive(false), + rng(std::random_device{}()), dist(-0.3f, 0.3f) { + window.setFramerateLimit(60); + font.loadFromFile("C:/Windows/Fonts/Arial.ttf"); + + initGameObjects(); + initUI(); + createBlocks(); + resetBall(); +} + +void Game::setupText(sf::Text& text, const std::string& str, float x, float y, unsigned int size) { + text.setFont(font); + text.setString(str); + text.setCharacterSize(size); + text.setFillColor(TEXT_COLOR); + text.setPosition(x, y); +} + +void Game::initGameObjects() { + paddle.setSize({paddleWidth, PADDLE_HEIGHT}); + paddle.setOrigin(paddleWidth / 2, PADDLE_HEIGHT / 2); + paddle.setPosition(WINDOW_WIDTH / 2, WINDOW_HEIGHT - 50); + paddle.setFillColor(sf::Color::White); + + ball.setRadius(BALL_RADIUS); + ball.setOrigin(BALL_RADIUS, BALL_RADIUS); + ball.setPosition(WINDOW_WIDTH / 2, WINDOW_HEIGHT / 2); + ball.setFillColor(sf::Color::White); +} + +void Game::initUI() { + setupText(scoreText, "Score: 0", 10, 10); + setupText(livesText, "Lives: " + std::to_string(lives), WINDOW_WIDTH - 150, 10); + setupText(gameOverText, "GAME OVER! Press Q to restart", WINDOW_WIDTH / 2 - 280, WINDOW_HEIGHT / 2 - 20, 40); + setupText(winText, "YOU WIN! Press Q to restart", WINDOW_WIDTH / 2 - 280, WINDOW_HEIGHT / 2 - 20, 40); + setupText(stickyText, "Press SPACE to launch ball", WINDOW_WIDTH / 2 - 100, WINDOW_HEIGHT - 100, 20); + + bottomShieldIndicator.setSize({ 50, 5 }); + bottomShieldIndicator.setFillColor(sf::Color::Yellow); + bottomShieldIndicator.setPosition(WINDOW_WIDTH / 2 - 25, WINDOW_HEIGHT - 5); +} + +void Game::createBlocks() { + blocks.clear(); + activeBonuses.clear(); + + const int rows = 4; + const int cols = 8; + const float setX = 60.0; + const float setY = 50.0; + + for (int i = 0; i < rows; ++i) { + for (int j = 0; j < cols; ++j) { + float x = setX + j * (BLOCK_WIDTH + 10); + float y = setY + i * (BLOCK_HEIGHT + 10); + + BlockType type; + int health = 1; + bool hasBonus = false; + BonusType bonusType = BonusType::BOTTOM_SHIELD; + + int randVal = std::uniform_int_distribution<>(0, 9)(rng); + + if (randVal == 0) { + type = BlockType::UNBREAKABLE; + } + else if (randVal == 1) { + type = BlockType::SPEED_CHANGE; + } + else if (randVal == 2) { + type = BlockType::WITH_BONUS; + hasBonus = true; + bonusType = static_cast(std::uniform_int_distribution<>(0, 6)(rng)); + } + else { + type = BlockType::REGULAR; + health = std::uniform_int_distribution<>(1, 3)(rng); + } + blocks.push_back(std::make_unique(x, y, type, health, hasBonus, bonusType)); + } + } +} + +void Game::resetBall() { + ball.setPosition(paddle.getPosition().x, paddle.getPosition().y - paddle.getSize().y / 2 - BALL_RADIUS); + ballStuck = true; + ballVelocity = sf::Vector2f(0, 0); +} + +bool Game::checkWinCondition() { + return std::all_of(blocks.begin(), blocks.end(), [](const auto& block) { + return block->isDestroyed() || block->getType() == BlockType::UNBREAKABLE; + }); +} + +void Game::updateBall(float deltaTime) { + if (!ballStuck) { + ball.move(ballVelocity * deltaTime); + + if (ball.getPosition().x - BALL_RADIUS < 0) { + ball.setPosition(BALL_RADIUS, ball.getPosition().y); + ballVelocity.x = -ballVelocity.x; + } + + if (ball.getPosition().x + BALL_RADIUS > WINDOW_WIDTH) { + ball.setPosition(WINDOW_WIDTH - BALL_RADIUS, ball.getPosition().y); + ballVelocity.x = -ballVelocity.x; + } + + if (ball.getPosition().y - BALL_RADIUS < 0) { + ball.setPosition(ball.getPosition().x, BALL_RADIUS); + ballVelocity.y = -ballVelocity.y; + } + + if (ball.getPosition().y + BALL_RADIUS > WINDOW_HEIGHT) { + handleBallLoss(); + } + } +} + +void Game::handleBallLoss() { + if (bottomShieldActive) { + bottomShieldActive = false; + ball.setPosition(ball.getPosition().x, WINDOW_HEIGHT - BALL_RADIUS); + ballVelocity.y = -std::abs(ballVelocity.y); + } + else { + lives--; + score -= LOSE_PENALTY; + + if (lives <= 0) { + gameOver = true; + } + else { + resetBall(); + } + } +} + +void Game::checkCollisions() { + if (ball.getGlobalBounds().intersects(paddle.getGlobalBounds())) { + handlePaddleCollision(); + } + + for (auto& block : blocks) { + if (!block->isDestroyed() && ball.getGlobalBounds().intersects(block->getBounds())) { + handleBlockCollision(*block); + break; + } + } +} + +void Game::handlePaddleCollision() { + float hitPos = (ball.getPosition().x - paddle.getPosition().x) / (paddle.getSize().x / 2); + float angle = hitPos * (M_PI / 4); + + float speed = std::sqrt(ballVelocity.x * ballVelocity.x + ballVelocity.y * ballVelocity.y); + ballVelocity.x = std::sin(angle) * speed; + ballVelocity.y = -std::cos(angle) * speed; + + if (stickyAvailable) { + ballStuck = true; + stickyAvailable = false; + } +} + +void Game::handleBlockCollision(Block& block) { + sf::FloatRect ballRect = ball.getGlobalBounds(); + sf::FloatRect blockRect = block.getBounds(); + + float overlapLeft = ballRect.left + ballRect.width - blockRect.left; + float overlapRight = blockRect.left + blockRect.width - ballRect.left; + float overlapTop = ballRect.top + ballRect.height - blockRect.top; + float overlapBottom = blockRect.top + blockRect.height - ballRect.top; + + bool fromLeft = std::abs(overlapLeft) < std::abs(overlapRight); + bool fromTop = std::abs(overlapTop) < std::abs(overlapBottom); + + float minOverlapX = fromLeft ? overlapLeft : overlapRight; + float minOverlapY = fromTop ? overlapTop : overlapBottom; + + if (std::abs(minOverlapX) < std::abs(minOverlapY)) { + if (fromLeft) { + ballVelocity.x = -std::abs(ballVelocity.x); + } + else { + ballVelocity.x = std::abs(ballVelocity.x); + } + } + else { + if (fromTop) { + ballVelocity.y = -std::abs(ballVelocity.y); + } + else { + ballVelocity.y = std::abs(ballVelocity.y); + } + } + + block.hit(); + if (block.getType() != BlockType::UNBREAKABLE) { + if (block.hasBonusInside() && block.isDestroyed()) { + sf::Vector2f pos = block.getPosition(); + activeBonuses.push_back(std::make_unique(pos.x, pos.y, block.getBonusType())); + } + + if (block.getType() == BlockType::SPEED_CHANGE) { + score += 4; + ballSpeed *= 1.2f; + ballVelocity = ballVelocity / std::sqrt(ballVelocity.x * ballVelocity.x + ballVelocity.y * ballVelocity.y) * ballSpeed; + } + score += 1; + } +} + +void Game::updateBonuses(float deltaTime) { + for (auto bonusIter = activeBonuses.begin(); bonusIter != activeBonuses.end();) { + auto& currentBonus = *bonusIter; + currentBonus->update(deltaTime); + + bool collectedByPaddle = currentBonus->getBounds().intersects(paddle.getGlobalBounds()); + bool fellOffScreen = currentBonus->getPosition().y > WINDOW_HEIGHT; + + if (collectedByPaddle) { + applyBonus(currentBonus->getType()); + bonusIter = activeBonuses.erase(bonusIter); + } + else if (fellOffScreen) { + bonusIter = activeBonuses.erase(bonusIter); + } + else { + ++bonusIter; + } + } +} + +void Game::applyBonus(BonusType type) { + switch (type) { + score += 4; + case BonusType::PADDLE_SIZE_INCREASE: + paddleWidth = std::min(paddleWidth * 1.5f, INITIAL_PADDLE_WIDTH * 2.0f); + paddle.setSize({ paddleWidth, PADDLE_HEIGHT }); + paddle.setOrigin(paddleWidth / 2, PADDLE_HEIGHT / 2); + break; + + case BonusType::PADDLE_SIZE_DECREASE: + paddleWidth = std::max(paddleWidth * 0.75f, INITIAL_PADDLE_WIDTH * 0.5f); + paddle.setSize({ paddleWidth, PADDLE_HEIGHT }); + paddle.setOrigin(paddleWidth / 2, PADDLE_HEIGHT / 2); + break; + + case BonusType::BALL_SPEED_INCREASE: + ballSpeed *= 1.2f; + ballVelocity = ballVelocity / std::sqrt(ballVelocity.x * ballVelocity.x + ballVelocity.y * ballVelocity.y) * ballSpeed; + break; + + case BonusType::BALL_SPEED_DECREASE: + ballSpeed *= 0.8f; + ballVelocity = ballVelocity / std::sqrt(ballVelocity.x * ballVelocity.x + ballVelocity.y * ballVelocity.y) * ballSpeed; + break; + + case BonusType::STICKY_PADDLE: + stickyAvailable = true; + break; + + case BonusType::BOTTOM_SHIELD: + bottomShieldActive = true; + break; + + case BonusType::RANDOM_TRAJECTORY_CHANGE: + float randomAngle = std::uniform_real_distribution(-M_PI/6.0, M_PI/6.0 )(rng); + ballVelocity = sf::Vector2f(std::sin(randomAngle) * ballSpeed, -std::cos(randomAngle) * ballSpeed); + break; + } +} + +void Game::updateUI() { + scoreText.setString("Score: " + std::to_string(score)); + livesText.setString("Lives: " + std::to_string(lives)); +} + +void Game::render() { + window.clear(); + window.draw(paddle); + window.draw(ball); + + for (const auto& block : blocks) { + block->draw(window); + } + + for (const auto& bonus : activeBonuses) { + bonus->draw(window); + } + + window.draw(scoreText); + window.draw(livesText); + + if (bottomShieldActive) { + window.draw(bottomShieldIndicator); + } + + if (ballStuck) { + window.draw(stickyText); + } + + if (gameOver) { + window.draw(gameOverText); + } + else if (gameWon) { + window.draw(winText); + } + window.display(); +} + +void Game::initGame() { + ballSpeed = INITIAL_BALL_SPEED; + paddleWidth = INITIAL_PADDLE_WIDTH; + stickyAvailable = false; + ballStuck = false; + bottomShieldActive = false; + score = 0; + lives = INITIAL_LIVES; + gameOver = false; + gameWon = false; + + initGameObjects(); + createBlocks(); + resetBall(); + updateUI(); +} + +void Game::run() { + sf::Clock clock; + + while (window.isOpen()) { + sf::Time deltaTime = clock.restart(); + processEvents(); + + if (!gameOver && !gameWon) { + update(deltaTime.asSeconds()); + + if (checkWinCondition() || score >= WIN_SCORE) { + gameWon = true; + } + } + render(); + } +} + +void Game::processEvents() { + sf::Event event; + while (window.pollEvent(event)) { + if (event.type == sf::Event::Closed) { + window.close(); + } + if (event.type == sf::Event::KeyPressed) { + if (event.key.code == sf::Keyboard::Space && ballStuck) { + ballStuck = false; + float angle = dist(rng); + ballVelocity.x = std::sin(angle) * ballSpeed; + ballVelocity.y = -std::abs(std::cos(angle)) * ballSpeed; + } + else if (event.key.code == sf::Keyboard::Q && (gameOver || gameWon)) { + initGame(); + } + } + } +} + +void Game::update(float deltaTime) { + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left) && + paddle.getPosition().x - paddle.getSize().x / 2 > 0) { + paddle.move(-PADDLE_SPEED * deltaTime, 0); + if (ballStuck) { + ball.setPosition(paddle.getPosition().x, paddle.getPosition().y - paddle.getSize().y / 2 - BALL_RADIUS); + } + } + + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right) && + paddle.getPosition().x + paddle.getSize().x / 2 < WINDOW_WIDTH) { + paddle.move(PADDLE_SPEED * deltaTime, 0); + if (ballStuck) { + ball.setPosition(paddle.getPosition().x, paddle.getPosition().y - paddle.getSize().y / 2 - BALL_RADIUS); + } + } + + if (!ballStuck) { + updateBall(deltaTime); + checkCollisions(); + } + + updateBonuses(deltaTime); + updateUI(); +} \ No newline at end of file diff --git a/Game.h b/Game.h new file mode 100644 index 0000000..4dc30e7 --- /dev/null +++ b/Game.h @@ -0,0 +1,59 @@ +#pragma once +#include +#include +#include +#include "Block.h" +#include "Bonus.h" +#include "Constants.h" + +class Game { +public: + Game(); + void run(); + +private: + void processEvents(); + void update(float deltaTime); + void render(); + void setupText(sf::Text& text, const std::string& str, float x, float y, unsigned int size = FONT_SIZE); + void initGameObjects(); + void initUI(); + void createBlocks(); + void resetBall(); + bool checkWinCondition(); + void updateBall(float deltaTime); + void handleBallLoss(); + void checkCollisions(); + void handlePaddleCollision(); + void handleBlockCollision(Block& block); + void updateBonuses(float deltaTime); + void applyBonus(BonusType type); + void updateUI(); + void initGame(); + + sf::RenderWindow window; + sf::Font font; + sf::RectangleShape paddle; + sf::CircleShape ball; + std::vector> blocks; + std::vector> activeBonuses; + sf::Vector2f ballVelocity; + float ballSpeed; + float paddleWidth; + int score; + int lives; + bool gameOver; + bool gameWon; + bool stickyAvailable; + bool ballStuck; + bool bottomShieldActive; + std::mt19937 rng; + std::uniform_real_distribution dist; + + sf::Text scoreText; + sf::Text livesText; + sf::Text gameOverText; + sf::Text winText; + sf::Text stickyText; + sf::RectangleShape bottomShieldIndicator; +}; \ No newline at end of file diff --git a/Main.cpp b/Main.cpp new file mode 100644 index 0000000..c1fc21d --- /dev/null +++ b/Main.cpp @@ -0,0 +1,7 @@ +#include "Game.h" + +int main() { + Game game; + game.run(); + return 0; +} \ No newline at end of file diff --git a/Types.h b/Types.h new file mode 100644 index 0000000..d08a4c8 --- /dev/null +++ b/Types.h @@ -0,0 +1,18 @@ +#pragma once + +enum class BlockType { + UNBREAKABLE, + REGULAR, + SPEED_CHANGE, + WITH_BONUS +}; + +enum class BonusType { + PADDLE_SIZE_INCREASE, + PADDLE_SIZE_DECREASE, + BALL_SPEED_INCREASE, + BALL_SPEED_DECREASE, + STICKY_PADDLE, + BOTTOM_SHIELD, + RANDOM_TRAJECTORY_CHANGE +}; \ No newline at end of file