├── .gitignore ├── .vscode └── settings.json ├── CMakeLists.txt ├── LICENSE ├── README.md ├── include └── apecs │ └── apecs.hpp └── tests ├── meta.cpp ├── registry.cpp └── sparse_set.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.default.configurationProvider": "vector-of-bool.cmake-tools", 3 | "files.associations": { 4 | "algorithm": "cpp", 5 | "functional": "cpp", 6 | "locale": "cpp", 7 | "memory": "cpp", 8 | "ostream": "cpp", 9 | "sstream": "cpp", 10 | "system_error": "cpp", 11 | "xfacet": "cpp", 12 | "xhash": "cpp", 13 | "xlocale": "cpp", 14 | "xlocbuf": "cpp", 15 | "xutility": "cpp", 16 | "iterator": "cpp", 17 | "array": "cpp", 18 | "deque": "cpp", 19 | "initializer_list": "cpp", 20 | "list": "cpp", 21 | "random": "cpp", 22 | "type_traits": "cpp", 23 | "vector": "cpp", 24 | "xstring": "cpp", 25 | "xtree": "cpp", 26 | "xtr1common": "cpp", 27 | "any": "cpp", 28 | "atomic": "cpp", 29 | "bit": "cpp", 30 | "cctype": "cpp", 31 | "chrono": "cpp", 32 | "clocale": "cpp", 33 | "cmath": "cpp", 34 | "compare": "cpp", 35 | "concepts": "cpp", 36 | "cstdarg": "cpp", 37 | "cstddef": "cpp", 38 | "cstdint": "cpp", 39 | "cstdio": "cpp", 40 | "cstdlib": "cpp", 41 | "cstring": "cpp", 42 | "ctime": "cpp", 43 | "cwchar": "cpp", 44 | "exception": "cpp", 45 | "coroutine": "cpp", 46 | "ios": "cpp", 47 | "iosfwd": "cpp", 48 | "istream": "cpp", 49 | "limits": "cpp", 50 | "map": "cpp", 51 | "new": "cpp", 52 | "ratio": "cpp", 53 | "set": "cpp", 54 | "stdexcept": "cpp", 55 | "stop_token": "cpp", 56 | "streambuf": "cpp", 57 | "string": "cpp", 58 | "thread": "cpp", 59 | "tuple": "cpp", 60 | "typeinfo": "cpp", 61 | "unordered_map": "cpp", 62 | "utility": "cpp", 63 | "xiosbase": "cpp", 64 | "xlocinfo": "cpp", 65 | "xlocmes": "cpp", 66 | "xlocmon": "cpp", 67 | "xlocnum": "cpp", 68 | "xloctime": "cpp", 69 | "xmemory": "cpp", 70 | "xstddef": "cpp", 71 | "iostream": "cpp", 72 | "iomanip": "cpp", 73 | "variant": "cpp", 74 | "format": "cpp", 75 | "forward_list": "cpp", 76 | "charconv": "cpp", 77 | "optional": "cpp", 78 | "ranges": "cpp", 79 | "span": "cpp" 80 | }, 81 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(apecs) 3 | set(CMAKE_CXX_STANDARD 20) 4 | 5 | add_library(apecs INTERFACE) 6 | target_include_directories(apecs INTERFACE include) 7 | 8 | if (APECS_BUILD_TESTS) 9 | find_package(GTest CONFIG REQUIRED) 10 | enable_testing() 11 | 12 | add_executable( 13 | tests 14 | tests/meta.cpp 15 | tests/sparse_set.cpp 16 | tests/registry.cpp 17 | ) 18 | 19 | target_link_libraries( 20 | tests apecs GTest::gtest_main 21 | ) 22 | endif() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Matt Cummins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apecs: A Petite Entity Component System 2 | A header-only, very small entity component system with no external dependencies. Simply pop the header into your own project and off you go! 3 | 4 | The API is very similar to EnTT, with the main difference being that all component types must be declared up front. This allows for an implementation that doesn't rely on type erasure, which in turn allows for more compile-time optimisations. 5 | 6 | Components are stored contiguously in `apx::sparse_set` objects, which are essentially a pair of `std::vector`s, one sparse and one packed, which allows for fast iteration over components. When deleting components, these sets may reorder themselves to maintain tight packing; as such, sorting isn't currently possibly, but also shouldn't be desired. 7 | 8 | This library also includes some very basic meta-programming functionality, found in the `apx::meta` namespace. 9 | 10 | This project was just a fun little project to allow me to learn more about ECSs and how to implement one, as well as metaprogramming and C++20 features. If you are building your own project and need an ECS, I would recommend you build your own or use EnTT instead. This was originally 11 | implemented using coroutines, however the allocations caused too much overhead, so I replaced them 12 | with an iterator implemenetation, before realising that coroutines were the wrong tool to begin 13 | with, and the correct tool was C++20 ranges. Ultimately the coroutine implementation was just 14 | transforming and filtering vectors of entities and components, which is now expressed directly 15 | in the code and no longer uses superfluous dynamic memory allocations. 16 | 17 | ## The Registry and Entities 18 | In `apecs`, an entity, `apx::entity`, is simply a 64-bit unsigned integer. All components attached to this entity are stored and accessed via the `apx::registry` class. To start, you can default construct a registry, with all of the component types declated up front 19 | ```cpp 20 | apx::registry registry; 21 | ``` 22 | Creating an empty entity is simple 23 | ```cpp 24 | apx::entity e = registry.create(); 25 | ``` 26 | Adding a component is also easy 27 | ```cpp 28 | transform t = { 0.0, 0.0, 0.0 }; // In this example, a transform consists of just a 3D coordinate 29 | registry.add(e, t); 30 | 31 | // Or more explicitly: 32 | registry.add(e, t); 33 | ``` 34 | Move construction is also allowed, as well as directly constructing via emplace 35 | ```cpp 36 | // Uses move constructor (not that there is any benefit with the simple trasnsform struct) 37 | registry.add(e, {0.0, 0.0, 0.0}); 38 | 39 | // Only constructs one instance and does no copying/moving 40 | registry.emplace(e, 0.0, 0.0, 0.0); 41 | ``` 42 | Removing is just as easy 43 | ```cpp 44 | registry.remove(e); 45 | registry.remove_all_components(e); 46 | ``` 47 | Components can be accessed by reference for modification, and entities may be queried to see if they contain the given component type 48 | ```cpp 49 | if (registry.has(e)) { 50 | auto& t = registry.get(e); 51 | update_transform(t); 52 | } 53 | ``` 54 | There are varidic versions of the above in the form of `*_all` 55 | ```cpp 56 | if (registry.has_all(e)) { 57 | auto [t, m] = registry.get_all(e); 58 | update_transform(t); 59 | } 60 | ``` 61 | If you want to know if an entity has *at least one* of a set of components: 62 | ```cpp 63 | registry.has_any(e); 64 | ``` 65 | There is also a noexcept version of `get` called `get_if` which returns a pointer to the component, and `nullptr` if it does not exist 66 | ```cpp 67 | if (auto* t = registry.get_if(e)) { 68 | update_transform(*t); 69 | } 70 | ``` 71 | 72 | Deleting an entity is also straightforward 73 | ```cpp 74 | registry.destroy(e); 75 | ``` 76 | You can also destroy any span of entities in a single call: 77 | ```cpp 78 | registry.destroy({e1, e2, e3, e4}); 79 | ``` 80 | Given that an `apx::entity` is just an identifier for an entity, it could be that an identifier 81 | is referring to an entity that has been destroyed. The registry provides a function to check this 82 | ```cpp 83 | registry.valid(e); // Returns true if the entity is still active and false otherwise. 84 | ``` 85 | The current size of the registry is the number of currently active entities 86 | ```cpp 87 | std::size_t active_entities = registry.size(); 88 | ``` 89 | Finally, a registry may also be cleared of all entities with 90 | ```cpp 91 | registry.clear(); 92 | ``` 93 | 94 | ## Iteration 95 | Iteration is implmented using C++20 ranges. There are two main ways of doing interation; iterating over all entities, and iterating over a *view*; a subset of the entities containing only a specific set of components. 96 | 97 | Iterating over all 98 | ```cpp 99 | for (auto entity : registry.all()) { 100 | ... 101 | } 102 | ``` 103 | Iterating over a view 104 | ```cpp 105 | for (auto entity : registry.view()) { 106 | ... 107 | } 108 | ``` 109 | When iterating over all entities, the iteration is done over the internal entity sparse set. When iterating over a view, we iterate over the sparse set of the first specified component, which can result in a much faster loop. Because of this, if you know that one of the component types is rarer than the others, put that as the first component. 110 | 111 | It is common that the current entity is not actually of direct interest, and is only used to fetch components. For this, there is `view_get` which instead returns a tuple of components instead of the entity id: 112 | ```cpp 113 | for (auto [t, m] : registry.view_get()) { 114 | ... 115 | } 116 | 117 | // The above is clearer than the more verbose and error prone: 118 | for (auto entity : registry.view()) { 119 | auto& t = registry.get(entity); 120 | auto& m = registry.get(entity); 121 | ... 122 | } 123 | ``` 124 | Note that you also don't need to care about constness in the `view_get` case. If the registry is not const, the values in the tuple will be references, and if you are accessing through a `const&` registry, the components will also be `const&`. If you dont have a `const&` to the registry, you can enforce it by creating one and viewing through that: 125 | ```cpp 126 | const auto& cregistry = registry; 127 | for (auto [t, m] : cregistry.view_get()) { 128 | // t and m are const& in this context 129 | } 130 | ``` 131 | 132 | ## Other Functionality 133 | The registry also contains some other useful functions for common uses of views: 134 | 135 | ### Finding an Entity via a Predicate 136 | You can look up an entity that satisfies a callback via 137 | ```cpp 138 | registry.find([](auto entity) -> bool { ... }); 139 | ``` 140 | By default this loops over all entities and returns the first one satisfying the given predicate, returning `apx::null` if there is no such entity. This can be optimized by looping over a view if desired: 141 | ```cpp 142 | registry.find([](auto entity) -> bool { ... }); 143 | ``` 144 | 145 | ### Copying Entities 146 | It might desirable to duplicate entities within a registry. More generally, given 147 | two different registries of the same templated type, it may also be useful to be 148 | able to copy an entity from one registry to another. For this, use `apx::copy`: 149 | ```cpp 150 | template 151 | entity copy(entity entity, const registry& src, registry& dst); 152 | ``` 153 | The given entity must be a valid entity in the src registry. 154 | 155 | ### Deleting Entities via a Predicate 156 | Deleting entities in a loop is undefined behaviour as you could be modifying the container you are iterating over. To delete a set of entities safely 157 | ```cpp 158 | registry.destroy_if([](auto entity) -> bool { ... }); 159 | ``` 160 | This can also take template parameters to do the loop over a view as well. 161 | 162 | ## Metaprogramming 163 | To implement many of these features, some metaprogramming techniques were required and are made available to users. First of all, `apx::tuple_contains` allows for checking at compile time if a given `std::tuple` type contains a specific type. This is used in the component getter/setter functions to give nicer compile errors if there is a type problem, but may be useful in other situations. 164 | ```cpp 165 | static_assert(apx::tuple_contains_v> == true); 166 | static_assert(apx::tuple_contains_v> == false); 167 | ``` 168 | When destroying an entity, we also need to loop over all types to delete the components and to make sure any `on_remove` callbacks are invoked. This can be done with `apx::for_each` 169 | ```cpp 170 | apx::meta::for_each(tuple, [](auto&& element) { 171 | ... 172 | }); 173 | ``` 174 | This of course needs to be generic lambda as this gets invoked for each typle in the tuple. 175 | 176 | In extension to the above, you may also find yourself needing to loop over all types within a reigstry. This can be achieved by creating a tuple of `apx::meta::tag` types and extracting the type from those in a for loop. The library provides some helpers for this. In particular, each registry provides an `inline static constexpr` version of this tuple as `registry::tags`: 177 | ```cpp 178 | apx::meta::for_each(registry.tags, [](apx::meta::tag) { 179 | ... 180 | }); 181 | ``` 182 | 183 | ## Upcoming Features 184 | - The ability to specify `const` in the getters, such as `registry.get(entity)`. -------------------------------------------------------------------------------- /include/apecs/apecs.hpp: -------------------------------------------------------------------------------- 1 | #ifndef APECS_HPP_ 2 | #define APECS_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace apx { 17 | namespace meta { 18 | 19 | template 20 | struct tuple_contains; 21 | 22 | template 23 | struct tuple_contains> : std::bool_constant<(std::is_same_v || ...)> {}; 24 | 25 | template 26 | inline constexpr bool tuple_contains_v = tuple_contains::value; 27 | 28 | template 29 | constexpr void for_each(Tuple&& tuple, F&& f) 30 | { 31 | std::apply([&](auto&&... x) { (f(x), ...); }, tuple); 32 | } 33 | 34 | template struct tag {}; 35 | 36 | template 37 | struct get_first; 38 | 39 | template 40 | struct get_first { using type = T; }; 41 | 42 | } 43 | 44 | template 45 | class sparse_set 46 | { 47 | public: 48 | using index_type = std::size_t; 49 | using value_type = T; 50 | 51 | using packed_type = std::vector>; 52 | using sparse_type = std::vector; 53 | 54 | private: 55 | static_assert(std::is_integral()); 56 | 57 | static constexpr index_type EMPTY = std::numeric_limits::max(); 58 | 59 | packed_type d_packed; 60 | sparse_type d_sparse; 61 | 62 | // Grows the sparse set so that the given index becomes valid. 63 | constexpr void assure(const index_type index) 64 | { 65 | assert(!has(index)); 66 | if (d_sparse.size() <= index) { 67 | d_sparse.resize(index + 1, EMPTY); 68 | } 69 | } 70 | 71 | public: 72 | constexpr sparse_set() noexcept = default; 73 | 74 | // Inserts the given value into the specified index. It is asserted that 75 | // no previous value exists at the index (see assert in assure()). 76 | constexpr value_type& insert(const index_type index, const value_type& value) 77 | { 78 | assure(index); 79 | d_sparse[index] = d_packed.size(); 80 | return d_packed.emplace_back(index, value).second; 81 | } 82 | 83 | constexpr value_type& insert(const index_type index, value_type&& value) 84 | { 85 | assure(index); 86 | d_sparse[index] = d_packed.size(); 87 | return d_packed.emplace_back(index, std::move(value)).second; 88 | } 89 | 90 | template 91 | constexpr value_type& emplace(const index_type index, Args&&... args) 92 | { 93 | assure(index); 94 | d_sparse[index] = d_packed.size(); 95 | return d_packed.emplace_back(std::piecewise_construct, 96 | std::forward_as_tuple(index), 97 | std::forward_as_tuple(std::forward(args)...)).second; 98 | } 99 | 100 | // Returns true if the specified index contains a value, and false otherwise. 101 | [[nodiscard]] bool has(const index_type index) const 102 | { 103 | return index < d_sparse.size() && d_sparse[index] != EMPTY; 104 | } 105 | 106 | // Removes all elements from the set. 107 | void clear() noexcept 108 | { 109 | d_packed.clear(); 110 | d_sparse.clear(); 111 | } 112 | 113 | // Removes the value at the specified index. The structure may reorder 114 | // itself to maintain contiguity for iteration. 115 | void erase(const index_type index) 116 | { 117 | assert(has(index)); 118 | 119 | if (d_sparse[index] == d_packed.size() - 1) { 120 | d_sparse[index] = EMPTY; 121 | d_packed.pop_back(); 122 | return; 123 | } 124 | 125 | // Pop the back element of the sparse_list 126 | auto back = d_packed.back(); 127 | d_packed.pop_back(); 128 | 129 | // Get the index of the outgoing value within the elements vector. 130 | const std::size_t packed_index = d_sparse[index]; 131 | d_sparse[index] = EMPTY; 132 | 133 | // Overwrite the outgoing value with the back value. 134 | d_packed[packed_index] = back; 135 | 136 | // Point the index for the back value to its new location. 137 | d_sparse[back.first] = packed_index; 138 | } 139 | 140 | // Removes the value at the specified index, and does nothing if the index 141 | // does not exist. The structure may reorder itself to maintain element contiguity. 142 | void erase_if_exists(index_type index) noexcept 143 | { 144 | if (has(index)) { 145 | erase(index); 146 | } 147 | } 148 | 149 | [[nodiscard]] std::size_t size() const noexcept 150 | { 151 | return d_packed.size(); 152 | } 153 | 154 | [[nodiscard]] value_type& operator[](const index_type index) 155 | { 156 | assert(has(index)); 157 | return d_packed[d_sparse[index]].second; 158 | } 159 | 160 | [[nodiscard]] const value_type& operator[](const index_type index) const 161 | { 162 | assert(has(index)); 163 | return d_packed[d_sparse[index]].second; 164 | } 165 | 166 | [[nodiscard]] auto each() noexcept 167 | { 168 | return d_packed | std::views::transform([](auto& element) { 169 | return std::make_pair(std::cref(element.first), std::ref(element.second)); 170 | }); 171 | } 172 | 173 | [[nodiscard]] auto each() const noexcept 174 | { 175 | return d_packed | std::views::transform([](const auto& element) { 176 | return std::make_pair(std::cref(element.first), std::cref(element.second)); 177 | }); 178 | } 179 | }; 180 | 181 | enum class entity : std::uint64_t {}; 182 | using index_t = std::uint32_t; 183 | using version_t = std::uint32_t; 184 | 185 | static constexpr apx::entity null{std::numeric_limits::max()}; 186 | 187 | inline std::pair split(const apx::entity id) 188 | { 189 | using Int = std::underlying_type_t; 190 | const Int id_int = static_cast(id); 191 | return {(index_t)(id_int >> 32), (version_t)id_int}; 192 | } 193 | 194 | inline apx::entity combine(const index_t i, const version_t v) 195 | { 196 | using Int = std::underlying_type_t; 197 | return static_cast(((Int)i << 32) + (Int)v); 198 | } 199 | 200 | inline apx::index_t to_index(const apx::entity entity) 201 | { 202 | return apx::split(entity).first; 203 | } 204 | 205 | template 206 | class registry 207 | { 208 | public: 209 | template 210 | using callback_t = std::function; 211 | 212 | using predicate_t = std::function; 213 | 214 | // A tuple of tag types for metaprogramming purposes 215 | inline static constexpr std::tuple...> tags{}; 216 | 217 | private: 218 | using tuple_type = std::tuple...>; 219 | 220 | apx::sparse_set d_entities; 221 | std::deque d_pool; 222 | 223 | tuple_type d_components; 224 | 225 | template 226 | void remove(const apx::entity entity, apx::sparse_set& component_set) 227 | { 228 | if (has(entity)) { 229 | component_set.erase(apx::to_index(entity)); 230 | } 231 | } 232 | 233 | template 234 | [[nodiscard]] apx::sparse_set& get_comps() 235 | { 236 | return std::get>(d_components); 237 | } 238 | 239 | template 240 | [[nodiscard]] const apx::sparse_set& get_comps() const 241 | { 242 | return std::get>(d_components); 243 | } 244 | 245 | public: 246 | ~registry() 247 | { 248 | clear(); 249 | } 250 | 251 | [[nodiscard]] apx::entity create() 252 | { 253 | index_t index = (index_t)d_entities.size(); 254 | version_t version = 0; 255 | if (!d_pool.empty()) { 256 | std::tie(index, version) = split(d_pool.front()); 257 | d_pool.pop_front(); 258 | ++version; 259 | } 260 | 261 | const apx::entity id = combine(index, version); 262 | d_entities.insert(index, id); 263 | return id; 264 | } 265 | 266 | [[nodiscard]] bool valid(const apx::entity entity) const noexcept 267 | { 268 | const apx::index_t index = apx::to_index(entity); 269 | return entity != apx::null 270 | && d_entities.has(index) 271 | && d_entities[index] == entity; 272 | } 273 | 274 | void destroy(const apx::entity entity) 275 | { 276 | assert(valid(entity)); 277 | remove_all_components(entity); 278 | d_pool.push_back(entity); 279 | d_entities.erase(apx::to_index(entity)); 280 | } 281 | 282 | void destroy(const std::span entities) 283 | { 284 | std::ranges::for_each(entities, [&](auto e) { destroy(e); }); 285 | } 286 | 287 | void destroy(const std::initializer_list entities) 288 | { 289 | std::ranges::for_each(entities, [&](auto e) { destroy(e); }); 290 | } 291 | 292 | [[nodiscard]] std::size_t size() const noexcept 293 | { 294 | return d_entities.size(); 295 | } 296 | 297 | void clear() 298 | { 299 | d_components = {}; 300 | d_entities.clear(); 301 | d_pool.clear(); 302 | } 303 | 304 | template 305 | Comp& add(const apx::entity entity, const Comp& component) 306 | { 307 | static_assert(apx::meta::tuple_contains_v, tuple_type>); 308 | assert(valid(entity)); 309 | return get_comps().insert(apx::to_index(entity), component); 310 | } 311 | 312 | template 313 | Comp& add(const apx::entity entity, Comp&& component) 314 | { 315 | using T = std::remove_cvref_t; 316 | static_assert(apx::meta::tuple_contains_v, tuple_type>); 317 | assert(valid(entity)); 318 | return get_comps().insert(apx::to_index(entity), std::forward(component)); 319 | } 320 | 321 | template 322 | Comp& emplace(const apx::entity entity, Args&&... args) 323 | { 324 | static_assert(apx::meta::tuple_contains_v, tuple_type>); 325 | assert(valid(entity)); 326 | return get_comps().emplace(apx::to_index(entity), std::forward(args)...); 327 | } 328 | 329 | template 330 | void remove(const apx::entity entity) 331 | { 332 | static_assert(apx::meta::tuple_contains_v, tuple_type>); 333 | assert(valid(entity)); 334 | if (has(entity)) { 335 | get_comps().erase(apx::to_index(entity)); 336 | } 337 | } 338 | 339 | void remove_all_components(apx::entity entity) 340 | { 341 | apx::meta::for_each(tags, [&] (apx::meta::tag) { 342 | remove(entity); 343 | }); 344 | } 345 | 346 | template 347 | [[nodiscard]] bool has(const apx::entity entity) const noexcept 348 | { 349 | static_assert(apx::meta::tuple_contains_v, tuple_type>); 350 | assert(valid(entity)); 351 | return get_comps().has(apx::to_index(entity)); 352 | } 353 | 354 | template 355 | [[nodiscard]] bool has_all(const apx::entity entity) const noexcept 356 | { 357 | assert(valid(entity)); 358 | return (has(entity) && ...); 359 | } 360 | 361 | template 362 | [[nodiscard]] bool has_any(const apx::entity entity) const noexcept 363 | { 364 | assert(valid(entity)); 365 | return (has(entity) || ...); 366 | } 367 | 368 | template 369 | [[nodiscard]] Comp& get(const apx::entity entity) noexcept 370 | { 371 | static_assert(apx::meta::tuple_contains_v, tuple_type>); 372 | assert(has(entity)); 373 | return get_comps()[apx::to_index(entity)]; 374 | } 375 | 376 | template 377 | [[nodiscard]] const Comp& get(const apx::entity entity) const noexcept 378 | { 379 | static_assert(apx::meta::tuple_contains_v, tuple_type>); 380 | assert(has(entity)); 381 | return get_comps()[apx::to_index(entity)]; 382 | } 383 | 384 | template 385 | [[nodiscard]] auto get_all(const apx::entity entity) noexcept 386 | { 387 | assert(has_all(entity)); 388 | return std::make_tuple(std::ref(get(entity))...); 389 | } 390 | 391 | template 392 | [[nodiscard]] auto get_all(const apx::entity entity) const noexcept 393 | { 394 | assert(has_all(entity)); 395 | return std::make_tuple(std::cref(get(entity))...); 396 | } 397 | 398 | template 399 | [[nodiscard]] Comp* get_if(const apx::entity entity) noexcept 400 | { 401 | static_assert(apx::meta::tuple_contains_v, tuple_type>); 402 | return has(entity) ? &get(entity) : nullptr; 403 | } 404 | 405 | apx::entity from_index(std::size_t index) const noexcept 406 | { 407 | return d_entities[index]; 408 | } 409 | 410 | [[nodiscard]] auto all() const noexcept 411 | { 412 | return d_entities.each() | std::views::values; 413 | } 414 | 415 | template 416 | [[nodiscard]] auto view() const noexcept 417 | { 418 | if constexpr (sizeof...(Comps) == 0) { 419 | return all(); 420 | } else { 421 | using Comp = typename apx::meta::get_first::type; 422 | const auto entity_view = get_comps().each() 423 | | std::views::keys 424 | | std::views::transform([&](auto index) { return from_index(index); }); 425 | 426 | if constexpr (sizeof...(Comps) > 1) { 427 | return entity_view | std::views::filter([&](auto entity) { 428 | return has_all(entity); 429 | }); 430 | } else { 431 | return entity_view; 432 | } 433 | } 434 | } 435 | 436 | template [[nodiscard]] auto view_get() noexcept 437 | { 438 | return view() | std::views::transform([&](auto entity) { 439 | return get_all(entity); 440 | }); 441 | } 442 | 443 | template [[nodiscard]] auto view_get() const noexcept 444 | { 445 | return view() | std::views::transform([&](auto entity) { 446 | return get_all(entity); 447 | }); 448 | } 449 | 450 | template 451 | void destroy_if(const predicate_t& cb) noexcept { 452 | auto v = view() | std::views::filter(cb); 453 | std::vector to_delete{v.begin(), v.end()}; 454 | destroy(to_delete); 455 | } 456 | 457 | template 458 | [[nodiscard]] apx::entity find(const predicate_t& predicate = [](apx::entity) { return true; }) const noexcept 459 | { 460 | auto v = view(); 461 | if (auto result = std::ranges::find_if(v, predicate); result != v.end()) { 462 | return *result; 463 | } 464 | return apx::null; 465 | } 466 | }; 467 | 468 | template 469 | apx::entity copy(apx::entity entity, const apx::registry& src, apx::registry& dst) 470 | { 471 | auto new_entity = dst.create(); 472 | apx::meta::for_each(apx::registry::tags, [&](apx::meta::tag) { 473 | if (src.has(entity)) { 474 | dst.add(new_entity, src.get(entity)); 475 | } 476 | }); 477 | return new_entity; 478 | } 479 | 480 | } 481 | 482 | #endif // APECS_HPP_ 483 | -------------------------------------------------------------------------------- /tests/meta.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | TEST(meta, contains_true) 6 | { 7 | static_assert(apx::meta::tuple_contains_v>); 8 | static_assert(apx::meta::tuple_contains_v>); 9 | } 10 | 11 | TEST(meta, contains_false) 12 | { 13 | static_assert(!apx::meta::tuple_contains_v>); 14 | } 15 | 16 | TEST(meta, tuple_for_each_calls_for_every_element) 17 | { 18 | std::tuple t; 19 | std::size_t count = 0; 20 | 21 | apx::meta::for_each(t, [&](auto&&) { 22 | ++count; 23 | }); 24 | 25 | ASSERT_EQ(count, 3); 26 | } -------------------------------------------------------------------------------- /tests/registry.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | struct foo { int value = 0; }; 5 | struct bar {}; 6 | 7 | TEST(registry, entity_invalid_after_destroying) 8 | { 9 | apx::registry reg; 10 | 11 | auto e = reg.create(); 12 | ASSERT_TRUE(reg.valid(e)); 13 | 14 | reg.destroy(e); 15 | ASSERT_FALSE(reg.valid(e)); 16 | } 17 | 18 | TEST(registry, size_of_registry) 19 | { 20 | apx::registry reg; 21 | 22 | auto e1 = reg.create(); 23 | ASSERT_EQ(reg.size(), 1); 24 | 25 | auto e2 = reg.create(); 26 | ASSERT_EQ(reg.size(), 2); 27 | 28 | auto e3 = reg.create(); 29 | ASSERT_EQ(reg.size(), 3); 30 | 31 | reg.destroy(e2); 32 | ASSERT_EQ(reg.size(), 2); 33 | 34 | reg.clear(); 35 | ASSERT_EQ(reg.size(), 0); 36 | } 37 | 38 | TEST(registry, for_each_type) 39 | { 40 | apx::registry reg; 41 | apx::entity e = reg.create(); 42 | std::size_t count = 0; 43 | 44 | apx::meta::for_each(reg.tags, [&] (apx::meta::tag) { 45 | ++count; 46 | }); 47 | 48 | ASSERT_EQ(count, 2); 49 | } 50 | 51 | TEST(registry, test_noexcept_get) 52 | { 53 | apx::registry reg; 54 | apx::entity e = reg.create(); 55 | 56 | reg.add(e, {}); 57 | 58 | foo* foo_get = reg.get_if(e); 59 | ASSERT_NE(foo_get, nullptr); 60 | 61 | bar* bar_get = reg.get_if(e); 62 | ASSERT_EQ(bar_get, nullptr); 63 | } 64 | 65 | TEST(registry, test_add) 66 | // registry::add should work with explicitly writing the template type 67 | // as well as by type deduction. The case that was broken was lvalue ref 68 | // with the type deduced, as Comp would be deduced as T&. This was fixed 69 | // by using std::remove_cvref_t in the forward ref overload of registry::add. 70 | // The other add function does not need this as it takes an lvalue ref, and 71 | // the emplace/remove/get functions also dont need this since the type 72 | // must be specified explicitly. 73 | { 74 | apx::registry reg; 75 | 76 | { // lvalue ref, explicit type 77 | apx::entity e = reg.create(); 78 | foo f; 79 | reg.add(e, f); 80 | ASSERT_TRUE(reg.has(e)); 81 | } 82 | 83 | { // rvalue ref, explicit type 84 | apx::entity e = reg.create(); 85 | reg.add(e, {}); 86 | ASSERT_TRUE(reg.has(e)); 87 | } 88 | 89 | { // lvalue ref, type deduced 90 | apx::entity e = reg.create(); 91 | foo f; 92 | reg.add(e, f); 93 | ASSERT_TRUE(reg.has(e)); 94 | } 95 | 96 | { // rvalue ref, type deduced 97 | apx::entity e = reg.create(); 98 | reg.add(e, foo{}); 99 | ASSERT_TRUE(reg.has(e)); 100 | } 101 | } 102 | 103 | TEST(registry, multi_destroy_vector) 104 | { 105 | apx::registry reg; 106 | auto e1 = reg.create(); 107 | auto e2 = reg.create(); 108 | auto e3 = reg.create(); 109 | ASSERT_EQ(reg.size(), 3); 110 | 111 | std::vector v{e1, e2, e3}; 112 | reg.destroy(v); 113 | } 114 | 115 | TEST(registry, multi_destroy_initializer_list) 116 | { 117 | apx::registry reg; 118 | auto e1 = reg.create(); 119 | auto e2 = reg.create(); 120 | auto e3 = reg.create(); 121 | ASSERT_EQ(reg.size(), 3); 122 | 123 | reg.destroy({e1, e2, e3}); 124 | } 125 | 126 | TEST(registry_iteration, view_for_loop) 127 | { 128 | apx::registry reg; 129 | 130 | auto e1 = reg.create(); 131 | reg.emplace(e1); 132 | reg.emplace(e1); 133 | 134 | auto e2 = reg.create(); 135 | reg.emplace(e2); 136 | 137 | std::size_t count = 0; 138 | for (auto entity : reg.view()) { 139 | ++count; 140 | } 141 | ASSERT_EQ(count, 1); 142 | } 143 | 144 | TEST(registry_iteration, view_for_loop_multi) 145 | { 146 | apx::registry reg; 147 | 148 | auto e1 = reg.create(); 149 | reg.emplace(e1); 150 | reg.emplace(e1); 151 | 152 | auto e2 = reg.create(); 153 | reg.emplace(e2); 154 | 155 | auto e3 = reg.create(); 156 | reg.emplace(e3); 157 | reg.emplace(e3); 158 | 159 | std::size_t count = 0; 160 | for (auto entity : reg.view()) { 161 | ++count; 162 | } 163 | ASSERT_EQ(count, 2); 164 | } 165 | 166 | TEST(registry_iteration, all_for_loop) 167 | { 168 | apx::registry reg; 169 | 170 | auto e1 = reg.create(); 171 | reg.emplace(e1); 172 | reg.emplace(e1); 173 | 174 | auto e2 = reg.create(); 175 | reg.emplace(e2); 176 | 177 | std::size_t count = 0; 178 | for (auto entity : reg.all()) { 179 | ++count; 180 | } 181 | ASSERT_EQ(count, 2); 182 | } 183 | 184 | TEST(registry_copying, copying_entities_within_reg) 185 | { 186 | apx::registry reg; 187 | 188 | auto e1 = reg.create(); 189 | reg.add(e1, {}); 190 | 191 | auto e2 = apx::copy(e1, reg, reg); 192 | ASSERT_TRUE(reg.valid(e2)); 193 | ASSERT_TRUE(reg.has(e2)); 194 | } 195 | 196 | TEST(registry_copying, copying_entities_different_reg) 197 | { 198 | apx::registry reg1, reg2; 199 | 200 | auto e1 = reg1.create(); 201 | reg1.add(e1, {}); 202 | 203 | auto e2 = apx::copy(e1, reg1, reg2); 204 | ASSERT_TRUE(reg2.valid(e2)); 205 | ASSERT_TRUE(reg2.has(e2)); 206 | } -------------------------------------------------------------------------------- /tests/sparse_set.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | TEST(sparse_set, set_and_get) 5 | { 6 | apx::sparse_set set; 7 | 8 | set.insert(2, 5); 9 | ASSERT_TRUE(set.has(2)); 10 | ASSERT_EQ(set[2], 5); 11 | } 12 | 13 | TEST(sparse_set, erase) 14 | { 15 | apx::sparse_set set; 16 | 17 | set.insert(2, 5); 18 | ASSERT_TRUE(set.has(2)); 19 | 20 | set.erase(2); 21 | ASSERT_FALSE(set.has(2)); 22 | } 23 | 24 | TEST(sparse_set, iterate_with_one_element_and_index_check) 25 | { 26 | apx::sparse_set set; 27 | set.insert(2, 5); 28 | 29 | for (const auto& [index, value] : set.each()) { 30 | ASSERT_EQ(index, 2); 31 | ASSERT_EQ(value, 5); 32 | 33 | } 34 | } 35 | 36 | TEST(sparse_set, each_can_modify_elements) 37 | { 38 | apx::sparse_set set; 39 | set.insert(1, 5); 40 | 41 | for (auto [index, value] : set.each()) { 42 | value = 6; 43 | } 44 | 45 | ASSERT_EQ(set[1], 6); 46 | } --------------------------------------------------------------------------------