add item spawner
This commit is contained in:
@@ -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})
|
||||||
132
main.cpp
132
main.cpp
@@ -5,6 +5,8 @@
|
|||||||
#include <raylib.h>
|
#include <raylib.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
#include "rng.h"
|
||||||
|
#include "spawner.h"
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
|
|
||||||
float screen_width = 800;
|
float screen_width = 800;
|
||||||
@@ -24,26 +26,10 @@ float tear_radius = 10.0f;
|
|||||||
double last_fired = 0;
|
double last_fired = 0;
|
||||||
double fire_rate = .5;
|
double fire_rate = .5;
|
||||||
|
|
||||||
std::mutex enemy_mutex;
|
|
||||||
float enemy_max_speed = 3.0;
|
float enemy_max_speed = 3.0;
|
||||||
float enemy_radius = 25.0;
|
float enemy_radius = 25.0;
|
||||||
|
|
||||||
struct Rng {
|
float item_radius = 10.0;
|
||||||
static std::random_device dev;
|
|
||||||
static std::mt19937 rng;
|
|
||||||
|
|
||||||
static float generate() {
|
|
||||||
return static_cast<float>(rng()) / static_cast<float>(std::mt19937::max());
|
|
||||||
}
|
|
||||||
|
|
||||||
static float generate(const uint32_t max) {
|
|
||||||
return generate() * static_cast<float>(max);
|
|
||||||
}
|
|
||||||
|
|
||||||
static float generate(const int min, const int max) {
|
|
||||||
return generate() * (static_cast<float>(max) - static_cast<float>(min)) + static_cast<float>(min);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
std::random_device Rng::dev{};
|
std::random_device Rng::dev{};
|
||||||
std::mt19937 Rng::rng{dev()};
|
std::mt19937 Rng::rng{dev()};
|
||||||
@@ -91,50 +77,6 @@ void Player::move(const float delta_x, const float delta_y) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T, size_t N>
|
|
||||||
struct Spawner {
|
|
||||||
std::mutex mutex;
|
|
||||||
std::condition_variable cv;
|
|
||||||
std::atomic<bool> running;
|
|
||||||
std::atomic<bool> paused;
|
|
||||||
std::optional<std::thread> timer;
|
|
||||||
long long rate_secs;
|
|
||||||
T values[N];
|
|
||||||
|
|
||||||
explicit Spawner(const long long rate_secs)
|
|
||||||
: rate_secs(rate_secs) {
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void spawn() {}
|
|
||||||
|
|
||||||
void start() {
|
|
||||||
running.store(true);
|
|
||||||
timer = std::thread([&] {
|
|
||||||
while (running.load()) {
|
|
||||||
const auto start = std::chrono::steady_clock::now();
|
|
||||||
|
|
||||||
if (!paused.load()) {
|
|
||||||
std::lock_guard lock(mutex);
|
|
||||||
|
|
||||||
spawn();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto next = start + std::chrono::seconds(rate_secs);
|
|
||||||
std::unique_lock lock{mutex};
|
|
||||||
cv.wait_until(lock, next, [&] { return !running.load(); });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual ~Spawner() {
|
|
||||||
running.store(false);
|
|
||||||
cv.notify_all();
|
|
||||||
if (timer) {
|
|
||||||
timer.value().join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct EnemySpawner final : Spawner<Enemy, 100> {
|
struct EnemySpawner final : Spawner<Enemy, 100> {
|
||||||
explicit EnemySpawner(const long long rate_secs)
|
explicit EnemySpawner(const long long rate_secs)
|
||||||
: Spawner<Enemy, 100>(rate_secs) {
|
: Spawner<Enemy, 100>(rate_secs) {
|
||||||
@@ -172,6 +114,30 @@ struct EnemySpawner final : Spawner<Enemy, 100> {
|
|||||||
|
|
||||||
EnemySpawner enemy_spawner{2};
|
EnemySpawner enemy_spawner{2};
|
||||||
|
|
||||||
|
struct ItemSpawner final : Spawner<Item, 100> {
|
||||||
|
explicit ItemSpawner(const long long rate_secs) : Spawner<Item, 100>(rate_secs) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void spawn() override {
|
||||||
|
const auto item_type = static_cast<ItemType>(Rng::generate(ItemType::ITEM_TYPE_COUNT));
|
||||||
|
const auto x = Rng::generate(static_cast<uint32_t>(screen_width));
|
||||||
|
const auto y = Rng::generate(static_cast<uint32_t>(screen_height));
|
||||||
|
|
||||||
|
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) {
|
||||||
@@ -194,13 +160,13 @@ void update_tears() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto &enemy: enemy_spawner.values) {
|
enemy_spawner.for_each([¢er, &active](auto &enemy) {
|
||||||
if (enemy.alive && CheckCollisionCircles(center, tear_radius, enemy.center, enemy.radius)) {
|
if (enemy.alive && CheckCollisionCircles(center, tear_radius, enemy.center, enemy.radius)) {
|
||||||
active = false;
|
active = false;
|
||||||
enemy.alive = false;
|
enemy.alive = false;
|
||||||
score++;
|
score++;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case UP:
|
case UP:
|
||||||
@@ -230,6 +196,7 @@ int main() {
|
|||||||
|
|
||||||
|
|
||||||
enemy_spawner.start();
|
enemy_spawner.start();
|
||||||
|
item_spawner.start();
|
||||||
|
|
||||||
player.width = 50;
|
player.width = 50;
|
||||||
player.height = 50;
|
player.height = 50;
|
||||||
@@ -296,26 +263,33 @@ int main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
enemy_spawner.for_each([now](auto &enemy) {
|
||||||
std::lock_guard enemy_guard{enemy_mutex};
|
if (enemy.alive) {
|
||||||
for (auto &[center, radius, speed, alive]: enemy_spawner.values) {
|
enemy.center = Vector2MoveTowards(enemy.center, player.center(), enemy.speed);
|
||||||
if (alive) {
|
DrawCircleV(enemy.center, enemy.radius, RED);
|
||||||
center = Vector2MoveTowards(center, player.center(), speed);
|
|
||||||
DrawCircleV(center, radius, RED);
|
|
||||||
|
|
||||||
if (center.x < 0 || screen_width < center.x ||
|
if (enemy.center.x < 0 || screen_width < enemy.center.x ||
|
||||||
center.y < 0 || screen_height < center.y) {
|
enemy.center.y < 0 || screen_height < enemy.center.y) {
|
||||||
alive = false;
|
enemy.alive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CheckCollisionCircleRec(center, radius, player.rect()) &&
|
if (CheckCollisionCircleRec(enemy.center, enemy.radius, player.rect()) &&
|
||||||
player.last_hit + player_invulnerability < now) {
|
player.last_hit + player_invulnerability < now) {
|
||||||
player.lives--;
|
player.lives--;
|
||||||
player.last_hit = now;
|
player.last_hit = now;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
item_spawner.for_each([](auto &item) {
|
||||||
|
if (!item.active) return;
|
||||||
|
|
||||||
|
DrawCircleV(item.center, item.radius, item.color());
|
||||||
|
|
||||||
|
if (CheckCollisionCircleRec(item.center, item.radius, player.rect())) {
|
||||||
|
item.active = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
std::format_to(std::back_inserter(score_text_buffer), "Score: {}", score);
|
std::format_to(std::back_inserter(score_text_buffer), "Score: {}", score);
|
||||||
DrawText(score_text_buffer.c_str(), 50, static_cast<int>(screen_height) - 100, 20, BLACK);
|
DrawText(score_text_buffer.c_str(), 50, static_cast<int>(screen_height) - 100, 20, BLACK);
|
||||||
|
|||||||
17
rng.cpp
Normal file
17
rng.cpp
Normal 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() * (static_cast<float>(max) - static_cast<float>(min)) + static_cast<float>(min);
|
||||||
|
}
|
||||||
17
rng.h
Normal file
17
rng.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// Created by Grant Horner on 12/1/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
struct Rng {
|
||||||
|
static std::random_device dev;
|
||||||
|
static std::mt19937 rng;
|
||||||
|
|
||||||
|
static float generate();
|
||||||
|
|
||||||
|
static float generate(uint32_t max);
|
||||||
|
|
||||||
|
static float generate(int min, int max);
|
||||||
|
};
|
||||||
68
spawner.h
Normal file
68
spawner.h
Normal 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> running;
|
||||||
|
std::atomic<bool> paused;
|
||||||
|
std::jthread timer;
|
||||||
|
long long rate_secs;
|
||||||
|
T values[N];
|
||||||
|
|
||||||
|
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) {
|
||||||
|
func(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
running.store(true);
|
||||||
|
timer = std::jthread([&] {
|
||||||
|
while (running.load()) {
|
||||||
|
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, [&] { return !running.load(); });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T, size_t N>
|
||||||
|
Spawner<T, N>::~Spawner() {
|
||||||
|
running.store(false);
|
||||||
|
cv.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
62
types.h
62
types.h
@@ -4,14 +4,19 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
#include "raymath.h"
|
#include "raymath.h"
|
||||||
#include "raylib.h"
|
#include "raylib.h"
|
||||||
|
|
||||||
struct Player : Rectangle {
|
struct Player : Rectangle {
|
||||||
uint8_t lives;
|
uint8_t lives;
|
||||||
double last_hit;
|
double last_hit;
|
||||||
|
|
||||||
[[nodiscard]] Vector2 center() const noexcept;
|
[[nodiscard]] Vector2 center() const noexcept;
|
||||||
|
|
||||||
[[nodiscard]] Rectangle rect() const noexcept;
|
[[nodiscard]] Rectangle rect() const noexcept;
|
||||||
|
|
||||||
[[nodiscard]] bool is_invulnerable(double now) const noexcept;
|
[[nodiscard]] bool is_invulnerable(double now) const noexcept;
|
||||||
|
|
||||||
void move(float delta_x, float delta_y);
|
void move(float delta_x, float delta_y);
|
||||||
@@ -41,3 +46,60 @@ struct Enemy {
|
|||||||
float speed;
|
float speed;
|
||||||
bool alive;
|
bool alive;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum ItemType {
|
||||||
|
TEARS_UP,
|
||||||
|
TEARS_DOWN,
|
||||||
|
RANGE_UP,
|
||||||
|
RANGE_DOWN,
|
||||||
|
ITEM_TYPE_COUNT
|
||||||
|
};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct std::formatter<ItemType> : std::formatter<string_view> {
|
||||||
|
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 = "TEARS_UP";
|
||||||
|
break;
|
||||||
|
case TEARS_DOWN:
|
||||||
|
item_type_str = "TEARS_DOWN";
|
||||||
|
break;
|
||||||
|
case RANGE_UP:
|
||||||
|
item_type_str = "RANGE_UP";
|
||||||
|
break;
|
||||||
|
case RANGE_DOWN:
|
||||||
|
item_type_str = "RANGE_DOWN";
|
||||||
|
break;
|
||||||
|
case ITEM_TYPE_COUNT:
|
||||||
|
item_type_str = "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.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user