diff --git a/tests/ecs/Component.cpp b/tests/ecs/Component.cpp new file mode 100644 index 0000000..698d1f5 --- /dev/null +++ b/tests/ecs/Component.cpp @@ -0,0 +1,276 @@ +#include + +#include +#include + +namespace ecs = arch::ecs; + +TEST(ECS, ComponentSimple) { + // to create your own component you need to perform very specific steps: + // 1. just create them + // 2. Lorem ipsum + // 3. You: Wait, it's that easy? + // 4. Me: Why would it be any different? + + struct Pos { + float x; + float y; + }; + + struct Vel { + float x; + float y; + }; + + // the above components are the simplest that can be + // plain structs like those model many crucial concepts (on them later) + + ecs::Domain domain; + + // components can be then added to entites: + auto e0 = domain.newEntity(); + + // create component Pos for e0 + domain.addComponent(e0); + + // they also are accessible by e0 handle: + domain.getComponent(e0).x = 4; + domain.getComponent(e0).y = 5; + + // each call to getComponent is O(1): + // 1 hash-map find, where hash result is compile-time + // 2 vector find, by index + // in Unity for example, analogous GetComponent is O(n): + // going through list of components until T is found + + // each entity can only have 1 component of each type + // this limits search time + + // attempting to create another component returns one already existing: + { + auto& oldPos = domain.getComponent(e0); + EXPECT_EQ(&oldPos, &domain.addComponent(e0)); + } + + // let's add another entity + auto e1 = domain.newEntity(); + + // and it's components + { + // addComponent returns reference to created component + auto& e1Pos = domain.addComponent(e1, 1, 2); + // arguments for creating Pos -------------^--^ + + // addComponent without template parameters + auto& e1Vel = domain.addComponent(e1, Vel{ .x = 4, .y = 6 }); + } + + // you can check if entity has component of given type + EXPECT_TRUE(domain.hasComponent(e0)); + EXPECT_FALSE(domain.hasComponent(e0)); + EXPECT_TRUE(domain.hasComponent(e1)); + EXPECT_TRUE(domain.hasComponent(e1)); + + // you can also remove components + domain.removeComponent(e1); + + // what happens when you call getComponent, but entity does not contain it? + // C++ standard calls this: Undefined Behavior (UB) + // basicly whatever can happen (most likely SEGFAULT crash) + + // how can you then shield yourself from UB? + // ofc by using optionals with tryGetComponent + { + auto e1Pos = domain.tryGetComponent(e1); + + // you can check if optional contains value: + EXPECT_FALSE(e1Pos); + EXPECT_FALSE(e1Pos.has_value()); + + // if it contains something, access it: + if (e1Pos or e1Pos.has_value()) { + auto& e1PosReference = e1Pos->get(); + // ... + } + } + + // ECS counts how many components of specific type are there + EXPECT_EQ(domain.count(), 1); + EXPECT_EQ(domain.count(), 1); + + // you can also iterate over all components of any type + for (auto&& [entity, pos] : domain.components()) { + // ... + } + for (auto&& [entity, vel] : domain.components()) { + // ... + } +} + +// what if you wanted to flag some entities? +// creating special component for that purpose is a solution + +struct WorseEnemyFlag {}; + +// however component like this will occupy space (not efficient) +// better way is to explicitly mark this component as a flag + +struct EnemyFlag { + static constexpr bool flagComponent = true; +}; + +// if empty class (no non-static field) has static compile-time constant +// flagComponent/FlagComponent/flag_component = true, then it is considered a flag-component +// flag-components are never instantiated, therefore occupy less space + +TEST(ECS, ComponentFlag) { + ecs::Domain domain; + + auto e0 = domain.newEntity(); + auto e1 = domain.newEntity(); + auto e2 = domain.newEntity(); + auto e3 = domain.newEntity(); + + domain.addComponent(e2); + domain.addComponent(e3); + + // for flag-components, getComponent == hasComponent + EXPECT_EQ(typeid(domain.getComponent(e2)), typeid(domain.hasComponent(e2))); + + // instead of reference to component, getComponent returns bool + EXPECT_TRUE(domain.getComponent(e2)); + EXPECT_TRUE(domain.getComponent(e3)); + // meaning getComponent will never result in an UB + EXPECT_FALSE(domain.getComponent(e0)); + EXPECT_FALSE(domain.getComponent(e1)); + + for (auto&& [entity, vel] : domain.components()) { + // ... + } +} + +// now let's dive deep into implementation details + +// in C++ there is a thing called 'concepts' +// they describe requirements for types, that need to be satisfied +// starting from memory size of type, ending at minute details about its methods + +// if type 'T' satisfies all requirements of a concept 'C', then we say that T models C +// for example float and double model std::floating_point and lambdas model std::invocable + +// concepts refine older C++ concept: Named Requirements +// those are abstract rules that types must follow if thew want to perform specific operaions +// concepts were added in C++20, meaning that for 22 years C++ standard library was written without them +// huge amount of std code does not depend on them, being the reason why C++ errors are so bizzare-looking +// (not to mention that for ~12 years C++ was not standardized) + +// the most important concept used in this ECS is std::movable +// it requires types to: +// be move-constructible (have move-constructor) +// be move-assignable (have move-assign operator) +// be swappable (have method swap() or have specialized std::swap) + +// now that you know what concepts are and understand what std::movable types are capable of +// I can now explain what in-place components are + +// in-place components are components which are not moved in the internal storage on basic operations like add/remove +// this means that any pointers or references to them will not be invalidated while performing +// operationson the other components of that type, in contrast to regular components +// this also means that storage for in-place components might not be random-access +// marking components are in-place components works simmilar to flag components +// you need to make compile-time constant named inPlaceComponent/InPlaceComponent/in_place_component = true + +struct Ship { + static constexpr bool inPlaceComponent = true; + + float health; + float bulletDamage; +}; + +// by default components are not in-place components +// you can still explicitly mark them as not in-place +// however if you do that, while components does not model std::movable +// you will get a compilation error, because non-movable components can only be in-place +// and non-movables will be marked as in-place by default + +// I have mentioned 'internal storage' before, but what does it mean? +// every instance of 'Ship', requires 8 bytes of memory +// this memory is handled internally by ComponentPools +// by default, this memory for components is segmented into pages of size 1024 (instances, not bytes) +// thanks to this, add operation on components will not reallocate nor move any components in memory +// It may only allocate new page if previous pages are full +// just as all the other settings, this also can be customized in class by compile-time constant +// componentPageSize/ComponentPageSize/component_page_size = +// pageSize is ignored if component is marked as flag-component +// ATTENTION! page size must be a power of two, to aquire (2^N) just write (1 << N) + +// those 3 settings can be written in definition of your class +// but it can also be put into specialization of arch::ecs::ComponentSpecs, which will override all other settings +// contrary to in-class settings, this method requires you to explicitly set all settings, not just one + +template<> +struct arch::ecs::ComponentSpecs { + // prolonging in-class setting + // if set to false, in-class setting would be overridden + static constexpr bool inPlace = true; + + // not empty -> not a flag + static constexpr bool flag = false; + + // let's change pageSize to something different + // for example: 2^5 = 32 + static constexpr size_t pageSize = (1 << 5); +}; + +// let's now check if our settings acually work + +TEST(ECS, ComponentInPlaceCustomPageSize) { + ecs::Domain domain; + + std::array entities; + for (size_t i = 0; i != 32 * 3; ++i) { + entities[i] = domain.newEntity(); + + domain.addComponent(entities[i]); + } + + // for later + auto& shipOf0 = domain.getComponent(entities[0]); + auto& shipOf31 = domain.getComponent(entities[31]); + + for (size_t i = 0; i != 32; ++i) { + // getting ships 32 (pageSize) instances apart + auto& ship1 = domain.getComponent(entities[i]); + auto& ship2 = domain.getComponent(entities[i + 32]); + auto& ship3 = domain.getComponent(entities[i + 64]); + + // normally ship2 would be 32 instances furhter than ship2 + // but since they are on different pages, they are not + // (there still is non-zero chance of them being that close, because of that tests below are commented out) + // ASSERT_NE(&ship1 + 32, &ship2); + // ASSERT_NE(&ship1 + 64, &ship3); + // ASSERT_NE(&ship2 + 32, &ship3); + } + + for (size_t i = 1; i != 31; ++i) { + // entities 0-31 have Ship component + // let's remove them from 1-30 + domain.removeComponent(entities[i]); + } + + // now we can be certain that Ships for 0 and 31 are not close in memory + // (by close means one instance after another) + ASSERT_NE(abs(&domain.getComponent(entities[31]) - &domain.getComponent(entities[0])), sizeof(Ship)); + + // also their place in memory has not changed + ASSERT_EQ(&shipOf0, &domain.getComponent(entities[0])); + ASSERT_EQ(&shipOf31, &domain.getComponent(entities[31])); +} + +// this is mostly it for components +// remember that these are just examples +// not every components needs to be full-public struct +// ECS supports classes with private fields/methods +// but doesn't suppot polymorphic classes +// because being polymorphic makes memory of an object +// very complex with virtual tables, and very 'implementation-defined' diff --git a/tests/ecs/Entity.cpp b/tests/ecs/Entity.cpp new file mode 100644 index 0000000..89c93b9 --- /dev/null +++ b/tests/ecs/Entity.cpp @@ -0,0 +1,186 @@ +#include +#include + +namespace ecs = arch::ecs; +using Traits = ecs::_details::EntityTraits; + +TEST(ECS, EntityCreationAlive) { + // e64 - 64-bit entity handle + // there also exists e32, with smaller range + ecs::Domain domain; + + for (size_t i = 0; i != 100; ++i) { + auto e = domain.newEntity(); + + // entities' ids start from 0 + EXPECT_EQ(Traits::Id::part(e), i); + // entities' versions start from 0 + EXPECT_EQ(Traits::Version::part(e), 0); + + EXPECT_TRUE(domain.alive(e)); + } +} + +TEST(ECS, EntityKillAlive) { + ecs::Domain domain; + std::array toDelete; + + for (size_t i = 0; i != 100; ++i) { + auto e = domain.newEntity(); + + // mark some entites for deletion + if (i % 10 == 0) { + toDelete[i / 10] = e; + } + + // entities' ids start from 0 + EXPECT_EQ(Traits::Id::part(e), i); + // entities' versions start from 0 + EXPECT_EQ(Traits::Version::part(e), 0); + + EXPECT_TRUE(domain.alive(e)); + } + for (auto&& e : toDelete) { + domain.kill(e); + + // killed entities are not alive + EXPECT_FALSE(domain.alive(e)); + } + + // discouraged usage vvv + // EXPECT_FALSE(domain.alive(Traits::Entity::fromParts(10, 0))); + // only handles aquired by interactions with domain are guaranteed to give valid results + // but since its C++ you can do whatever you want +} + +TEST(ECS, EntityRecycle) { + ecs::Domain domain; + + // 10 entites + for (size_t i = 0; i != 10; ++i) { + auto e = domain.newEntity(); + } + + // 10 entites, but marked for deletion + std::array toDelete; + for (size_t i = 0; i != 10; ++i) { + toDelete[i] = domain.newEntity(); + } + + // 10 entites + for (size_t i = 0; i != 10; ++i) { + auto e = domain.newEntity(); + } + + // 30 entities total + + for (auto&& td : toDelete) { + domain.kill(td); + + EXPECT_FALSE(domain.alive(td)); + } + + // recycle entities in reverse order + for (auto&& toRecycle : std::views::reverse(toDelete)) { + auto recycled = domain.recycleEntity(toRecycle); + + // recycling entities by handle preserves version + EXPECT_EQ(recycled, toRecycle); + + // meaning both recycled and toRecycle are equal + EXPECT_TRUE(domain.alive(recycled)); + EXPECT_TRUE(domain.alive(toRecycle)); + } + for (auto&& td : toDelete) { + domain.kill(td); + + EXPECT_FALSE(domain.alive(td)); + } + + for (auto&& toAdd : std::views::reverse(toDelete)) { + // recycling entities' ids + auto recycled = domain.recycleId(Traits::Id::part(toAdd)); + + // recycling ids does not preserve version, its incremented + EXPECT_EQ(Traits::Version::part(recycled), 1); + + // that's why recycled is alive, toRecycle is not + EXPECT_TRUE(domain.alive(recycled)); + EXPECT_FALSE(domain.alive(toAdd)); + } +} + +TEST(ECS, EntityRecycleExisting) { + ecs::Domain domain; + + // 10 entites + for (size_t i = 0; i != 10; ++i) { + auto e = domain.newEntity(); + } + + // 10 entites, but marked for deletion + std::array toRecycle; + for (size_t i = 0; i != 10; ++i) { + toRecycle[i] = domain.newEntity(); + } + + // 10 entites + for (size_t i = 0; i != 10; ++i) { + auto e = domain.newEntity(); + } + + // 30 entities total, none deleted + + for (auto&& e : toRecycle) { + // recycling existing entites/ids + auto recycled1 = domain.recycleEntity(e); + auto recycled2 = domain.recycleId(Traits::Id::part(e)); + // returns null entity + EXPECT_EQ(recycled1, domain.null); + EXPECT_EQ(recycled2, domain.null); + } +} + +// normally there are two entity types: e32 and e64 +// they are not special in themselves, just enum types +// ECS allows you to create your own entity types like this: + +// 16-bit entity +enum class Entity16Bit : uint16_t; + +// however, Entity16Bit is not usable yet +// lets create example configuration +struct Entity16BitConfiguration { + // type of entity + using EntityT = Entity16Bit; + // bit lenght of id part of handle, for example 12 + static inline constexpr size_t idLength = 12; + // type of id part of handle, must be big enough to store 12 bits + using IdT = uint16_t; + // type of version part of handle, must be big enough to store 4 bits + using VersionT = uint8_t; + // size of pages in sparse sets, default is 1024 + // ATTENTION! pageSize must be a power of 2 + // tip: (2^N) can be written as (1 << N) + static inline constexpr size_t pageSize = 1'024; +}; + +// to be visible to ECS you need to specialize arch::ecs::EntitySpecs +// you can use AutoEntitySpecs +template<> +struct arch::ecs::EntitySpecs: arch::ecs::AutoEntitySpecs {}; + +// or your previous configuration: Entity16BitConfiguration + +// and thats basically it, ECS will compute rest of the info by itself +// you can now proudly use you entities + +TEST(ECS, EntityCustom) { + ecs::Domain domain; + + auto e0 = domain.newEntity(); + + // ... + + EXPECT_EQ(typeid(e0), typeid(Entity16Bit)); +} diff --git a/tests/ecs/View.cpp b/tests/ecs/View.cpp new file mode 100644 index 0000000..48cf1b2 --- /dev/null +++ b/tests/ecs/View.cpp @@ -0,0 +1,91 @@ +#include + +#include +#include + +namespace ecs = arch::ecs; + +TEST(ECS, ViewsSimple) { + struct Pos { + float x; + float y; + }; + + struct Vel { + float x; + float y; + }; + + ecs::Domain domain; + + // let's create some entities with some components + + for (size_t i = 0; i != 10'000; ++i) { + auto e = domain.newEntity(); + + domain.addComponent(e); + auto ifToAdd = rand(); + if (i % (ifToAdd ? ifToAdd : 1)) { + domain.addComponent(e); + } + } + + // that sure is a lot of entites and components + // now let's update their positions + // + // ... + // + // wait, which ones? + // if only you could find all entites with Pos and Vel + // actually, you can using views + + { auto viewPosVal = domain.view(); } + + // hold on, what if you accidentaly also update Vel? + // add 'const' to Vel to make it readonly, any change to Vel will now be an error + + auto viewPosVel = domain.view(); + + // now you have to choose how you update all positions + + // option 1. manually obtain components + for (ecs::e32 entity : viewPosVel) { + { + // get tuple with references + auto posVelTuple = viewPosVel.get(entity); + // access tuple + auto& pos = std::get<0>(posVelTuple); + auto& vel = std::get<1>(posVelTuple); + } + + // the above can be abbreviated with: + auto&& [pos, vel] = viewPosVel.get(entity); + + pos.x += vel.x; + pos.y += vel.y; + + break; // exit for + } + + // option 2. use view.all() + for (auto&& [entity, pos, vel] : viewPosVel.all()) { + pos.x += vel.x; + pos.y += vel.y; + + break; // exit for + } + + // option 3. use view.forEach(entity) + viewPosVel.forEach([&viewPosVel /* capture view */](ecs::e32 entity) { + // auto&& [pos, vel] = viewPosVel.get(entity); + + // pos.x += vel.x; + // pos.y += vel.y; + }); + + // option 4. use view.forEach(entity, &pos, const &vel) + viewPosVel.forEach([&viewPosVel /* capture view */](ecs::e32 entity, Pos& pos, const Vel& vel) { + // pos.x += vel.x; + // pos.y += vel.y; + }); +}