Freecam and Custom Systems
How to build a custom component, load it from scene JSON, and run a system, based on Nevo-base Freecam.
Source Example
The reference implementation lives in:
Nevo-base/game/systems/freecam/Freecam.hpp
Nevo-base/game/systems/freecam/FreecamSystem.hpp
Nevo-base/src/main.cpp
Nevo-base/game/levels/physics.jsonIt demonstrates the full game-side pattern:
- Define a plain C++ component.
- Write a JSON creator function.
- Register the scene component name before
engine.Initialize(). - Register ECS component storage after
engine.Initialize(). - Register and initialize a system.
- Set the system signature.
- Add the component in scene JSON.
Component
#pragma once
struct Freecam {
float speed = 5.0f;
float sensitivity = 0.1f;
float yaw = 0.0f;
float pitch = 0.0f;
bool synced = false;
};Components are data only. The behavior belongs in the system.
Scene Creator
inline void CreateFreecamComponent(Public::ECS::EntityID entity, const json& data) {
Freecam comp{};
comp.sensitivity = data.value("sensitivity", comp.sensitivity);
comp.speed = data.value("speed", comp.speed);
comp.yaw = data.value("yaw", comp.yaw);
comp.pitch = data.value("pitch", comp.pitch);
Public::Engine::getInstance().GetCoordinator().addComponent(entity, comp);
}The sample currently uses direct data["key"].get<float>(); using value is safer for hand-authored scenes.
Register in main.cpp
auto& engine = Public::Engine::getInstance();
engine.RegisterComponent("Freecam", CreateFreecamComponent);
engine.Initialize();
auto& coord = engine.GetCoordinator();
coord.registerComponent<Freecam>();
auto freecamSystem = coord.registerSystem<FreecamSystem>();
freecamSystem->Initialize(coord.getComponentManager(), &engine.GetInputManager(), &engine);
Public::ECS::ComponentSignature signature;
signature.set(coord.getComponentType<Public::ECS::Components::Transform>());
signature.set(coord.getComponentType<Public::ECS::Components::Camera>());
signature.set(coord.getComponentType<Freecam>());
coord.setSystemSignature<FreecamSystem>(signature);Engine::RegisterComponent and Coordinator::registerComponent are separate operations. You need both if the component appears in scene JSON.
Scene JSON
From the physics.json pattern:
{
"id": 1,
"components": [
{
"type": "Transform",
"data": {
"position": [0.0, 2.0, -8.0],
"rotation": [0.0, 0.0, 0.0, 1.0],
"scale": [1.0, 1.0, 1.0]
}
},
{
"type": "Camera",
"data": {
"fov": 60.0,
"aspectRatio": 2.125,
"nearPlane": 0.1,
"farPlane": 1000.0
}
},
{
"type": "Freecam",
"data": {
"pitch": 0.0,
"sensitivity": 0.001,
"speed": 15.0,
"yaw": 0.0
}
}
]
}The entity matches the Freecam system only because it has Transform, Camera, and Freecam.
System Behavior
The Freecam system:
- toggles cursor lock with
F1 - ignores input when the window is not focused
- uses mouse delta for yaw/pitch
- clamps pitch just under vertical
- keeps quaternion hemisphere stable
- moves with WASD/QE in camera-local space
Core input pattern:
if (inputManager->GetKeyDown(Public::Input::Key::Code::F1)) {
lock = !lock;
}
if (lock) {
inputManager->LockCursor();
} else {
inputManager->UnlockCursor();
inputManager->ShowCursor();
}
if (!inputManager->IsWindowFocused() || !lock) {
return;
}Movement Pattern
Eigen::Vector3f moveDir = Eigen::Vector3f::Zero();
if (input.IsKeyDown(Public::Input::Key::Code::W)) moveDir.z() += 1.0f;
if (input.IsKeyDown(Public::Input::Key::Code::S)) moveDir.z() -= 1.0f;
if (input.IsKeyDown(Public::Input::Key::Code::A)) moveDir.x() -= 1.0f;
if (input.IsKeyDown(Public::Input::Key::Code::D)) moveDir.x() += 1.0f;
if (input.IsKeyDown(Public::Input::Key::Code::Q)) moveDir.y() -= 1.0f;
if (input.IsKeyDown(Public::Input::Key::Code::E)) moveDir.y() += 1.0f;
if (moveDir.squaredNorm() > 0.0f) {
moveDir.normalize();
moveDir *= freecam.speed * dt;
transform.position = Eigen::Vector3f(transform.position) + (rotation * moveDir);
}Normalize the direction before multiplying by speed so diagonals are not faster.
Save/Editor Note
Runtime-only editor components such as Freecam are filtered out of saved editor scenes. For game-authored scenes, custom components remain valid as long as the game registers their creators.
