Examples

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.json

It demonstrates the full game-side pattern:

  1. Define a plain C++ component.
  2. Write a JSON creator function.
  3. Register the scene component name before engine.Initialize().
  4. Register ECS component storage after engine.Initialize().
  5. Register and initialize a system.
  6. Set the system signature.
  7. 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.