Compare commits

..

3 Commits

Author SHA1 Message Date
3ac663714f
feat: add tile geometry utilities and enhance Vec2 class with additional methods
Signed-off-by: szdytom <szdytom@qq.com>
2025-08-05 17:38:02 +08:00
0edb71385f
feat: implement TickSystem for managing world ticks and initialize World constructor
Signed-off-by: szdytom <szdytom@qq.com>
2025-08-05 16:09:37 +08:00
998668802f
feat: add System and SystemRegistry classes for managing systems in the game engine
Signed-off-by: szdytom <szdytom@qq.com>
2025-08-05 16:09:22 +08:00
13 changed files with 348 additions and 30 deletions

View File

@ -0,0 +1,50 @@
#ifndef ISTD_CORE_SYSTEM_H
#define ISTD_CORE_SYSTEM_H
#include "istd_core/world.h"
#include <cstdint>
#include <string_view>
#include <tuple>
#include <vector>
namespace istd {
struct System {
virtual void tick(World &world) const = 0;
virtual std::string_view name() const noexcept = 0; // for debugging
// No virtual destructor: static lifetime and no member variables is the
// intended use case
struct Precedence {
// Smaller value means higher precedence or earlier execution
enum {
Highest = 0,
ResetVelocity,
DeviceAccumulateVelocity,
UpdateKinematics,
};
};
};
class SystemRegistry {
public:
static SystemRegistry &instance() noexcept;
void register_system(
std::uint32_t precedence, const System *system
) noexcept;
void tick(World &world) const noexcept;
struct Registar {
Registar(std::uint32_t precedence, const System *system);
};
private:
std::vector<std::tuple<std::uint32_t, const System *>> systems_;
};
} // namespace istd
#endif

View File

@ -2,17 +2,21 @@
#define ISTD_CORE_WORLD_H
#include "istd_core/room.h"
#include "tilemap/generation.h"
#include "tilemap/tilemap.h"
#include <vector>
namespace istd {
struct World {
std::uint32_t tick;
TileMap tilemap;
std::vector<std::vector<Room>> rooms;
entt::registry registry;
World(std::uint8_t size);
void generateTilemap(const GenerationConfig &config);
};
} // namespace istd

38
core/src/system.cpp Normal file
View File

@ -0,0 +1,38 @@
#include "istd_core/system.h"
#include <algorithm>
namespace istd {
SystemRegistry &SystemRegistry::instance() noexcept {
static SystemRegistry registry;
return registry;
}
void SystemRegistry::register_system(
std::uint32_t precedence, const System *system
) noexcept {
auto cmp = [](const std::tuple<std::uint32_t, const System *> &a,
std::uint32_t b) {
return std::get<0>(a) < b;
};
auto it = std::lower_bound(
systems_.begin(), systems_.end(), precedence, cmp
);
systems_.insert(it, std::make_tuple(precedence, system));
}
void SystemRegistry::tick(World &world) const noexcept {
for (const auto &[_, system] : systems_) {
system->tick(world);
}
}
SystemRegistry::Registar::Registar(
std::uint32_t precedence, const System *system
) {
SystemRegistry::instance().register_system(precedence, system);
}
} // namespace istd

39
core/src/world.cpp Normal file
View File

@ -0,0 +1,39 @@
#include "istd_core/world.h"
#include "istd_core/system.h"
#include "tilemap/generation.h"
namespace istd {
namespace {
struct TickSystem : public System {
void tick(World &world) const noexcept override {
world.tick += 1;
}
std::string_view name() const noexcept override {
return "Tick System";
}
};
static const TickSystem tick_system;
static const SystemRegistry::Registar tick_registrar(
System::Precedence::Highest, &tick_system
);
} // namespace
World::World(std::uint8_t size)
: tick(0), tilemap(size), rooms(size, std::vector<Room>(size, {0, 0})) {
for (std::uint8_t x = 0; x < size; ++x) {
for (std::uint8_t y = 0; y < size; ++y) {
rooms[x][y] = {x, y};
}
}
}
void World::generateTilemap(const GenerationConfig &config) {
map_generate(tilemap, config);
}
} // namespace istd

View File

@ -4,6 +4,7 @@ cmake_minimum_required(VERSION 3.27)
# Define util library sources
add_library(istd_util STATIC
src/vec2.cpp
src/tile_geometry.cpp
)
target_include_directories(istd_util PUBLIC include)
target_compile_features(istd_util PUBLIC cxx_std_23)

View File

@ -0,0 +1,33 @@
#ifndef ISTD_UTIL_TILE_GEOMETRY_H
#define ISTD_UTIL_TILE_GEOMETRY_H
#include "istd_util/vec2.h"
#include <cstdint>
#include <generator>
#include <tuple>
namespace istd {
/**
* @brief Iterates all tile coordinates traversed by a line segment on a
* tilemap.
*
* Uses the Amanatides-Woo algorithm to efficiently enumerate all integer tile
* positions that a segment from p1 to p2 passes through, including both
* endpoints.
*
* @note x points downward, y points rightward,
* i.e. x is row index, y is column index.
*
* @param p1 The starting point of the segment (floating point coordinates).
* @param p2 The ending point of the segment (floating point coordinates).
* @return Generator yielding (i, j) tuples for each tile crossed by the
* segment.
*/
std::generator<std::tuple<std::int32_t, std::int32_t>> tiles_on_segment(
Vec2 p1, Vec2 p2
) noexcept;
} // namespace istd
#endif

View File

@ -28,10 +28,12 @@ struct Vec2 {
* @param y Y component
*/
Vec2(float x, float y) noexcept;
/**
* @brief Returns a zero vector (0, 0).
*/
static Vec2 zero() noexcept;
/**
* @brief Returns a vector rotated by the given angle.
* @param rad Angle in radians
@ -40,9 +42,15 @@ struct Vec2 {
static Vec2 rotated(float rad, float len = 1.0) noexcept;
/**
* @name Symmetric operations
* @{
* @brief Returns a vector with infinite components.
*/
static Vec2 inf() noexcept;
/**
* @brief Returns a vector with NaN components.
*/
static Vec2 invalid() noexcept;
/**
* @brief Vector addition.
*/
@ -67,12 +75,7 @@ struct Vec2 {
* @brief Three-way comparison operator.
*/
friend std::strong_ordering operator<=>(Vec2 a, Vec2 b) noexcept;
/** @} */
/**
* @name Assignment operations
* @{
*/
/**
* @brief Adds another vector to this vector.
*/
@ -89,7 +92,6 @@ struct Vec2 {
* @brief Divides this vector by a scalar.
*/
Vec2 &operator/=(float k) noexcept;
/** @} */
/**
* @brief Access vector components by index.
@ -123,22 +125,31 @@ struct Vec2 {
throw std::out_of_range("Index out of range for Vec2");
}
/**
* @brief Checks if the vector is valid (not NaN).
*/
bool is_valid(this const Vec2 self) noexcept;
/**
* @brief Returns the length (magnitude) of the vector.
*/
float length(this const Vec2 self) noexcept;
/**
* @brief Returns the squared length of the vector.
*/
float length_squared(this const Vec2 self) noexcept;
/**
* @brief Returns a normalized (unit length) vector.
*/
Vec2 normalized(this const Vec2 self) noexcept;
/**
* @brief Returns a tuple of floored components.
*/
std::tuple<int, int> floor(this const Vec2 self) noexcept;
/**
* @brief Returns a tuple of rounded components.
*/
@ -147,11 +158,12 @@ struct Vec2 {
/**
* @brief Returns the dot product of two vectors.
*/
friend float dot(Vec2 a, Vec2 b) noexcept;
static float dot(Vec2 a, Vec2 b) noexcept;
/**
* @brief Returns the cross product of two vectors.
*/
friend float cross(Vec2 a, Vec2 b) noexcept;
static float cross(Vec2 a, Vec2 b) noexcept;
};
} // namespace istd

View File

@ -1 +1,3 @@
#include "istd_util/small_map.h"
#include "istd_util/tile_geometry.h"
#include "istd_util/vec2.h"

View File

@ -0,0 +1,52 @@
#include "istd_util/tile_geometry.h"
namespace istd {
// Amanatides-Woo Algorithm
std::generator<std::tuple<std::int32_t, std::int32_t>> tiles_on_segment(
Vec2 p1, Vec2 p2
) noexcept {
auto [i, j] = p1.floor();
co_yield {i, j};
if (p1.floor() == p2.floor()) {
co_return;
}
auto delta = p2 - p1;
int step_x = 0, step_y = 0;
auto t_max = Vec2::inf(), t_delta = Vec2::inf();
if (delta.x > 0) {
step_x = 1;
t_max.x = (i + 1 - p1.x) / delta.x;
t_delta.x = 1.0f / delta.x;
} else if (delta.x < 0) {
step_x = -1;
t_max.x = (i - p1.x) / delta.x;
t_delta.x = -1.0f / delta.x;
}
if (delta.y > 0) {
step_y = 1;
t_max.y = (j + 1 - p1.y) / delta.y;
t_delta.y = 1.0f / delta.y;
} else if (delta.y < 0) {
step_y = -1;
t_max.y = (j - p1.y) / delta.y;
t_delta.y = -1.0f / delta.y;
}
auto [end_i, end_j] = p2.floor();
while (i != end_i || j != end_j) {
if (t_max.x < t_max.y) {
i += step_x;
t_max.x += t_delta.x;
} else {
j += step_y;
t_max.y += t_delta.y;
}
co_yield {i, j};
}
}
} // namespace istd

View File

@ -7,11 +7,25 @@ namespace istd {
Vec2::Vec2(float x_, float y_) noexcept: x(x_), y(y_) {}
Vec2 Vec2::zero() noexcept {
return Vec2(0.0f, 0.0f);
return {0.0f, 0.0f};
}
Vec2 Vec2::rotated(float rad, float len) noexcept {
return Vec2(std::cos(rad) * len, std::sin(rad) * len);
return {std::cos(rad) * len, std::sin(rad) * len};
}
Vec2 Vec2::inf() noexcept {
return Vec2(
std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity()
);
}
Vec2 Vec2::invalid() noexcept {
return Vec2(
std::numeric_limits<float>::quiet_NaN(),
std::numeric_limits<float>::quiet_NaN()
);
}
Vec2 operator+(Vec2 a, Vec2 b) noexcept {
@ -74,6 +88,10 @@ Vec2 &Vec2::operator/=(float k) noexcept {
return *this;
}
bool Vec2::is_valid(this const Vec2 self) noexcept {
return !std::isnan(self.x) && !std::isnan(self.y);
}
float Vec2::length(this const Vec2 self) noexcept {
return std::sqrt(self.x * self.x + self.y * self.y);
}
@ -104,11 +122,11 @@ std::tuple<int, int> Vec2::round(this const Vec2 self) noexcept {
);
}
float dot(Vec2 a, Vec2 b) noexcept {
float Vec2::dot(Vec2 a, Vec2 b) noexcept {
return a.x * b.x + a.y * b.y;
}
float cross(Vec2 a, Vec2 b) noexcept {
float Vec2::cross(Vec2 a, Vec2 b) noexcept {
return a.x * b.y - a.y * b.x;
}

View File

@ -1,16 +1,15 @@
cmake_minimum_required(VERSION 3.27)
add_executable(test_small_map small_map.cpp)
target_link_libraries(test_small_map PRIVATE istd_util)
target_compile_features(test_small_map PRIVATE cxx_std_23)
add_executable(test_vec2 test_vec2.cpp)
target_link_libraries(test_vec2 PRIVATE istd_util)
target_compile_features(test_vec2 PRIVATE cxx_std_23)
include(CTest)
enable_testing()
add_test(NAME util_small_map COMMAND test_small_map)
add_test(NAME util_vec2 COMMAND test_vec2)
function(declare_istd_util_test name src)
add_executable(${name} ${src})
target_link_libraries(${name} PRIVATE istd_util)
target_compile_features(${name} PRIVATE cxx_std_23)
add_test(NAME ${name} COMMAND ${name})
endfunction()
declare_istd_util_test(test_small_map small_map.cpp)
declare_istd_util_test(test_vec2 test_vec2.cpp)
declare_istd_util_test(test_tile_geometry test_tile_geometry.cpp)

View File

@ -0,0 +1,60 @@
#include "istd_util/tile_geometry.h"
#include <cassert>
#include <tuple>
#include <vector>
using namespace istd;
int main() {
// Test a simple horizontal segment
Vec2 p1(0.5f, 1.2f);
Vec2 p2(0.5f, 4.8f);
std::vector<std::tuple<int, int>> result;
for (auto [i, j] : tiles_on_segment(p1, p2)) {
result.emplace_back(i, j);
}
// Should traverse columns 1 to 4, row 0
assert(result.size() == 4);
assert(result[0] == std::make_tuple(0, 1));
assert(result[1] == std::make_tuple(0, 2));
assert(result[2] == std::make_tuple(0, 3));
assert(result[3] == std::make_tuple(0, 4));
// Test a diagonal segment
p1 = Vec2(1.1f, 1.1f);
p2 = Vec2(3.9f, 3.9f);
result.clear();
for (auto [i, j] : tiles_on_segment(p1, p2)) {
result.emplace_back(i, j);
}
// Should traverse (1,1), (2,2), (3,3)
assert(result.size() == 3);
assert(result[0] == std::make_tuple(1, 1));
assert(result[1] == std::make_tuple(2, 2));
assert(result[2] == std::make_tuple(3, 3));
// Test vertical segment
p1 = Vec2(2.2f, 0.5f);
p2 = Vec2(5.7f, 0.5f);
result.clear();
for (auto [i, j] : tiles_on_segment(p1, p2)) {
result.emplace_back(i, j);
}
// Should traverse rows 2 to 5, column 0
assert(result.size() == 4);
assert(result[0] == std::make_tuple(2, 0));
assert(result[1] == std::make_tuple(3, 0));
assert(result[2] == std::make_tuple(4, 0));
assert(result[3] == std::make_tuple(5, 0));
// Test single tile
p1 = Vec2(7.3f, 8.9f);
p2 = Vec2(7.7f, 8.1f);
result.clear();
for (auto [i, j] : tiles_on_segment(p1, p2)) {
result.emplace_back(i, j);
}
assert(result.size() == 1);
assert(result[0] == std::make_tuple(7, 8));
return 0;
}

View File

@ -20,15 +20,25 @@ int main() {
Vec2 v4 = v1 - v2;
assert(v4.x == 2.0f && v4.y == 2.0f);
// Test dot and cross
assert(dot(v1, v2) == 11.0f);
assert(cross(v1, v2) == 2.0f);
// Test floor and round
Vec2 v5(1.7f, -2.3f);
auto f = v5.floor();
auto r = v5.round();
assert(f == std::make_tuple(1, -3));
assert(r == std::make_tuple(2, -2));
// Test inf and invalid
Vec2 vinf = Vec2::inf();
assert(std::isinf(vinf.x) && std::isinf(vinf.y));
Vec2 vinvalid = Vec2::invalid();
assert(std::isnan(vinvalid.x) && std::isnan(vinvalid.y));
// Test is_valid
assert(v1.is_valid());
assert(!vinvalid.is_valid());
// Test static dot and cross
assert(Vec2::dot(v1, v2) == 11.0f);
assert(Vec2::cross(v1, v2) == 2.0f);
return 0;
}