Skip to content

Latest commit

 

History

History
375 lines (277 loc) · 7.77 KB

README.md

File metadata and controls

375 lines (277 loc) · 7.77 KB

Trollworks SDK Core

tests license version

Trollworks is an (unfinished) game engine in C++ I've been working on for a while.

This repository contains the basis of the SDK, as a header-only C++23 library. It is built around EnTT, an ECS library.

This library provides:

  • a game loop abstraction (with fixed updates, updates and late updates)
  • some EnTT utilities as singletons:
    • asset manager
    • event dispatcher
    • scene manager
    • job manager
  • a UI component framework inspired by React to help organize immediate-mode UI code
  • coroutines inspired by Unity coroutines

List of backends:

Name 2D/3D URL
SDL2 2D https://github.com/trollworks/sdk-backend-sdl
raylib 2D https://github.com/trollworks/sdk-backend-raylib (TODO)
glfw 3D https://github.com/trollworks/sdk-backend-glfw (TODO)

Installation

Clone the repository in your project (don't forget to pull the submodules) and add the include/ folder to your include paths.

Or, if you are using Shipp, add to your dependencies:

{
  "dependencies": [
    {
      "name": "trollworks-sdk-core",
      "url": "https://github.com/trollworks/sdk-core.git",
      "version": "v0.3.0"
    }
  ]
}

Usage

Game loop

#include <trollworks.hpp>

struct game_state {
  // ...
};

struct listener {
  game_state& gs;

  void on_setup(tw::controlflow& cf) {
    // create window and opengl context, load resources
  }

  void on_teardown() {
    // free resources, opengl context and window
  }

  void on_frame_begin(tw::controlflow& cf) {
    // process window events and inputs
  }

  void on_fixed_update(float delta_time, tw::controlflow& cf) {
    // physics updates
  }

  void on_update(float delta_time, tw::controlflow& cf) {
    // game logic
  }

  void on_late_update(float delta_time, tw::controlflow& cf) {
    // more game logic
  }

  void on_render() {
    // render graphics
  }

  void on_frame_end(tw::controlflow& cf) {
    // ...
  }
};

int main() {
  auto gs = game_state{};
  auto l = listener{.gs = gs};
  auto loop = tw::game_loop{};

  loop
    .with_fps(60)
    .with_ups(50)
    .on_setup<&listener::on_setup>(l)
    .on_teardown<&listener::on_teardown>(l)
    .on_frame_begin<&listener::on_frame_begin>(l)
    .on_frame_end<&listener::on_frame_end>(l)
    .on_update<&listener::on_update>(l)
    .on_fixed_update<&listener::on_fixed_update>(l)
    .on_late_update<&listener::on_late_update>(l)
    .on_render<&listener::on_render>(l)
    .run();

  return 0;
}

NB: A concept backend_trait is also provided to facilitate pluging in a specific window/event/render system (SDL, raylib, glfw, ...):

struct sdl_backend {
  SDL_Window *window;
  SDL_Renderer *renderer;

  void setup(tw::controlflow& cf) {
    // create window, opengl context, ...
  }

  void teardown() {
    // free opengl context and window
  }

  void poll_events(tw::controlflow& cf) {
    // process event, for example with SDL:
    SDL_Event evt;

    while (SDL_PollEvents(&evt)) {
      switch (evt.type) {
        case SDL_QUIT:
          cf = tw::controlflow::exit;
          break;

        default:
          // ...
          break;
      }
    }
  }

  void render() {
    // get current scene's entity registry
    auto& registry = tw::scene_manager::main().registry();

    SDL_RenderClear(renderer);

    // iterate over your entities to draw them

    // render UI with imgui, or nuklear, or other

    SDL_RenderPresent(renderer);
  }
};

int main() {
  auto back = sdl_backend{};
  auto loop = tw::game_loop{};

  loop
    .with_fps(60)
    .with_ups(50)
    .with_backend(back)
    .run();

  return 0;
}

Coroutines

First, create your coroutine function:

tw::coroutine count(int n) {
  for (auto i = 0; i < n; i++) {
    co_yield tw::coroutine::none{};
  }
}

Then, in your game loop:

tw::coroutine_manager::main().start_coroutine(count(5));

Coroutines are run after the update hook and before the late update hook.

Coroutines can also be chained, like in Unity:

tw::coroutine count2(int a, int b) {
  co_yield count(a);
  co_yield count(b);
}

Scene management

A scene is a class providing 2 methods (load and unload):

class my_scene final : public tw::scene {
  public:
    virtual void load(entt::registry& registry) override {
      // create entities and components
    }

    virtual void unload(entt::registry& registry) override {
      // destroy entities and components
    }
};

Then:

tw::scene_manager::main().load(my_scene{});

Assets

The asset manager provides a singleton per asset type. The singleton is simply a resource cache from EnTT, for more information consult this page.

struct my_asset {
  // ...

  using resource_type = my_asset;

  struct loader_type {
    using result_type = std::shared_ptr<resource_type>;

    result_type operator()(/* ... */) const {
      // ...
    }
  };
};

auto& cache = tw::asset_manager<my_asset>::cache();

NB: The resource_type type name may seem redundant, but it is there for assets that loads the same type of resources, consider the following example:

struct spritesheet {
  // ...
};

struct aseprite_sheet {
  using resource_type = spritesheet;

  struct loader_type {
    using result_type = std::shared_ptr<resource_type>;

    result_type operator()(/* ... */) const {
      // ...
    }
  };
};

struct texturepacker_sheet {
  using resource_type = spritesheet;

  struct loader_type {
    using result_type = std::shared_ptr<resource_type>;

    result_type operator()(/* ... */) const {
      // ...
    }
  };
};

Both aseprite_sheet and texturepacker_sheet assets will return a spritesheet resource:

auto [it, loaded] = tw::asset_manager<aseprite_sheet>::cache().load(/* ... */);
auto [id, sheet] = *it;
// sheet is entt::resource<spritesheet>
auto [it, loaded] = tw::asset_manager<texturepacker_sheet>::cache().load(/* ... */);
auto [id, sheet] = *it;
// sheet is entt::resource<spritesheet>

Messaging

The message bus is simply a singleton returning an entt::dispatcher. For more information, please consult this page.

auto& dispatcher = tw::message_bus::main();

Queued messages are dispatched after the late update hook an before rendering.

Jobs

The job manager is simply a singleton returing an entt::basic_scheduler<float>. For more information, please consult this page.

UI framework

using namespace entt::literals;

struct foo {
  void operator()(tw::ui::hooks& h) {
    auto& local_state = h.use_state<int>(0);

    // gui code
  }
};

struct bar {
  void operator()(tw::ui::hooks& h) {
    auto& local_state = h.use_state<float>(0.0f);

    // gui code
  }
};

struct root {
  bool condition;

  void operator()(tw::ui::hooks& h) {
    h.render<foo>("a"_hs);

    if (condition) {
      h.render<foo>("b"_hs);
    }

    h.render<bar>("c"_hs);
  }
};

Then in your render hook:

tw::ui::h<root>("root"_hs, root{.condition = true});

The ids given to the UI hooks must be unique within the component, not globally.

License

This project is released under the terms of the MIT License.