diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b019071 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.10) +project(Arkanoid) + +set(CMAKE_CXX_STANDARD 11) + +find_package(SFML 2.5 COMPONENTS graphics window system REQUIRED) + +include_directories(include) +file(GLOB SOURCES "src/*.cpp") + +add_executable(arkanoid ${SOURCES}) +target_link_libraries(arkanoid sfml-graphics sfml-window sfml-system) \ No newline at end of file diff --git a/include/Ball.h b/include/Ball.h new file mode 100644 index 0000000..a3d3a23 --- /dev/null +++ b/include/Ball.h @@ -0,0 +1,33 @@ +#ifndef BALL_H +#define BALL_H + +#include + +class Ball : public sf::CircleShape { +public: + Ball(float windowWidth, float windowHeight); + void update(sf::Time deltaTime); + void bounceX(); + void bounceY(); + void bounceFromPaddle(float hitPosition); + void randomBounce(); + void stickToPaddle(const sf::RectangleShape& paddle); + void release(); + void reset(); + void increaseSpeed(); + void decreaseSpeed(); + + sf::Vector2f getPosition() const; + float getRadius() const; + sf::FloatRect getGlobalBounds() const; + +private: + float mWindowWidth; + float mWindowHeight; + float mDefaultRadius; + sf::Vector2f mVelocity; + float mDefaultSpeed; + bool mIsReleased; +}; + +#endif // BALL_H \ No newline at end of file diff --git a/include/Block.h b/include/Block.h new file mode 100644 index 0000000..dbc7cbb --- /dev/null +++ b/include/Block.h @@ -0,0 +1,28 @@ +#ifndef BLOCK_H +#define BLOCK_H + +#include + +enum class BlockType { + Regular, + Unbreakable, + WithBonus, + SpeedUp +}; + +class Block : public sf::RectangleShape { +public: + Block(const sf::Vector2f& position, const sf::Vector2f& size, BlockType type, int health); + bool hit(); + bool isDestroyed() const; + int getHealth() const; + BlockType getType() const; + bool hasBonus() const; + +private: + BlockType mType; + int mHealth; + bool mIsDestroyed; +}; + +#endif // BLOCK_H \ No newline at end of file diff --git a/include/Bonus.h b/include/Bonus.h new file mode 100644 index 0000000..5d06675 --- /dev/null +++ b/include/Bonus.h @@ -0,0 +1,29 @@ +#ifndef BONUS_H +#define BONUS_H + +#include + +enum class BonusType { + EnlargePaddle, + ShrinkPaddle, + StickyPaddle, + BottomShield, + RandomTrajectory, + SpeedUp, + SlowDown +}; + +class Bonus : public sf::CircleShape { +public: + Bonus(const sf::Vector2f& position, BonusType type); + void update(sf::Time deltaTime); + bool isDestroyed() const; + BonusType getType() const; + +private: + BonusType mType; + float mSpeed; + bool mIsDestroyed; +}; + +#endif // BONUS_H \ No newline at end of file diff --git a/include/Constants.h b/include/Constants.h new file mode 100644 index 0000000..8f8e5ef --- /dev/null +++ b/include/Constants.h @@ -0,0 +1,16 @@ +#ifndef CONSTANTS_H +#define CONSTANTS_H + +namespace Constants { + constexpr float WINDOW_WIDTH = 800.f; + constexpr float WINDOW_HEIGHT = 600.f; + constexpr float PADDLE_WIDTH = 100.f; + constexpr float PADDLE_HEIGHT = 20.f; + constexpr float BALL_RADIUS = 10.f; + constexpr float BONUS_RADIUS = 8.f; + constexpr float BLOCK_WIDTH = 70.f; + constexpr float BLOCK_HEIGHT = 30.f; + constexpr float BLOCK_PADDING = 5.f; +} + +#endif CONSTANTS_H \ No newline at end of file diff --git a/include/Game.h b/include/Game.h new file mode 100644 index 0000000..6a95e0b --- /dev/null +++ b/include/Game.h @@ -0,0 +1,44 @@ +#ifndef GAME_H +#define GAME_H + +#include +#include +#include +#include "Paddle.h" +#include "Ball.h" +#include "Block.h" +#include "Bonus.h" + +class Game { +public: + Game(); + void run(); + +private: + void processEvents(); + void update(sf::Time deltaTime); + void render(); + void handleCollisions(); + void spawnBonus(const sf::Vector2f& position, BonusType type); + void activateBonus(BonusType type); + void resetGame(); + + sf::RenderWindow mWindow; + Paddle mPaddle; + Ball mBall; + std::vector> mBlocks; + std::vector> mBonuses; + + int mScore; + int mLives; + bool mIsBallSticky; + bool mHasBottomShield; + bool mIsGameOver; + + sf::Font mFont; + sf::Text mScoreText; + sf::Text mLivesText; + sf::Text mGameOverText; +}; + +#endif // GAME_H \ No newline at end of file diff --git a/include/Paddle.h b/include/Paddle.h new file mode 100644 index 0000000..e4fa4c0 --- /dev/null +++ b/include/Paddle.h @@ -0,0 +1,24 @@ +#ifndef PADDLE_H +#define PADDLE_H + +#include + +class Paddle : public sf::RectangleShape { +public: + Paddle(float windowWidth, float windowHeight); + void handleInput(); + void enlarge(); + void shrink(); + void reset(); + + sf::Vector2f getSize() const; + +private: + float mWindowWidth; + float mWindowHeight; + float mDefaultWidth; + float mDefaultHeight; + float mSpeed; +}; + +#endif // PADDLE_H \ No newline at end of file diff --git a/src/Ball.cpp b/src/Ball.cpp new file mode 100644 index 0000000..d1515f0 --- /dev/null +++ b/src/Ball.cpp @@ -0,0 +1,101 @@ +#include "Ball.h" +#include "Constants.h" +#include +#include + +Ball::Ball() +: mDefaultRadius(Constants::BALL_RADIUS), + mDefaultSpeed(300.f), + mIsReleased(false) { + + setRadius(mDefaultRadius); + setOrigin(getRadius(), getRadius()); + reset(); + setFillColor(sf::Color::White); +} + +void Ball::update(sf::Time deltaTime) { + if (!mIsReleased) return; + + move(mVelocity * deltaTime.asSeconds()); + + // Bounce from walls + if (getPosition().x - getRadius() < 0 || getPosition().x + getRadius() > mWindowWidth) { + bounceX(); + } + + if (getPosition().y - getRadius() < 0) { + bounceY(); + } +} + +void Ball::bounceX() { + mVelocity.x = -mVelocity.x; +} + +void Ball::bounceY() { + mVelocity.y = -mVelocity.y; +} + +void Ball::bounceFromPaddle(float hitPosition) { + mVelocity.x = mDefaultSpeed * hitPosition * 0.5f; + mVelocity.y = -std::abs(mVelocity.y); +} + +void Ball::randomBounce() { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<> dist(-1.f, 1.f); + + mVelocity.x = mDefaultSpeed * dist(gen); + mVelocity.y = -std::abs(mDefaultSpeed * dist(gen)); + + // Normalize to maintain speed + float length = std::sqrt(mVelocity.x * mVelocity.x + mVelocity.y * mVelocity.y); + mVelocity = mVelocity / length * mDefaultSpeed; +} + +void Ball::stickToPaddle(const sf::RectangleShape& paddle) { + setPosition(paddle.getPosition().x, paddle.getPosition().y - paddle.getSize().y / 2.f - getRadius()); + mVelocity = sf::Vector2f(0.f, 0.f); +} + +void Ball::release() { + mIsReleased = true; + mVelocity = sf::Vector2f(0.f, -mDefaultSpeed); +} + +void Ball::reset() { + setPosition(mWindowWidth / 2.f, mWindowHeight / 2.f); + mVelocity = sf::Vector2f(0.f, 0.f); + mIsReleased = false; +} + +void Ball::increaseSpeed() { + mDefaultSpeed *= 1.2f; + float length = std::sqrt(mVelocity.x * mVelocity.x + mVelocity.y * mVelocity.y); + if (length > 0) { + mVelocity = mVelocity / length * mDefaultSpeed; + } +} + +void Ball::decreaseSpeed() { + mDefaultSpeed *= 0.8f; + if (mDefaultSpeed < 150.f) mDefaultSpeed = 150.f; + float length = std::sqrt(mVelocity.x * mVelocity.x + mVelocity.y * mVelocity.y); + if (length > 0) { + mVelocity = mVelocity / length * mDefaultSpeed; + } +} + +sf::Vector2f Ball::getPosition() const { + return sf::CircleShape::getPosition(); +} + +float Ball::getRadius() const { + return sf::CircleShape::getRadius(); +} + +sf::FloatRect Ball::getGlobalBounds() const { + return sf::CircleShape::getGlobalBounds(); +} \ No newline at end of file diff --git a/src/Block.cpp b/src/Block.cpp new file mode 100644 index 0000000..dd66178 --- /dev/null +++ b/src/Block.cpp @@ -0,0 +1,60 @@ +#include "Block.h" + +Block::Block(const sf::Vector2f& position, const sf::Vector2f& size, BlockType type, int health) +: mType(type), mHealth(health), mIsDestroyed(false) { + + setPosition(position); + setSize(size); + setOrigin(getSize() / 2.f); + + switch (mType) { + case BlockType::Regular: + setFillColor(sf::Color::Red); + break; + case BlockType::Unbreakable: + setFillColor(sf::Color::Gray); + break; + case BlockType::WithBonus: + setFillColor(sf::Color::Green); + break; + case BlockType::SpeedUp: + setFillColor(sf::Color::Yellow); + break; + } +} + +bool Block::hit() { + if (mType == BlockType::Unbreakable) { + return false; + } + + mHealth--; + + if (mHealth <= 0) { + mIsDestroyed = true; + return true; + } + + // Change color based on health + sf::Color color = getFillColor(); + color.a = 100 + 155 * mHealth / 3; + setFillColor(color); + + return false; +} + +bool Block::isDestroyed() const { + return mIsDestroyed; +} + +int Block::getHealth() const { + return mHealth; +} + +BlockType Block::getType() const { + return mType; +} + +bool Block::hasBonus() const { + return mType == BlockType::WithBonus && mIsDestroyed; +} \ No newline at end of file diff --git a/src/Bonus.cpp b/src/Bonus.cpp new file mode 100644 index 0000000..311b0c8 --- /dev/null +++ b/src/Bonus.cpp @@ -0,0 +1,45 @@ +#include "Bonus.h" + +Bonus::Bonus(const sf::Vector2f& position, BonusType type) +: mType(type), mSpeed(150.f), mIsDestroyed(false) { + + setRadius(8.f); + setOrigin(getRadius(), getRadius()); + setPosition(position); + + switch (mType) { + case BonusType::EnlargePaddle: + setFillColor(sf::Color::Cyan); + break; + case BonusType::ShrinkPaddle: + setFillColor(sf::Color::Magenta); + break; + case BonusType::StickyPaddle: + setFillColor(sf::Color::Blue); + break; + case BonusType::BottomShield: + setFillColor(sf::Color::White); + break; + case BonusType::RandomTrajectory: + setFillColor(sf::Color(255, 165, 0)); // Orange + break; + case BonusType::SpeedUp: + setFillColor(sf::Color::Yellow); + break; + case BonusType::SlowDown: + setFillColor(sf::Color(100, 100, 255)); // Light blue + break; + } +} + +void Bonus::update(sf::Time deltaTime) { + move(0.f, mSpeed * deltaTime.asSeconds()); +} + +bool Bonus::isDestroyed() const { + return mIsDestroyed; +} + +BonusType Bonus::getType() const { + return mType; +} \ No newline at end of file diff --git a/src/Game.cpp b/src/Game.cpp new file mode 100644 index 0000000..5800211 --- /dev/null +++ b/src/Game.cpp @@ -0,0 +1,329 @@ +#include "Game.h" +#include "Constants.h" +#include +#include + +const sf::Time TimePerFrame = sf::seconds(1.f/60.f); + +Game::Game() +: mWindow(sf::VideoMode(Constants::WINDOW_WIDTH, Constants::WINDOW_HEIGHT), "Arkanoid"), + mScore(0), mLives(3), mIsBallSticky(false), + mHasBottomShield(false), mIsGameOver(false) { + + mWindow.setFramerateLimit(60); + + if (!mFont.loadFromFile("arial.ttf")) { + std::cerr << "Failed to load font" << std::endl; + } + + mScoreText.setFont(mFont); + mScoreText.setCharacterSize(20); + mScoreText.setPosition(10.f, 10.f); + mScoreText.setString("Score: 0"); + + mLivesText.setFont(mFont); + mLivesText.setCharacterSize(20); + mLivesText.setPosition(10.f, 40.f); + mLivesText.setString("Lives: 3"); + + mGameOverText.setFont(mFont); + mGameOverText.setCharacterSize(50); + mGameOverText.setString("Game Over\nPress R to restart"); + mGameOverText.setPosition( + mWindow.getSize().x / 2.f - mGameOverText.getLocalBounds().width / 2.f, + mWindow.getSize().y / 2.f - mGameOverText.getLocalBounds().height / 2.f + ); + + // Create blocks + const int blockRows = 5; + const int blockCols = 10; + const float blockWidth = 70.f; + const float blockHeight = 30.f; + const float blockPadding = 5.f; + const float startX = (mWindow.getSize().x - (blockCols * (blockWidth + blockPadding))) / 2.f; + const float startY = 50.f; + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> typeDist(1, 4); + std::uniform_int_distribution<> healthDist(1, 3); + + for (int i = 0; i < blockRows; ++i) { + for (int j = 0; j < blockCols; ++j) { + float x = startX + j * (blockWidth + blockPadding); + float y = startY + i * (blockHeight + blockPadding); + + BlockType type = static_cast(typeDist(gen)); + int health = healthDist(gen); + + mBlocks.push_back(std::make_unique( + sf::Vector2f(x, y), + sf::Vector2f(blockWidth, blockHeight), + type, + health + )); + } + } +} + +void Game::run() { + sf::Clock clock; + sf::Time timeSinceLastUpdate = sf::Time::Zero; + + while (mWindow.isOpen()) { + timeSinceLastUpdate += clock.restart(); + + while (timeSinceLastUpdate > TimePerFrame) { + timeSinceLastUpdate -= TimePerFrame; + processEvents(); + + if (!mIsGameOver) { + update(TimePerFrame); + } + } + + render(); + } +} + +void Game::processEvents() { + sf::Event event; + while (mWindow.pollEvent(event)) { + if (event.type == sf::Event::Closed) { + mWindow.close(); + } + + if (event.type == sf::Event::KeyPressed) { + if (event.key.code == sf::Keyboard::R && mIsGameOver) { + resetGame(); + } + + if (event.key.code == sf::Keyboard::Space && mIsBallSticky) { + mBall.release(); + mIsBallSticky = false; + } + } + } + + mPaddle.handleInput(); +} + +void Game::update(sf::Time deltaTime) { + mBall.update(deltaTime); + + if (mIsBallSticky) { + mBall.stickToPaddle(mPaddle); + } + + for (auto& bonus : mBonuses) { + bonus->update(deltaTime); + } + + // Remove out of screen bonuses + mBonuses.erase( + std::remove_if(mBonuses.begin(), mBonuses.end(), + [this](const std::unique_ptr& bonus) { + return bonus->isDestroyed() || + bonus->getPosition().y > mWindow.getSize().y; + }), + mBonuses.end() + ); + + handleCollisions(); + + // Check if ball is out of screen + if (mBall.getPosition().y > mWindow.getSize().y) { + if (mHasBottomShield) { + mBall.bounceY(); + mHasBottomShield = false; + } else { + mLives--; + mLivesText.setString("Lives: " + std::to_string(mLives)); + + if (mLives <= 0) { + mIsGameOver = true; + } else { + mBall.reset(); + mIsBallSticky = true; + } + } + } + + // Check if all blocks are destroyed + if (mBlocks.empty()) { + resetGame(); + } +} + +void Game::render() { + mWindow.clear(sf::Color::Black); + + mWindow.draw(mPaddle); + mWindow.draw(mBall); + + for (const auto& block : mBlocks) { + mWindow.draw(*block); + } + + for (const auto& bonus : mBonuses) { + mWindow.draw(*bonus); + } + + mWindow.draw(mScoreText); + mWindow.draw(mLivesText); + + if (mIsGameOver) { + mWindow.draw(mGameOverText); + } + + mWindow.display(); +} + +void Game::handleCollisions() { + // Ball and paddle collision + if (mBall.getGlobalBounds().intersects(mPaddle.getGlobalBounds())) { + float ballCenter = mBall.getPosition().x + mBall.getRadius(); + float paddleCenter = mPaddle.getPosition().x + mPaddle.getSize().x / 2.f; + float hitPosition = (ballCenter - paddleCenter) / (mPaddle.getSize().x / 2.f); + + mBall.bounceFromPaddle(hitPosition); + + if (mIsBallSticky) { + mIsBallSticky = false; + } + } + + // Ball and blocks collision + for (auto it = mBlocks.begin(); it != mBlocks.end(); ) { + if (mBall.getGlobalBounds().intersects((*it)->getGlobalBounds())) { + sf::FloatRect blockBounds = (*it)->getGlobalBounds(); + sf::FloatRect ballBounds = mBall.getGlobalBounds(); + + // Calculate overlap in all directions + float overlapLeft = ballBounds.left + ballBounds.width - blockBounds.left; + float overlapRight = blockBounds.left + blockBounds.width - ballBounds.left; + float overlapTop = ballBounds.top + ballBounds.height - blockBounds.top; + float overlapBottom = blockBounds.top + blockBounds.height - ballBounds.top; + + // Find the minimum overlap + bool fromLeft = overlapLeft < overlapRight; + bool fromTop = overlapTop < overlapBottom; + float minOverlapX = fromLeft ? overlapLeft : overlapRight; + float minOverlapY = fromTop ? overlapTop : overlapBottom; + + // Resolve collision based on minimum overlap + if (minOverlapX < minOverlapY) { + mBall.bounceX(); + } else { + mBall.bounceY(); + } + + // Handle block hit + if ((*it)->hit()) { + mScore += (*it)->getHealth(); + mScoreText.setString("Score: " + std::to_string(mScore)); + + // Check if block has bonus + if ((*it)->hasBonus()) { + spawnBonus((*it)->getPosition(), static_cast(rand() % 5)); + } + + // Check if block affects ball speed + if ((*it)->getType() == BlockType::SpeedUp) { + mBall.increaseSpeed(); + } + + it = mBlocks.erase(it); + continue; + } + } + ++it; + } + + // Paddle and bonuses collision + for (auto it = mBonuses.begin(); it != mBonuses.end(); ) { + if ((*it)->getGlobalBounds().intersects(mPaddle.getGlobalBounds())) { + activateBonus((*it)->getType()); + it = mBonuses.erase(it); + } else { + ++it; + } + } +} + +void Game::spawnBonus(const sf::Vector2f& position, BonusType type) { + mBonuses.push_back(std::make_unique(position, type)); +} + +void Game::activateBonus(BonusType type) { + switch (type) { + case BonusType::EnlargePaddle: + mPaddle.enlarge(); + break; + case BonusType::ShrinkPaddle: + mPaddle.shrink(); + break; + case BonusType::StickyPaddle: + mIsBallSticky = true; + break; + case BonusType::BottomShield: + mHasBottomShield = true; + break; + case BonusType::RandomTrajectory: + mBall.randomBounce(); + break; + case BonusType::SpeedUp: + mBall.increaseSpeed(); + break; + case BonusType::SlowDown: + mBall.decreaseSpeed(); + break; + } +} + +void Game::resetGame() { + mBlocks.clear(); + mBonuses.clear(); + mScore = 0; + mLives = 3; + mIsBallSticky = true; + mHasBottomShield = false; + mIsGameOver = false; + + mScoreText.setString("Score: 0"); + mLivesText.setString("Lives: 3"); + + mBall.reset(); + mPaddle.reset(); + + // Recreate blocks + const int blockRows = 5; + const int blockCols = 10; + const float blockWidth = 70.f; + const float blockHeight = 30.f; + const float blockPadding = 5.f; + const float startX = (mWindow.getSize().x - (blockCols * (blockWidth + blockPadding))) / 2.f; + const float startY = 50.f; + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> typeDist(1, 4); + std::uniform_int_distribution<> healthDist(1, 3); + + for (int i = 0; i < blockRows; ++i) { + for (int j = 0; j < blockCols; ++j) { + float x = startX + j * (blockWidth + blockPadding); + float y = startY + i * (blockHeight + blockPadding); + + BlockType type = static_cast(typeDist(gen)); + int health = healthDist(gen); + + mBlocks.push_back(std::make_unique( + sf::Vector2f(x, y), + sf::Vector2f(blockWidth, blockHeight), + type, + health + )); + } + } +} \ No newline at end of file diff --git a/src/Paddle.cpp b/src/Paddle.cpp new file mode 100644 index 0000000..e5156fb --- /dev/null +++ b/src/Paddle.cpp @@ -0,0 +1,46 @@ +#include "Paddle.h" +#include "Constants.h" +#include + +Paddle::Paddle() +: mDefaultWidth(Constants::PADDLE_WIDTH), + mDefaultHeight(Constants::PADDLE_HEIGHT), + mSpeed(500.f) { + + setSize(sf::Vector2f(mDefaultWidth, mDefaultHeight)); + setOrigin(getSize() / 2.f); + setPosition(Constants::WINDOW_WIDTH / 2.f, Constants::WINDOW_HEIGHT - 50.f); + setFillColor(sf::Color::White); +} + +void Paddle::handleInput() { + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left) && getPosition().x - getSize().x / 2.f > 0) { + move(-mSpeed * 0.016f, 0.f); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right) && getPosition().x + getSize().x / 2.f < mWindowWidth) { + move(mSpeed * 0.016f, 0.f); + } +} + +void Paddle::enlarge() { + float newWidth = getSize().x * 1.5f; + if (newWidth <= mDefaultWidth * 2.f) { + setSize(sf::Vector2f(newWidth, getSize().y)); + } +} + +void Paddle::shrink() { + float newWidth = getSize().x * 0.75f; + if (newWidth >= mDefaultWidth / 2.f) { + setSize(sf::Vector2f(newWidth, getSize().y)); + } +} + +void Paddle::reset() { + setSize(sf::Vector2f(mDefaultWidth, mDefaultHeight)); + setPosition(mWindowWidth / 2.f, mWindowHeight - 50.f); +} + +sf::Vector2f Paddle::getSize() const { + return sf::RectangleShape::getSize(); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..dcfbcee --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,7 @@ +#include "Game.h" + +int main() { + Game game; + game.run(); + return 0; +} \ No newline at end of file