├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── include ├── entt │ └── entt.hpp ├── trollworks.hpp └── trollworks │ ├── assets.hpp │ ├── controlflow.hpp │ ├── coroutine.hpp │ ├── game-loop.hpp │ ├── jobs.hpp │ ├── messaging.hpp │ ├── scene.hpp │ └── ui.hpp ├── shipp.json └── tests ├── Makefile ├── coroutine.spec.cpp ├── doctest.h ├── game-loop.spec.cpp ├── main.cpp ├── messaging.spec.cpp ├── scene.spec.cpp └── ui.spec.cpp /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [linkdd] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tests 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | compiler: 15 | - package: gcc-13 16 | bin: g++ 17 | os: ubuntu-24.04 18 | - package: gcc-12 19 | bin: g++ 20 | os: ubuntu-latest 21 | - package: gcc-11 22 | bin: g++ 23 | os: ubuntu-latest 24 | runs-on: ${{ matrix.compiler.os }} 25 | steps: 26 | - name: checkout-scm 27 | uses: actions/checkout@v3 28 | with: 29 | submodules: recursive 30 | - name: setup 31 | run: sudo apt install ${{ matrix.compiler.package }} make 32 | - name: test 33 | run: make test CXX=${{ matrix.compiler.bin }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.vscode 3 | /.shipp 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2024 David Delassus 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR := /usr/local 2 | 3 | .PHONY: test 4 | test: 5 | @make -C tests all 6 | 7 | .PHONY: install 8 | install: 9 | @mkdir -p $(DESTDIR)/include 10 | @cp -R include $(DESTDIR) 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trollworks SDK Core 2 | 3 | 4 |
5 | 6 | ![tests](https://img.shields.io/github/actions/workflow/status/trollworks/sdk-core/tests.yml?style=flat-square&logo=github&label=tests) 7 | ![license](https://img.shields.io/github/license/trollworks/sdk-core?style=flat-square&color=blue) 8 | ![version](https://img.shields.io/github/v/release/trollworks/sdk-core?style=flat-square&color=red) 9 | 10 |
11 | 12 | Trollworks is an (unfinished) game engine in C++ I've been working on for a 13 | while. 14 | 15 | This repository contains the basis of the SDK, as a header-only C++23 library. 16 | It is built around [EnTT](https://github.com/skypjack/entt), an ECS library. 17 | 18 | This library provides: 19 | 20 | - a game loop abstraction (with fixed updates, updates and late updates) 21 | - some EnTT utilities as singletons: 22 | - asset manager 23 | - event dispatcher 24 | - scene manager 25 | - job manager 26 | - a UI component framework inspired by React to help organize immediate-mode UI code 27 | - coroutines inspired by Unity coroutines 28 | 29 | List of backends: 30 | 31 | | Name | 2D/3D | URL | 32 | | --- | --- | --- | 33 | | SDL2 | 2D | https://github.com/trollworks/sdk-backend-sdl | 34 | | raylib | 2D | https://github.com/trollworks/sdk-backend-raylib (TODO) | 35 | | glfw | 3D | https://github.com/trollworks/sdk-backend-glfw (TODO) | 36 | 37 | ## Installation 38 | 39 | Clone the repository in your project (don't forget to pull the submodules) and 40 | add the `include/` folder to your include paths. 41 | 42 | Or, if you are using [Shipp](https://github.com/linkdd/shipp), add to your 43 | dependencies: 44 | 45 | ```json 46 | { 47 | "dependencies": [ 48 | { 49 | "name": "trollworks-sdk-core", 50 | "url": "https://github.com/trollworks/sdk-core.git", 51 | "version": "v0.3.0" 52 | } 53 | ] 54 | } 55 | ``` 56 | 57 | ## Usage 58 | 59 | ### Game loop 60 | 61 | ```cpp 62 | #include 63 | 64 | struct game_state { 65 | // ... 66 | }; 67 | 68 | struct listener { 69 | game_state& gs; 70 | 71 | void on_setup(tw::controlflow& cf) { 72 | // create window and opengl context, load resources 73 | } 74 | 75 | void on_teardown() { 76 | // free resources, opengl context and window 77 | } 78 | 79 | void on_frame_begin(tw::controlflow& cf) { 80 | // process window events and inputs 81 | } 82 | 83 | void on_fixed_update(float delta_time, tw::controlflow& cf) { 84 | // physics updates 85 | } 86 | 87 | void on_update(float delta_time, tw::controlflow& cf) { 88 | // game logic 89 | } 90 | 91 | void on_late_update(float delta_time, tw::controlflow& cf) { 92 | // more game logic 93 | } 94 | 95 | void on_render() { 96 | // render graphics 97 | } 98 | 99 | void on_frame_end(tw::controlflow& cf) { 100 | // ... 101 | } 102 | }; 103 | 104 | int main() { 105 | auto gs = game_state{}; 106 | auto l = listener{.gs = gs}; 107 | auto loop = tw::game_loop{}; 108 | 109 | loop 110 | .with_fps(60) 111 | .with_ups(50) 112 | .on_setup<&listener::on_setup>(l) 113 | .on_teardown<&listener::on_teardown>(l) 114 | .on_frame_begin<&listener::on_frame_begin>(l) 115 | .on_frame_end<&listener::on_frame_end>(l) 116 | .on_update<&listener::on_update>(l) 117 | .on_fixed_update<&listener::on_fixed_update>(l) 118 | .on_late_update<&listener::on_late_update>(l) 119 | .on_render<&listener::on_render>(l) 120 | .run(); 121 | 122 | return 0; 123 | } 124 | ``` 125 | 126 | **NB:** A concept `backend_trait` is also provided to facilitate pluging in a 127 | specific window/event/render system (SDL, raylib, glfw, ...): 128 | 129 | ```cpp 130 | struct sdl_backend { 131 | SDL_Window *window; 132 | SDL_Renderer *renderer; 133 | 134 | void setup(tw::controlflow& cf) { 135 | // create window, opengl context, ... 136 | } 137 | 138 | void teardown() { 139 | // free opengl context and window 140 | } 141 | 142 | void poll_events(tw::controlflow& cf) { 143 | // process event, for example with SDL: 144 | SDL_Event evt; 145 | 146 | while (SDL_PollEvents(&evt)) { 147 | switch (evt.type) { 148 | case SDL_QUIT: 149 | cf = tw::controlflow::exit; 150 | break; 151 | 152 | default: 153 | // ... 154 | break; 155 | } 156 | } 157 | } 158 | 159 | void render() { 160 | // get current scene's entity registry 161 | auto& registry = tw::scene_manager::main().registry(); 162 | 163 | SDL_RenderClear(renderer); 164 | 165 | // iterate over your entities to draw them 166 | 167 | // render UI with imgui, or nuklear, or other 168 | 169 | SDL_RenderPresent(renderer); 170 | } 171 | }; 172 | 173 | int main() { 174 | auto back = sdl_backend{}; 175 | auto loop = tw::game_loop{}; 176 | 177 | loop 178 | .with_fps(60) 179 | .with_ups(50) 180 | .with_backend(back) 181 | .run(); 182 | 183 | return 0; 184 | } 185 | ``` 186 | 187 | ### Coroutines 188 | 189 | First, create your coroutine function: 190 | 191 | ```cpp 192 | tw::coroutine count(int n) { 193 | for (auto i = 0; i < n; i++) { 194 | co_yield tw::coroutine::none{}; 195 | } 196 | } 197 | ``` 198 | 199 | Then, in your game loop: 200 | 201 | ```cpp 202 | tw::coroutine_manager::main().start_coroutine(count(5)); 203 | ``` 204 | 205 | Coroutines are run after the update hook and before the late update hook. 206 | 207 | Coroutines can also be chained, like in Unity: 208 | 209 | ```cpp 210 | tw::coroutine count2(int a, int b) { 211 | co_yield count(a); 212 | co_yield count(b); 213 | } 214 | ``` 215 | 216 | ### Scene management 217 | 218 | A scene is a class providing 2 methods (`load` and `unload`): 219 | 220 | ```cpp 221 | class my_scene final : public tw::scene { 222 | public: 223 | virtual void load(entt::registry& registry) override { 224 | // create entities and components 225 | } 226 | 227 | virtual void unload(entt::registry& registry) override { 228 | // destroy entities and components 229 | } 230 | }; 231 | ``` 232 | 233 | Then: 234 | 235 | ```cpp 236 | tw::scene_manager::main().load(my_scene{}); 237 | ``` 238 | 239 | ### Assets 240 | 241 | The asset manager provides a singleton per asset type. The singleton is simply a 242 | resource cache from EnTT, for more information consult 243 | [this page](https://github.com/skypjack/entt/wiki/Crash-Course:-resource-management). 244 | 245 | ```cpp 246 | struct my_asset { 247 | // ... 248 | 249 | using resource_type = my_asset; 250 | 251 | struct loader_type { 252 | using result_type = std::shared_ptr; 253 | 254 | result_type operator()(/* ... */) const { 255 | // ... 256 | } 257 | }; 258 | }; 259 | 260 | auto& cache = tw::asset_manager::cache(); 261 | ``` 262 | 263 | > **NB:** The `resource_type` type name may seem redundant, but it is there for 264 | > assets that loads the same type of resources, consider the following example: 265 | 266 | ```cpp 267 | struct spritesheet { 268 | // ... 269 | }; 270 | 271 | struct aseprite_sheet { 272 | using resource_type = spritesheet; 273 | 274 | struct loader_type { 275 | using result_type = std::shared_ptr; 276 | 277 | result_type operator()(/* ... */) const { 278 | // ... 279 | } 280 | }; 281 | }; 282 | 283 | struct texturepacker_sheet { 284 | using resource_type = spritesheet; 285 | 286 | struct loader_type { 287 | using result_type = std::shared_ptr; 288 | 289 | result_type operator()(/* ... */) const { 290 | // ... 291 | } 292 | }; 293 | }; 294 | ``` 295 | 296 | Both `aseprite_sheet` and `texturepacker_sheet` assets will return a 297 | `spritesheet` resource: 298 | 299 | ```cpp 300 | auto [it, loaded] = tw::asset_manager::cache().load(/* ... */); 301 | auto [id, sheet] = *it; 302 | // sheet is entt::resource 303 | ``` 304 | 305 | ```cpp 306 | auto [it, loaded] = tw::asset_manager::cache().load(/* ... */); 307 | auto [id, sheet] = *it; 308 | // sheet is entt::resource 309 | ``` 310 | 311 | ### Messaging 312 | 313 | The message bus is simply a singleton returning an `entt::dispatcher`. For more 314 | information, please consult 315 | [this page](https://github.com/skypjack/entt/wiki/Crash-Course:-events,-signals-and-everything-in-between#event-dispatcher). 316 | 317 | ```cpp 318 | auto& dispatcher = tw::message_bus::main(); 319 | ``` 320 | 321 | Queued messages are dispatched after the late update hook an before rendering. 322 | 323 | ### Jobs 324 | 325 | The job manager is simply a singleton returing an `entt::basic_scheduler`. 326 | For more information, please consult 327 | [this page](https://github.com/skypjack/entt/wiki/Crash-Course:-cooperative-scheduler). 328 | 329 | ### UI framework 330 | 331 | ```cpp 332 | using namespace entt::literals; 333 | 334 | struct foo { 335 | void operator()(tw::ui::hooks& h) { 336 | auto& local_state = h.use_state(0); 337 | 338 | // gui code 339 | } 340 | }; 341 | 342 | struct bar { 343 | void operator()(tw::ui::hooks& h) { 344 | auto& local_state = h.use_state(0.0f); 345 | 346 | // gui code 347 | } 348 | }; 349 | 350 | struct root { 351 | bool condition; 352 | 353 | void operator()(tw::ui::hooks& h) { 354 | h.render("a"_hs); 355 | 356 | if (condition) { 357 | h.render("b"_hs); 358 | } 359 | 360 | h.render("c"_hs); 361 | } 362 | }; 363 | ``` 364 | 365 | Then in your render hook: 366 | 367 | ```cpp 368 | tw::ui::h("root"_hs, root{.condition = true}); 369 | ``` 370 | 371 | The ids given to the UI hooks must be unique within the component, not globally. 372 | 373 | ## License 374 | 375 | This project is released under the terms of the [MIT License](./LICENSE.txt). 376 | -------------------------------------------------------------------------------- /include/trollworks.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./trollworks/assets.hpp" 4 | #include "./trollworks/coroutine.hpp" 5 | #include "./trollworks/game-loop.hpp" 6 | #include "./trollworks/scene.hpp" 7 | #include "./trollworks/messaging.hpp" 8 | #include "./trollworks/jobs.hpp" 9 | #include "./trollworks/ui.hpp" 10 | -------------------------------------------------------------------------------- /include/trollworks/assets.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../entt/entt.hpp" 7 | 8 | namespace tw { 9 | template 10 | concept asset_trait = requires(T& asset) { 11 | typename T::resource_type; 12 | typename T::loader_type; 13 | }; 14 | 15 | template 16 | class asset_manager { 17 | public: 18 | using cache_type = entt::resource_cache< 19 | typename A::resource_type, 20 | typename A::loader_type 21 | >; 22 | 23 | static cache_type& cache() { 24 | if (!entt::locator::has_value()) { 25 | entt::locator::emplace(); 26 | } 27 | 28 | return entt::locator::value(); 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /include/trollworks/controlflow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace tw { 4 | enum class controlflow { 5 | running, 6 | exit 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /include/trollworks/coroutine.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../entt/entt.hpp" 9 | 10 | namespace tw { 11 | class coroutine { 12 | public: 13 | struct none {}; 14 | class promise_type; 15 | using handle_type = std::coroutine_handle; 16 | 17 | class promise_type { 18 | public: 19 | promise_type() = default; 20 | promise_type(const promise_type&) = delete; 21 | promise_type(promise_type&&) = delete; 22 | 23 | coroutine get_return_object() noexcept { 24 | return coroutine{*this}; 25 | } 26 | 27 | std::suspend_always initial_suspend() noexcept { 28 | return {}; 29 | } 30 | 31 | std::suspend_always final_suspend() noexcept { 32 | return {}; 33 | } 34 | 35 | void unhandled_exception() noexcept { 36 | m_exc = std::current_exception(); 37 | } 38 | 39 | void return_void() noexcept {} 40 | 41 | std::suspend_always yield_value(none&) noexcept { 42 | return {}; 43 | } 44 | 45 | std::suspend_always yield_value(none&&) noexcept { 46 | return {}; 47 | } 48 | 49 | auto yield_value(coroutine& from) noexcept { 50 | class awaitable { 51 | public: 52 | awaitable(promise_type* child) : m_child(child) {} 53 | 54 | bool await_ready() const noexcept { 55 | return m_child == nullptr; 56 | } 57 | 58 | void await_suspend(handle_type) noexcept {} 59 | 60 | void await_resume() noexcept { 61 | if (m_child != nullptr) { 62 | m_child->throw_if_exception(); 63 | } 64 | } 65 | 66 | private: 67 | promise_type* m_child; 68 | }; 69 | 70 | if (from.m_promise != nullptr) { 71 | m_root->m_parent = from.m_promise; 72 | from.m_promise->m_root = m_root; 73 | from.m_promise->m_parent = this; 74 | from.m_promise->resume(); 75 | 76 | if (!from.m_promise->done()) { 77 | return awaitable{from.m_promise}; 78 | } 79 | 80 | m_root->m_parent = this; 81 | } 82 | 83 | return awaitable{nullptr}; 84 | } 85 | 86 | auto yield_value(coroutine&& from) noexcept { 87 | return yield_value(from); 88 | } 89 | 90 | template 91 | std::suspend_never await_transform(T&&) = delete; 92 | 93 | void destroy() noexcept { 94 | handle_type::from_promise(*this).destroy(); 95 | } 96 | 97 | void throw_if_exception() { 98 | if (m_exc) { 99 | std::rethrow_exception(m_exc); 100 | } 101 | } 102 | 103 | bool done() noexcept { 104 | return handle_type::from_promise(*this).done(); 105 | } 106 | 107 | void poll() noexcept { 108 | m_parent->resume(); 109 | 110 | while (m_parent != this && m_parent->done()) { 111 | m_parent = m_parent->m_parent; 112 | m_parent->resume(); 113 | } 114 | } 115 | 116 | private: 117 | void resume() noexcept { 118 | handle_type::from_promise(*this).resume(); 119 | } 120 | 121 | private: 122 | std::exception_ptr m_exc{nullptr}; 123 | promise_type *m_root{this}; 124 | promise_type *m_parent{this}; 125 | }; 126 | 127 | public: 128 | coroutine() = default; 129 | coroutine(promise_type& promise) : m_promise(&promise) {} 130 | coroutine(coroutine&& other) : m_promise(other.m_promise) { 131 | other.m_promise = nullptr; 132 | } 133 | coroutine(const coroutine&) = delete; 134 | coroutine& operator=(const coroutine&) = delete; 135 | 136 | coroutine& operator=(coroutine&& other) noexcept { 137 | if (this != &other) { 138 | if (m_promise != nullptr) { 139 | m_promise->destroy(); 140 | } 141 | 142 | m_promise = other.m_promise; 143 | other.m_promise = nullptr; 144 | } 145 | 146 | return *this; 147 | } 148 | 149 | ~coroutine() { 150 | if (m_promise != nullptr) { 151 | m_promise->destroy(); 152 | } 153 | } 154 | 155 | bool done() const { 156 | return m_promise == nullptr || m_promise->done(); 157 | } 158 | 159 | void resume() { 160 | m_promise->poll(); 161 | 162 | if (m_promise->done()) { 163 | auto* temp = m_promise; 164 | m_promise = nullptr; 165 | temp->throw_if_exception(); 166 | } 167 | } 168 | 169 | private: 170 | promise_type *m_promise{nullptr}; 171 | }; 172 | 173 | class coroutine_manager { 174 | public: 175 | static coroutine_manager& main() { 176 | if (!entt::locator::has_value()) { 177 | entt::locator::emplace(); 178 | } 179 | 180 | return entt::locator::value(); 181 | } 182 | 183 | void start_coroutine(coroutine&& coro) { 184 | m_coroutines.push_back(std::move(coro)); 185 | } 186 | 187 | void update() { 188 | for (auto& coro : m_coroutines) { 189 | coro.resume(); 190 | } 191 | 192 | m_coroutines.erase( 193 | std::remove_if( 194 | m_coroutines.begin(), 195 | m_coroutines.end(), 196 | [](const coroutine& coro) { 197 | return coro.done(); 198 | } 199 | ), 200 | m_coroutines.end() 201 | ); 202 | } 203 | 204 | bool empty() const { 205 | return m_coroutines.empty(); 206 | } 207 | 208 | private: 209 | std::vector m_coroutines; 210 | }; 211 | } 212 | -------------------------------------------------------------------------------- /include/trollworks/game-loop.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "../entt/entt.hpp" 13 | 14 | #include "./controlflow.hpp" 15 | #include "./coroutine.hpp" 16 | #include "./messaging.hpp" 17 | #include "./jobs.hpp" 18 | 19 | namespace tw { 20 | template 21 | concept backend_trait = requires(B& backend, controlflow& cf) { 22 | { backend.setup(cf) } -> std::same_as; 23 | { backend.teardown() } -> std::same_as; 24 | { backend.poll_events(cf) } -> std::same_as; 25 | { backend.render() } -> std::same_as; 26 | }; 27 | 28 | class game_loop { 29 | private: 30 | using cb_setup_type = entt::delegate; 31 | using cb_teardown_type = entt::delegate; 32 | 33 | using cb_frame_begin_type = entt::delegate; 34 | using cb_frame_end_type = entt::delegate; 35 | 36 | using cb_fixed_update_type = entt::delegate; 37 | using cb_update_type = entt::delegate; 38 | using cb_late_update_type = entt::delegate; 39 | 40 | using cb_render_type = entt::delegate; 41 | 42 | std::vector m_sig_setup; 43 | std::vector m_sig_teardown; 44 | 45 | std::vector m_sig_frame_begin; 46 | std::vector m_sig_frame_end; 47 | 48 | std::vector m_sig_fixed_update; 49 | std::vector m_sig_update; 50 | std::vector m_sig_late_update; 51 | 52 | std::vector m_sig_render; 53 | 54 | public: 55 | game_loop() = default; 56 | game_loop(const game_loop&) = delete; 57 | 58 | game_loop& with_fps(float fps) { 59 | if (fps < 0.0f) { 60 | throw std::invalid_argument("expected fps >= 0"); 61 | } 62 | 63 | m_fps = fps; 64 | return *this; 65 | } 66 | 67 | game_loop& with_ups(float ups) { 68 | if (ups <= 0.0f || (m_fps > 0.0f && ups > m_fps)) { 69 | throw std::invalid_argument("expected 0 < ups <= fps"); 70 | } 71 | 72 | m_ups = ups; 73 | return *this; 74 | } 75 | 76 | template 77 | game_loop& with_backend(B& backend) { 78 | on_setup<&B::setup>(backend); 79 | on_teardown<&B::teardown>(backend); 80 | on_frame_begin<&B::poll_events>(backend); 81 | on_render<&B::render>(backend); 82 | return *this; 83 | } 84 | 85 | template 86 | game_loop& on_setup(Type&&... args) { 87 | auto delegate = cb_setup_type{}; 88 | delegate.template connect(std::forward(args)...); 89 | m_sig_setup.push_back(delegate); 90 | return *this; 91 | } 92 | 93 | template 94 | game_loop& on_teardown(Type&&... args) { 95 | auto delegate = cb_teardown_type{}; 96 | delegate.template connect(std::forward(args)...); 97 | m_sig_teardown.push_back(delegate); 98 | return *this; 99 | } 100 | 101 | template 102 | game_loop& on_frame_begin(Type&&... args) { 103 | auto delegate = cb_frame_begin_type{}; 104 | delegate.template connect(std::forward(args)...); 105 | m_sig_frame_begin.push_back(delegate); 106 | return *this; 107 | } 108 | 109 | template 110 | game_loop& on_frame_end(Type&&... args) { 111 | auto delegate = cb_frame_end_type{}; 112 | delegate.template connect(std::forward(args)...); 113 | m_sig_frame_end.push_back(delegate); 114 | return *this; 115 | } 116 | 117 | template 118 | game_loop& on_fixed_update(Type&&... args) { 119 | auto delegate = cb_fixed_update_type{}; 120 | delegate.template connect(std::forward(args)...); 121 | m_sig_fixed_update.push_back(delegate); 122 | return *this; 123 | } 124 | 125 | template 126 | game_loop& on_update(Type&&... args) { 127 | auto delegate = cb_update_type{}; 128 | delegate.template connect(std::forward(args)...); 129 | m_sig_update.push_back(delegate); 130 | return *this; 131 | } 132 | 133 | template 134 | game_loop& on_late_update(Type&&... args) { 135 | auto delegate = cb_late_update_type{}; 136 | delegate.template connect(std::forward(args)...); 137 | m_sig_late_update.push_back(delegate); 138 | return *this; 139 | } 140 | 141 | template 142 | game_loop& on_render(Type&&... args) { 143 | auto delegate = cb_render_type{}; 144 | delegate.template connect(std::forward(args)...); 145 | m_sig_render.push_back(delegate); 146 | return *this; 147 | } 148 | 149 | void run() { 150 | auto cf = controlflow::running; 151 | 152 | publish(m_sig_setup, cf); 153 | 154 | auto last_time = std::chrono::high_resolution_clock::now(); 155 | auto lag = 0.0f; 156 | 157 | while (cf == controlflow::running) { 158 | auto current_time = std::chrono::high_resolution_clock::now(); 159 | auto elapsed_time = current_time - last_time; 160 | auto delta_time = std::chrono::duration(elapsed_time).count(); 161 | 162 | lag += delta_time; 163 | last_time = current_time; 164 | 165 | publish(m_sig_frame_begin, cf); 166 | 167 | auto fixed_delta_time = 1.0f / m_ups; 168 | while (lag >= fixed_delta_time) { 169 | publish(m_sig_fixed_update, fixed_delta_time, cf); 170 | lag -= fixed_delta_time; 171 | } 172 | 173 | publish(m_sig_update, delta_time, cf); 174 | coroutine_manager::main().update(); 175 | publish(m_sig_late_update, delta_time, cf); 176 | job_manager::main().update(delta_time, &cf); 177 | message_bus::main().update(); 178 | 179 | publish(m_sig_render); 180 | 181 | publish(m_sig_frame_end, cf); 182 | 183 | auto frame_end = std::chrono::high_resolution_clock::now(); 184 | auto frame_duration = frame_end - current_time; 185 | auto frame_time = std::chrono::duration(frame_duration).count(); 186 | 187 | if (m_fps > 0.0f) { 188 | auto max_time = 1.0f / m_fps; 189 | 190 | if (frame_time < max_time) { 191 | auto wait = static_cast((max_time - frame_time) * 1000.0f); 192 | std::this_thread::sleep_for(std::chrono::milliseconds(wait)); 193 | } 194 | } 195 | } 196 | 197 | publish(std::ranges::reverse_view{m_sig_teardown}); 198 | } 199 | 200 | private: 201 | template 202 | void publish(Signal s, Args&&... args) { 203 | for (auto& delegate : s) { 204 | delegate(std::forward(args)...); 205 | } 206 | } 207 | 208 | private: 209 | float m_fps{0.0f}; 210 | float m_ups{50.0f}; 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /include/trollworks/jobs.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../entt/entt.hpp" 4 | 5 | namespace tw { 6 | template 7 | using job = entt::process; 8 | 9 | class job_manager { 10 | public: 11 | using scheduler = entt::basic_scheduler; 12 | 13 | static scheduler& main() { 14 | if (!entt::locator::has_value()) { 15 | entt::locator::emplace(); 16 | } 17 | 18 | return entt::locator::value(); 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /include/trollworks/messaging.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../entt/entt.hpp" 4 | 5 | namespace tw { 6 | class message_bus { 7 | public: 8 | static entt::dispatcher& main() { 9 | if (!entt::locator::has_value()) { 10 | entt::locator::emplace(); 11 | } 12 | 13 | return entt::locator::value(); 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /include/trollworks/scene.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "../entt/entt.hpp" 8 | 9 | namespace tw { 10 | class scene { 11 | public: 12 | virtual ~scene() = default; 13 | 14 | virtual void load(entt::registry& registry) = 0; 15 | virtual void unload(entt::registry& registry) = 0; 16 | }; 17 | 18 | template 19 | concept scene_trait = std::derived_from; 20 | 21 | class scene_manager { 22 | public: 23 | static scene_manager& main() { 24 | if (!entt::locator::has_value()) { 25 | entt::locator::emplace(); 26 | } 27 | 28 | return entt::locator::value(); 29 | } 30 | 31 | template 32 | void load(S scene) { 33 | if (m_scene) { 34 | m_scene->unload(m_registry); 35 | } 36 | 37 | m_scene = std::make_unique(std::move(scene)); 38 | m_scene->load(m_registry); 39 | } 40 | 41 | const entt::registry& registry() const { 42 | return m_registry; 43 | } 44 | 45 | entt::registry& registry() { 46 | return m_registry; 47 | } 48 | 49 | private: 50 | std::unique_ptr m_scene{nullptr}; 51 | entt::registry m_registry; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /include/trollworks/ui.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../entt/entt.hpp" 7 | 8 | namespace tw::ui { 9 | class hooks; 10 | 11 | template 12 | concept component_trait = requires(T& cmp, hooks& hooks) { 13 | { cmp(hooks) } -> std::same_as; 14 | }; 15 | 16 | class hooks { 17 | public: 18 | static hooks& main() { 19 | if (!entt::locator::has_value()) { 20 | entt::locator::emplace(); 21 | } 22 | 23 | return entt::locator::value(); 24 | } 25 | 26 | void reset() { 27 | m_state_index = 0; 28 | } 29 | 30 | template 31 | T& use_state(T initial_val) { 32 | entt::id_type key = m_state_index++; 33 | 34 | if (!m_state.ctx().contains(key)) { 35 | m_state.ctx().emplace_as(key, initial_val); 36 | } 37 | 38 | return m_state.ctx().get(key); 39 | } 40 | 41 | template 42 | void render(entt::id_type key, Args&&... args) { 43 | if (!m_components.ctx().contains(key)) { 44 | m_components.ctx().emplace_as(key); 45 | } 46 | 47 | auto component = Component{std::forward(args)...}; 48 | auto& child_hooks = m_components.ctx().get(key); 49 | child_hooks.reset(); 50 | component(child_hooks); 51 | } 52 | 53 | private: 54 | entt::id_type m_state_index{0}; 55 | entt::registry m_state; 56 | entt::registry m_components; 57 | }; 58 | 59 | template 60 | void h(entt::id_type key, Args&&... args) { 61 | hooks::main().reset(); 62 | hooks::main().render(key, std::forward(args)...); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /shipp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trollworks-sdk-core", 3 | "version": "0.3.0", 4 | "scripts": { 5 | "build": "true", 6 | "install": "make install DESTDIR=$SHIPP_DIST_DIR" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR = ../build/tests/ 2 | 3 | CXXFLAGS := -std=c++23 -O2 -g 4 | SOURCES = $(wildcard *.cpp) 5 | TARGET = trollworks-test-runner 6 | 7 | .PHONY: all 8 | all: 9 | @echo " CXX $(TARGET)" 10 | @mkdir -p $(DESTDIR) 11 | @$(CXX) $(CXXFLAGS) $(SOURCES) -o $(DESTDIR)/$(TARGET) 12 | @echo " RUN $(TARGET)" 13 | @exec $(DESTDIR)/$(TARGET) 14 | -------------------------------------------------------------------------------- /tests/coroutine.spec.cpp: -------------------------------------------------------------------------------- 1 | #include "doctest.h" 2 | 3 | #include "../include/trollworks.hpp" 4 | 5 | struct world { 6 | int x{0}; 7 | int y{0}; 8 | }; 9 | 10 | tw::coroutine test_count(int& dest, int n) { 11 | for (int i = 0; i < n; ++i) { 12 | dest++; 13 | co_yield tw::coroutine::none{}; 14 | } 15 | } 16 | 17 | tw::coroutine test_count2(world& dest, int a, int b) { 18 | co_yield test_count(dest.x, a); 19 | co_yield test_count(dest.y, b); 20 | } 21 | 22 | TEST_CASE("coroutine") { 23 | auto &coromgr = tw::coroutine_manager::main(); 24 | auto w = world{}; 25 | 26 | coromgr.start_coroutine(test_count2(w, 3, 5)); 27 | 28 | while (!coromgr.empty()) { 29 | coromgr.update(); 30 | } 31 | 32 | CHECK(w.x == 3); 33 | CHECK(w.y == 5); 34 | } 35 | -------------------------------------------------------------------------------- /tests/game-loop.spec.cpp: -------------------------------------------------------------------------------- 1 | #include "doctest.h" 2 | 3 | #include "../include/trollworks.hpp" 4 | 5 | struct game_state { 6 | int setup{0}; 7 | int teardown{0}; 8 | }; 9 | 10 | struct listener { 11 | game_state& gs; 12 | int val{0}; 13 | 14 | void on_setup(tw::controlflow& cf) { 15 | gs.setup = val; 16 | } 17 | 18 | void on_teardown() { 19 | gs.teardown = val; 20 | } 21 | 22 | void on_update(float dt, tw::controlflow& cf) { 23 | cf = tw::controlflow::exit; 24 | } 25 | }; 26 | 27 | TEST_CASE("game_loop") { 28 | auto gs = game_state{}; 29 | auto l1 = listener{.gs = gs, .val = 1}; 30 | auto l2 = listener{.gs = gs, .val = 2}; 31 | auto loop = tw::game_loop{}; 32 | 33 | loop 34 | .on_setup<&listener::on_setup>(l1) 35 | .on_teardown<&listener::on_teardown>(l1) 36 | .on_update<&listener::on_update>(l1) 37 | .on_setup<&listener::on_setup>(l2) 38 | .on_teardown<&listener::on_teardown>(l2) 39 | .run(); 40 | 41 | CHECK(gs.setup == 2); 42 | CHECK(gs.teardown == 1); 43 | } 44 | -------------------------------------------------------------------------------- /tests/main.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include "doctest.h" 3 | -------------------------------------------------------------------------------- /tests/messaging.spec.cpp: -------------------------------------------------------------------------------- 1 | #include "doctest.h" 2 | 3 | #include "../include/trollworks.hpp" 4 | 5 | struct an_event { 6 | int value; 7 | }; 8 | 9 | struct world { 10 | int value{0}; 11 | }; 12 | 13 | struct listener { 14 | world& w; 15 | 16 | void on_event(const an_event& e) { 17 | w.value = e.value; 18 | } 19 | }; 20 | 21 | TEST_CASE("message bus") { 22 | auto &bus = tw::message_bus::main(); 23 | auto w = world{}; 24 | auto l = listener{w}; 25 | 26 | bus.sink().connect<&listener::on_event>(l); 27 | 28 | bus.trigger(an_event{42}); 29 | CHECK(w.value == 42); 30 | 31 | bus.enqueue(an_event{24}); 32 | CHECK(w.value == 42); 33 | bus.update(); 34 | CHECK(w.value == 24); 35 | } 36 | -------------------------------------------------------------------------------- /tests/scene.spec.cpp: -------------------------------------------------------------------------------- 1 | #include "doctest.h" 2 | 3 | #include "../include/trollworks.hpp" 4 | 5 | struct world { 6 | int loaded{0}; 7 | int unloaded{0}; 8 | }; 9 | 10 | class scene final : public tw::scene { 11 | public: 12 | scene(int value) : m_value(value) {} 13 | 14 | virtual void load(entt::registry& registry) override { 15 | registry.ctx().get().loaded = m_value; 16 | } 17 | 18 | virtual void unload(entt::registry& registry) override { 19 | registry.ctx().get().unloaded = m_value; 20 | } 21 | 22 | private: 23 | int m_value; 24 | }; 25 | 26 | TEST_CASE("scene manager") { 27 | auto& mgr = tw::scene_manager::main(); 28 | auto w = world{}; 29 | mgr.registry().ctx().emplace(w); 30 | 31 | mgr.load(scene{42}); 32 | CHECK(w.loaded == 42); 33 | 34 | mgr.load(scene{24}); 35 | CHECK(w.unloaded == 42); 36 | CHECK(w.loaded == 24); 37 | } 38 | -------------------------------------------------------------------------------- /tests/ui.spec.cpp: -------------------------------------------------------------------------------- 1 | #include "doctest.h" 2 | 3 | #include "../include/trollworks.hpp" 4 | 5 | using namespace entt::literals; 6 | 7 | struct world { 8 | int foo{0}; 9 | float bar{0.0f}; 10 | }; 11 | 12 | static world w = world{}; 13 | 14 | struct foo { 15 | void operator()(tw::ui::hooks& h) { 16 | auto& i = h.use_state(42); 17 | w.foo = i; 18 | i = 24; 19 | } 20 | }; 21 | 22 | struct bar { 23 | void operator()(tw::ui::hooks& h) { 24 | auto& f = h.use_state(42.0f); 25 | w.bar = f; 26 | f = 24.0f; 27 | } 28 | }; 29 | 30 | struct root { 31 | void operator()(tw::ui::hooks& h) { 32 | h.render(0); 33 | if (w.foo == 42) { 34 | h.render(1); 35 | } 36 | h.render(2); 37 | } 38 | }; 39 | 40 | TEST_CASE("ui component tree") { 41 | tw::ui::h(0); 42 | CHECK(w.foo == 42); 43 | CHECK(w.bar == 42.0f); 44 | 45 | tw::ui::h(0); 46 | CHECK(w.foo == 24); 47 | CHECK(w.bar == 24.0f); 48 | } 49 | --------------------------------------------------------------------------------