Compare commits

...

13 Commits

Author SHA1 Message Date
5abfedb205 add cancel parameter for Spawner::for_each 2025-12-04 10:30:05 -05:00
eec22cd791 give enemies hp 2025-12-02 19:53:44 -05:00
8e89f1bf70 refactor viewport 2025-12-02 19:40:54 -05:00
d4f69278d9 refactor speed, invulnerability; refactor move to use Vec2 2025-12-02 17:09:57 -05:00
6a6249798e refactor tear properties into Player 2025-12-01 12:41:07 -05:00
ee5cb71560 thread stop clean up 2025-12-01 12:15:08 -05:00
17663a1f75 add level edge 2025-12-01 11:26:47 -05:00
77cb83cfd5 made items affect stats 2025-12-01 10:59:00 -05:00
d6dff2a539 add item spawner 2025-12-01 10:38:44 -05:00
49751ea09e refactor spawner 2025-11-30 21:00:04 -05:00
b77af66c9c added lives 2025-11-30 20:13:49 -05:00
275da1bd65 cleanup Rng impl 2025-11-30 19:51:27 -05:00
53f1264c5f kill enemies for score 2025-11-30 19:17:51 -05:00
6 changed files with 446 additions and 124 deletions

View File

@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 4.0)
project(isaac__) project(isaac__)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexperimental-library")
include(FetchContent) include(FetchContent)
@@ -17,6 +18,9 @@ FetchContent_MakeAvailable(raylib)
add_executable(isaac__ main.cpp add_executable(isaac__ main.cpp
types.h types.h
spawner.h
rng.cpp
rng.h
) )
target_link_libraries(${PROJECT_NAME} PUBLIC raylib) target_link_libraries(${PROJECT_NAME} PUBLIC raylib)
target_include_directories(${PROJECT_NAME} PUBLIC ${raylib_public_headers}) target_include_directories(${PROJECT_NAME} PUBLIC ${raylib_public_headers})

316
main.cpp
View File

@@ -1,58 +1,71 @@
#include <iterator>
#include <mutex> #include <mutex>
#include <print> #include <print>
#include <random> #include <random>
#include <raylib.h> #include <raylib.h>
#include <thread>
#include "raymath.h"
#include "rng.h"
#include "spawner.h"
#include "types.h" #include "types.h"
auto screen_width = 800; Viewport viewport{800, 600};
auto screen_height = 600;
Rectangle walls[] = { Rectangle walls[] = {};
{.x = 100, .y = 100, .width = 5, .height = 100}
float player_width = 50;
Player player = {
.rect = {
.x = viewport.level_width / 2 - player_width / 2,
.y = viewport.level_height / 2 - player_width / 2,
.width = player_width,
.height = player_width,
},
.lives = 3,
.speed = 5.0,
.invulnerability_secs = 1,
.last_hit = -10,
.tear_speed = 10,
.tear_range = 300,
.tear_radius = 10.0f,
.last_fired = 0,
.fire_rate = .5,
}; };
Player player; uint32_t score;
auto player_speed = 5.0f;
Tear tears[100] = {}; std::array<Tear, 100> tears;
float tear_speed = 10;
float tear_range = 500;
float tear_radius = 10.0f;
double last_fired = 0;
double fire_rate = .5;
std::random_device dev; float enemy_max_speed = 3.0;
std::mt19937 rng(dev()); float enemy_radius = 25.0;
std::mutex enemy_mutex; float item_radius = 10.0;
float enemy_max_speed = 5.0;
float enemy_radius = 50.0;
Enemy enemies[100] = {};
Vector2 Player::center() const noexcept { Vector2 Player::center() const noexcept {
return {.x = this->x + this->width / 2, .y = this->y + this->height / 2}; return {.x = this->rect.x + this->rect.width / 2, .y = this->rect.y + this->rect.height / 2};
} }
void Player::move(const float delta_x, const float delta_y) { bool Player::is_invulnerable(const double now) const noexcept {
auto x = std::min( return now < last_hit + invulnerability_secs;
std::max(this->x + delta_x, 0.0f), }
static_cast<float>(screen_width) - this->width);
auto y = std::min( void Player::move(const Vector2 delta) {
std::max(this->y + delta_y, 0.0f), const auto [next_x, next_y] = Vector2Clamp(
static_cast<float>(screen_height) - this->height); Vector2Add({this->rect.x, this->rect.y}, delta),
{viewport.level_left, viewport.level_top},
Vector2Subtract({viewport.level_right, viewport.level_bottom}, {this->rect.width, this->rect.height})
);
bool collided = false; bool collided = false;
const Rectangle next_position = { const Rectangle next_rect = {
.x = x, .x = next_x,
.y = y, .y = next_y,
.width = this->width, .width = this->rect.width,
.height = this->height .height = this->rect.height
}; };
for (const auto wall: walls) { for (const auto wall: walls) {
if (CheckCollisionRecs(wall, next_position)) { if (CheckCollisionRecs(wall, next_rect)) {
collided = true; collided = true;
break; break;
} }
@@ -60,74 +73,75 @@ void Player::move(const float delta_x, const float delta_y) {
if (collided) { if (collided) {
} else { } else {
this->x = x; this->rect.x = next_x;
this->y = y; this->rect.y = next_y;
} }
} }
struct EnemySpawner { struct EnemySpawner final : Spawner<Enemy, 100> {
std::mutex m;
std::condition_variable cv;
std::atomic<bool> running;
std::atomic<bool> paused;
std::thread timer;
long long rate_secs;
explicit EnemySpawner(const long long rate_secs) explicit EnemySpawner(const long long rate_secs)
: rate_secs(rate_secs) { : Spawner<Enemy, 100>(rate_secs) {
} }
void start() { void spawn() override {
running.store(true); const auto num = Rng::generate();
timer = std::thread([&] { const auto starting_x = Rng::generate(static_cast<int>(viewport.level_left),
while (running.load()) { static_cast<int>(viewport.level_right));
const auto start = std::chrono::steady_clock::now(); const auto starting_y = Rng::generate(static_cast<int>(viewport.level_top),
static_cast<int>(viewport.level_bottom));
if (!paused.load()) { for (auto &enemy: this->values) {
std::lock_guard lock(enemy_mutex); if (enemy.alive()) continue;
const auto num = static_cast<float>(rng()) / static_cast<float>(std::mt19937::max()); enemy.radius = enemy_radius;
const auto dir = static_cast<float>(rng()) / static_cast<float>(std::mt19937::max()); auto hp = Rng::generate(1, 4);
enemy.hp = static_cast<uint32_t>(hp);
for (auto &[center, radius, moving, alive]: enemies) {
if (alive) continue;
alive = true;
radius = enemy_radius;
if (num < 0.25) { if (num < 0.25) {
center = {.x = static_cast<float>(screen_width) / 2, .y = 0}; enemy.center = {.x = starting_x, .y = viewport.level_top};
moving = {.x = (dir - 1) * 2, .y = 1};
} else if (0.25 <= num && num < 0.5) { } else if (0.25 <= num && num < 0.5) {
center = {.x = static_cast<float>(screen_width) / 2, .y = static_cast<float>(screen_height)}; enemy.center = {
moving = {.x = (dir - 1) * 2, .y = -1}; .x = starting_x, .y = viewport.level_bottom
};
} else if (0.5 <= num && num < 0.75) { } else if (0.5 <= num && num < 0.75) {
center = {.x = 0, .y = static_cast<float>(screen_height) / 2}; enemy.center = {.x = viewport.level_left, .y = starting_y};
moving = {.x = 1, .y = (dir - 1) * 2};
} else { } else {
center = {.x = static_cast<float>(screen_width), .y = static_cast<float>(screen_height) / 2}; enemy.center = {
moving = {.x = -1, .y = (dir - 1) * 2}; .x = viewport.level_right, .y = starting_y
};
} }
const auto speed_ratio = static_cast<float>(rng()) / static_cast<float>(std::mt19937::max()); enemy.speed = Rng::generate(static_cast<uint32_t>(enemy_max_speed));
const auto speed = speed_ratio * enemy_max_speed;
moving = Vector2Scale(moving, speed);
break; break;
} }
} }
};
auto next = start + std::chrono::seconds(rate_secs); EnemySpawner enemy_spawner{1};
std::unique_lock lock{m};
cv.wait_until(lock, next, [&] { return !running.load(); }); struct ItemSpawner final : Spawner<Item, 100> {
} explicit ItemSpawner(const long long rate_secs) : Spawner<Item, 100>(rate_secs) {
});
} }
~EnemySpawner() { void spawn() override {
running.store(false); const auto item_type = static_cast<ItemType>(Rng::generate(ITEM_TYPE_COUNT));
cv.notify_all(); const auto x = Rng::generate(static_cast<int>(viewport.level_left), static_cast<int>(viewport.level_right));
timer.join(); const auto y = Rng::generate(static_cast<int>(viewport.level_top), static_cast<int>(viewport.level_bottom));
for (auto &item: this->values) {
if (item.active) continue;
item.active = true;
item.type = item_type;
item.center = Vector2{x, y};
item.radius = item_radius;
break;
}
} }
}; };
ItemSpawner item_spawner{5};
void spawn_tear(const Vector2 center, const Direction direction) { void spawn_tear(const Vector2 center, const Direction direction) {
for (auto &[tear_center, tear_direction, tear_active, starting_center]: tears) { for (auto &[tear_center, tear_direction, tear_active, starting_center]: tears) {
if (!tear_active) { if (!tear_active) {
@@ -144,27 +158,36 @@ void update_tears() {
for (auto &[center, direction, active, starting_center]: tears) { for (auto &[center, direction, active, starting_center]: tears) {
if (active) { if (active) {
for (const auto wall: walls) { for (const auto wall: walls) {
if (CheckCollisionCircleRec(center, tear_radius, wall)) { if (CheckCollisionCircleRec(center, player.tear_radius, wall)) {
active = false; active = false;
break; break;
} }
} }
enemy_spawner.for_each([&center, &active](auto &enemy, auto& cancel) {
if (enemy.alive() && CheckCollisionCircles(center, player.tear_radius, enemy.center, enemy.radius)) {
active = false;
if (--enemy.hp == 0) score++;
cancel = true;
}
});
switch (direction) { switch (direction) {
case UP: case UP:
center.y -= tear_speed; center.y -= player.tear_speed;
break; break;
case DOWN: case DOWN:
center.y += tear_speed; center.y += player.tear_speed;
break; break;
case LEFT: case LEFT:
center.x -= tear_speed; center.x -= player.tear_speed;
break; break;
case RIGHT: case RIGHT:
center.x += tear_speed; center.x += player.tear_speed;
break; break;
} }
if (tear_range < Vector2Distance(center, starting_center)) { if (player.tear_range < Vector2Distance(center, starting_center)) {
active = false; active = false;
} }
} }
@@ -172,30 +195,37 @@ void update_tears() {
} }
int main() { int main() {
SetConfigFlags(FLAG_WINDOW_RESIZABLE);
InitWindow(800, 600, "Isaac++"); InitWindow(800, 600, "Isaac++");
SetTargetFPS(60); SetTargetFPS(60);
EnemySpawner spawner{3}; enemy_spawner.start();
spawner.start(); item_spawner.start();
player.x = 10;
player.y = 10; std::string score_text_buffer;
player.width = 50; score_text_buffer.reserve(64);
player.height = 50;
std::string lives_text_buffer;
lives_text_buffer.reserve(64);
std::string item_pickup_text_buffer;
item_pickup_text_buffer.reserve(64);
double item_last_picked_up = 0;
while (!WindowShouldClose()) { while (!WindowShouldClose()) {
constexpr double item_pickup_message_duration = 3;
// float dt_secs = GetFrameTime();
if (IsWindowResized()) { if (IsWindowResized()) {
screen_height = GetScreenHeight(); viewport = {static_cast<float>(GetScreenWidth()), static_cast<float>(GetScreenHeight())};
screen_width = GetScreenWidth();
} }
float delta_x = 0;
float delta_y = 0;
if (IsKeyDown(KEY_W)) delta_y -= player_speed; Vector2 delta{};
if (IsKeyDown(KEY_S)) delta_y += player_speed; if (IsKeyDown(KEY_W)) delta.y -= player.speed;
if (IsKeyDown(KEY_A)) delta_x -= player_speed; if (IsKeyDown(KEY_S)) delta.y += player.speed;
if (IsKeyDown(KEY_D)) delta_x += player_speed; if (IsKeyDown(KEY_A)) delta.x -= player.speed;
player.move(delta_x, delta_y); if (IsKeyDown(KEY_D)) delta.x += player.speed;
player.move(delta);
std::optional<Direction> tear_direction; std::optional<Direction> tear_direction;
@@ -212,16 +242,19 @@ int main() {
tear_direction = DOWN; tear_direction = DOWN;
} }
if (const auto now = GetTime(); last_fired + fire_rate < now && tear_direction.has_value()) { const auto now = GetTime();
last_fired = now; if (player.last_fired + player.fire_rate < now && tear_direction.has_value()) {
player.last_fired = now;
spawn_tear(player.center(), tear_direction.value()); spawn_tear(player.center(), tear_direction.value());
} }
update_tears(); update_tears();
BeginDrawing(); BeginDrawing();
ClearBackground(RAYWHITE); ClearBackground(GRAY);
DrawRectangleRec(player, BLUE); DrawRectangle(50, 50, static_cast<int>(viewport.level_width), static_cast<int>(viewport.level_height),
RAYWHITE);
DrawRectangleRec(player.rect, player.is_invulnerable(now) ? GOLD : BLUE);
for (const auto wall: walls) { for (const auto wall: walls) {
DrawRectangleRec(wall, GREEN); DrawRectangleRec(wall, GREEN);
@@ -229,15 +262,82 @@ int main() {
for (const auto tear: tears) { for (const auto tear: tears) {
if (tear.active) { if (tear.active) {
DrawCircleV(tear.center, tear_radius, SKYBLUE); DrawCircleV(tear.center, player.tear_radius, SKYBLUE);
} }
} }
for (auto& enemy: enemies) { enemy_spawner.for_each([now](Enemy &enemy, auto& cancel) {
if (enemy.alive) { if (enemy.alive()) {
enemy.center = Vector2Add(enemy.center, enemy.moving); enemy.center = Vector2MoveTowards(enemy.center, player.center(), enemy.speed);
DrawCircleV(enemy.center, enemy.radius, RED); const Color enemy_color = enemy.hp == 1 ? PINK : enemy.hp == 2 ? ORANGE : RED;
DrawCircleV(enemy.center, enemy.radius, enemy_color);
if (enemy.center.x < viewport.level_left || viewport.level_right < enemy.center.x ||
enemy.center.y < viewport.level_top || viewport.level_bottom < enemy.center.y) {
// Basically removing the enemy from the pool;
enemy.hp = 0;
} }
if (CheckCollisionCircleRec(enemy.center, enemy.radius, player.rect) &&
player.last_hit + player.invulnerability_secs < now) {
player.lives--;
player.last_hit = now;
}
}
});
item_spawner.for_each([&item_pickup_text_buffer, now, &item_last_picked_up](Item &item, auto& cancel) {
if (!item.active) return;
DrawCircleV(item.center, item.radius, item.color());
if (CheckCollisionCircleRec(item.center, item.radius, player.rect)) {
switch (item.type) {
case TEARS_UP:
player.fire_rate = std::max(player.fire_rate - 0.1, 0.1);
break;
case TEARS_DOWN:
player.fire_rate += 0.1;
break;
case RANGE_UP:
player.tear_range += 100.0f;
break;
case RANGE_DOWN:
player.tear_range = std::max(player.tear_range - 100.0f, 100.0f);
break;
case ITEM_TYPE_COUNT:
break;
}
if (item_pickup_text_buffer.empty()) {
std::format_to(std::back_inserter(item_pickup_text_buffer), "{:t}!", item.type);
} else {
std::format_to(std::back_inserter(item_pickup_text_buffer), " {:t}!", item.type);
}
item_last_picked_up = now;
item.active = false;
}
});
const int text_left = static_cast<int>(viewport.level_left) + 10;
DrawRectangle(static_cast<int>(viewport.level_left), static_cast<int>(viewport.level_height) - 110, 200, 110,
{130, 130, 130, 100});
std::format_to(std::back_inserter(score_text_buffer), "Score: {}", score);
DrawText(score_text_buffer.c_str(), text_left, static_cast<int>(viewport.level_height) - 100, 20, BLACK);
score_text_buffer.clear();
std::format_to(std::back_inserter(lives_text_buffer), "Lives: {}", player.lives);
DrawText(lives_text_buffer.c_str(), text_left, static_cast<int>(viewport.level_height) - 75, 20, BLACK);
lives_text_buffer.clear();
if (now < item_last_picked_up + item_pickup_message_duration && !item_pickup_text_buffer.empty()) {
DrawText(item_pickup_text_buffer.c_str(), text_left, static_cast<int>(viewport.level_height) - 50, 20,
BLACK);
}
if (item_last_picked_up + item_pickup_message_duration <= now && !item_pickup_text_buffer.empty()) {
item_pickup_text_buffer.clear();
} }
EndDrawing(); EndDrawing();

17
rng.cpp Normal file
View File

@@ -0,0 +1,17 @@
//
// Created by Grant Horner on 12/1/25.
//
#include "rng.h"
float Rng::generate() {
return static_cast<float>(rng()) / static_cast<float>(std::mt19937::max());
}
float Rng::generate(const uint32_t max) {
return generate() * static_cast<float>(max);
}
float Rng::generate(const int min, const int max) {
return generate(max - min) + static_cast<float>(min);
}

17
rng.h Normal file
View File

@@ -0,0 +1,17 @@
//
// Created by Grant Horner on 12/1/25.
//
#pragma once
#include <random>
struct Rng {
inline static std::random_device dev;
inline static std::mt19937 rng{dev()};
static float generate();
static float generate(uint32_t max);
static float generate(int min, int max);
};

68
spawner.h Normal file
View File

@@ -0,0 +1,68 @@
//
// Created by Grant Horner on 12/1/25.
//
#pragma once
#include <atomic>
#include <mutex>
#include <thread>
template<typename T, size_t N>
struct Spawner {
std::mutex mutex;
std::condition_variable cv;
std::atomic<bool> paused;
std::jthread timer;
std::atomic<unsigned long long> rate_secs;
std::array<T, N> values;
explicit Spawner(long long rate_secs);
virtual void spawn();
template <typename F>
void for_each(F&& func) {
std::lock_guard lock{mutex};
for (auto& value : values) {
bool cancel = false;
func(value, cancel);
if (cancel) break;;
}
}
void start();
virtual ~Spawner();
};
template<typename T, size_t N>
Spawner<T, N>::Spawner(const long long rate_secs) : rate_secs(rate_secs) {
}
template<typename T, size_t N>
void Spawner<T, N>::spawn() {
}
template<typename T, size_t N>
void Spawner<T, N>::start() {
timer = std::jthread([&] (const std::stop_token &st) {
while (!st.stop_requested()) {
const auto start = std::chrono::steady_clock::now();
std::unique_lock lock{mutex};
if (!paused.load()) {
spawn();
}
auto next = start + std::chrono::seconds(rate_secs);
cv.wait_until(lock, next);
}
});
}
template<typename T, size_t N>
Spawner<T, N>::~Spawner() {
timer.request_stop();
cv.notify_all();
}

126
types.h
View File

@@ -4,13 +4,27 @@
#pragma once #pragma once
#include "raymath.h" #include <cassert>
#include "raylib.h" #include "raylib.h"
struct Player : Rectangle { struct Player {
Rectangle rect;
uint8_t lives;
float speed;
float invulnerability_secs;
double last_hit;
float tear_speed;
float tear_range;
float tear_radius;
double last_fired;
double fire_rate;
[[nodiscard]] Vector2 center() const noexcept; [[nodiscard]] Vector2 center() const noexcept;
void move(float delta_x, float delta_y); [[nodiscard]] bool is_invulnerable(double now) const noexcept;
void move(Vector2 delta);
}; };
enum Direction { enum Direction {
@@ -34,6 +48,108 @@ struct Tear {
struct Enemy { struct Enemy {
Vector2 center; Vector2 center;
float radius; float radius;
Vector2 moving; float speed;
bool alive; uint32_t hp;
[[nodiscard]] bool alive() const noexcept { return hp != 0; }
};
enum ItemType {
TEARS_UP,
TEARS_DOWN,
RANGE_UP,
RANGE_DOWN,
ITEM_TYPE_COUNT
};
template<>
struct std::formatter<ItemType> : std::formatter<string_view> {
bool title_case = false;
constexpr auto parse(std::format_parse_context &ctx) {
auto it = ctx.begin();
if (it == ctx.end()) return it;
if (*it == 't') {
title_case = true;
++it;
}
return it;
}
auto format(const ItemType &item_type, std::format_context &ctx) const {
std::string_view item_type_str;
switch (item_type) {
case TEARS_UP:
item_type_str = title_case ? "Tears Up" : "TEARS_UP";
break;
case TEARS_DOWN:
item_type_str = title_case ? "Tears Down" : "TEARS_DOWN";
break;
case RANGE_UP:
item_type_str = title_case ? "Range Up" : "RANGE_UP";
break;
case RANGE_DOWN:
item_type_str = title_case ? "Range Down" : "RANGE_DOWN";
break;
case ITEM_TYPE_COUNT:
item_type_str = title_case ? "Item Type Count" : "ITEM_TYPE_COUNT";
break;
}
return std::formatter<string_view>::format(item_type_str, ctx);;
}
};
struct Item {
Vector2 center;
float radius;
ItemType type;
bool active;
[[nodiscard]] Color color() const noexcept {
switch (type) {
case TEARS_UP:
return BLUE;
case TEARS_DOWN:
return GREEN;
case RANGE_UP:
return RED;
case RANGE_DOWN:
return ORANGE;
case ITEM_TYPE_COUNT:
assert(0 && "ITEM_TYPE_COUNT should not be instantiated.");
}
assert(0 && "Unreachable.");
}
};
struct Viewport {
Viewport() = default;
Viewport(const float screen_width, const float screen_height)
: screen_width(screen_width),
screen_height(screen_height),
padding(50),
level_width(screen_width - padding * 2),
level_height(screen_height - padding * 2),
level_left(padding),
level_top(padding),
level_right(level_width + padding),
level_bottom(level_height + padding) {
}
Viewport& operator=(const Viewport &other) = default;
float screen_width;
float screen_height;
float padding;
float level_width;
float level_height;
float level_left;
float level_top;
float level_right;
float level_bottom;
}; };