C++
The two main ways of writing game logic is either in C++ or through scripting. Scripting is an easy way of iterating quickly while modules written in C++ is more suitable for computationally heavy tasks.
Hiber3D modules are written following the ECS pattern and the Hiber3D implementation is based off the entt library .
main.cpp
After creating a new project, there should be a file called main.cpp
that looks something like this:
class MainModule : public Hiber3D::Module {
public:
void onRegister(Hiber3D::InitContext& context) override {
// Registers all Hiber3D modules
context.registerModule<Hiber3D::Hiber3DModule>();
// Extends a Hiber3D module (with a Hiber3D type)
context.getModule<Hiber3D::JavaScriptScriptingModule>().registerComponent<Hiber3D::ExternalImpulse>(context);
// Registers a custom module
context.registerModule<MyModule>();
}
};
Hiber3D modules
Hiber3D supplies a wide range of optional modules, available for you to pick-and-choose from. They can be found under the Hiber3D/ directory, such as <Hiber3D/Physics/PhysicsModule.hpp>
. By default, the Hiber3DModule bundle registers the most commonly used Hiber3D modules.
If you want full control of what Hiber3D modules to include in your project, you can replace this universal Hiber3DModule with explicitly listing what modules you want to register. This allows you to register the modules with customized settings like so:
context.registerModule<Hiber3D::LogModule>(Hiber3D::LogSettings{.logLevel = Hiber3D::LogLevel::INFO});
context.registerModule<Hiber3D::SceneManagerModule>(Hiber3D::SceneManagerSettings{.defaultScene = "scenes/RoboRun.scene"});
Custom modules
To register a custom module, you need to:
- Add your C++ files to the
CMakeList.txt
- Register the new module in an already registered module (like
main.cpp
)
C++ types
Conceptually, there are three main C++ types: components, events, and singletons. These are at their core just normal C++ structs: they are not mutually exlusive and can be used interchangeably.
Components
A component represent a modular characteristic that can be added to entities in your scene. Components can be emplaced, mutated, or removed either through the editor using the entity inspector or at runtime in code.
E.g. Health, Jumping, Player, Gun, Npc, Particle, etc.
Events
The event bus is the way to access the interop layer: the main way of communicating between web and game code, see Web Integration - Interop events.
E.g. GunFired, HealthChanged, PlayerDied, GameOver, etc.
Singletons
Singletons are globally accessable data that does not exist tied to any specific entity.
E.g. Player, Inventory, GameState, ScreenState, Weather, etc.
Note: Singletons always need to be registered:
context.registerSingleton<MySingleton>();
Systems
Writing systems is the main way of creating your game loop.
The same system can be added multiple times, and with different schedules.
System schedules
Systems can be configured to run at different system schedules.
Some of the most common ones are:
ON_START
- runs once, when entering play modeON_TICK
- for “normal” game logicON_FRAME
- for rendering specific systems
Each schedule also has a _EDIT
version, e.g. ON_TICK_EDIT
, for running in edit mode.
For a comprehensive list, see: <Hiber3D/Core/SystemTypes.hpp>
.
Custom modules examples
Here are some C++ examples showing various ways of modelling gameplay:
.hpp
#pragma once
#include <Hiber3D/Core/InitContext.hpp>
#include <Hiber3D/Hiber3D.hpp>
class MyModule : public Hiber3D::Module {
public:
void onRegister(Hiber3D::InitContext& context) override;
};
.cpp - component view
// Components
struct Rotate{
Hiber3D::float3 axis{0.0f, 1.0f, 0.0f};
float speed = PI * 0.5f; // radians per second
};
// Systems
void rotate(
Hiber3D::View<const Rotate, Hiber3D::Transform> entityView){
for(auto& [entity, rotate, transform] : entityView.each()){
transform.rotation.rotateAroundAxis(axis, speed);
}
}
void MyModule::onRegister(Hiber3D::InitContext& context) {
context.addSystem(Hiber3D::Schedule::ON_TICK, rotate);
}
.cpp - events
// Events
struct HealthChanged{
Hiber3D::Entity entity;
float healthChange;
};
struct EntityDied{
Hiber3D::Entity entity;
};
// Components
struct Health{
float health = 100.0f;
bool isAlive(){
return health > 0.0f;
}
};
// Systems
void handleHealthChanges(
Hiber3D::EventView<HealthChanged> healthChangedEvents,
Hiber3D::View<Health> entityView,
Hiber3D::EventWriter<EntityDied>& entityDiedWriter){
for(const auto& event : healthChangedEvents){
entityView.withComponent(event.entity, [&](Health& health) {
const auto wasAlive = health.isAlive();
health.health += healthChange;
const auto isAlive = health.isAlive();
if(wasAlive && !isAlive)){
entityDiedWriter.writeEvent({.entity = entity});
}
});
}
}
void MyModule::onRegister(Hiber3D::InitContext& context) {
context.addSystem(Hiber3D::Schedule::ON_TICK, handleHealthChanges);
}
.cpp - Singleton, ViewWithExclude, ViewWithFilter
// Singletons
struct MyKeys{
int jumpKey = 1;
};
// Components
struct StartJumpingOnInput{
};
struct Jumping{
Hiber3D::Time startTime;
};
// Systems
void handleStartJumpingOnInput(
Hiber3D::Registry& registry,
Hiber3D::Singleton<KeyboardState> myKeys,
Hiber3D::Singleton<KeyboardState> keyboardState,
Hiber3D::ViewWithExclude<Hiber3D::Get<const StartJumpingOnInput>, Hiber3D::Exclude<Jumping>> entityView){
for(const auto& entity : entityView){
if (keyboardState.justPressed(myKeys.jumpKey)) {
registry.emplace<Jumping>(entity, Jumping{.startTime = Hiber3D::Time::now()});
}
}
}
void handleJumpingAdded(
Hiber3D::ViewWithFilter<Hiber3D::Get<Entity>, Hiber3D::Filter<Hiber3D::Added<Jumping>>> entityView){
// do something when starting to jump, like trigger animation or audio
}
void MyModule::onRegister(Hiber3D::InitContext& context) {
context.registerSingleton<MyKeys>();
context.addSystem(Hiber3D::Schedule::ON_TICK, handleStartJumpingOnInput);
context.addSystem(Hiber3D::Schedule::ON_TICK, handleJumpingAdded);
}
C++ registration
You can extend any module yourself from your C++ code, allowing you to expose new types and functions.
Type registration (components, events, & singletons)
Here is an example of how you can register custom types to a module:
void MyModule::onRegister(Hiber3D::InitContext& context) {
// Registering your types to the EditorModule makes them appear in the editor
if (context.isModuleRegistered<Hiber3D::EditorModule>()) {
context.getModule<Hiber3D::EditorModule>().registerComponent<MyComponent>(context);
}
// Registering your types to the JavaScriptScriptingModule makes them available while scripting
if (context.isModuleRegistered<Hiber3D::JavaScriptScriptingModule>()) {
context.getModule<Hiber3D::JavaScriptScriptingModule>().registerComponent<MyComponent>(context);
context.getModule<Hiber3D::JavaScriptScriptingModule>().registerEvent<MyEvent>(context);
context.getModule<Hiber3D::JavaScriptScriptingModule>().registerSingleton<MySingleton>(context);
}
// Registering your types to the SceneModule makes them savable in scenes
if (context.isModuleRegistered<Hiber3D::SceneModule>()) {
context.getModule<Hiber3D::SceneModule>().registerComponent<MyComponent>(context);
}
}
For the types to be serialized correctly, you need to ensure you reflect these properly like so:
#include <Hiber3D/Interop/Defines.hpp>
struct MyType {
Hiber3D::Entity entity;
std::string name;
bool loop = true;
};
HIBER3D_REFLECT(HIBER3D_TYPE(MyType), HIBER3D_MEMBER(entity), HIBER3D_MEMBER(name), HIBER3D_MEMBER(loop));
Note: Nested types need to be reflected as well.
Function registration
You can register C++ functions as well to the JavaScriptScriptingModule, allowing you to re-use functionality across C++ and the scripting world. Additionally, C++ is more performant than scripting, allowing you to offload some of the more performance impacting computations.
context.getModule<Hiber3D::JavaScriptScriptingModule>().registerFunction<[](int myInput) { return myFunction(myInput); }>(context, "myFunction");
context.getModule<Hiber3D::JavaScriptScriptingModule>().registerFunction<[](const Hiber3D::Registry& registry, int someNumber) { return registry.singleton<const Hiber3D::SomeSingleton>().someFunction(someNumber); }>(context, "someFunction");
then later in your scripts:
const resultFromMyFunction = hiber3d.call("myFunction", 1);
const resultFromSomeFunction = hiber3d.call("someFunction", 1);