From d5b20184b8faf4264eb96b6a946b98d148a651bd Mon Sep 17 00:00:00 2001 From: NukeBird Date: Wed, 19 Mar 2025 21:08:58 +0300 Subject: [PATCH] init --- .gitignore | 1 - CMakeLists.txt | 36 +- CMakePresets.json | 28 ++ conanfile.txt | 9 - system_scheduler.hpp | 55 --- tests/zecsy.c | 9 + tests/zecsy.cpp | 989 ------------------------------------------- zecsy.h | 8 + zecsy.hpp | 391 ----------------- 9 files changed, 59 insertions(+), 1467 deletions(-) create mode 100644 CMakePresets.json delete mode 100644 conanfile.txt delete mode 100644 system_scheduler.hpp create mode 100644 tests/zecsy.c delete mode 100644 tests/zecsy.cpp create mode 100644 zecsy.h delete mode 100644 zecsy.hpp diff --git a/.gitignore b/.gitignore index a2a8066..a4fb4fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ build/ .cache/ -CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 652f810..8b50986 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,33 +1,25 @@ cmake_minimum_required(VERSION 3.22.0) -set(PROJECT_NAME zecsy) +project(zecsy LANGUAGES C) -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) -set(CMAKE_CXX_FLAGS "-Wall -Wextra") -set(CMAKE_CXX_FLAGS_DEBUG "-g") -set(CMAKE_CXX_FLAGS_RELEASE "-O3") - -project(${PROJECT_NAME}) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - option(BUILD_ZECSY_TESTS "Build tests?" ON) -find_package(nlohmann_json) +add_library(zecsy STATIC zecsy.h) +set_target_properties(zecsy PROPERTIES LINKER_LANGUAGE C) -add_library(zecsy STATIC zecsy.hpp) -set_target_properties(zecsy PROPERTIES LINKER_LANGUAGE CXX) - -####################################################### if(${BUILD_ZECSY_TESTS}) - find_package(Catch2 REQUIRED) - file(GLOB TEST_SRC ./tests/*.cpp ./tests/*.hpp ./tests/*.h) - add_executable(tests ${TEST_SRC}) - target_link_libraries(tests PRIVATE Catch2::Catch2WithMain zecsy) + Include(FetchContent) + FetchContent_Declare( + clove-unit + GIT_REPOSITORY https://github.com/fdefelici/clove-unit.git + GIT_TAG master # or eventually any branch, tag or commit sha + ) + FetchContent_MakeAvailable(clove-unit) - include(CTest) - include(Catch) - catch_discover_tests(tests) + add_executable(tests tests/zecsy.c) + target_link_libraries(tests clove-unit) endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..08e168c --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,28 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 15, + "patch": 0 + }, + "configurePresets": [ + { + "name": "clang", + "displayName": "Clang Compiler", + "description": "Use Clang compilers", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang" + }, + "environment": {} + } + ], + "buildPresets": [ + { + "name": "clang-build", + "configurePreset": "clang", + "configuration": "Release" + } + ] +} diff --git a/conanfile.txt b/conanfile.txt deleted file mode 100644 index 30594fc..0000000 --- a/conanfile.txt +++ /dev/null @@ -1,9 +0,0 @@ -[requires] -catch2/3.8.0 - -[generators] -CMakeDeps -CMakeToolchain - -[layout] -cmake_layout diff --git a/system_scheduler.hpp b/system_scheduler.hpp deleted file mode 100644 index 9c4fb05..0000000 --- a/system_scheduler.hpp +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once -#include -#include -#include - -namespace zecsy -{ - class system_scheduler final - { - public: - void add_system(float freq, std::invocable auto&& func); - - void add_system(int freq, std::invocable auto&& func); - - void update(float dt); - - private: - struct system_handler - { - double interval; - double accumulator = 0.0f; - std::function callback; - }; - - std::vector systems; - }; - - inline void system_scheduler::add_system(float freq, - std::invocable auto&& func) - { - systems.emplace_back(1.0f / freq, 0.0f, - std::forward(func)); - } - - inline void system_scheduler::add_system(int freq, - std::invocable auto&& func) - { - add_system(float(freq), func); - } - - inline void system_scheduler::update(float dt) - { - dt = std::max(0.0f, dt); - - for(auto& s: systems) - { - s.accumulator += dt; - while(s.accumulator >= s.interval) - { - s.callback(dt); - s.accumulator -= s.interval; - } - } - } -} // namespace zecsy diff --git a/tests/zecsy.c b/tests/zecsy.c new file mode 100644 index 0000000..f6c9067 --- /dev/null +++ b/tests/zecsy.c @@ -0,0 +1,9 @@ +#define CLOVE_IMPLEMENTATION +#include + +CLOVE_TEST(oh_hi_mark) +{ + CLOVE_IS_TRUE(true); +} + +CLOVE_RUNNER() diff --git a/tests/zecsy.cpp b/tests/zecsy.cpp deleted file mode 100644 index 01f2b7f..0000000 --- a/tests/zecsy.cpp +++ /dev/null @@ -1,989 +0,0 @@ -#include -#define CATCH_CONFIG_MAIN -#include - -#include "../system_scheduler.hpp" -#include "../zecsy.hpp" - -using namespace zecsy; - -TEST_CASE("Create a single entity and verify its existence", "[test]") -{ - world w; - - auto e = w.make_entity(); - - REQUIRE(w.is_alive(e)); -} - -TEST_CASE("Destroy an entity and ensure it no longer exists in the world", - "[test]") -{ - world w; - - auto e = w.make_entity(); - w.destroy_entity(e); - - REQUIRE_FALSE(w.is_alive(e)); -} - -TEST_CASE("Entity #0 should be reserved and never used", "[test]") -{ - world w; - - auto e = w.make_entity(); - - REQUIRE(e != 0); - REQUIRE_FALSE(w.is_alive(0)); -} - -struct ChosenOne -{ -}; - -TEST_CASE("Entity shouldn't have a component that wasn't attached to it", - "[test]") -{ - world w; - - auto e = w.make_entity(); - - REQUIRE_FALSE(w.has(e)); -} - -TEST_CASE("Attempt of getting non-owned component should throw", "[test]") -{ - world w; - - auto e = w.make_entity(); - - REQUIRE_THROWS(w.get(e)); -} - -TEST_CASE("Attach a simple component to an entity and verify it is correctly " - "associated", - "[test]") -{ - world w; - - auto e1 = w.make_entity(); - - w.set(e1, ChosenOne{}); - REQUIRE(w.has(e1)); - - auto e2 = w.make_entity(); - w.set(e2, ChosenOne{}); - REQUIRE(w.has(e2)); -} - -struct Comp -{ - int v = 0; -}; - -TEST_CASE("Retrieve a component from an entity and verify its data matches " - "what was set", - "[test]") -{ - world w; - - auto e = w.make_entity(); - w.set(e, Comp()); - - REQUIRE(w.get(e).v == 0); - - w.get(e).v = 77; - REQUIRE(w.get(e).v == 77); - - w.ensure(e).v = 4; - REQUIRE(w.ensure(e).v == 4); - REQUIRE(w.get(e).v == 4); - - w.remove(e); - w.ensure(e).v = 123; - REQUIRE(w.get(e).v == 123); -} - -TEST_CASE( - "Remove a component from an entity and verify it is no longer attached", - "[test]") -{ - world w; - - auto e = w.make_entity(); - w.set(e, ChosenOne{}); - REQUIRE_NOTHROW(w.remove(e)); - - REQUIRE_FALSE(w.has(e)); - - w.set(e, ChosenOne{}); - REQUIRE_NOTHROW(w.remove(e)); - REQUIRE_FALSE(w.has(e)); -} - -TEST_CASE("Addresses of removed components should be reused", "[test]") -{ - world w; - std::vector entities; - std::vector addr; - - const int N = 4; - - for(int i = 0; i < 2; ++i) - { - for(int j = 0; j < N; ++j) - { - entities.emplace_back(w.make_entity()); - w.set(entities.back()); - } - - if(addr.empty()) - { - for(int j = 0; j < N; ++j) - { - addr.emplace_back(&w.get(entities[j])); - } - } - else - { - /* - * Gotta reverse it because now we reuse ids in LIFO order - */ - std::reverse(addr.begin(), addr.end()); - - for(int j = 0; j < N; ++j) - { - REQUIRE(&w.get(entities[j]) == addr[j]); - } - } - - for(auto e: entities) - { - w.remove(e); - } - entities.clear(); - } -} - -TEST_CASE("Attach multiple components to an entity and verify all are " - "correctly stored and retrievable", - "[test]") -{ - world w; - - auto e = w.make_entity(); - w.set(e, ChosenOne{}, Comp{}); - - REQUIRE(w.has(e)); - - w.remove(e); - - REQUIRE_FALSE(w.has(e)); - REQUIRE_FALSE(w.has(e)); - REQUIRE_FALSE(w.has(e)); -} - -TEST_CASE("Create a simple system that processes entities with a specific " - "component and verify it executes correctly", - "[test]") -{ - struct Component - { - int value = 0; - }; - - world w; - auto e0 = w.make_entity(), e1 = w.make_entity(); - - w.set(e0); // or e0.set(Component{}) - w.set(e1, Component{20}); - - REQUIRE(w.get(e0).value == 0); - REQUIRE(w.get(e1).value == 20); - - /* - * Really wanna deduce it to w.query([](Component&){}), - * but I have some troubles with it - */ - w.query([](entity_id e, Component& c) { c.value++; }); - - REQUIRE(w.filter().size() == 2); - REQUIRE(w.get(e0).value == 1); - REQUIRE(w.get(e1).value == 21); -} - -TEST_CASE("Test a systems ability to query and process only entities with a " - "specific combination of components", - "[test]") -{ - struct C0 - { - int value = 0; - }; - - struct C1 - { - int value = 10; - }; - - world w; - - auto e0 = w.make_entity(); - w.set(e0, C0{}, C1{}); - - auto e1 = w.make_entity(); - w.set(e1, C0{}); - - auto e2 = w.make_entity(); - w.set(e2, C1{}); - - REQUIRE(w.get(e0).value == 0); - REQUIRE(w.get(e0).value == 10); - - w.query( - [e0](entity_id e, C0& c0, C1& c1) - { - REQUIRE(e == e0); - c0.value++; - c1.value++; - }); - - REQUIRE(w.filter().size() == 1); - REQUIRE(w.filter()[0] == e0); - - REQUIRE(w.get(e0).value == 1); - REQUIRE(w.get(e0).value == 11); - - REQUIRE(w.get(e1).value == 0); - REQUIRE(w.get(e2).value == 10); - - REQUIRE_FALSE(w.has(e1)); - REQUIRE_FALSE(w.has(e2)); -} - -TEST_CASE("Systems execute at correct frequencies", "[test]") -{ - world w; - system_scheduler scheduler; - - int fast_count = 0; - int slow_count = 0; - - // Add a fast system (60 Hz) - scheduler.add_system(60, [&](float dt) { fast_count++; }); - - // Add a slow system (1 Hz) - scheduler.add_system(1, [&](float dt) { slow_count++; }); - - // Simulate 2 seconds of updates at 120 FPS - for(int i = 0; i < 240; ++i) - { - scheduler.update(1.0f / 120.0f); - } - - // Verify counts - REQUIRE(fast_count == 120); // 60 Hz system should execute 60 times - REQUIRE(slow_count == 2); // 1 Hz system should execute 1 time -} - -TEST_CASE("Systems handle zero-frequency gracefully", "[test]") -{ - world w; - system_scheduler scheduler; - - int zero_count = 0; - - // Add a zero-frequency system (should never execute) - scheduler.add_system(0, [&](float dt) { zero_count++; }); - - // Simulate 1 second of updates at 60 FPS - for(int i = 0; i < 60; ++i) - { - scheduler.update(1.0f / 60.0f); - } - - // Verify zero-frequency system never executes - REQUIRE(zero_count == 0); -} - -TEST_CASE("Systems handle varying update rates", "[test]") -{ - world w; - system_scheduler scheduler; - - int varying_count = 0; - - // Add a system with varying frequency (10 Hz) - scheduler.add_system(10, [&](float dt) { varying_count++; }); - - // Simulate 1 second of updates at 30 FPS - for(int i = 0; i < 30; ++i) - { - scheduler.update(1.0f / 30.0f); - } - - // Verify varying-frequency system executes 10 times - REQUIRE(varying_count == 10); -} - -TEST_CASE("Systems handle large time steps", "[test]") -{ - world w; - system_scheduler scheduler; - - int large_step_count = 0; - - // Add a system (1 Hz) - scheduler.add_system(1, [&](float dt) { large_step_count++; }); - - // Simulate a large time step (2 seconds) - scheduler.update(2.0f); - - // Verify system executes twice (accumulator handles large steps) - REQUIRE(large_step_count == 2); -} - -TEST_CASE("Systems handle multiple frequencies", "[test]") -{ - world w; - system_scheduler scheduler; - - int fast_count = 0; - int medium_count = 0; - int slow_count = 0; - - // Add systems with different frequencies - scheduler.add_system(60, [&](float dt) { fast_count++; }); - scheduler.add_system(30, [&](float dt) { medium_count++; }); - scheduler.add_system(1, [&](float dt) { slow_count++; }); - - // Simulate 1 second of updates at 120 FPS - for(int i = 0; i < 120; ++i) - { - scheduler.update(1.0f / 120.0f); - } - - // Verify counts - REQUIRE(fast_count == 60); // 60 Hz system - REQUIRE(medium_count == 30); // 30 Hz system - REQUIRE(slow_count == 1); // 1 Hz system -} - -TEST_CASE("Systems handle fractional frequencies", "[test]") -{ - world w; - system_scheduler scheduler; - - int fractional_count = 0; - - // Add a system with fractional frequency (0.5 Hz) - scheduler.add_system(0.5f, [&](float dt) { fractional_count++; }); - - // Simulate 4 seconds of updates at 60 FPS - for(int i = 0; i < 240; ++i) - { - scheduler.update(1.0f / 60.0f); - } - - // Verify fractional-frequency system executes twice (0.5 Hz = 2 times in 4 - // seconds) - REQUIRE(fractional_count == 2); -} - -TEST_CASE("Systems handle zero delta time", "[test]") -{ - world w; - system_scheduler scheduler; - - int zero_dt_count = 0; - - // Add a system (1 Hz) - scheduler.add_system(1, [&](float dt) { zero_dt_count++; }); - - // Simulate zero delta time - scheduler.update(0.0f); - - // Verify system does not execute - REQUIRE(zero_dt_count == 0); -} - -TEST_CASE("Systems handle negative delta time", "[test]") -{ - world w; - system_scheduler scheduler; - - int count = 0; - - // Add a system (1 Hz) - scheduler.add_system(1, [&](float dt) { count++; }); - - // Simulate negative delta time - scheduler.update(-1.0f); - - // Verify system does not execute - REQUIRE(count == 0); - - scheduler.update(2.0f); - - REQUIRE(count == 2); -} - -TEST_CASE("Entity count tracking", "[test]") -{ - world w; - REQUIRE(w.entity_count() == 0); - - const auto e1 = w.make_entity(); - const auto e2 = w.make_entity(); - REQUIRE(w.entity_count() == 2); - - w.destroy_entity(e1); - REQUIRE(w.entity_count() == 1); -} - -TEST_CASE("Component counting mechanisms", "[test]") -{ - struct Health - { - int value; - }; - - struct Position - { - float x, y; - }; - - world w; - auto e = w.make_entity(); - - REQUIRE(w.component_count() == 0); - REQUIRE(w.total_component_count() == 0); - - w.set(e); - REQUIRE(w.component_count() == 1); - REQUIRE(w.total_component_count() == 1); - - w.set(e); - REQUIRE(w.component_count() == 1); - REQUIRE(w.total_component_count() == 2); - - w.remove(e); - REQUIRE(w.component_count() == 0); - REQUIRE(w.total_component_count() == 1); -} - -TEST_CASE("Archetype signature management", "[test]") -{ - struct A - { - }; - - struct B - { - }; - - struct C - { - }; - - world w; - - // Initial state: empty archetype - REQUIRE(w.archetype_count() == 0); - - auto e0 = w.make_entity(); - REQUIRE(w.archetype_count() == 1); //<> - - // Add first component - w.set(e0); - REQUIRE(w.archetype_count() == 1); // - - w.set(e0); - REQUIRE(w.archetype_count() == 1); // - - w.set(e0); - REQUIRE(w.archetype_count() == 1); // - - w.remove(e0); - REQUIRE(w.archetype_count() == 1); // - - auto e1 = w.make_entity(); - w.set(e1); - REQUIRE(w.archetype_count() == 2); //, - - w.remove(e0); - REQUIRE(w.archetype_count() == 2); //<>, - - w.set(e0); - REQUIRE(w.archetype_count() == 2); //, - - w.set(e0); - REQUIRE(w.archetype_count() == 1); // - - w.destroy_entity(e0); - REQUIRE(w.archetype_count() == 1); // - - w.destroy_entity(e1); - REQUIRE(w.archetype_count() == 0); -} - -TEST_CASE("Component distribution across archetypes", "[test]") -{ - struct A - { - }; - - struct B - { - }; - - world w; - - // Create 10 entities in different configurations - for(int i = 0; i < 5; ++i) - { - auto e = w.make_entity(); - w.set(e); - } - - for(int i = 0; i < 3; ++i) - { - auto e = w.make_entity(); - w.set(e); - } - - for(int i = 0; i < 2; ++i) - { - auto e = w.make_entity(); - w.set(e); - } - - // Verify distribution - REQUIRE(w.entity_count() == 10); - REQUIRE(w.component_count() == 8); - REQUIRE(w.component_count() == 5); - REQUIRE(w.archetype_count() == 3); //, , -} - -TEST_CASE("Entity inspection", "[test]") -{ - struct Transform - { - float x, y; - }; - - struct Renderable - { - }; - - world w; - auto e = w.make_entity(); - - REQUIRE(w.components_in_entity(e) == 0); - - w.set(e); - REQUIRE(w.components_in_entity(e) == 1); - - w.set(e); - REQUIRE(w.components_in_entity(e) == 2); - - w.remove(e); - REQUIRE(w.components_in_entity(e) == 1); -} - -namespace -{ - // Benchmark components - struct Position - { - float x, y; - }; - struct Velocity - { - float dx, dy; - }; - struct Health - { - int value; - }; - struct BigData - { - char buffer[4096]; - }; - - // Benchmark entity counts - constexpr int SMALL = 1'000; - constexpr int MEDIUM = 10'000; - constexpr int LARGE = 50'000; -} // namespace - -TEST_CASE("Core operations benchmarks", "[benchmark]") -{ - BENCHMARK_ADVANCED("Create entities [1000]")( - Catch::Benchmark::Chronometer meter) - { - meter.measure( - [&] - { - world w; - for(int i = 0; i < SMALL; ++i) - { - w.make_entity(); - } - return w.entity_count(); - }); - }; - - BENCHMARK_ADVANCED("Create entities [10000]")( - Catch::Benchmark::Chronometer meter) - { - meter.measure( - [&] - { - world w; - for(int i = 0; i < MEDIUM; ++i) - { - w.make_entity(); - } - return w.entity_count(); - }); - }; - - BENCHMARK_ADVANCED("Create entities [50000]")( - Catch::Benchmark::Chronometer meter) - { - meter.measure( - [&] - { - world w; - for(int i = 0; i < LARGE; ++i) - { - w.make_entity(); - } - return w.entity_count(); - }); - }; - - BENCHMARK_ADVANCED("Create entities with components [1000]")( - Catch::Benchmark::Chronometer meter) - { - meter.measure( - [&] - { - world w; - for(int i = 0; i < SMALL; ++i) - { - auto e = w.make_entity(); - w.set(e, {1.0f, 2.0f}); - w.set(e, {3.0f, 4.0f}); - } - return w.total_component_count(); - }); - }; - - BENCHMARK_ADVANCED("Create entities with components [10000]")( - Catch::Benchmark::Chronometer meter) - { - meter.measure( - [&] - { - world w; - for(int i = 0; i < MEDIUM; ++i) - { - auto e = w.make_entity(); - w.set(e, {1.0f, 2.0f}); - w.set(e, {3.0f, 4.0f}); - } - return w.total_component_count(); - }); - }; - - BENCHMARK_ADVANCED("Create entities with components [50000]")( - Catch::Benchmark::Chronometer meter) - { - meter.measure( - [&] - { - world w; - for(int i = 0; i < LARGE; ++i) - { - auto e = w.make_entity(); - w.set(e, {1.0f, 2.0f}); - w.set(e, {3.0f, 4.0f}); - } - return w.total_component_count(); - }); - }; -} - -TEST_CASE("Component operations benchmarks", "[benchmark]") -{ - BENCHMARK_ADVANCED("Add component to existing entities [1000]")( - Catch::Benchmark::Chronometer meter) - { - world w; - std::vector entities; - for(int i = 0; i < SMALL; ++i) - { - entities.push_back(w.make_entity()); - } - - meter.measure( - [&] - { - for(auto e: entities) - { - w.set(e, {100}); - } - return w.component_count(); - }); - }; - - BENCHMARK_ADVANCED("Add component to existing entities [10000]")( - Catch::Benchmark::Chronometer meter) - { - world w; - std::vector entities; - for(int i = 0; i < MEDIUM; ++i) - { - entities.push_back(w.make_entity()); - } - - meter.measure( - [&] - { - for(auto e: entities) - { - w.set(e, {100}); - } - return w.component_count(); - }); - }; - - BENCHMARK_ADVANCED("Add component to existing entities [50000]")( - Catch::Benchmark::Chronometer meter) - { - world w; - std::vector entities; - for(int i = 0; i < LARGE; ++i) - { - entities.push_back(w.make_entity()); - } - - meter.measure( - [&] - { - for(auto e: entities) - { - w.set(e, {100}); - } - return w.component_count(); - }); - }; - - BENCHMARK_ADVANCED("Remove component from entities [1000]")( - Catch::Benchmark::Chronometer meter) - { - world w; - std::vector entities; - for(int i = 0; i < SMALL; ++i) - { - auto e = w.make_entity(); - w.set(e, {100}); - entities.push_back(e); - } - - meter.measure( - [&] - { - for(auto e: entities) - { - w.remove(e); - } - return w.component_count(); - }); - }; - - BENCHMARK_ADVANCED("Remove component from entities [10000]")( - Catch::Benchmark::Chronometer meter) - { - world w; - std::vector entities; - for(int i = 0; i < MEDIUM; ++i) - { - auto e = w.make_entity(); - w.set(e, {100}); - entities.push_back(e); - } - - meter.measure( - [&] - { - for(auto e: entities) - { - w.remove(e); - } - return w.component_count(); - }); - }; - - BENCHMARK_ADVANCED("Remove component from entities [50000]")( - Catch::Benchmark::Chronometer meter) - { - world w; - std::vector entities; - for(int i = 0; i < LARGE; ++i) - { - auto e = w.make_entity(); - w.set(e, {100}); - entities.push_back(e); - } - - meter.measure( - [&] - { - for(auto e: entities) - { - w.remove(e); - } - return w.component_count(); - }); - }; -} - -TEST_CASE("Query performance benchmarks", "[benchmark]") -{ - BENCHMARK_ADVANCED("Dense query (90% match)")( - Catch::Benchmark::Chronometer meter) - { - world w; - for(int i = 0; i < LARGE; ++i) - { - auto e = w.make_entity(); - w.set(e); - if(i % 10 != 0) - w.set(e); // 90% match - } - - meter.measure( - [&] - { - int count = 0; - w.query([&](auto...) { count++; }); - return count; - }); - }; - - BENCHMARK_ADVANCED("Sparse query (10% match)")( - Catch::Benchmark::Chronometer meter) - { - world w; - for(int i = 0; i < LARGE; ++i) - { - auto e = w.make_entity(); - w.set(e); - if(i % 10 == 0) - w.set(e); // 10% match - } - - meter.measure( - [&] - { - int count = 0; - w.query([&](auto...) { count++; }); - return count; - }); - }; -} - -TEST_CASE("Memory intensive benchmarks", "[benchmark]") -{ - BENCHMARK_ADVANCED("Large component allocation")( - Catch::Benchmark::Chronometer meter) - { - meter.measure( - [&] - { - world w; - for(int i = 0; i < SMALL; ++i) - { - auto e = w.make_entity(); - w.set(e); - } - return w.total_component_count(); - }); - }; - - BENCHMARK_ADVANCED("Component memory reuse")( - Catch::Benchmark::Chronometer meter) - { - world w; - std::vector entities; - for(int i = 0; i < SMALL; ++i) - { - auto e = w.make_entity(); - w.set(e); - entities.push_back(e); - } - - meter.measure( - [&] - { - // Remove and re-add components - for(auto e: entities) - { - w.remove(e); - w.set(e); - } - return w.component_count(); - }); - }; -} - -TEST_CASE("Archetype transition benchmarks", "[benchmark]") -{ - BENCHMARK_ADVANCED("Single component addition")( - Catch::Benchmark::Chronometer meter) - { - world w; - std::vector entities; - for(int i = 0; i < MEDIUM; ++i) - { - entities.push_back(w.make_entity()); - } - - meter.measure( - [&] - { - for(auto e: entities) - { - w.set(e); - } - return w.archetype_count(); - }); - }; - - BENCHMARK_ADVANCED("Multi-component transition")( - Catch::Benchmark::Chronometer meter) - { - world w; - std::vector entities; - for(int i = 0; i < MEDIUM; ++i) - { - auto e = w.make_entity(); - w.set(e); - entities.push_back(e); - } - - meter.measure( - [&] - { - for(auto e: entities) - { - w.set(e); - w.set(e); - } - return w.archetype_count(); - }); - }; -} diff --git a/zecsy.h b/zecsy.h new file mode 100644 index 0000000..5fad448 --- /dev/null +++ b/zecsy.h @@ -0,0 +1,8 @@ +#ifndef __ZECSY_H +#define __ZECSY_H + +#endif // !__ZECSY_H + +#ifdef ZECSY_IMPLEMENTATION + +#endif // ZECSY_IMPLEMENTATION diff --git a/zecsy.hpp b/zecsy.hpp deleted file mode 100644 index a756813..0000000 --- a/zecsy.hpp +++ /dev/null @@ -1,391 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef ZECSY_MAX_COMPONENTS -#define ZECSY_MAX_COMPONENTS 32 -#endif // !ZECSY_MAX_COMPONENTS - -namespace zecsy -{ - using entity_id = uint64_t; - - template - concept Component = [] - { - static_assert((std::is_default_constructible_v && ...), - "Should have a default constructor"); - static_assert((std::is_trivially_copyable_v && ...), - "Should be trivially copyable"); - static_assert((std::is_trivially_destructible_v && ...), - "Should be trivially destructible"); - static_assert((std::is_standard_layout_v && ...), - "Should have standard layout"); - return true; - }(); - - class world final - { - public: - entity_id make_entity(); - void destroy_entity(entity_id e); - bool is_alive(entity_id e) const; - size_t components_in_entity(entity_id e) const; - - size_t entity_count() const; - size_t total_component_count() const; - size_t archetype_count() const; - - template - size_t component_count(); - - template - bool has(entity_id e) const; - - template - bool has(entity_id e) const; - - template - T& get(entity_id e); - - template - void set(entity_id e); - - template - void set(entity_id e, const T& comp); - - template - void set(entity_id e); - - template - void set(entity_id e, const First& comp0, const Second& comp1, - const Rest&... rest_comps); - - template - T& ensure(entity_id e); - - template - void remove(entity_id e); - - template - void remove(entity_id e); - - template - std::vector filter(); - - template - void query(std::invocable auto&& system); - - size_t get_archetypes_checked() const; - size_t get_entities_processed() const; - - private: - using comp_id = size_t; - using entity_group = std::set; - using archetype_signature = std::bitset; - - std::unordered_map entity_to_comps; - entity_id entity_counter = 0; - - size_t query_archetypes_checked = 0; - size_t query_entities_processed = 0; - - struct component_pool - { - std::vector data; - std::vector free_list; - std::unordered_map entity_to_index; - }; - - std::unordered_map pools; - std::unordered_map archetypes; - - template - static comp_id get_component_id(); - - static comp_id next_component_id; - }; - - template - inline world::comp_id world::get_component_id() - { - static comp_id id = next_component_id++; - return id; - } - - inline world::comp_id world::next_component_id = 0; - - inline size_t world::components_in_entity(entity_id e) const - { - return entity_to_comps.contains(e) ? entity_to_comps.at(e).count() : 0; - } - - inline size_t world::entity_count() const - { - return entity_to_comps.size(); - } - - inline size_t world::total_component_count() const - { - size_t count = 0; - - for(const auto& [id, pool]: pools) - { - count += pool.entity_to_index.size(); - } - - return count; - } - - inline size_t world::archetype_count() const - { - return archetypes.size(); - } - - template - inline size_t world::component_count() - { - const comp_id id = get_component_id(); - const auto it = pools.find(id); - return it != pools.end() ? it->second.entity_to_index.size() : 0; - } - - inline size_t world::get_archetypes_checked() const - { - return query_archetypes_checked; - } - - inline size_t world::get_entities_processed() const - { - return query_entities_processed; - } - - inline entity_id world::make_entity() - { - auto id = ++entity_counter; - entity_to_comps[id] = {}; - - archetype_signature key; - auto& group = archetypes[key]; - group.emplace(id); - - return id; - } - - inline void world::destroy_entity(entity_id e) - { - auto archetype = entity_to_comps[e]; - - auto& group = archetypes[archetype]; - group.erase(e); - - if(archetypes[archetype].empty()) - { - archetypes.erase(archetype); - } - - for(int id = 0; id < ZECSY_MAX_COMPONENTS; ++id) - { - if(archetype.test(id)) - { - auto& pool = pools[id]; - auto index = pool.entity_to_index[e]; - pool.entity_to_index.erase(e); - pool.free_list.emplace_back(index); - } - } - - entity_to_comps.erase(e); - } - - inline bool world::is_alive(entity_id e) const - { - return entity_to_comps.contains(e); - } - - template - inline bool world::has(entity_id e) const - { - if(entity_to_comps.contains(e)) - { - return entity_to_comps.at(e).test(get_component_id()); - } - return false; - } - - template - T& world::ensure(entity_id e) - { - if(!has(e)) - { - set(e); - } - - return get(e); - } - - template - inline T& world::get(entity_id e) - { - auto id = get_component_id(); - if(!has(e)) - { - throw std::runtime_error( - std::format("Entity #{} doesn't have {}", e, typeid(T).name())); - } - - auto& pool = pools.at(id); - auto index = pool.entity_to_index.at(e); - return *reinterpret_cast(&pool.data[index * sizeof(T)]); - } - - template - inline void world::set(entity_id e, const T& comp) - { - if(has(e)) - { - get(e) = comp; - return; - } - - auto id = get_component_id(); - auto& pool = pools[id]; - - size_t index; - if(!pool.free_list.empty()) - { - index = pool.free_list.back(); - pool.free_list.pop_back(); - } - else - { - index = pool.data.size() / sizeof(T); - pool.data.resize(pool.data.size() + sizeof(T)); - } - - new(&pool.data[index * sizeof(T)]) T(comp); - pool.entity_to_index[e] = index; - - auto& archetype = entity_to_comps[e]; - auto old_archetype = archetype; - archetype.set(id); - - auto& group = archetypes[old_archetype]; - group.erase(e); - - if(archetypes[old_archetype].empty()) - { - archetypes.erase(old_archetype); - } - - archetypes[archetype].emplace(e); - } - - template - inline void world::set(entity_id e) - { - set(e, T{}); - } - - template - inline void world::remove(entity_id e) - { - if(!has(e)) - { - return; - } - - auto id = get_component_id(); - auto& archetype = entity_to_comps[e]; - - auto& old_group = archetypes[archetype]; - old_group.erase(e); - - if(old_group.empty()) - { - archetypes.erase(archetype); - } - - archetype.reset(id); - archetypes[archetype].emplace(e); - - auto& pool = pools[id]; - auto index = pool.entity_to_index[e]; - pool.free_list.push_back(index); - pool.entity_to_index.erase(e); - } - - template - inline bool world::has(entity_id e) const - { - return has(e) && has(e) && (has(e) && ...); - } - - template - inline void world::set(entity_id e) - { - set(e, First{}); - set(e, Second{}); - (set(e, Rest{}), ...); - } - - template - inline void world::set(entity_id e, const First& comp0, const Second& comp1, - const Rest&... rest_comps) - { - set(e, comp0); - set(e, comp1); - (set(e, rest_comps), ...); - } - - template - inline void world::remove(entity_id e) - { - remove(e); - remove(e); - (remove(e), ...); - } - - template - inline std::vector world::filter() - { - archetype_signature required; - - (required.set(get_component_id()), ...); - - query_archetypes_checked = 0; - query_entities_processed = 0; - - std::vector result; - - for(const auto& [archetype_key, entities]: archetypes) - { - query_archetypes_checked++; - - if((archetype_key & required) == required) - { - query_entities_processed += entities.size(); - result.insert(result.end(), entities.begin(), entities.end()); - } - } - - return result; - } - - template - inline void world::query(std::invocable auto&& system) - { - for(auto e: filter()) - { - system(e, get(e)...); - } - } -}; // namespace zecsy