├── .clang-format ├── .gitignore ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── docs ├── Makefile ├── advanced.rst ├── conf.py ├── index.rst ├── make.bat ├── tutorial.rst └── visit.rst ├── examples ├── CMakeLists.txt ├── enemy_game.cpp ├── everything.cpp ├── position_velocity.cpp └── rendering.cpp ├── include └── ginseng │ └── ginseng.hpp └── src ├── catch.hpp ├── main.cpp ├── test.cpp ├── test_bitset.cpp ├── test_count.cpp ├── test_primary.cpp ├── test_stress.cpp └── test_tags.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AllowShortFunctionsOnASingleLine: Empty 6 | AlwaysBreakTemplateDeclarations: true 7 | ColumnLimit: 0 8 | IndentWidth: 4 9 | PointerAlignment: Left 10 | Standard: Cpp11 11 | TabWidth: 4 12 | ... 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !.gitignore 3 | !.clang-format 4 | !*.md 5 | !*.txt 6 | !examples 7 | !include 8 | !src 9 | !docs 10 | /docs/_build 11 | /docs/_static 12 | /docs/_templates 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.0) 2 | project(Ginseng) 3 | 4 | set(GINSENG_BUILD_EXAMPLES No CACHE BOOL "Build examples") 5 | 6 | add_library(ginseng INTERFACE) 7 | set_property(TARGET ginseng PROPERTY INTERFACE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/include/ginseng/ginseng.hpp) 8 | target_include_directories(ginseng INTERFACE include) 9 | 10 | add_executable(test_ginseng EXCLUDE_FROM_ALL 11 | src/main.cpp 12 | src/test.cpp 13 | src/catch.hpp 14 | src/test_tags.cpp 15 | src/test_primary.cpp 16 | src/test_stress.cpp 17 | src/test_bitset.cpp 18 | src/test_count.cpp) 19 | set_property(TARGET test_ginseng PROPERTY CXX_STANDARD 17) 20 | target_link_libraries(test_ginseng ginseng) 21 | 22 | if (MSVC) 23 | target_compile_options(test_ginseng PUBLIC /W4 /WX) 24 | else() 25 | target_compile_options(test_ginseng PUBLIC -Wall -Wextra -pedantic -Werror) 26 | endif() 27 | 28 | if(GINSENG_BUILD_EXAMPLES) 29 | add_subdirectory(examples) 30 | endif() 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Apples 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ginseng 2 | 3 | Ginseng is an entity-component-system (ECS) library designed for use in games. 4 | 5 | The main advantage over similar libraries is that the component types do not need to be listed or registered. 6 | Component types are detected dynamically. 7 | 8 | Any function-like object can be used as a system. 9 | The function's parameters are used to determine the required components. 10 | 11 | Here's a pseudo-example of what it looks like to use Ginseng: 12 | 13 | ```cpp 14 | auto db = ginseng::database{}; 15 | 16 | // entity 17 | auto goomba = db.create_entity(); 18 | 19 | // component 20 | db.add_component(goomba, component::position{10, 20}); 21 | db.add_component(goomba, component::sprite{"goomba.png"}); 22 | db.add_component(goomba, component::behavior{"walk_left"}); 23 | 24 | // system 25 | db.visit([](component::position& pos, const component::behavior& behavior) { 26 | if (behavior.state == "walk_left") { 27 | pos.x -= 1; 28 | } else { 29 | pos.x += 1; 30 | } 31 | }); 32 | ``` 33 | 34 | ## Documentation 35 | 36 | Full documentation can be found at [https://ginseng.readthedocs.io/](https://ginseng.readthedocs.io/). 37 | 38 | ## Features 39 | 40 | - Fully type safe! 41 | - No dynamic casts. 42 | - No intrusive inheritance. 43 | - No exceptions are thrown. 44 | - Unlimited component types. 45 | - Systems are just regular functions. 46 | - Component objects are stable and can be added or removed freely. 47 | - Entity IDs are versioned, so no worries about double-deletion or stale IDs. 48 | 49 | ## Status 50 | 51 | Feature-complete and stable API. 52 | 53 | ## Dependencies 54 | 55 | There are none! Ginseng is a single-header library that only requires C++17. 56 | 57 | ## Examples 58 | 59 | See the `examples/` directory. 60 | 61 | ## License 62 | 63 | MIT 64 | 65 | See [LICENSE.txt](https://github.com/dbralir/ginseng/blob/master/LICENSE.txt). 66 | 67 | Copyright 2015-2021 Apples 68 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/advanced.rst: -------------------------------------------------------------------------------- 1 | Advanced 2 | ######## 3 | 4 | Entity Indices and Versions 5 | *************************** 6 | 7 | Each entity occupies a "slot" within the database, and each slot is given a unique index. 8 | An ``ent_id`` is conceptually just the index (see :ref:`get_index()`) of the entity slot, though it is more than just a number. 9 | 10 | All ``ent_id``s also have an opaque version identifier. 11 | This version identifier is changed whenever an entity is created in an index which was previously occupied by another entity. 12 | 13 | Some ``ent_id`` objects might have the same index, but have referred to different entities, and therefore will have different versions. 14 | Because of this, you should not rely on ``get_index()`` for anything other than hashing and debugging, 15 | and should use ``==`` to compare two ``ent_id`` values directly. 16 | 17 | Because an ``ent_id`` has both an index and a version, 18 | Ginseng is able to determine if an ``ent_id`` refers to a destroyed entity, even if a new entity is occupying the same index. 19 | Also, Ginseng is able to differentiate between several ``ent_id``s which pointed to different objects, even if they share the same index. 20 | 21 | Remember: although indices may get recycled, only one unique entity will exist at a specific index at a specific time. 22 | 23 | Advanced Database Methods 24 | ************************* 25 | 26 | ``exists(ent_id)`` 27 | ================== 28 | 29 | This method determines if the given ``ent_id`` points to a currently valid entity. 30 | 31 | Even if an entity with the same index currently exists, 32 | this function will return ``false`` if the ``ent_id`` was created from an older entity which occupied the same index. 33 | 34 | ``get_component_by_id(com_id)`` 35 | =============================== 36 | 37 | .. note:: 38 | This function is almost never necessary. 39 | You should always prefer the usual ``get_component(ent_id)`` function unless you have a specific need for this version. 40 | 41 | This function directly obtains a component value from the given ``com_id``. 42 | The only way to obtain a ``com_id`` is from a call to ``add_component()`` (tags do not have a ``com_id``). 43 | 44 | A ``com_id`` is a simple value that points directly to the component's value. 45 | 46 | .. warning:: 47 | Behavior is undefined when an invalid or expired ``com_id`` is used, so you must be extra careful when using this function! 48 | 49 | ``size()`` 50 | ========== 51 | 52 | Returns the number of entities in the database. 53 | 54 | ``count()`` 55 | ================ 56 | 57 | Returns the number of entities which have the specified component. 58 | 59 | ``to_ptr(ent_id)`` and ``from_ptr(void*)`` 60 | ========================================== 61 | 62 | ``to_ptr(ent_id)`` converts an ``ent_id`` to a ``void*`` for storage purposes (and no other purposes!). 63 | 64 | Use this function to essentially serialize an ``ent_id``. 65 | 66 | To convert the ``void*`` back to the original ``ent_id``, use ``from_ptr(void*)``. 67 | It must only be given a ``void*`` which was obtained from ``to_ptr(ent_id)``. 68 | 69 | .. note:: 70 | The type ``void*`` is used because many language interop layers (e.g. Lua) also use ``void*`` as a generic "custom value". 71 | Returning ``void*`` here makes using such libraries smoother. 72 | Additionally, I feel like using ``void*`` instead of e.g. a ``byte[]`` discourages persistent storage. 73 | 74 | .. warning:: 75 | This is not a valid pointer and relies on widespread compiler-specific behavior. 76 | Do not ever dereference the pointer. 77 | 78 | .. warning:: 79 | Entity version checking is not preserved through conversion to and from pointers. 80 | 81 | .. warning:: 82 | Null pointers are a valid result and may represent an actual entity. 83 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | import sphinx_rtd_theme 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'Ginseng' 22 | copyright = '2021, Apples' 23 | author = 'Apples' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = '1.1' 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx_rtd_theme', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # List of patterns, relative to source directory, that match files and 42 | # directories to ignore when looking for source files. 43 | # This pattern also affects html_static_path and html_extra_path. 44 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 45 | 46 | 47 | # -- Options for HTML output ------------------------------------------------- 48 | 49 | # The theme to use for HTML and HTML Help pages. See the documentation for 50 | # a list of builtin themes. 51 | # 52 | html_theme = 'sphinx_rtd_theme' 53 | 54 | # Add any paths that contain custom static files (such as style sheets) here, 55 | # relative to this directory. They are copied after the builtin static files, 56 | # so a file named "default.css" will overwrite the builtin "default.css". 57 | html_static_path = ['_static'] -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Ginseng documentation master file, created by 2 | sphinx-quickstart on Sat Jul 24 18:09:22 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Ginseng's documentation! 7 | =================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | tutorial 14 | visit 15 | advanced 16 | 17 | Ginseng is a lightweight Entity-Component-System library aimed at rapid prototyping and modular engines. 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ######## 3 | 4 | Installation 5 | ************ 6 | 7 | Ginseng is a header-only library. 8 | 9 | The only thing you need to do to include it in your project is Add the ``include`` directory to your project's compile include directories. 10 | 11 | Ginseng requires C++17. 12 | 13 | For example, using CMake: 14 | 15 | .. code-block:: cmake 16 | 17 | add_executable(my_game main.cpp) 18 | target_compile_features(my_game PRIVATE cxx_std_17) 19 | target_include_directories(my_game PRIVATE 20 | "${CMAKE_SOURCE_DIR}/ext/ginseng/include") 21 | 22 | Then, just include the main header in your source code: 23 | 24 | .. code-block:: cpp 25 | 26 | #include 27 | 28 | You may want to use a few type aliases to make things easier: 29 | 30 | .. code-block:: cpp 31 | 32 | using database = ginseng::database; 33 | using ent_id = database::ent_id; 34 | using com_id = database::com_id; 35 | 36 | Constructing a Database 37 | *********************** 38 | 39 | You need a ``ginseng::database`` to hold your entities. 40 | Typically this would be placed directly inside of some kind of ``game_state`` class, or whatever your equivalent is. 41 | 42 | .. code-block:: cpp 43 | 44 | struct game_state { 45 | ginseng::database ent_db; 46 | }; 47 | 48 | There are no constructor parameters or any other setup needed. 49 | 50 | Components and systems do not need to be "registered" like in many other ECS libraries. 51 | 52 | Defining Components 53 | ******************* 54 | 55 | In Ginseng, any value type is a valid component. Even ``int`` is a valid component. 56 | 57 | Typically, you should define your components as simple structs: 58 | 59 | .. code-block:: cpp 60 | 61 | namespace component { 62 | 63 | struct position { 64 | float x; 65 | float y; 66 | }; 67 | 68 | struct velocity { 69 | float vx; 70 | float vy; 71 | }; 72 | 73 | } 74 | 75 | There is no need to register these components, you can start using them right away. 76 | 77 | There is no strict requirement, but your components should ideally be simple struct types, which are easily serializable. 78 | Features such as virtual functions or inheritance will likely not work intuitively in Ginseng. 79 | 80 | Components must be a non-pointer value type. Pointers and references must be wrapped in a struct or class component. 81 | 82 | Using Entities 83 | ************** 84 | 85 | An entity is essentially just a collection of components, where each component has a distinct type. 86 | 87 | Typically, an entity will correspond to a game object or actor. 88 | 89 | There is no limit to the number of entities you may create. 90 | 91 | Creating Entities 92 | ================= 93 | 94 | To create an entity, simply call the database's ``create_entity`` method: 95 | 96 | .. code-block:: cpp 97 | 98 | auto my_ent = ent_db.create_entity(); 99 | 100 | The returned value is a handle to the created entity, of type ``ginseng::database::ent_id``. 101 | 102 | It is a trivially copyable value type, so feel free to store and copy it as if it were a pointer type. 103 | 104 | Destroying Entities 105 | =================== 106 | 107 | Destroy entities with the ``destroy_entity`` method: 108 | 109 | .. code-block:: cpp 110 | 111 | ent_db.destroy_entity(my_ent); 112 | 113 | All of the components attached to the entity will also be destroyed. 114 | 115 | Using Components 116 | **************** 117 | 118 | Components are pieces of data which can be attached to an entity. 119 | 120 | Adding Components to Entities 121 | ============================= 122 | 123 | To add a component to an existing entity, use the ``add_component`` method: 124 | 125 | .. code-block:: cpp 126 | 127 | ent_db.add_component(my_ent, component::position{x, y}); 128 | 129 | This will copy (or move) the given component and attach it to the entity. 130 | 131 | .. warning:: 132 | Attempting to add a component to an entity which already has a component of the same type will **overwrite** the existing component. 133 | An entity cannot have two components of the same type. 134 | 135 | Removing Components from Entities 136 | ================================= 137 | 138 | To remove an existing component from an entity, use the ``remove_component`` method: 139 | 140 | .. code-block:: cpp 141 | 142 | ent_db.remove_component(my_ent); 143 | 144 | This will detach the component from the entity and delete the component. 145 | 146 | Notice that the component type must be passed as a template parameter, since there is no function parameter to deduce it from. 147 | 148 | .. warning:: 149 | Calling ``remove_component`` for a component type which does not exist on that entity is **undefined behavior**. 150 | There is no error-checking for this scenario. 151 | 152 | Determine if an Entity has a Component 153 | ====================================== 154 | 155 | To determing if an entity has a component of a certain type, use the ``has_component`` method: 156 | 157 | .. code-block:: cpp 158 | 159 | if (ent_db.has_component(my_ent)) { 160 | std::cout << "Position exists!\n"; 161 | } 162 | 163 | .. note:: 164 | If you need to retrieve a component that may not exist, 165 | the preferred way to do this is to call the ``get_component`` method with a component pointer type. 166 | 167 | Getting a Component From an Entity 168 | ================================== 169 | 170 | If you know a component exists on an entity, you can use the ``get_component`` method to retrieve it directly: 171 | 172 | .. code-block:: cpp 173 | 174 | auto& pos = ent_db.get_component(my_ent); 175 | 176 | If you are unsure if the component exists or not, use ``get_component`` with a component pointer type: 177 | 178 | .. code-block:: cpp 179 | 180 | if (auto* pos_ptr = ent_db.get_component(my_ent)) { 181 | std::cout << "Position exists.\n"; 182 | } else { 183 | std::cout << "Position does not exist.\n"; 184 | } 185 | 186 | Either method above will return a direct, mutable reference to the component data. 187 | Feel free to modify it as you please. 188 | 189 | Tag Components 190 | ************** 191 | 192 | Tag components work similarly to normal components, except they do not have a value. 193 | 194 | Typically a tag component is used when you need to identify an entity as being part of some broader category. 195 | 196 | Define tag components using the ``ginseng::tag`` template: 197 | 198 | .. code-block:: cpp 199 | 200 | namespace component { 201 | 202 | using player_tag = ginseng::tag; 203 | 204 | using enemy_tag = ginseng::tag; 205 | 206 | } 207 | 208 | Adding and removing tag components works the same as with normal components: 209 | 210 | .. code-block:: cpp 211 | 212 | ent_db.add_component(my_ent, component::player_tag{}); 213 | 214 | ent_db.has_component(my_ent); 215 | 216 | ent_db.remove_component(my_ent); 217 | 218 | However, because tag components have no value, you cannot use them with the ``get_component`` database method. 219 | 220 | Tag components can also be used in visitor functions just like regular components, 221 | the only difference being their lack of a data value: 222 | 223 | .. code-block:: cpp 224 | 225 | ent_db.visit([](component::player_tag, component::position& pos) { 226 | process_player_movement(pos); 227 | }); 228 | 229 | Using Visitors (aka Systems) 230 | **************************** 231 | 232 | Ginseng does not have traditional ECS "systems" that need to be registered. 233 | Instead, visitor functions provide the same functionality in a more immediate style. 234 | 235 | To run a visitor on your entities, use the ``visit`` method: 236 | 237 | .. code-block:: cpp 238 | 239 | ent_db.visit([](const component::velocity& vel, component::position& pos) { 240 | pos.x += vel.x; 241 | pos.y += vel.y; 242 | }); 243 | 244 | See the :doc:`visit` page for more details and examples. 245 | -------------------------------------------------------------------------------- /docs/visit.rst: -------------------------------------------------------------------------------- 1 | Visitors 2 | ######## 3 | 4 | Intro 5 | ***** 6 | 7 | Ginseng does not have traditional ECS "systems" that need to be registered. 8 | Instead, visitor functions provide the same functionality in a more immediate style. 9 | 10 | It is recommended to prefer using many specialized visitors, instead of having only a few visitors that do a lot of work. 11 | Common examples of visitors are: drawing sprites, updating physics, updating timers, running AI logic, etc. 12 | 13 | Typically, your game's update loop will consist mostly of running visitors. 14 | 15 | Running a Visitor 16 | ***************** 17 | 18 | To run a visitor on your entities, use the ``visit`` method: 19 | 20 | .. code-block:: cpp 21 | 22 | ent_db.visit([](const component::velocity& vel, component::position& pos) { 23 | pos.x += vel.x; 24 | pos.y += vel.y; 25 | }); 26 | 27 | The visitor function is called on every entity, as long as that entity can provide the requested component parameters. 28 | 29 | A non-tag component parameter must match the deduced type of either ``T``, ``T&``, or ``const T&``. 30 | 31 | For tag components, the type must match ``T`` or ``const T&``. A mutable reference is not allowed, since tags have no value. 32 | 33 | Special Parameter Types 34 | *********************** 35 | 36 | Other than component types, there are a few special types that you can use as well. 37 | 38 | These parameters must match the deduced type of either ``T`` or ``const T&``. 39 | Mutable references are not allowed because all of these parameters are either valueless or temporary. 40 | 41 | .. note:: 42 | These special parameters are ignored when determining the "primary" component for a visitor. 43 | The primary component will always be the first parameter type which is a concrete component type. 44 | 45 | ``ginseng::database::ent_id`` 46 | ============================= 47 | 48 | A parameter of this type will be the ``ent_id`` of the current entity that is being visited. 49 | 50 | You may freely add, get, or remove components from this entity during the visit, 51 | and the ``ent_id`` will remain valid if copied out of the visitor. 52 | 53 | You can also destroy the entity itself. 54 | 55 | Example: 56 | 57 | .. code-block:: cpp 58 | 59 | ent_db.visit([](ginseng::database::ent_id id, component::timer& timer) { 60 | if (timer.remaining <= 0) { 61 | ent_db.destroy_entity(id); 62 | } 63 | }); 64 | 65 | ``ginseng::optional`` 66 | ======================== 67 | 68 | This type represents an optional component. All entites will match this parameter. 69 | 70 | This type should be treated as a pointer. 71 | 72 | It is explicitly convertible to ``bool``, which indicates whether the entity has this component. 73 | 74 | Additionally, for non-tag components, you can dereference it (with ``*`` or ``->``) to get the component's data. 75 | 76 | There is also a ``.get()`` method which does the same as dereferencing. 77 | 78 | Optional tag components cannot be dereferenced and have no ``.get()`` method. 79 | 80 | Example: 81 | 82 | .. code-block:: cpp 83 | 84 | ent_db.visit([](const component::sprite& sprite, ginseng::optional anim) { 85 | if (anim) { 86 | draw_with_animation(sprite, *anim); 87 | } else { 88 | draw_without_animation(sprite); 89 | } 90 | }); 91 | 92 | ``ginseng::require`` 93 | ======================= 94 | 95 | A parameter of this type works the same way as a normal component type, except the component's data is not retrieved. 96 | 97 | Use this when you need to visit entities which have a certain component, but you don't actually care about the value of that component. 98 | 99 | This is only an optimization, and in most cases is not needed. 100 | 101 | Example: 102 | 103 | .. code-block:: cpp 104 | 105 | ent_db.visit([](ginseng::require, component::position& pos) { 106 | process_player_movement(pos); 107 | }); 108 | 109 | ``ginseng::deny`` 110 | ==================== 111 | 112 | This does the opposite of ``ginseng::require``. Only components which **do not** have a component of this type will be visited. 113 | 114 | .. note:: 115 | Usually, it is better to create a tag component and add that tag to entities you care about, 116 | since ``ginseng::deny`` might match a broader category of entities than you expect, 117 | especially as your project evolves over time. 118 | 119 | Example: 120 | 121 | .. code-block:: cpp 122 | 123 | ent_db.visit([](ginseng::deny, component::position& pos) { 124 | process_npc_movement(pos); 125 | }); 126 | 127 | Primary Component 128 | ***************** 129 | 130 | .. note:: 131 | This is purely a discussion of optimization. 132 | You can use Ginseng perfectly fine without this knowledge. 133 | 134 | The first normal component parameter of the visitor function will be used as the "primary" component. 135 | 136 | The ``visit`` method is optimized to only examine entities which definitely have the primary component. 137 | 138 | For example, let's say we've set up three entities as follows: 139 | 140 | .. code-block:: cpp 141 | 142 | auto ent1 = ent_db.create_entity(); 143 | ent_db.add_component(ent1, component::position{}); 144 | 145 | auto ent2 = ent_db.create_entity(); 146 | ent_db.add_component(ent1, component::position{}); 147 | 148 | auto ent3 = ent_db.create_entity(); 149 | ent_db.add_component(ent1, component::position{}); 150 | ent_db.add_component(ent1, component::velocity{}); 151 | 152 | Now, if we run this visitor function: 153 | 154 | .. code-block:: cpp 155 | 156 | ent_db.visit([](const component::velocity& vel, component::position& pos) { 157 | pos.x += vel.x; 158 | pos.y += vel.y; 159 | }); 160 | 161 | Since ``component::velocity`` is the first component parameter, it will be the primary component. 162 | 163 | Therefore, only ``ent3`` will be considered for the visitor. Entities ``ent1`` and ``ent2`` will not even be considered. 164 | 165 | This can be a huge optimization in the case where you have many entities, but a specific component type will be used by only a few. 166 | 167 | An extreme example would be if your game has thousands of entities, but only one entity has the ``component::player`` component. 168 | A visitor function which uses ``component::player`` as its primary component would immediately visit the player entity, and no other entities would even be examined. 169 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.0) 2 | project(GinsengExamples) 3 | 4 | function(ginseng_add_example example) 5 | add_executable(ginseng_${example} ${CMAKE_CURRENT_SOURCE_DIR}/${example}.cpp) 6 | set_property(TARGET ginseng_${example} PROPERTY CXX_STANDARD 17) 7 | target_link_libraries(ginseng_${example} ginseng) 8 | endfunction() 9 | 10 | ginseng_add_example(enemy_game) 11 | ginseng_add_example(everything) 12 | ginseng_add_example(position_velocity) 13 | -------------------------------------------------------------------------------- /examples/enemy_game.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | using ginseng::database; 7 | using ginseng::tag; 8 | using ginseng::deny; 9 | 10 | // Components can be any value type. 11 | 12 | struct NameCom { 13 | std::string name; 14 | }; 15 | 16 | struct PositionCom { 17 | double x; 18 | double y; 19 | }; 20 | 21 | // Tag components will not contain a value (no allocation). 22 | using IsEnemyTag = tag; 23 | 24 | struct Game { 25 | database db; // Databases are value types. 26 | 27 | Game() { 28 | // db.create_entity() returns an entity ID. 29 | auto player = db.create_entity(); 30 | 31 | // db.add_component() emplaces the given component into the entity. 32 | db.add_component(player, NameCom{"The Player"}); 33 | db.add_component(player, PositionCom{12, 42}); 34 | 35 | auto enemy = db.create_entity(); 36 | db.add_component(enemy, NameCom{"An Enemy"}); 37 | db.add_component(enemy, PositionCom{7, 53}); 38 | db.add_component(enemy, IsEnemyTag{}); 39 | } 40 | 41 | void run_game() { 42 | // db.visit() automatically detects visitor parameters. 43 | db.visit([](const NameCom& name, const PositionCom& pos){ 44 | std::cout << "Entity " << name.name 45 | << " is at (" << pos.x << "," << pos.y << ")." 46 | << std::endl; 47 | }); 48 | 49 | // The deny<> annotation can be used to skip unwanted entities. 50 | db.visit([](const NameCom& name, deny){ 51 | std::cout << name.name << " is not an enemy." << std::endl; 52 | }); 53 | } 54 | }; 55 | 56 | int main() { 57 | Game g; 58 | g.run_game(); 59 | } 60 | -------------------------------------------------------------------------------- /examples/everything.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Common type aliases 4 | 5 | using ginseng::database; 6 | using ent_id = database::ent_id; 7 | using ginseng::tag; 8 | 9 | // Standard component 10 | 11 | struct posvel { 12 | float x = 0; 13 | float y = 0; 14 | float vx = 0; 15 | float vy = 0; 16 | }; 17 | 18 | // Tag components 19 | 20 | using wall_hit = tag; 21 | using no_hit = tag; 22 | 23 | // Position update system 24 | 25 | void update_positions(database& db) { 26 | db.visit([&](ent_id eid, posvel& pv) { 27 | pv.x += pv.vx; 28 | pv.y += pv.vy; 29 | 30 | if (pv.x > 100 || pv.x < -100 || pv.y > 100 || pv.y < -100) { 31 | db.add_component(eid, wall_hit{}); 32 | } 33 | }); 34 | } 35 | 36 | // No hit tag system 37 | 38 | void update_no_hits(database& db) { 39 | db.visit([&](ent_id eid, ginseng::deny) { 40 | db.add_component(eid, no_hit{}); 41 | }); 42 | } 43 | 44 | // Remove wall hitters system 45 | 46 | void update_wall_hits(database& db) { 47 | db.visit([&](ent_id eid, wall_hit) { 48 | db.destroy_entity(eid); 49 | }); 50 | } 51 | 52 | int main() { 53 | auto db = ginseng::database{}; 54 | 55 | auto make_ent = [&db]{ 56 | auto ent = db.create_entity(); 57 | db.add_component(ent, posvel{}); 58 | }; 59 | 60 | for (int i = 0; i < 10; ++i) { 61 | make_ent(); 62 | } 63 | 64 | update_positions(db); 65 | update_no_hits(db); 66 | update_wall_hits(db); 67 | } 68 | -------------------------------------------------------------------------------- /examples/position_velocity.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct position { 4 | float x = 0; 5 | float y = 0; 6 | }; 7 | 8 | struct velocity { 9 | float x = 0; 10 | float y = 0; 11 | }; 12 | 13 | int main() { 14 | auto db = ginseng::database{}; 15 | 16 | auto make_ent = [&db]{ 17 | auto ent = db.create_entity(); 18 | db.add_component(ent, position{}); 19 | db.add_component(ent, velocity{}); 20 | }; 21 | 22 | make_ent(); 23 | make_ent(); 24 | make_ent(); 25 | 26 | db.visit([&](position& pos, const velocity& vel) { 27 | pos.x += vel.x; 28 | pos.y += vel.y; 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /examples/rendering.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct position { 4 | float x = 0; 5 | float y = 0; 6 | }; 7 | 8 | struct sprite { 9 | std::string texture; 10 | }; 11 | 12 | void do_render(ginseng::database& render) { 13 | // sprite is the primary component 14 | db.visit([&](const sprite& spr, const position& pos) { 15 | draw_sprite(pos.x, pos.y, spr.texture); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /include/ginseng/ginseng.hpp: -------------------------------------------------------------------------------- 1 | #ifndef GINSENG_GINSENG_HPP 2 | #define GINSENG_GINSENG_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | namespace ginseng { 13 | 14 | namespace _detail { 15 | 16 | // Find Type 17 | 18 | template 19 | struct index_of; 20 | 21 | template 22 | constexpr auto index_of_v = index_of::value; 23 | 24 | template 25 | struct index_of : std::integral_constant {}; 26 | 27 | template 28 | struct index_of : std::integral_constant> {}; 29 | 30 | // Type Guid 31 | 32 | using type_guid = std::size_t; 33 | 34 | inline type_guid get_next_type_guid() noexcept { 35 | static type_guid x = 0; 36 | return ++x; 37 | } 38 | 39 | template 40 | type_guid get_type_guid() { 41 | static const type_guid my_guid = get_next_type_guid(); 42 | return my_guid; 43 | } 44 | 45 | // Dynamic Bitset 46 | 47 | class dynamic_bitset { 48 | public: 49 | using size_type = std::size_t; 50 | 51 | static constexpr size_type word_size = 64; 52 | 53 | using bitset = std::bitset; 54 | using bitset_array = bitset*; 55 | 56 | dynamic_bitset() 57 | : sdo(0), numbits(word_size) {} 58 | 59 | dynamic_bitset(const dynamic_bitset&) = delete; 60 | dynamic_bitset& operator=(const dynamic_bitset&) = delete; 61 | 62 | dynamic_bitset(dynamic_bitset&& other) { 63 | if (other.using_sdo()) { 64 | new (&sdo) bitset(std::move(other.sdo)); 65 | numbits = other.numbits; 66 | } else { 67 | dyna = other.dyna; 68 | numbits = other.numbits; 69 | other.numbits = word_size; 70 | new (&other.sdo) bitset(0); 71 | } 72 | } 73 | 74 | dynamic_bitset& operator=(dynamic_bitset&& other) { 75 | if (using_sdo()) { 76 | sdo.~bitset(); 77 | } else { 78 | delete[] dyna; 79 | } 80 | if (other.using_sdo()) { 81 | new (&sdo) bitset(std::move(other.sdo)); 82 | } else { 83 | dyna = other.dyna; 84 | } 85 | numbits = other.numbits; 86 | new (&other.sdo) bitset(0); 87 | other.numbits = word_size; 88 | return *this; 89 | } 90 | 91 | ~dynamic_bitset() { 92 | if (using_sdo()) { 93 | sdo.~bitset(); 94 | } else { 95 | delete[] dyna; 96 | } 97 | } 98 | 99 | size_type size() const { 100 | return numbits; 101 | } 102 | 103 | bool using_sdo() const { 104 | return numbits == word_size; 105 | } 106 | 107 | void resize(size_type i) { 108 | if (i > numbits) { 109 | auto count = (i + word_size - 1) / word_size; 110 | auto newptr = new bitset[count]; 111 | auto newlen = count * word_size; 112 | auto bitarr = using_sdo() ? &sdo : dyna; 113 | std::copy(bitarr, bitarr + (numbits / word_size), newptr); 114 | if (using_sdo()) { 115 | sdo.~bitset(); 116 | } else { 117 | delete[] dyna; 118 | } 119 | dyna = newptr; 120 | numbits = newlen; 121 | } 122 | } 123 | 124 | bool get(size_type i) const { 125 | if (i >= numbits) return false; 126 | const auto& bits = using_sdo() ? sdo : dyna[i / word_size]; 127 | return bits[i % word_size]; 128 | } 129 | 130 | void set(size_type i) { 131 | if (using_sdo() && i < word_size) { 132 | sdo[i] = true; 133 | } else { 134 | resize(i + 1); 135 | auto& bits = using_sdo() ? sdo : dyna[i / word_size]; 136 | bits[i % word_size] = true; 137 | } 138 | } 139 | 140 | void unset(size_type i) { 141 | if (i < numbits) { 142 | auto& bits = using_sdo() ? sdo : dyna[i / word_size]; 143 | bits[i % word_size] = false; 144 | } 145 | } 146 | 147 | void zero() { 148 | auto bitarr = using_sdo() ? &sdo : dyna; 149 | std::fill(bitarr, bitarr + numbits / word_size, 0); 150 | } 151 | 152 | private: 153 | union { 154 | bitset sdo; 155 | bitset_array dyna; 156 | }; 157 | size_type numbits; 158 | }; 159 | 160 | // Entity 161 | 162 | struct entity { 163 | using version_type = std::size_t; 164 | dynamic_bitset components = {}; 165 | version_type version = 0; 166 | }; 167 | 168 | // False Type 169 | 170 | template 171 | struct false_t : std::false_type {}; 172 | 173 | // Queries 174 | 175 | /*! Tag component 176 | * 177 | * When a tag component is stored in an entity, only the fact that it exists is recorded, no data is stored. 178 | */ 179 | template 180 | struct tag {}; 181 | 182 | /*! Require component 183 | * 184 | * When used as a visitor parameter, applies the matching logic for the parameter, but does not load the component. 185 | */ 186 | template 187 | struct require {}; 188 | 189 | /*! Deny component 190 | * 191 | * When used as a visitor parameter, entities that have the component will fail to match. 192 | */ 193 | template 194 | struct deny {}; 195 | 196 | /*! Optional component 197 | * 198 | * When used as a visitor parameter, loads the component if it exists, and does not cause matching to fail. 199 | * 200 | * Provides pointer-like access to the parameter. 201 | */ 202 | template 203 | class optional { 204 | public: 205 | optional() 206 | : com(nullptr) {} 207 | explicit optional(T& c) 208 | : com(&c) {} 209 | optional(const optional&) = default; 210 | optional(optional&&) = default; 211 | optional& operator=(const optional&) = default; 212 | optional& operator=(optional&&) = default; 213 | T* operator->() const { 214 | return com; 215 | } 216 | T& operator*() const { 217 | return *com; 218 | } 219 | explicit operator bool() const { 220 | return com; 221 | } 222 | T& get() const { 223 | return *com; 224 | } 225 | 226 | private: 227 | T* com; 228 | }; 229 | 230 | /*! Optional Tag component 231 | * 232 | * When used as a visitor parameter, checks if the tag exists, but does not cause matching to fail. 233 | */ 234 | template 235 | class optional> { 236 | public: 237 | optional() 238 | : tag(false) {} 239 | explicit optional(bool t) 240 | : tag(t) {} 241 | optional(const optional&) = default; 242 | optional(optional&&) = default; 243 | optional& operator=(const optional&) = default; 244 | optional& operator=(optional&&) = default; 245 | explicit operator bool() const { 246 | return tag; 247 | } 248 | 249 | private: 250 | bool tag; 251 | }; 252 | 253 | template 254 | class optional> { 255 | public: 256 | static_assert(false_t::value, "Optional require parameters not allowed."); 257 | }; 258 | 259 | template 260 | class optional> { 261 | public: 262 | static_assert(false_t::value, "Optional deny parameters not allowed."); 263 | }; 264 | 265 | template 266 | class optional> { 267 | public: 268 | static_assert(false_t::value, "Optional optional parameters not allowed."); 269 | }; 270 | 271 | // Component Tags 272 | 273 | namespace component_tags { 274 | 275 | struct unit {}; 276 | struct positive {}; 277 | struct normal : positive {}; 278 | struct noload : positive, unit {}; 279 | struct tagged : positive, unit {}; 280 | struct meta {}; 281 | struct optional : meta {}; 282 | struct eid : meta {}; 283 | struct inverted : noload {}; 284 | 285 | } // namespace component_tags 286 | 287 | // Component Traits 288 | 289 | template 290 | struct component_traits { 291 | using category = component_tags::normal; 292 | using component = Component; 293 | }; 294 | 295 | template 296 | struct component_traits> { 297 | using category = component_tags::noload; 298 | using component = Component; 299 | }; 300 | 301 | template 302 | struct component_traits> { 303 | using category = component_tags::tagged; 304 | using component = tag; 305 | }; 306 | 307 | template 308 | struct component_traits> { 309 | using category = component_tags::optional; 310 | using component = Component; 311 | }; 312 | 313 | template 314 | struct component_traits> { 315 | using category = component_tags::inverted; 316 | using component = Component; 317 | }; 318 | 319 | template 320 | struct component_traits { 321 | using category = component_tags::eid; 322 | using component = void; 323 | }; 324 | 325 | // First 326 | 327 | template 328 | struct first { 329 | using type = T; 330 | }; 331 | 332 | template 333 | using first_t = typename first::type; 334 | 335 | // Primary 336 | 337 | template 338 | struct primary { 339 | using type = T; 340 | }; 341 | 342 | // GetPrimary 343 | 344 | template 345 | struct get_primary; 346 | 347 | template 348 | using get_primary_t = typename get_primary::type; 349 | 350 | template 351 | struct get_primary { 352 | using category = typename component_traits::category; 353 | using type = std::conditional_t, primary, get_primary_t>; 354 | }; 355 | 356 | template 357 | struct get_primary { 358 | using type = primary; 359 | }; 360 | 361 | // Database Traits 362 | 363 | template 364 | struct database_traits { 365 | 366 | using ent_id = typename DB::ent_id; 367 | using com_id = typename DB::com_id; 368 | 369 | template 370 | using component_traits = component_traits; 371 | 372 | template 373 | using get_primary_t = get_primary_t; 374 | 375 | // VisitorKey 376 | 377 | template 378 | class visitor_key; 379 | 380 | template 381 | class visitor_key, Coms...> { 382 | public: 383 | template 384 | using tag_t = typename component_traits::category; 385 | 386 | template 387 | using com_t = typename component_traits::component; 388 | 389 | bool check(DB& db, ent_id eid) const { 390 | return (check(db, eid, get_guid(index_of_v, com_t...>), tag_t{}) && ...); 391 | } 392 | 393 | type_guid get_guid(std::size_t i) const { 394 | if (i < sizeof...(Coms)) { 395 | return guids[i]; 396 | } else { 397 | return 0; 398 | } 399 | } 400 | 401 | private: 402 | template 403 | static bool check(DB& db, ent_id eid, type_guid guid, component_tags::positive) { 404 | using component = typename component_traits::component; 405 | if constexpr (std::is_same_v) { 406 | return true; 407 | } else { 408 | return db.template has_component(eid, guid); 409 | } 410 | } 411 | 412 | template 413 | static bool check(DB& db, ent_id eid, type_guid guid, component_tags::inverted) { 414 | using component = typename component_traits::component; 415 | if constexpr (std::is_same_v) { 416 | return false; 417 | } else { 418 | return !db.template has_component(eid, guid); 419 | } 420 | } 421 | 422 | template 423 | static bool check([[maybe_unused]] DB& db, [[maybe_unused]] ent_id eid, [[maybe_unused]] type_guid guid, component_tags::meta) { 424 | return true; 425 | } 426 | 427 | type_guid guids[sizeof...(Coms)] = {get_type_guid>()...}; 428 | }; 429 | 430 | // VisitorTraits 431 | 432 | template 433 | struct visitor_traits_impl { 434 | using ent_id = typename DB::ent_id; 435 | using com_id = typename DB::com_id; 436 | using primary_component = get_primary_t; 437 | 438 | template 439 | using tag_t = typename component_traits::category; 440 | 441 | template 442 | using com_t = typename component_traits::component; 443 | 444 | template 445 | type_guid get_guid() const { 446 | return key.get_guid(index_of_v, com_t...>); 447 | } 448 | 449 | template 450 | auto apply(DB& db, ent_id eid, com_id primary_cid, Visitor&& visitor) { 451 | if (key.check(db, eid)) { 452 | return std::forward(visitor)(get_com(tag_t{}, db, eid, primary_cid, get_guid(), primary_component{})...); 453 | } 454 | } 455 | 456 | private: 457 | template 458 | static Com& get_com(component_tags::normal, DB& db, const ent_id& eid, const com_id& primary_cid, type_guid guid, primary) { 459 | if constexpr (std::is_same_v) { 460 | return db.template get_component_by_id(primary_cid, guid); 461 | } else { 462 | return db.template get_component(eid, guid); 463 | } 464 | } 465 | 466 | template 467 | static Com get_com(component_tags::optional, DB& db, const ent_id& eid, const com_id& primary_cid, type_guid guid, primary) { 468 | using traits = component_traits; 469 | using inner_component = typename traits::component; 470 | using inner_traits = component_traits; 471 | using inner_category = typename inner_traits::category; 472 | return get_com_optional(inner_category{}, db, eid, primary_cid, guid, primary{}); 473 | } 474 | 475 | template 476 | static optional get_com_optional(component_tags::normal, DB& db, const ent_id& eid, const com_id& primary_cid, type_guid guid, primary) { 477 | if constexpr (std::is_same_v) { 478 | return db.template get_component_by_id(primary_cid, guid); 479 | } else { 480 | if (db.template has_component(eid)) { 481 | return optional(db.template get_component(eid, guid)); 482 | } else { 483 | return optional(); 484 | } 485 | } 486 | } 487 | 488 | template 489 | static optional get_com_optional(component_tags::tagged, DB& db, const ent_id& eid, [[maybe_unused]] const com_id& primary_cid, type_guid guid, primary) { 490 | return optional(db.template has_component(eid, guid)); 491 | } 492 | 493 | template 494 | static const ent_id& get_com(component_tags::eid, [[maybe_unused]] DB& db, const ent_id& eid, [[maybe_unused]] const com_id& primary_cid, [[maybe_unused]] type_guid guid, primary) { 495 | return eid; 496 | } 497 | 498 | template 499 | static Com get_com(component_tags::unit, [[maybe_unused]] DB& db, [[maybe_unused]] const ent_id& eid, [[maybe_unused]] const com_id& primary_cid, [[maybe_unused]] type_guid guid, primary) { 500 | return {}; 501 | } 502 | 503 | visitor_key key; 504 | }; 505 | 506 | template 507 | struct visitor_traits : visitor_traits::operator())> {}; 508 | 509 | template 510 | struct visitor_traits : visitor_traits_impl...> {}; 511 | 512 | template 513 | struct visitor_traits : visitor_traits_impl...> {}; 514 | 515 | template 516 | struct visitor_traits : visitor_traits_impl...> {}; 517 | 518 | template 519 | struct visitor_traits : visitor_traits_impl...> {}; 520 | 521 | template 522 | struct visitor_traits : visitor_traits_impl...> {}; 523 | 524 | template 525 | struct visitor_traits : visitor_traits_impl...> {}; 526 | }; 527 | 528 | // Component Set 529 | 530 | class component_set { 531 | public: 532 | using size_type = std::size_t; 533 | virtual ~component_set() = 0; 534 | virtual void remove(size_type entid) = 0; 535 | 536 | size_type get_count() const { 537 | return count; 538 | } 539 | 540 | protected: 541 | void set_count(size_type new_count) { 542 | count = new_count; 543 | } 544 | 545 | private: 546 | size_type count = 0; 547 | }; 548 | 549 | inline component_set::~component_set() = default; 550 | 551 | template 552 | class component_set_impl final : public component_set { 553 | public: 554 | virtual ~component_set_impl() override { 555 | for (auto i = size_type{0}, sz = capacity(); i < sz; ++i) { 556 | if (is_valid(i)) { 557 | auto bucket = get_bucket_index(i); 558 | auto rel_index = get_relative_index(i); 559 | buckets[bucket][rel_index].component.~T(); 560 | } 561 | } 562 | } 563 | 564 | size_type assign(size_type entid, T com) { 565 | if (entid >= entid_to_comid.size()) { 566 | entid_to_comid.resize((entid + 1) * 3 / 2); 567 | } 568 | 569 | auto index = free_head; 570 | auto bucket = get_bucket_index(index); 571 | auto rel_index = get_relative_index(index); 572 | storage* slot; 573 | 574 | if (index == back_index) { 575 | if (bucket == buckets.size()) { 576 | buckets.push_back(std::make_unique(bucket_size)); 577 | comid_to_entid.resize(comid_to_entid.size() + bucket_size, null_id); 578 | } 579 | 580 | slot = &buckets[bucket][rel_index]; 581 | ++back_index; 582 | free_head = back_index; 583 | } else { 584 | slot = &buckets[bucket][rel_index]; 585 | free_head = slot->next_free; 586 | } 587 | 588 | new (&slot->component) T(std::move(com)); 589 | entid_to_comid[entid] = index; 590 | comid_to_entid[index] = entid; 591 | 592 | set_count(get_count() + 1); 593 | 594 | return index; 595 | } 596 | 597 | virtual void remove(size_type entid) override final { 598 | auto index = entid_to_comid[entid]; 599 | auto bucket = get_bucket_index(index); 600 | auto rel_index = get_relative_index(index); 601 | auto& slot = buckets[bucket][rel_index]; 602 | 603 | slot.component.~T(); 604 | slot.next_free = free_head; 605 | free_head = index; 606 | comid_to_entid[index] = null_id; 607 | 608 | set_count(get_count() - 1); 609 | } 610 | 611 | bool is_valid(size_type comid) const { 612 | return comid_to_entid[comid] != null_id; 613 | } 614 | 615 | size_type get_comid(size_type entid) const { 616 | return entid_to_comid[entid]; 617 | } 618 | 619 | T& get_com(size_type comid) { 620 | auto bucket = get_bucket_index(comid); 621 | auto rel_index = get_relative_index(comid); 622 | auto& slot = buckets[bucket][rel_index]; 623 | return slot.component; 624 | } 625 | 626 | size_type get_entid(size_type comid) const { 627 | return comid_to_entid[comid]; 628 | } 629 | 630 | size_type capacity() const { 631 | return back_index; 632 | } 633 | 634 | private: 635 | union storage { 636 | size_type next_free; 637 | T component; 638 | 639 | storage() {} 640 | ~storage() {} 641 | }; 642 | 643 | std::vector entid_to_comid; 644 | std::vector comid_to_entid; 645 | std::vector> buckets; 646 | size_type free_head = 0; 647 | size_type back_index = 0; 648 | 649 | static constexpr size_type bucket_size = 4096 * 8; 650 | static constexpr size_type null_id = static_cast(-1); 651 | 652 | static size_type get_bucket_index(size_type idx) { 653 | return idx / bucket_size; 654 | } 655 | 656 | static size_type get_relative_index(size_type idx) { 657 | return idx % bucket_size; 658 | } 659 | 660 | static size_type get_total_size(size_type num_buckets) { 661 | return num_buckets * bucket_size; 662 | } 663 | }; 664 | 665 | template 666 | class component_set_impl> final : public component_set { 667 | public: 668 | virtual ~component_set_impl() = default; 669 | virtual void remove([[maybe_unused]] size_type entid) override final {} 670 | }; 671 | 672 | // Opaque index 673 | 674 | template 675 | class opaque_index { 676 | public: 677 | opaque_index() = default; 678 | 679 | bool operator==(const opaque_index& other) { 680 | return index == other.index; 681 | } 682 | 683 | Index get_index() const { 684 | return index; 685 | } 686 | 687 | private: 688 | friend Friend; 689 | 690 | opaque_index(Index i) 691 | : index(i) {} 692 | 693 | operator Index() const { 694 | return index; 695 | } 696 | 697 | opaque_index& operator++() { 698 | ++index; 699 | return *this; 700 | } 701 | 702 | Index index; 703 | }; 704 | 705 | /*! Database 706 | * 707 | * An Entity component Database. Uses the given allocator to allocate 708 | * components, and may also use the same allocator for internal data. 709 | * 710 | * @warning 711 | * This container does not perform any synchronization. Therefore, it is not 712 | * considered "thread-safe". 713 | */ 714 | class database { 715 | public: 716 | // IDs 717 | 718 | /*! Entity ID. 719 | */ 720 | class ent_id { 721 | public: 722 | friend class database; 723 | using index_type = std::vector::size_type; 724 | using version_type = entity::version_type; 725 | 726 | bool operator==(const ent_id& other) const { 727 | return index == other.index && version == other.version; 728 | } 729 | 730 | index_type get_index() const { 731 | return index; 732 | } 733 | 734 | private: 735 | ent_id(index_type i, version_type v) 736 | : index(i), version(v) {} 737 | 738 | index_type index = 0; 739 | version_type version = 0; 740 | }; 741 | 742 | /*! Component ID. 743 | */ 744 | using com_id = opaque_index; 745 | 746 | /*! Creates a new Entity. 747 | * 748 | * Creates a new Entity that has no components. 749 | * 750 | * @return ID of the new Entity. 751 | */ 752 | ent_id create_entity() { 753 | ent_id::index_type index; 754 | 755 | if (free_entities.empty()) { 756 | index = entities.size(); 757 | entities.emplace_back(); 758 | } else { 759 | index = free_entities.back(); 760 | free_entities.pop_back(); 761 | } 762 | 763 | entities[index].components.set(0); 764 | 765 | return {index, entities[index].version}; 766 | } 767 | 768 | /*! Destroys an Entity. 769 | * 770 | * Destroys the given Entity and all associated components. 771 | * 772 | * If the Entity does not exist, no work is done. 773 | * 774 | * @param eid ID of the Entity to erase. 775 | */ 776 | void destroy_entity(const ent_id& eid) { 777 | const auto index = eid.get_index(); 778 | 779 | if (entities[index].version != eid.version) { 780 | return; 781 | } 782 | 783 | for (dynamic_bitset::size_type i = 1; i < entities[index].components.size(); ++i) { 784 | if (entities[index].components.get(i)) { 785 | component_sets[i]->remove(index); 786 | } 787 | } 788 | 789 | entities[index].components.zero(); 790 | ++entities[index].version; 791 | free_entities.push_back(index); 792 | } 793 | 794 | /*! Determines whether or not an entity exists. 795 | * 796 | * @param eid ID of the Entity to check. 797 | */ 798 | bool exists(const ent_id& eid) const { 799 | return entities[eid.index].version == eid.version && entities[eid.index].components.get(0); 800 | } 801 | 802 | /*! Adds a component to an entity. 803 | * 804 | * If a component of the same type already exists for this entity, 805 | * the given component will be forward-assigned to it. 806 | * 807 | * Otherwise, moves or copies the given component and adds it to the entity. 808 | * 809 | * @param eid Entity to attach new component to. 810 | * @param com Component value. 811 | * @return ID of component. 812 | */ 813 | template 814 | com_id add_component(const ent_id& eid, T&& com) { 815 | using com_type = std::decay_t; 816 | auto index = eid.get_index(); 817 | auto guid = get_type_guid(); 818 | auto& ent_coms = entities[index].components; 819 | auto& com_set = get_or_create_com_set(); 820 | 821 | com_id cid; 822 | 823 | if (guid < ent_coms.size() && ent_coms.get(guid)) { 824 | cid = com_set.get_comid(index); 825 | com_set.get_com(cid) = std::forward(com); 826 | } else { 827 | cid = com_set.assign(index, std::forward(com)); 828 | ent_coms.set(guid); 829 | } 830 | 831 | return cid; 832 | } 833 | 834 | /*! Create new Tag component. 835 | * 836 | * Adds the tag to the entity if it does not already exist. 837 | * 838 | * @param eid Entity to attach new Tag component to. 839 | * @param com Tag value. 840 | */ 841 | template 842 | void add_component(ent_id eid, tag) { 843 | auto index = eid.get_index(); 844 | auto guid = get_type_guid>(); 845 | auto& ent_coms = entities[index].components; 846 | 847 | get_or_create_com_set>(); 848 | 849 | ent_coms.set(guid); 850 | } 851 | 852 | template 853 | void add_component(ent_id eid, require com) = delete; 854 | 855 | template 856 | void add_component(ent_id eid, deny com) = delete; 857 | 858 | template 859 | void add_component(ent_id eid, optional com) = delete; 860 | 861 | template 862 | void add_component(ent_id eid, ent_id com) = delete; 863 | 864 | /*! Remove a component from an entity. 865 | * 866 | * Removes the component from the entity and destroys it. 867 | * 868 | * If the entity does not exist, no work is done. 869 | * 870 | * @warning 871 | * All ComIDs associated with the removed component will be invalidated. 872 | * 873 | * @tparam Com Type of the component to erase. 874 | * 875 | * @param eid ID of the entity. 876 | */ 877 | template 878 | void remove_component(ent_id eid) { 879 | auto index = eid.get_index(); 880 | 881 | if (entities[index].version != eid.version) { 882 | return; 883 | } 884 | 885 | auto guid = get_type_guid(); 886 | auto& com_set = *get_com_set(); 887 | com_set.remove(index); 888 | entities[index].components.unset(guid); 889 | } 890 | 891 | /*! Get a component. 892 | * 893 | * If Com is a non-pointer type, returns a reference to the component without performing safe checks for existence. 894 | * 895 | * Otherwise, if Com is a pointer type, 896 | * returns a pointer to the component of the pointed-to type that is associated with the given entity. 897 | * 898 | * If the entity has no associated component of the given type, returns nullptr. 899 | * 900 | * If the entity does not exist, returns nullptr. 901 | * 902 | * @tparam Com Type of the component to get. 903 | * 904 | * @param eid ID of the entity. 905 | * @return Either a reference to the component, or a pointer to the component, or nullptr. 906 | */ 907 | template 908 | auto get_component(ent_id eid) -> std::conditional_t, Com, Com&> { 909 | auto index = eid.index; 910 | 911 | if constexpr (std::is_pointer_v) { 912 | using component_t = std::remove_pointer_t; 913 | 914 | if (entities[index].version != eid.version) { 915 | return nullptr; 916 | } 917 | 918 | auto guid = get_type_guid(); 919 | 920 | if (has_component(eid, guid)) { 921 | auto& com_set = *get_com_set(guid); 922 | auto cid = com_set.get_comid(index); 923 | return &com_set.get_com(cid); 924 | } else { 925 | return nullptr; 926 | } 927 | } else { 928 | auto& com_set = *get_com_set(); 929 | auto cid = com_set.get_comid(index); 930 | return com_set.get_com(cid); 931 | } 932 | } 933 | 934 | /*! Get a component by its ID. 935 | * 936 | * Gets a reference to the component of the given type that has the given ID. 937 | * 938 | * @warning 939 | * Behavior is undefined when no component is associated with the given ID. 940 | * 941 | * @tparam Com Type of the component to get. 942 | * 943 | * @param cid ID of the component. 944 | * @return Reference to the component. 945 | */ 946 | template 947 | Com& get_component_by_id(com_id cid) { 948 | auto& com_set = *get_com_set(); 949 | return com_set.get_com(cid); 950 | } 951 | 952 | /*! Checks if an entity has a component. 953 | * 954 | * Returns whether or not the entity has a component of the 955 | * associated type. 956 | * 957 | * If the entity does not exist, returns false. 958 | * 959 | * @tparam Com Type of the component to check. 960 | * 961 | * @param eid ID of the entity. 962 | * @return True if the component exists. 963 | */ 964 | template 965 | bool has_component(ent_id eid) { 966 | auto index = eid.get_index(); 967 | 968 | if (entities[index].version != eid.version) { 969 | return false; 970 | } 971 | 972 | return has_component(eid, get_type_guid()); 973 | } 974 | 975 | /*! Visit the Database. 976 | * 977 | * Visit the Entities in the Database that match the given function's parameter types. 978 | * 979 | * The following parameter categories are accepted: 980 | * 981 | * - `T`, matches entities that have component `T`, and loads the component. 982 | * - `tag`, matches entities that have component `tag`. 983 | * - `require`, matches entities that have component `T`, but does not load it. 984 | * - `optional`, matches all entities, loads component `T` if it exists. 985 | * - `deny`, matches entities that do *not* match component `T`. 986 | * - `ent_id`, matches all entities, provides the `ent_id` of the current entity. 987 | * 988 | * `T` and `optional` parameters will refer to the entity's matching component. 989 | * `ent_id` parameters will contain the entity's `ent_id`. 990 | * Other parameters will be their default value. 991 | * 992 | * Entities that do not match all given parameter conditions will be skipped. 993 | * 994 | * @warning Entities are visited in no particular order, so creating and destroying 995 | * entities or adding or removing components from within the visitor 996 | * could result in weird behavior. 997 | * 998 | * @tparam Visitor Visitor function type. 999 | * @param visitor Visitor function. 1000 | */ 1001 | template 1002 | void visit(Visitor&& visitor) { 1003 | using db_traits = database_traits; 1004 | using traits = typename db_traits::visitor_traits; 1005 | using primary_component = typename traits::primary_component; 1006 | 1007 | return visit_helper(std::forward(visitor), primary_component{}); 1008 | } 1009 | 1010 | /*! Get the number of entities in the Database. 1011 | * 1012 | * @return Number of entities in the Database. 1013 | */ 1014 | auto size() const { 1015 | return entities.size() - free_entities.size(); 1016 | } 1017 | 1018 | /*! Get the number of components of a certain type in the Database. 1019 | * 1020 | * @return Number of components of type Com. 1021 | */ 1022 | template 1023 | component_set::size_type count() const { 1024 | if (auto set = get_com_set()) { 1025 | return set->get_count(); 1026 | } else { 1027 | return 0; 1028 | } 1029 | } 1030 | 1031 | /*! Converts an ent_id to a void* for storage purposes. 1032 | * 1033 | * @warning This is not a valid pointer and relies on widespread compiler-specific behavior. 1034 | * Do not ever dereference the pointer. 1035 | * Entity version checking is not preserved through conversion to and from pointers. 1036 | * Null pointers are a valid result. 1037 | * 1038 | * @param eid Entity ID to convert to a pointer. 1039 | * @return A pointer which may be converted back into the same ID using from_ptr(ptr). 1040 | */ 1041 | auto to_ptr(const ent_id& eid) const -> void* { 1042 | static_assert(sizeof(void*) >= sizeof(ent_id::index_type), "Pointer conversion not possible"); 1043 | return reinterpret_cast(eid.get_index()); 1044 | } 1045 | 1046 | /*! Converts a void* to an ent_id. The pointer must have been returned from to_ptr(eid). 1047 | * 1048 | * @warning The entity must not have been deleted. Version checking is not applied. 1049 | * 1050 | * @param ptr Pointer which was obtained from to_ptr(eid). 1051 | * @return The original ent_id that was passed to to_ptr(eid). 1052 | */ 1053 | auto from_ptr(void* ptr) const -> ent_id { 1054 | auto i = reinterpret_cast(ptr); 1055 | return ent_id{i, entities[i].version}; 1056 | } 1057 | 1058 | private: 1059 | friend struct database_traits; 1060 | 1061 | template 1062 | Com& get_component(ent_id eid, type_guid guid) { 1063 | auto& com_set = *unsafe_get_com_set(guid); 1064 | auto cid = com_set.get_comid(eid.get_index()); 1065 | return com_set.get_com(cid); 1066 | } 1067 | 1068 | template 1069 | Com& get_component_by_id(com_id cid, type_guid guid) { 1070 | auto& com_set = *unsafe_get_com_set(guid); 1071 | return com_set.get_com(cid); 1072 | } 1073 | 1074 | template 1075 | bool has_component(ent_id eid, type_guid guid) { 1076 | auto& ent_coms = entities[eid.get_index()].components; 1077 | return ent_coms.get(guid); 1078 | } 1079 | 1080 | template 1081 | component_set_impl* get_com_set() { 1082 | return get_com_set(get_type_guid()); 1083 | } 1084 | 1085 | template 1086 | const component_set_impl* get_com_set() const { 1087 | return get_com_set(get_type_guid()); 1088 | } 1089 | 1090 | template 1091 | component_set_impl* get_com_set(type_guid guid) { 1092 | if (guid >= component_sets.size()) { 1093 | return nullptr; 1094 | } 1095 | return unsafe_get_com_set(guid); 1096 | } 1097 | 1098 | template 1099 | const component_set_impl* get_com_set(type_guid guid) const { 1100 | if (guid >= component_sets.size()) { 1101 | return nullptr; 1102 | } 1103 | return unsafe_get_com_set(guid); 1104 | } 1105 | 1106 | template 1107 | component_set_impl* unsafe_get_com_set(type_guid guid) { 1108 | auto& com_set = component_sets[guid]; 1109 | auto com_set_impl = static_cast*>(com_set.get()); 1110 | return com_set_impl; 1111 | } 1112 | 1113 | template 1114 | const component_set_impl* unsafe_get_com_set(type_guid guid) const { 1115 | auto& com_set = component_sets[guid]; 1116 | auto com_set_impl = static_cast*>(com_set.get()); 1117 | return com_set_impl; 1118 | } 1119 | 1120 | template 1121 | component_set_impl& get_or_create_com_set() { 1122 | auto guid = get_type_guid(); 1123 | if (component_sets.size() <= guid) { 1124 | component_sets.resize(guid + 1); 1125 | } 1126 | auto& com_set = component_sets[guid]; 1127 | if (!com_set) { 1128 | com_set = std::make_unique>(); 1129 | } 1130 | auto com_set_impl = static_cast*>(com_set.get()); 1131 | return *com_set_impl; 1132 | } 1133 | 1134 | template 1135 | void visit_helper(Visitor&& visitor, primary) { 1136 | using db_traits = database_traits; 1137 | using visitor_traits = typename db_traits::visitor_traits; 1138 | 1139 | auto traits = visitor_traits{}; 1140 | 1141 | if (auto com_set_ptr = get_com_set(traits.template get_guid())) { 1142 | auto& com_set = *com_set_ptr; 1143 | 1144 | for (com_id cid = 0, sz = com_set.capacity(); cid < sz; ++cid) { 1145 | if (com_set.is_valid(cid)) { 1146 | auto i = com_set.get_entid(cid); 1147 | traits.apply(*this, {i, entities[i].version}, cid, visitor); 1148 | } 1149 | } 1150 | } 1151 | } 1152 | 1153 | template 1154 | void visit_helper(Visitor&& visitor, primary) { 1155 | using db_traits = database_traits; 1156 | using visitor_traits = typename db_traits::visitor_traits; 1157 | 1158 | auto traits = visitor_traits{}; 1159 | 1160 | for (auto i = 0u; i < entities.size(); ++i) { 1161 | if (entities[i].components.get(0)) { 1162 | traits.apply(*this, {i, entities[i].version}, {}, visitor); 1163 | } 1164 | } 1165 | } 1166 | 1167 | std::vector entities; 1168 | std::vector free_entities; 1169 | std::vector> component_sets; 1170 | }; 1171 | 1172 | } // namespace _detail 1173 | 1174 | using _detail::database; 1175 | using _detail::require; 1176 | using _detail::optional; 1177 | using _detail::deny; 1178 | using _detail::tag; 1179 | 1180 | } // namespace Ginseng 1181 | 1182 | #endif // GINSENG_GINSENG_HPP 1183 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include "catch.hpp" 3 | -------------------------------------------------------------------------------- /src/test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "catch.hpp" 7 | 8 | using DB = ginseng::database; 9 | using ginseng::deny; 10 | using ginseng::optional; 11 | using ent_id = DB::ent_id; 12 | using com_id = DB::com_id; 13 | 14 | TEST_CASE("Entities can be added and removed from a database", "[ginseng]") 15 | { 16 | DB db; 17 | REQUIRE(db.size() == 0); 18 | 19 | ent_id ent1 = db.create_entity(); 20 | REQUIRE(db.size() == 1); 21 | 22 | ent_id ent2 = db.create_entity(); 23 | REQUIRE(db.size() == 2); 24 | 25 | db.destroy_entity(ent2); 26 | REQUIRE(db.size() == 1); 27 | 28 | db.destroy_entity(ent1); 29 | REQUIRE(db.size() == 0); 30 | } 31 | 32 | TEST_CASE("Components can be added, accessed, and removed from entities", "[ginseng]") 33 | { 34 | DB db; 35 | auto ent = db.create_entity(); 36 | 37 | struct ComA { 38 | int x; 39 | }; 40 | struct ComB { 41 | double y; 42 | }; 43 | 44 | db.add_component(ent, ComA{7}); 45 | REQUIRE(db.has_component(ent) == true); 46 | ComA *com1ptr1 = &db.get_component(ent); 47 | REQUIRE(com1ptr1 != nullptr); 48 | 49 | db.add_component(ent, ComB{4.2}); 50 | REQUIRE(db.has_component(ent) == true); 51 | ComB *com2ptr1 = &db.get_component(ent); 52 | REQUIRE(com2ptr1 != nullptr); 53 | 54 | REQUIRE(&db.get_component(ent) == com1ptr1); 55 | REQUIRE(db.get_component(ent).x == 7); 56 | 57 | REQUIRE(&db.get_component(ent) == com2ptr1); 58 | REQUIRE(db.get_component(ent).y == 4.2); 59 | 60 | db.remove_component(ent); 61 | REQUIRE(db.has_component(ent) == false); 62 | REQUIRE(db.has_component(ent) == true); 63 | 64 | REQUIRE(&db.get_component(ent) == com2ptr1); 65 | REQUIRE(db.get_component(ent).y == 4.2); 66 | 67 | db.remove_component(ent); 68 | REQUIRE(db.has_component(ent) == false); 69 | REQUIRE(db.has_component(ent) == false); 70 | } 71 | 72 | TEST_CASE("get_optional works", "[ginseng]") 73 | { 74 | DB db; 75 | auto ent = db.create_entity(); 76 | 77 | struct ComA { 78 | int x; 79 | }; 80 | 81 | db.add_component(ent, ComA{2}); 82 | 83 | REQUIRE(db.get_component(ent) == &db.get_component(ent)); 84 | 85 | db.remove_component(ent); 86 | 87 | REQUIRE(db.get_component(ent) == nullptr); 88 | } 89 | 90 | TEST_CASE("Databases can visit entities with specific components", "[ginseng]") 91 | { 92 | DB db; 93 | 94 | struct ID { int id; }; 95 | struct Data1 { double val; }; 96 | struct Data2 { std::unique_ptr no_move; }; 97 | 98 | int next_id = 0; 99 | 100 | auto make_ent = [&](bool give_Data1, bool give_Data2) 101 | { 102 | auto ent = db.create_entity(); 103 | db.add_component(ent, ID{next_id}); 104 | ++next_id; 105 | if (give_Data1) { db.add_component(ent, Data1{7}); } 106 | if (give_Data2) { db.add_component(ent, Data2{nullptr}); } 107 | return ent; 108 | }; 109 | 110 | make_ent(false, false); 111 | make_ent(true, false); 112 | make_ent(true, false); 113 | make_ent(false, true); 114 | make_ent(false, true); 115 | make_ent(false, true); 116 | make_ent(true, true); 117 | make_ent(true, true); 118 | make_ent(true, true); 119 | make_ent(true, true); 120 | 121 | REQUIRE(next_id == 10); 122 | REQUIRE(db.size() == next_id); 123 | 124 | std::array visited; 125 | std::array expected_visited; 126 | 127 | visited = {}; 128 | expected_visited = {{1,1,1,1,1,1,1,1,1,1}}; 129 | db.visit([&](ID& id){ 130 | ++visited[id.id]; 131 | }); 132 | REQUIRE(visited == expected_visited); 133 | 134 | visited = {}; 135 | expected_visited = {{0,1,1,0,0,0,1,1,1,1}}; 136 | db.visit([&](ID& id, Data1&){ 137 | ++visited[id.id]; 138 | }); 139 | REQUIRE(visited == expected_visited); 140 | 141 | visited = {}; 142 | expected_visited = {{0,0,0,1,1,1,1,1,1,1}}; 143 | db.visit([&](ID& id, Data2&){ 144 | ++visited[id.id]; 145 | }); 146 | REQUIRE(visited == expected_visited); 147 | 148 | visited = {}; 149 | expected_visited = {{0,0,0,0,0,0,1,1,1,1}}; 150 | db.visit([&](ID& id, Data1&, Data2&){ 151 | ++visited[id.id]; 152 | }); 153 | REQUIRE(visited == expected_visited); 154 | 155 | visited = {}; 156 | expected_visited = {{1,0,0,1,1,1,0,0,0,0}}; 157 | db.visit([&](ID& id, deny){ 158 | ++visited[id.id]; 159 | }); 160 | REQUIRE(visited == expected_visited); 161 | 162 | visited = {}; 163 | expected_visited = {{0,0,0,1,1,1,0,0,0,0}}; 164 | db.visit([&](ID& id, deny, Data2&){ 165 | ++visited[id.id]; 166 | }); 167 | REQUIRE(visited == expected_visited); 168 | 169 | visited = {}; 170 | expected_visited = {{0,1,1,0,0,0,0,0,0,0}}; 171 | db.visit([&](ID& id, Data1&, deny){ 172 | ++visited[id.id]; 173 | }); 174 | REQUIRE(visited == expected_visited); 175 | 176 | visited = {}; 177 | expected_visited = {{1,0,0,0,0,0,0,0,0,0}}; 178 | db.visit([&](ID& id, deny, deny){ 179 | ++visited[id.id]; 180 | }); 181 | REQUIRE(visited == expected_visited); 182 | 183 | int num_visited = 0; 184 | db.visit([&](deny){ 185 | ++num_visited; 186 | }); 187 | REQUIRE(num_visited == 0); 188 | } 189 | 190 | TEST_CASE("ent_id matches all entities and is the entity's id", "[ginseng]") 191 | { 192 | DB db; 193 | 194 | db.create_entity(); 195 | db.create_entity(); 196 | db.create_entity(); 197 | 198 | int visited[] = {0, 0, 0}; 199 | 200 | db.visit([&](DB::ent_id eid){ 201 | ++visited[eid.get_index()]; 202 | }); 203 | 204 | REQUIRE(visited[0] == 1); 205 | REQUIRE(visited[1] == 1); 206 | REQUIRE(visited[2] == 1); 207 | } 208 | 209 | TEST_CASE("optional can be used instead of components", "[ginseng]") 210 | { 211 | DB db; 212 | 213 | struct Data {}; 214 | struct Data2 {}; 215 | 216 | auto ent = db.create_entity(); 217 | db.add_component(ent,Data{}); 218 | 219 | int visited = 0; 220 | optional mdata; 221 | optional mdata2; 222 | db.visit([&](optional data, optional data2){ 223 | ++visited; 224 | mdata = data; 225 | mdata2 = data2; 226 | }); 227 | REQUIRE(visited == 1); 228 | REQUIRE(bool(mdata) == true); 229 | REQUIRE(bool(mdata2) == false); 230 | } 231 | 232 | TEST_CASE("deleted entites are not revisited", "[ginseng]") 233 | { 234 | DB db; 235 | 236 | struct Data {}; 237 | 238 | auto ent = db.create_entity(); 239 | db.create_entity(); 240 | db.create_entity(); 241 | 242 | int visited = 0; 243 | db.visit([&](ent_id eid){ 244 | ++visited; 245 | db.add_component(eid, Data{}); 246 | }); 247 | REQUIRE(visited == 3); 248 | 249 | visited = 0; 250 | db.visit([&](ent_id, Data&){ 251 | ++visited; 252 | }); 253 | REQUIRE(visited == 3); 254 | 255 | db.destroy_entity(ent); 256 | 257 | visited = 0; 258 | db.visit([&](ent_id){ 259 | ++visited; 260 | }); 261 | REQUIRE(visited == 2); 262 | 263 | visited = 0; 264 | db.visit([&](ent_id, Data&){ 265 | ++visited; 266 | }); 267 | REQUIRE(visited == 2); 268 | } 269 | 270 | TEST_CASE("versioning works", "[ginseng]") 271 | { 272 | DB db; 273 | 274 | auto ent1 = db.create_entity(); 275 | auto ent2 = db.create_entity(); 276 | auto ent3 = db.create_entity(); 277 | 278 | REQUIRE(db.exists(ent1)); 279 | REQUIRE(db.exists(ent2)); 280 | REQUIRE(db.exists(ent3)); 281 | 282 | db.destroy_entity(ent2); 283 | 284 | REQUIRE(db.exists(ent1)); 285 | REQUIRE(!db.exists(ent2)); 286 | REQUIRE(db.exists(ent3)); 287 | 288 | db.destroy_entity(ent1); 289 | 290 | REQUIRE(!db.exists(ent1)); 291 | REQUIRE(!db.exists(ent2)); 292 | REQUIRE(db.exists(ent3)); 293 | 294 | db.destroy_entity(ent3); 295 | 296 | REQUIRE(!db.exists(ent1)); 297 | REQUIRE(!db.exists(ent2)); 298 | REQUIRE(!db.exists(ent3)); 299 | 300 | auto ent4 = db.create_entity(); 301 | 302 | REQUIRE(!(ent4 == ent1)); 303 | REQUIRE(!(ent4 == ent2)); 304 | REQUIRE(!(ent4 == ent3)); 305 | REQUIRE(!db.exists(ent1)); 306 | REQUIRE(!db.exists(ent2)); 307 | REQUIRE(!db.exists(ent3)); 308 | REQUIRE(db.exists(ent4)); 309 | } 310 | 311 | TEST_CASE("ent_ids can be copied", "[ginseng]") 312 | { 313 | struct Data { int x; }; 314 | 315 | DB db; 316 | 317 | auto ent1 = db.create_entity(); 318 | 319 | db.add_component(ent1, Data{42}); 320 | 321 | auto ent2 = ent1; 322 | 323 | REQUIRE(ent1 == ent2); 324 | 325 | auto& a = db.get_component(ent1); 326 | auto& b = db.get_component(ent2); 327 | 328 | REQUIRE(&a == &b); 329 | 330 | ent2 = ent1; 331 | 332 | REQUIRE(ent1 == ent2); 333 | 334 | auto& c = db.get_component(ent1); 335 | auto& d = db.get_component(ent2); 336 | 337 | REQUIRE(&a == &c); 338 | REQUIRE(&c == &d); 339 | } 340 | 341 | TEST_CASE("to_ptr and from_ptr work round-trip", "[ginseng]") 342 | { 343 | DB db; 344 | 345 | for (int i = 0; i < 10; ++i) { 346 | db.create_entity(); 347 | } 348 | 349 | auto ent = db.create_entity(); 350 | 351 | auto ptr = db.to_ptr(ent); 352 | 353 | REQUIRE(reinterpret_cast(ptr) == 10); 354 | 355 | auto ent2 = db.from_ptr(ptr); 356 | 357 | REQUIRE(ent == ent2); 358 | } 359 | -------------------------------------------------------------------------------- /src/test_bitset.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "catch.hpp" 4 | 5 | using dynamic_bitset = ginseng::_detail::dynamic_bitset; 6 | constexpr auto word_size = dynamic_bitset::word_size; 7 | 8 | TEST_CASE("dynamic_bitset is initialized to zero", "[dynamic_bitset]") { 9 | dynamic_bitset db; 10 | 11 | REQUIRE(db.size() == word_size); 12 | 13 | for (auto i = 0u; i < word_size; ++i) { 14 | REQUIRE(db.get(i) == false); 15 | } 16 | 17 | REQUIRE(db.size() == word_size); 18 | } 19 | 20 | TEST_CASE("setting an existing bit does not change size", "[dynamic_bitset]") { 21 | { 22 | dynamic_bitset db; 23 | 24 | REQUIRE(db.size() == word_size); 25 | 26 | for (auto i = 0u; i < word_size; ++i) { 27 | REQUIRE(db.get(i) == false); 28 | db.set(i); 29 | REQUIRE(db.get(i) == true); 30 | } 31 | 32 | REQUIRE(db.size() == word_size); 33 | } 34 | { 35 | dynamic_bitset db; 36 | 37 | db.resize(word_size * 2); 38 | 39 | REQUIRE(db.size() == word_size * 2); 40 | 41 | for (auto i = 0u; i < word_size * 2; ++i) { 42 | REQUIRE(db.get(i) == false); 43 | db.set(i); 44 | REQUIRE(db.get(i) == true); 45 | } 46 | 47 | REQUIRE(db.size() == word_size * 2); 48 | } 49 | } 50 | 51 | TEST_CASE("setting a bit past the end resizes to the next multiple of word size", "[dynamic_bitset]") { 52 | dynamic_bitset db; 53 | 54 | REQUIRE(db.size() == word_size); 55 | 56 | db.set(word_size); 57 | 58 | REQUIRE(db.size() == word_size * 2); 59 | } 60 | 61 | TEST_CASE("resize does not change existing bits", "[dynamic_bitset]") { 62 | dynamic_bitset db; 63 | 64 | db.set(0); 65 | 66 | REQUIRE(db.size() == word_size); 67 | 68 | db.set(word_size); 69 | 70 | REQUIRE(db.size() == word_size * 2); 71 | 72 | REQUIRE(db.get(0) == true); 73 | REQUIRE(db.get(word_size) == true); 74 | } 75 | 76 | TEST_CASE("unsetting a bit past the end has no effect", "[dynamic_bitset]") { 77 | dynamic_bitset db; 78 | 79 | db.set(0); 80 | db.set(word_size - 1); 81 | 82 | REQUIRE(db.size() == word_size); 83 | 84 | db.unset(word_size); 85 | 86 | REQUIRE(db.size() == word_size); 87 | REQUIRE(db.get(0) == true); 88 | REQUIRE(db.get(word_size - 1) == true); 89 | } 90 | 91 | TEST_CASE("zero works", "[dynamic_bitset]") { 92 | dynamic_bitset db; 93 | 94 | for (auto i = 0u; i < word_size; ++i) { 95 | db.set(i); 96 | } 97 | 98 | for (auto i = 0u; i < word_size; ++i) { 99 | REQUIRE(db.get(i) == true); 100 | } 101 | 102 | db.zero(); 103 | 104 | REQUIRE(db.size() == word_size); 105 | 106 | for (auto i = 0u; i < word_size; ++i) { 107 | REQUIRE(db.get(i) == false); 108 | } 109 | 110 | for (auto i = 0u; i < word_size * 2; ++i) { 111 | db.set(i); 112 | } 113 | 114 | for (auto i = 0u; i < word_size * 2; ++i) { 115 | REQUIRE(db.get(i) == true); 116 | } 117 | 118 | db.zero(); 119 | 120 | REQUIRE(db.size() == word_size * 2); 121 | 122 | for (auto i = 0u; i < word_size * 2; ++i) { 123 | REQUIRE(db.get(i) == false); 124 | } 125 | } 126 | 127 | TEST_CASE("dynamic_bitset is moved properly", "[dynamic_bitset]") { 128 | { 129 | dynamic_bitset db1; 130 | 131 | db1.set(5); 132 | 133 | dynamic_bitset db2 = std::move(db1); 134 | 135 | REQUIRE(db1.size() == word_size); 136 | REQUIRE(db2.size() == word_size); 137 | REQUIRE(db2.get(5) == true); 138 | } 139 | { 140 | dynamic_bitset db1; 141 | 142 | db1.set(5); 143 | db1.set(word_size + 5); 144 | 145 | dynamic_bitset db2 = std::move(db1); 146 | 147 | REQUIRE(db1.size() == word_size); 148 | REQUIRE(db2.size() == word_size * 2); 149 | REQUIRE(db2.get(5) == true); 150 | REQUIRE(db2.get(word_size + 5) == true); 151 | } 152 | } 153 | 154 | TEST_CASE("setting bits under the word size does not disable sdo", "[dynamic_bitset]") { 155 | dynamic_bitset db; 156 | 157 | REQUIRE(db.using_sdo()); 158 | 159 | for (auto i = 0u; i < word_size; ++i) { 160 | db.set(i); 161 | REQUIRE(db.using_sdo() == true); 162 | } 163 | } 164 | 165 | TEST_CASE("setting bits over the word size disables sdo", "[dynamic_bitset]") { 166 | dynamic_bitset db; 167 | 168 | db.set(word_size + 10); 169 | REQUIRE(db.using_sdo() == false); 170 | 171 | db.set(0); 172 | REQUIRE(db.using_sdo() == false); 173 | } 174 | 175 | TEST_CASE("unsetting bits past the end does not disable sdo", "[dynamic_bitset]") { 176 | dynamic_bitset db; 177 | 178 | db.unset(word_size + 10); 179 | REQUIRE(db.using_sdo() == true); 180 | } 181 | -------------------------------------------------------------------------------- /src/test_count.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "catch.hpp" 4 | 5 | using DB = ginseng::database; 6 | using ginseng::deny; 7 | using ginseng::tag; 8 | using ginseng::optional; 9 | using ent_id = DB::ent_id; 10 | using com_id = DB::com_id; 11 | 12 | struct ComA {}; 13 | struct ComB {}; 14 | 15 | TEST_CASE("component counts are zero by default", "[ginseng]") { 16 | DB db; 17 | 18 | REQUIRE(db.count() == 0); 19 | REQUIRE(db.count() == 0); 20 | } 21 | 22 | TEST_CASE("component counts increase monotonically", "[ginseng]") { 23 | DB db; 24 | 25 | for (int i = 0; i < 5; ++i) { 26 | auto e = db.create_entity(); 27 | db.add_component(e, ComA{}); 28 | REQUIRE(db.count() == i+1); 29 | } 30 | 31 | REQUIRE(db.count() == 5); 32 | } 33 | 34 | TEST_CASE("component counts decrease monotonically", "[ginseng]") { 35 | DB db; 36 | 37 | for (int i = 0; i < 5; ++i) { 38 | auto e = db.create_entity(); 39 | db.add_component(e, ComA{}); 40 | } 41 | 42 | REQUIRE(db.count() == 5); 43 | 44 | auto x = 5; 45 | 46 | db.visit([&](ent_id eid, const ComA&) { 47 | db.remove_component(eid); 48 | --x; 49 | REQUIRE(db.count() == x); 50 | }); 51 | 52 | REQUIRE(db.count() == 0); 53 | } 54 | 55 | TEST_CASE("component counts are independent", "[ginseng]") { 56 | DB db; 57 | 58 | for (int i = 0; i < 5; ++i) { 59 | auto e = db.create_entity(); 60 | db.add_component(e, ComA{}); 61 | } 62 | 63 | REQUIRE(db.count() == 5); 64 | 65 | for (int i = 0; i < 5; ++i) { 66 | auto e = db.create_entity(); 67 | db.add_component(e, ComB{}); 68 | REQUIRE(db.count() == 5); 69 | REQUIRE(db.count() == i+1); 70 | } 71 | 72 | REQUIRE(db.count() == 5); 73 | 74 | auto x = 5; 75 | 76 | db.visit([&](ent_id eid, const ComA&) { 77 | if (x%2 == 0) db.remove_component(eid); 78 | --x; 79 | REQUIRE(db.count() == 5); 80 | }); 81 | 82 | REQUIRE(db.count() == 3); 83 | REQUIRE(db.count() == 5); 84 | } 85 | -------------------------------------------------------------------------------- /src/test_primary.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "catch.hpp" 6 | 7 | using DB = ginseng::database; 8 | using ginseng::deny; 9 | using ginseng::tag; 10 | using ginseng::optional; 11 | using ent_id = DB::ent_id; 12 | using com_id = DB::com_id; 13 | 14 | TEST_CASE("Primary components are visited in cache-freindly order", "[ginseng]") 15 | { 16 | DB db; 17 | 18 | struct ID { int id; }; 19 | struct Data { float val; }; 20 | 21 | int next_id = 0; 22 | 23 | std::vector eids; 24 | eids.push_back(db.create_entity()); 25 | eids.push_back(db.create_entity()); 26 | eids.push_back(db.create_entity()); 27 | eids.push_back(db.create_entity()); 28 | eids.push_back(db.create_entity()); 29 | 30 | db.add_component(eids[1], ID{next_id++}); 31 | db.add_component(eids[3], ID{next_id++}); 32 | db.add_component(eids[0], ID{next_id++}); 33 | db.add_component(eids[4], ID{next_id++}); 34 | db.add_component(eids[2], ID{next_id++}); 35 | 36 | std::vector ptrs; 37 | ptrs.reserve(5); 38 | 39 | db.visit([&](ID& id) { ptrs.push_back(&id); }); 40 | REQUIRE(std::is_sorted(begin(ptrs), end(ptrs))); 41 | 42 | ptrs.clear(); 43 | db.visit([&](ent_id, ID& id) { ptrs.push_back(&id); }); 44 | REQUIRE(std::is_sorted(begin(ptrs), end(ptrs))); 45 | 46 | db.add_component(eids[0], Data{7.5}); 47 | db.add_component(eids[3], Data{7.5}); 48 | db.add_component(eids[1], Data{7.5}); 49 | db.add_component(eids[2], Data{7.5}); 50 | db.add_component(eids[4], Data{7.5}); 51 | 52 | ptrs.clear(); 53 | db.visit([&](ID& id) { ptrs.push_back(&id); }); 54 | REQUIRE(std::is_sorted(begin(ptrs), end(ptrs))); 55 | 56 | std::vector dataptrs; 57 | dataptrs.reserve(5); 58 | 59 | ptrs.clear(); 60 | db.visit([&](ID& id, Data& data) { ptrs.push_back(&id); dataptrs.push_back(&data); }); 61 | REQUIRE(std::is_sorted(begin(ptrs), end(ptrs))); 62 | REQUIRE(!std::is_sorted(begin(dataptrs), end(dataptrs))); 63 | 64 | ptrs.clear(); 65 | dataptrs.clear(); 66 | db.visit([&](Data& data, ID& id) { ptrs.push_back(&id); dataptrs.push_back(&data); }); 67 | REQUIRE(!std::is_sorted(begin(ptrs), end(ptrs))); 68 | REQUIRE(std::is_sorted(begin(dataptrs), end(dataptrs))); 69 | } 70 | -------------------------------------------------------------------------------- /src/test_stress.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "catch.hpp" 6 | 7 | using DB = ginseng::database; 8 | using ginseng::deny; 9 | using ginseng::tag; 10 | using ginseng::optional; 11 | using ent_id = DB::ent_id; 12 | using com_id = DB::com_id; 13 | 14 | TEST_CASE("Component sets can handle many components", "[ginseng]") 15 | { 16 | DB db; 17 | 18 | struct ID { int id; }; 19 | 20 | int next_id = 0; 21 | 22 | auto make_ent = [&]() 23 | { 24 | auto ent = db.create_entity(); 25 | db.add_component(ent, ID{next_id}); 26 | ++next_id; 27 | return ent; 28 | }; 29 | 30 | constexpr auto SZ = 10000000; 31 | 32 | for (int i=0; i 2 | 3 | #include "catch.hpp" 4 | 5 | using DB = ginseng::database; 6 | using ginseng::deny; 7 | using ginseng::tag; 8 | using ginseng::optional; 9 | using ent_id = DB::ent_id; 10 | using com_id = DB::com_id; 11 | 12 | TEST_CASE("tag types do not use dynamic allocation", "[ginseng]") 13 | { 14 | // Not sure how to actually test this. 15 | // Instead, I'll just check to make sure it actually compiles. 16 | 17 | DB db; 18 | 19 | struct Sometag {}; 20 | struct Sometag2 {}; 21 | 22 | auto ent = db.create_entity(); 23 | db.add_component(ent, tag{}); 24 | 25 | int visited; 26 | 27 | visited = 0; 28 | db.visit([&](tag){ 29 | ++visited; 30 | }); 31 | REQUIRE(visited == 1); 32 | 33 | visited = 0; 34 | db.visit([&](tag){ 35 | ++visited; 36 | }); 37 | REQUIRE(visited == 0); 38 | 39 | optional> minfo; 40 | visited = 0; 41 | db.visit([&](optional> info){ 42 | ++visited; 43 | minfo = info; 44 | }); 45 | REQUIRE(visited == 1); 46 | REQUIRE(bool(minfo) == true); 47 | 48 | optional> minfo2; 49 | visited = 0; 50 | db.visit([&](optional> info){ 51 | ++visited; 52 | minfo2 = info; 53 | }); 54 | REQUIRE(visited == 1); 55 | REQUIRE(bool(minfo2) == false); 56 | } 57 | --------------------------------------------------------------------------------