diff --git a/lab1/CMakeLists.txt b/lab1/CMakeLists.txt new file mode 100644 index 0000000..d380580 --- /dev/null +++ b/lab1/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.15) +project(arkanoid) + +set(CMAKE_CXX_STANDARD 17) + +include(FetchContent) +FetchContent_Declare( + SDL3 + GIT_REPOSITORY "https://github.com/libsdl-org/SDL.git" + GIT_TAG "main" + EXCLUDE_FROM_ALL +) +FetchContent_MakeAvailable(SDL3) + +add_executable(arkanoid + src/main.cpp + src/Game.cpp + src/Block.cpp + src/bonuses/Bonus.cpp + src/bonuses/AccelerateBallBonus.cpp + src/bonuses/ExpandPaddleBonus.cpp + src/bonuses/RandomBallDirectionBonus.cpp + src/bonuses/SafetyNetBonus.cpp + src/bonuses/ShrinkPaddleBonus.cpp + src/bonuses/StickyPaddleBonus.cpp +) + +target_include_directories(arkanoid PRIVATE include) +target_link_libraries(arkanoid PRIVATE SDL3::SDL3) \ No newline at end of file diff --git a/lab1/README.md b/lab1/README.md index bbaf8aa..5f177b4 100644 --- a/lab1/README.md +++ b/lab1/README.md @@ -1,2 +1,30 @@ CPP laboratories -sheesh \ No newline at end of file +sheesh + +## Чтобы собрать игру: + +```bash +mkdir build +cd build +cmake .. +cmake --build . +``` +Важно: SDL3.dll должен быть в одной папке с бинарником, SDL очень топит за dynamic link +Если что, CMake fetch content сам скачает и соберет всё что нужно за нас, но .dll куда надо не положит + +У неразрушаемых блоков есть белая рамка +У блоков, ускоряющих мяч есть зеленая рамка + +## Управление: +1. Стрелки влево/вправо — движение каретки +2. Также каретка следует за мышью +3. R — рестарт игры +4. SPACE или нажатие любой кнопки мыши - отпустить прилипший к каретке мяч (он изначально прилипший) +5. ESCAPE - выйти из игры + +## Эффекты бонусов: +- Увеличение/уменьшение каретки и скорости мяча (мгновенное) (увеличение - голубой цвет бонуса, уменьшение - красный) +- Ускорение мяча (мгновенное) (жёлтый цвет) +- Липкая каретка (действует 10 секунд) (зелёный цвет. Также меняет цвет каретки на зелёный) +- Защитная сетка (действует 10 секунд) ('одноразовое дно для шарика' из условия) (Оранжевый цвет бонуса) +- Случайное направление (мгновенное изменение траектории) (Фиолетовый цвет бонуса) diff --git a/lab1/include/Ball.hpp b/lab1/include/Ball.hpp new file mode 100644 index 0000000..2d2021b --- /dev/null +++ b/lab1/include/Ball.hpp @@ -0,0 +1,112 @@ +#pragma once +#include +#include +#include + +#include "ICollidable.hpp" +#include "IRenderable.hpp" +#include "Paddle.hpp" +#include "SDL3/SDL_pixels.h" +#include "SDL3/SDL_render.h" + +struct GameState; + +class Ball : public IRenderable, public ICollidable { + SDL_FRect rect; + constexpr static SDL_Color COLOR{255, 255, 255, 255}; + +public: + Ball(float x, float y, float w, float h) : rect{x, y, w, h} {} + + void render(SDL_Renderer* renderer) override { + SDL_SetRenderDrawColor(renderer, COLOR.r, COLOR.g, COLOR.b, COLOR.a); + SDL_RenderFillRect(renderer, &rect); + } + + SDL_FRect& getCollisionRect() override { + return rect; + } + + bool checkCollisionWith(ICollidable& other, GameState& state) override { + bool r = ICollidable::checkCollisionWith(other, state); + if (!r) return false; + if (dynamic_cast(&other)) { + if (!state.stickyPaddle) { + float hitPosition = (rect.x + rect.w / 2 - other.getCollisionRect().x) / other.getCollisionRect().w - + 0.5f; + float prevVelocity = sqrtf( + state.ballVelocity.x * state.ballVelocity.x + state.ballVelocity.y * state.ballVelocity.y); + state.ballVelocity.x = hitPosition * prevVelocity; + state.ballVelocity.y = - + sqrtf(prevVelocity * prevVelocity - state.ballVelocity.x * state.ballVelocity.x); + } + else { + state.ballSticked = true; + state.stickyPaddle = false; + } + } + return true; + } + + void hit(GameState& gameState, SDL_FRect& collisionResult) override { + float overlapLeft = rect.x + rect.w - collisionResult.x; + float overlapRight = collisionResult.x + collisionResult.w - rect.x; + float overlapTop = rect.y + rect.h - collisionResult.y; + float overlapBottom = collisionResult.y + collisionResult.h - rect.y; + + bool horizontalCollision = std::min(overlapLeft, overlapRight) < + std::min(overlapTop, overlapBottom); + + if (horizontalCollision) { + gameState.ballVelocity.x = (overlapLeft < overlapRight) + ? -std::fabs(gameState.ballVelocity.x) + : fabs(gameState.ballVelocity.x); + rect.x += (overlapLeft < overlapRight) ? -collisionResult.w : collisionResult.w; + } + else { + gameState.ballVelocity.y = (overlapTop < overlapBottom) + ? -fabs(gameState.ballVelocity.y) + : fabs(gameState.ballVelocity.y); + rect.y += (overlapTop < overlapBottom) ? -collisionResult.h : collisionResult.h; + } + } + + bool update(float deltaTime, GameState& state, Paddle& paddle) { + if (state.ballSticked) { + rect.x = paddle.getCollisionRect().x + paddle.getCollisionRect().w / 2 - rect.w / 2; + rect.y = paddle.getCollisionRect().y - rect.h; + return false; + } + + rect.x += state.ballVelocity.x * deltaTime * state.ballSpeedMultiplier; + rect.y += state.ballVelocity.y * deltaTime * state.ballSpeedMultiplier; + + // Проверка поражения + if (rect.y + rect.h > SCREEN_HEIGHT) { + if (state.safetyNetActive) { + // Отскок от защитной сетки + state.ballVelocity.y = -fabs(state.ballVelocity.y); + rect.y = SCREEN_HEIGHT - 2 * BASE_BALL_HEIGHT; + state.safetyNetActive = false; + } + else { + state.lives--; + if (state.lives > 0) { + // Респавн мяча + rect = { + SCREEN_WIDTH / 2 - BASE_BALL_WIDTH / 2, SCREEN_HEIGHT / 2 - BASE_BALL_HEIGHT / 2, + BASE_BALL_WIDTH, BASE_BALL_HEIGHT + }; + state.ballVelocity = {0, -BASE_BALL_SPEED}; + state.ballSticked = true; + } + else { + return true; + } + } + } + return false; + } + + ~Ball() override = default; +}; diff --git a/lab1/include/Block.hpp b/lab1/include/Block.hpp new file mode 100644 index 0000000..18dd31f --- /dev/null +++ b/lab1/include/Block.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include "GameState.hpp" +#include "bonuses/Bonus.hpp" + +constexpr float ACCELERATION_BLOCK_MULTIPLIER = 1.05f; + +struct Block : ICollidable, IRenderable { + SDL_FRect rect; + int health; + bool indestructible; + bool accelerateBall; + SDL_Color color; + Bonus* innerBonus; + + Block(float x, float y, float w, float h, int hp, bool indestruct, bool accelerateBall, Bonus* bonus); + bool IsDestroyed() const; + + void render(SDL_Renderer* renderer) override { + SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); + SDL_RenderFillRect(renderer, &rect); + if (indestructible) { + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); + SDL_RenderRect(renderer, &rect); + } + if (accelerateBall) { + SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); + SDL_RenderRect(renderer, &rect); + } + } + + SDL_FRect& getCollisionRect() override { return rect; }; + void hit(GameState& gameState, SDL_FRect& collisionResult) override; + + ~Block() override = default; +}; diff --git a/lab1/include/Game.hpp b/lab1/include/Game.hpp new file mode 100644 index 0000000..ce9e190 --- /dev/null +++ b/lab1/include/Game.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include +#include + +#include "Ball.hpp" +#include "Block.hpp" +#include "Paddle.hpp" +#include "bonuses/Bonus.hpp" + +class Game { +public: + Game(); + ~Game(); + + bool Initialize(); + void Run(); + void ResetGame(); + +private: + void ProcessInput(); + void Update(float deltaTime); + void UpdateBonuses(float deltaTime); + void UpdatePaddle(float deltaTime); + void Render(); + void CheckCollisions(); + void HandleBallBlockCollision(Block& block); + void SpawnBonus(float x, float y); + void HandleBlockHit(Block& block); + + SDL_Window* window; + SDL_Renderer* renderer; + bool isRunning; + bool needsReset; + + // Game objects + Paddle paddle; + Ball ball; + + std::vector> blocks; + std::vector> activeBonuses; + + + GameState state{}; + static constexpr GameState DEFAULT_STATE{}; +}; diff --git a/lab1/include/GameState.hpp b/lab1/include/GameState.hpp new file mode 100644 index 0000000..98b69ff --- /dev/null +++ b/lab1/include/GameState.hpp @@ -0,0 +1,39 @@ +#pragma once + +constexpr int LIVES = 1; +constexpr int SCREEN_WIDTH = 800; +constexpr int SCREEN_HEIGHT = 600; +constexpr float BASE_BALL_SPEED = 300.0f; +constexpr int BASE_PADDLE_WIDTH = 100; +constexpr int BASE_PADDLE_HEIGHT = 20; +constexpr float BASE_BALL_HEIGHT = 20; +constexpr float BASE_BALL_WIDTH = 20; + +struct GameState { + GameState() = default; + + //copy assignment + GameState& operator=(GameState const& other) { + score = other.score; + lives = other.lives; + stickyPaddle = other.stickyPaddle; + safetyNetActive = other.safetyNetActive; + ballSticked = other.ballSticked; + paddleSpeedMultiplier = other.paddleSpeedMultiplier; + ballSpeedMultiplier = other.ballSpeedMultiplier; + paddleWidthMultiplier = other.paddleWidthMultiplier; + ballVelocity = other.ballVelocity; + return *this; + } + + int score = 0; + int lives = LIVES; + bool stickyPaddle = false; + bool safetyNetActive = true; + bool ballSticked = true; + float paddleSpeedMultiplier = 1.0f; + float ballSpeedMultiplier = 1.0f; + float paddleWidthMultiplier = 1.0f; + SDL_FPoint ballVelocity{0, -BASE_BALL_SPEED}; + const float maxBallSpeed = 500.0f; // Максимальная скорость мяча +}; diff --git a/lab1/include/ICollidable.hpp b/lab1/include/ICollidable.hpp new file mode 100644 index 0000000..941bbd9 --- /dev/null +++ b/lab1/include/ICollidable.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "SDL3/SDL_rect.h" + +struct GameState; + +class ICollidable { +protected: + virtual void hit(GameState& gameState, SDL_FRect& collisionResult) = 0; + +public: + virtual SDL_FRect& getCollisionRect() = 0; + + virtual bool checkCollisionWith(ICollidable& other, GameState& state) { + SDL_FRect intersection; + if (SDL_GetRectIntersectionFloat(&other.getCollisionRect(), &getCollisionRect(), &intersection)) { + //collision + hit(state, intersection); + other.hit(state, intersection); + return true; + } + return false; + } + + virtual ~ICollidable() = default; +}; diff --git a/lab1/include/IExpirable.hpp b/lab1/include/IExpirable.hpp new file mode 100644 index 0000000..4719bc0 --- /dev/null +++ b/lab1/include/IExpirable.hpp @@ -0,0 +1,19 @@ +#pragma once +class IExpirable { +public: + virtual ~IExpirable() = default; + + bool activated = false; + + IExpirable(float duration) : duration(duration) {} + + virtual void update(const float dt, GameState& state) { + duration -= dt; + if (duration < 0) expire(state); + } + +protected: + float duration; // in seconds + + virtual void expire(GameState& state) = 0; +}; diff --git a/lab1/include/IRenderable.hpp b/lab1/include/IRenderable.hpp new file mode 100644 index 0000000..b8d8245 --- /dev/null +++ b/lab1/include/IRenderable.hpp @@ -0,0 +1,7 @@ +#pragma once + +class IRenderable { +public: + virtual void render(SDL_Renderer* renderer) = 0; + virtual ~IRenderable() = default; +}; diff --git a/lab1/include/Paddle.hpp b/lab1/include/Paddle.hpp new file mode 100644 index 0000000..79dacee --- /dev/null +++ b/lab1/include/Paddle.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "GameState.hpp" +#include "bonuses/Bonus.hpp" +#include "SDL3/SDL_pixels.h" +#include "SDL3/SDL_render.h" + +constexpr float BASE_PADDLE_SPEED = 200.0f; + +class Paddle : public ICollidable, public IRenderable { + SDL_FRect rect; + constexpr static SDL_Color COLOR{255, 255, 255, 255}; + constexpr static SDL_Color STICKY_COLOR{255, 255, 255, 255}; + +public: + Paddle(float x, float y, float w, float h) : rect{x, y, w, h} {} + + void render(SDL_Renderer* renderer) override { + SDL_SetRenderDrawColor(renderer, COLOR.r, COLOR.g, COLOR.b, COLOR.a); + SDL_RenderFillRect(renderer, &rect); + } + + SDL_FRect& getCollisionRect() override { + return rect; + } + + void update(float deltaTime, GameState& state) { + // Клавиатурный ввод для движения каретки + const bool* keyboardState = SDL_GetKeyboardState(nullptr); + float moveSpeed = BASE_PADDLE_SPEED * state.paddleSpeedMultiplier * deltaTime; + + if (keyboardState[SDL_SCANCODE_LEFT]) { + rect.x -= moveSpeed; + } + if (keyboardState[SDL_SCANCODE_RIGHT]) { + rect.x += moveSpeed; + } + + rect.w = state.paddleWidthMultiplier * BASE_PADDLE_WIDTH; + + + // Ограничение движения каретки + if (rect.x < 0) rect.x = 0; + if (rect.x > SCREEN_WIDTH - rect.w) { + rect.x = SCREEN_WIDTH - rect.w; + } + } + + void hit(GameState& gameState, SDL_FRect& collisionResult) override {} + + ~Paddle() override = default; +}; diff --git a/lab1/include/bonuses/AccelerateBallBonus.hpp b/lab1/include/bonuses/AccelerateBallBonus.hpp new file mode 100644 index 0000000..1858854 --- /dev/null +++ b/lab1/include/bonuses/AccelerateBallBonus.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "Bonus.hpp" + +#include "IExpirable.hpp" + +class AccelerateBallBonus : Bonus, IExpirable { + static constexpr SDL_Color COLOR{255, 255, 0, 255}; + static constexpr float ACCELERATE_BALL_MULTIPLIER = 1.2f; + static constexpr float DURATION = 5; //5s +public: + AccelerateBallBonus(int x, int y) : Bonus(x, y), IExpirable(DURATION) {} + + void render(SDL_Renderer* renderer) override; + + void update(float deltaTime, GameState& state) override { + Bonus::update(deltaTime, state); + if (activated) IExpirable::update(deltaTime, state); + } + + void expire(GameState& state) override { + deactivate(state); + } + + void activate(GameState& game) override; + void deactivate(GameState& game) override; + + + ~AccelerateBallBonus() override = default; +}; diff --git a/lab1/include/bonuses/Bonus.hpp b/lab1/include/bonuses/Bonus.hpp new file mode 100644 index 0000000..7ac1cd6 --- /dev/null +++ b/lab1/include/bonuses/Bonus.hpp @@ -0,0 +1,44 @@ +#pragma once +#include + +#include "GameState.hpp" +#include "IRenderable.hpp" +#include "ICollidable.hpp" + +constexpr float fallSpeed = 100; //pixels/sec +constexpr int BONUS_WIDTH = 40; +constexpr int BONUS_HEIGHT = 40; + +class Bonus : public IRenderable, public ICollidable { +protected: + SDL_FRect rect; + bool active = true; + +public: + Bonus(float x, float y); + bool doRender = false; + + SDL_FRect& getCollisionRect() override { return rect; } + + void hit(GameState& game, SDL_FRect& collisionResult) override { + activate(game); + disableRendering(); + } + + virtual void enableRendering() { + doRender = true; + } + + virtual void disableRendering() { + doRender = false; + } + + virtual bool isActive() const { return active; } + + void render(SDL_Renderer* renderer) override = 0; + virtual void update(float deltaTime, GameState& state); + virtual void activate(GameState& game) = 0; + virtual void deactivate(GameState& game) = 0; + + ~Bonus() override = default; +}; diff --git a/lab1/include/bonuses/ExpandPaddleBonus.hpp b/lab1/include/bonuses/ExpandPaddleBonus.hpp new file mode 100644 index 0000000..e3b6c1c --- /dev/null +++ b/lab1/include/bonuses/ExpandPaddleBonus.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "Bonus.hpp" +#include "IExpirable.hpp" + +class ExpandPaddleBonus : Bonus, IExpirable { + static constexpr SDL_Color COLOR{0, 255, 255, 255}; + static constexpr float EXPAND_PADDLE_MULTIPLIER = 1.2f; + static constexpr float DURATION = 5.0f; //5s + + bool activated = false; + +public: + ExpandPaddleBonus(int x, int y) : Bonus(x, y), IExpirable(DURATION) {} + + void render(SDL_Renderer* renderer) override; + + void update(float deltaTime, GameState& state) override { + Bonus::update(deltaTime, state); + if (activated) IExpirable::update(deltaTime, state); + } + + void expire(GameState& state) override { + deactivate(state); + } + + void activate(GameState& game) override; + void deactivate(GameState& game) override; + + ~ExpandPaddleBonus() override = default; +}; diff --git a/lab1/include/bonuses/RandomBallDirectionBonus.hpp b/lab1/include/bonuses/RandomBallDirectionBonus.hpp new file mode 100644 index 0000000..3e2576b --- /dev/null +++ b/lab1/include/bonuses/RandomBallDirectionBonus.hpp @@ -0,0 +1,16 @@ +#pragma once +#include "Bonus.hpp" + +class RandomBallDirectionBonus : Bonus { + static constexpr SDL_Color COLOR{255, 0, 255, 255}; + +public: + RandomBallDirectionBonus(int x, int y) : Bonus(x,y) {} + + void render(SDL_Renderer* renderer) override; + + void activate(GameState& game) override; + void deactivate(GameState& game) override; + + ~RandomBallDirectionBonus() override = default; +}; diff --git a/lab1/include/bonuses/SafetyNetBonus.hpp b/lab1/include/bonuses/SafetyNetBonus.hpp new file mode 100644 index 0000000..dda8abe --- /dev/null +++ b/lab1/include/bonuses/SafetyNetBonus.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "Bonus.hpp" +#include "IExpirable.hpp" + +class SafetyNetBonus : Bonus, IExpirable { + static constexpr SDL_Color COLOR{255, 165, 0, 255}; + static constexpr float DURATION = 3.0f; //3s + +public: + SafetyNetBonus(int x, int y) : Bonus(x, y), IExpirable(DURATION) {} + + void render(SDL_Renderer* renderer) override; + + void update(float deltaTime, GameState& state) override { + Bonus::update(deltaTime, state); + if (activated) IExpirable::update(deltaTime, state); + } + + void expire(GameState& state) override { + deactivate(state); + } + + void activate(GameState& game) override; + void deactivate(GameState& game) override; + + ~SafetyNetBonus() override = default; +}; diff --git a/lab1/include/bonuses/ShrinkPaddleBonus.hpp b/lab1/include/bonuses/ShrinkPaddleBonus.hpp new file mode 100644 index 0000000..2b89e56 --- /dev/null +++ b/lab1/include/bonuses/ShrinkPaddleBonus.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "Bonus.hpp" +#include "IExpirable.hpp" + +class ShrinkPaddleBonus : Bonus, IExpirable { + static constexpr SDL_Color COLOR{255, 0, 0, 255}; + static constexpr float SHRINK_PADDLE_MULTIPLIER = 1.2f; + static constexpr float DURATION = 5.0f; //5s + bool activated = false; + +public: + ShrinkPaddleBonus(int x, int y) : Bonus(x, y), IExpirable(DURATION) {} + + void render(SDL_Renderer* renderer) override; + + void update(float deltaTime, GameState& state) override { + Bonus::update(deltaTime, state); + if (activated) IExpirable::update(deltaTime, state); + } + + void expire(GameState& state) override { + deactivate(state); + } + + void activate(GameState& game) override; + void deactivate(GameState& game) override; + + ~ShrinkPaddleBonus() override = default; +}; diff --git a/lab1/include/bonuses/StickyPaddleBonus.hpp b/lab1/include/bonuses/StickyPaddleBonus.hpp new file mode 100644 index 0000000..db4f9f8 --- /dev/null +++ b/lab1/include/bonuses/StickyPaddleBonus.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "Bonus.hpp" +#include "IExpirable.hpp" + +class StickyPaddleBonus : Bonus, IExpirable { + static constexpr SDL_Color COLOR{0, 255, 0, 255}; + static constexpr float DURATION = 4.0f; //4s + +public: + StickyPaddleBonus(int x, int y) : Bonus(x, y), IExpirable(DURATION) {} + + void render(SDL_Renderer* renderer) override; + + void update(float deltaTime, GameState& state) override; + + void expire(GameState& state) override; + + void activate(GameState& game) override; + void deactivate(GameState& game) override; + + ~StickyPaddleBonus() override = default; +}; diff --git a/lab1/src/Block.cpp b/lab1/src/Block.cpp new file mode 100644 index 0000000..e5a96e5 --- /dev/null +++ b/lab1/src/Block.cpp @@ -0,0 +1,23 @@ +#include "Block.hpp" + +Block::Block(float x, float y, float w, float h, int hp, bool indestruct, bool accelerateBall, Bonus* bonus) : + rect{x, y, w, h}, + health(hp), + indestructible(indestruct), + accelerateBall(accelerateBall), + color{128, 128, 128, 255}, + innerBonus(bonus) {} + +void Block::hit(GameState& gameState, SDL_FRect& collisionResult) { + if (!indestructible) { + health--; + gameState.score++; + } + if (accelerateBall) { + gameState.ballSpeedMultiplier *= ACCELERATION_BLOCK_MULTIPLIER; + } +} + +bool Block::IsDestroyed() const { + return health <= 0 && !indestructible; +} diff --git a/lab1/src/Game.cpp b/lab1/src/Game.cpp new file mode 100644 index 0000000..fe5866d --- /dev/null +++ b/lab1/src/Game.cpp @@ -0,0 +1,286 @@ +#include "Game.hpp" +#include +#include +#include +#include + +#include "bonuses/AccelerateBallBonus.hpp" +#include "bonuses/ExpandPaddleBonus.hpp" +#include "bonuses/RandomBallDirectionBonus.hpp" +#include "bonuses/SafetyNetBonus.hpp" +#include "bonuses/ShrinkPaddleBonus.hpp" +#include "bonuses/StickyPaddleBonus.hpp" + +Game::Game() : window(nullptr), renderer(nullptr), isRunning(true), needsReset(false), + ball{ + SCREEN_WIDTH / 2 - BASE_BALL_WIDTH / 2, SCREEN_HEIGHT / 2 - BASE_BALL_WIDTH / 2, BASE_BALL_WIDTH, + BASE_BALL_HEIGHT + }, paddle{ + SCREEN_WIDTH / 2 - BASE_PADDLE_WIDTH / 2, SCREEN_HEIGHT - 2 * BASE_PADDLE_HEIGHT, BASE_PADDLE_WIDTH, + BASE_PADDLE_HEIGHT + } {} + +void Game::ResetGame() { + // Reset game state + state = DEFAULT_STATE; + paddle = { + SCREEN_WIDTH / 2 - BASE_PADDLE_WIDTH / 2, SCREEN_HEIGHT - 2 * BASE_PADDLE_HEIGHT, BASE_PADDLE_WIDTH, + BASE_PADDLE_HEIGHT + }; + ball = { + SCREEN_WIDTH / 2 - BASE_BALL_WIDTH / 2, SCREEN_HEIGHT / 2 - BASE_BALL_WIDTH / 2, BASE_BALL_WIDTH, + BASE_BALL_HEIGHT + }; + // Rebuild blocks + blocks.clear(); + const int rows = 4; + const int cols = 10; + const float blockWidth = 70; + const float blockHeight = 20; + const float spacing = 5; + + static std::random_device rd; + static std::mt19937 gen(rd()); + static std::uniform_int_distribution<> bonusDist(0, 5); + + const static std::array, 6> bonusFactories = { + [](int x, int y) { return (Bonus*)new AccelerateBallBonus(x, y); }, + [](int x, int y) { return (Bonus*)new ExpandPaddleBonus(x, y); }, + [](int x, int y) { return (Bonus*)new RandomBallDirectionBonus(x, y); }, + [](int x, int y) { return (Bonus*)new SafetyNetBonus(x, y); }, + [](int x, int y) { return (Bonus*)new ShrinkPaddleBonus(x, y); }, + [](int x, int y) { return (Bonus*)new StickyPaddleBonus(x, y); } + }; + + for (int i = 0; i < rows; ++i) { + for (int j = 0; j < cols; ++j) { + float x = j * (blockWidth + spacing) + spacing; + float y = i * (blockHeight + spacing) + 50; + bool indestruct = (i == 0); + bool accelerate = (i == 2); + int health = indestruct ? 1 : (rows - i); + Bonus* bonus = nullptr; + + if (!indestruct && (gen() % 4) == 0) { + bonus = bonusFactories[bonusDist(gen)](x, y); + } + + blocks.emplace_back(std::make_unique( + x, y, blockWidth, blockHeight, + health, indestruct, accelerate, bonus + )); + } + } + + activeBonuses.clear(); +} + +void Game::CheckCollisions() { + // Обработка границ экрана + if (ball.getCollisionRect().x < 0) { + ball.getCollisionRect().x = 0; + state.ballVelocity.x = fabs(state.ballVelocity.x); + } + if (ball.getCollisionRect().x + ball.getCollisionRect().w > SCREEN_WIDTH) { + ball.getCollisionRect().x = SCREEN_WIDTH - ball.getCollisionRect().w; + state.ballVelocity.x = -fabs(state.ballVelocity.x); + } + if (ball.getCollisionRect().y < 0) { + ball.getCollisionRect().y = 0; + state.ballVelocity.y = fabs(state.ballVelocity.y); + } + + if (ball.checkCollisionWith(paddle, state)) { + //collision + if (!state.stickyPaddle) { + // Расчет точки удара [-0.5, 0.5] + float hitPosition = (ball.getCollisionRect().x + ball.getCollisionRect().w / 2 - paddle.getCollisionRect(). + x) / paddle.getCollisionRect().w - 0.5f; + float prevVelocity = sqrtf( + state.ballVelocity.x * state.ballVelocity.x + state.ballVelocity.y * state.ballVelocity.y); + state.ballVelocity.x = hitPosition * prevVelocity; + state.ballVelocity.y = -sqrtf(prevVelocity * prevVelocity - state.ballVelocity.x * state.ballVelocity.x); + } + else { + state.ballSticked = true; + state.stickyPaddle = false; + } + } + + // Коллизия бонусов с кареткой + for (auto& bonus : activeBonuses) { + if (bonus->doRender) bonus->checkCollisionWith(paddle, state); + } + + // Коллизия блоков с мячом + for (auto& block : blocks) { + block->checkCollisionWith(ball, state); + } +} + +void Game::Run() { + Uint64 previousTicks = SDL_GetTicks(); + + while (isRunning) { + Uint64 currentTicks = SDL_GetTicks(); + float deltaTime = (currentTicks - previousTicks) / 1000.0f; + previousTicks = currentTicks; + + ProcessInput(); + SDL_Delay(std::max(0.0f, 16 - deltaTime*1000)); + deltaTime = 0.016f; + + Update(deltaTime); + Render(); + } +} + +void Game::UpdatePaddle(const float deltaTime) { + paddle.update(deltaTime, state); +} + +void Game::UpdateBonuses(const float deltaTime) { + // Движение бонусов вниз + for (auto& bonus : activeBonuses) { + bonus->update(deltaTime, state); + } + + // Удаление неактивных бонусов + activeBonuses.erase( + std::remove_if(activeBonuses.begin(), activeBonuses.end(), + [](const auto& b) { return !b->isActive(); }), + activeBonuses.end() + ); +} + +void Game::Update(const float deltaTime) { + // Обновление позиции мяча с учётом множителя скорости + if (ball.update(deltaTime, state, paddle)) ResetGame(); + + CheckCollisions(); + UpdateBonuses(deltaTime); + UpdatePaddle(deltaTime); + + // Удаление разрушенных блоков и спавн бонусов + auto bonusesRef = std::ref(activeBonuses); + blocks.erase( + std::remove_if(blocks.begin(), blocks.end(), + [bonusesRef](const std::unique_ptr& block) { + if (block->IsDestroyed()) { + //spawn bonus + if (block->innerBonus) { + bonusesRef.get().emplace_back(block->innerBonus); + block->innerBonus->enableRendering(); + } + return true; + } + return false; + }), + blocks.end() + ); +} + +void Game::Render() { + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + + paddle.render(renderer); + ball.render(renderer); + + // Отрисовка блоков + for (const auto& block : blocks) { + block->render(renderer); + } + + // Отрисовка бонусов + for (const auto& bonus : activeBonuses) { + bonus->render(renderer); + } + + // Отрисовка HUD + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); + + if (state.safetyNetActive) { + SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); + SDL_RenderLine(renderer, 0, SCREEN_HEIGHT - 20, SCREEN_WIDTH, SCREEN_HEIGHT - 20); + } + + SDL_RenderPresent(renderer); +} + +void Game::ProcessInput() { + SDL_Event event; + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_EVENT_QUIT: + isRunning = false; + break; + + case SDL_EVENT_KEY_DOWN: + // Рестарт игры по R + if (event.key.key == SDLK_R) { + ResetGame(); + } + // Отпускание мяча при липкой каретке + else if (state.ballSticked && event.key.key == SDLK_SPACE + ) { + state.ballSticked = false; + state.ballVelocity = {0, -BASE_BALL_SPEED * state.ballSpeedMultiplier}; + } + else if (event.key.key == SDLK_ESCAPE) { + isRunning = false; + } + break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: + // Отпускание мяча по клику мыши + if (state.ballSticked) { + state.ballSticked = false; + state.ballVelocity = {0, -BASE_BALL_SPEED * state.ballSpeedMultiplier}; + } + break; + + case SDL_EVENT_MOUSE_MOTION: + // Управление кареткой мышью + paddle.getCollisionRect().x = event.motion.x - paddle.getCollisionRect().w / 2; + break; + default: continue; + } + } +} + +bool Game::Initialize() { + // Инициализация SDL + if (!SDL_Init(SDL_INIT_VIDEO)) { + SDL_Log("Failed to initialize SDL: %s", SDL_GetError()); + return false; + } + + // Создание окна + window = SDL_CreateWindow("Arkanoid", + SCREEN_WIDTH, + SCREEN_HEIGHT, + SDL_WINDOW_RESIZABLE); + if (!window) { + SDL_Log("Failed to create window: %s", SDL_GetError()); + SDL_Quit(); + return false; + } + + // Создание рендерера + renderer = SDL_CreateRenderer(window, + nullptr); + if (!renderer) { + SDL_Log("Failed to create renderer: %s", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return false; + } + + // Инициализация игровых объектов + ResetGame(); + + return true; +} + +Game::~Game() = default; diff --git a/lab1/src/bonuses/AccelerateBallBonus.cpp b/lab1/src/bonuses/AccelerateBallBonus.cpp new file mode 100644 index 0000000..3919a72 --- /dev/null +++ b/lab1/src/bonuses/AccelerateBallBonus.cpp @@ -0,0 +1,20 @@ +#include "bonuses/AccelerateBallBonus.hpp" + +void AccelerateBallBonus::render(SDL_Renderer* renderer) { + if (!doRender) return; + SDL_SetRenderDrawColor(renderer, COLOR.r, COLOR.g, COLOR.b, COLOR.a); + SDL_RenderFillRect(renderer, &rect); + activated = true; +} + +void AccelerateBallBonus::activate(GameState& game) { + game.ballSpeedMultiplier *= ACCELERATE_BALL_MULTIPLIER; + activated = true; + doRender = false; +} + +void AccelerateBallBonus::deactivate(GameState& game) { + game.ballSpeedMultiplier /= ACCELERATE_BALL_MULTIPLIER; + active = false; + activated = false; +} diff --git a/lab1/src/bonuses/Bonus.cpp b/lab1/src/bonuses/Bonus.cpp new file mode 100644 index 0000000..4fdeb2c --- /dev/null +++ b/lab1/src/bonuses/Bonus.cpp @@ -0,0 +1,11 @@ +#include "bonuses/Bonus.hpp" + +Bonus::Bonus(float x, float y) : rect{x, y, BONUS_WIDTH, BONUS_HEIGHT} {} + +void Bonus::update(float deltaTime, GameState& state) {\ + if (!doRender) return; + rect.y += fallSpeed * deltaTime; + if (rect.y > SCREEN_HEIGHT) { + active = false; + } +} diff --git a/lab1/src/bonuses/ExpandPaddleBonus.cpp b/lab1/src/bonuses/ExpandPaddleBonus.cpp new file mode 100644 index 0000000..ab635d3 --- /dev/null +++ b/lab1/src/bonuses/ExpandPaddleBonus.cpp @@ -0,0 +1,19 @@ +#include "bonuses/ExpandPaddleBonus.hpp" + +void ExpandPaddleBonus::render(SDL_Renderer* renderer) { + if (!doRender) return; + SDL_SetRenderDrawColor(renderer, COLOR.r, COLOR.g, COLOR.b, COLOR.a); + SDL_RenderFillRect(renderer, &rect); +} + +void ExpandPaddleBonus::activate(GameState& game) { + game.paddleWidthMultiplier *= EXPAND_PADDLE_MULTIPLIER; + activated = true; + doRender = false; +} + +void ExpandPaddleBonus::deactivate(GameState& game) { + game.paddleWidthMultiplier /= EXPAND_PADDLE_MULTIPLIER; + active = false; + activated = false; +} diff --git a/lab1/src/bonuses/RandomBallDirectionBonus.cpp b/lab1/src/bonuses/RandomBallDirectionBonus.cpp new file mode 100644 index 0000000..825d9df --- /dev/null +++ b/lab1/src/bonuses/RandomBallDirectionBonus.cpp @@ -0,0 +1,26 @@ +#include "bonuses/RandomBallDirectionBonus.hpp" + +#include + +void RandomBallDirectionBonus::render(SDL_Renderer* renderer) { + if (!doRender) return; + SDL_SetRenderDrawColor(renderer, COLOR.r, COLOR.g, COLOR.b, COLOR.a); + SDL_RenderFillRect(renderer, &rect); +} + +void RandomBallDirectionBonus::activate(GameState& game) { + static std::random_device rd; + static std::mt19937 gen(rd()); + static std::uniform_real_distribution angleDist(0.0f, 2 * M_PI); + float angle = angleDist(gen); + float xMult = std::cos(angle); + float yMult = std::sin(angle); + game.ballVelocity.x *= xMult; + game.ballVelocity.y *= yMult; + deactivate(game); +} + +void RandomBallDirectionBonus::deactivate(GameState& game) { + active = false; + doRender = false; +} diff --git a/lab1/src/bonuses/SafetyNetBonus.cpp b/lab1/src/bonuses/SafetyNetBonus.cpp new file mode 100644 index 0000000..08ae08b --- /dev/null +++ b/lab1/src/bonuses/SafetyNetBonus.cpp @@ -0,0 +1,19 @@ +#include "bonuses/SafetyNetBonus.hpp" + +void SafetyNetBonus::render(SDL_Renderer* renderer) { + if (!doRender) return; + SDL_SetRenderDrawColor(renderer, COLOR.r, COLOR.g, COLOR.b, COLOR.a); + SDL_RenderFillRect(renderer, &rect); +} + +void SafetyNetBonus::activate(GameState& game) { + game.safetyNetActive = true; + activated = true; + doRender = false; +} + +void SafetyNetBonus::deactivate(GameState& game) { + game.safetyNetActive = false; + active = false; + activated = false; +} diff --git a/lab1/src/bonuses/ShrinkPaddleBonus.cpp b/lab1/src/bonuses/ShrinkPaddleBonus.cpp new file mode 100644 index 0000000..86f0997 --- /dev/null +++ b/lab1/src/bonuses/ShrinkPaddleBonus.cpp @@ -0,0 +1,20 @@ +#include "bonuses/ShrinkPaddleBonus.hpp" + +void ShrinkPaddleBonus::render(SDL_Renderer* renderer) { + if (!doRender) return; + SDL_SetRenderDrawColor(renderer, COLOR.r, COLOR.g, COLOR.b, COLOR.a); + SDL_RenderFillRect(renderer, &rect); +} + +void ShrinkPaddleBonus::activate(GameState& game) { + activated = true; + game.paddleWidthMultiplier /= SHRINK_PADDLE_MULTIPLIER; + doRender=false; + activated=true; +} + +void ShrinkPaddleBonus::deactivate(GameState& game) { + game.paddleWidthMultiplier *= SHRINK_PADDLE_MULTIPLIER; + active = false; + activated=false; +} diff --git a/lab1/src/bonuses/StickyPaddleBonus.cpp b/lab1/src/bonuses/StickyPaddleBonus.cpp new file mode 100644 index 0000000..7d97a7b --- /dev/null +++ b/lab1/src/bonuses/StickyPaddleBonus.cpp @@ -0,0 +1,27 @@ +#include "bonuses/StickyPaddleBonus.hpp" + +void StickyPaddleBonus::render(SDL_Renderer* renderer) { + if (!doRender) return; + SDL_SetRenderDrawColor(renderer, COLOR.r, COLOR.g, COLOR.b, COLOR.a); + SDL_RenderFillRect(renderer, &rect); +} + +void StickyPaddleBonus::activate(GameState& game) { + game.stickyPaddle = true; + activated = true; + doRender = false; +} + +void StickyPaddleBonus::deactivate(GameState& game) { + game.stickyPaddle = false; + active = false; +} + +void StickyPaddleBonus::expire(GameState& state) { + deactivate(state); +} + +void StickyPaddleBonus::update(float deltaTime, GameState& state) { + Bonus::update(deltaTime, state); + if (activated) IExpirable::update(deltaTime, state); +} diff --git a/lab1/src/main.cpp b/lab1/src/main.cpp new file mode 100644 index 0000000..04e3699 --- /dev/null +++ b/lab1/src/main.cpp @@ -0,0 +1,9 @@ +#include "Game.hpp" + +int main() { + Game game; + if(game.Initialize()) { + game.Run(); + } + return 0; +} \ No newline at end of file