Camera Scripting
Headers: Core/include/Core/Components.h, Core/include/Core/CameraUtils.h
Camera Component
The Camera component defines a viewpoint for rendering:
Setting Up a Camera
- Create an entity in the Scene Hierarchy
- Add a Transform component (position and rotation)
- Add a Camera component
- Check isMain to make it the active camera
When you press Play, the engine switches from the editor camera to the entity marked as isMain.
Multiple Cameras
You can have multiple camera entities in a scene. Only one should have isMain = true at a time. Switch between them in code:
void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
auto &input = engine.GetInput();
if (input.IsKeyPressed(GLFW_KEY_1)) {
auto cam1 = engine.GetWorld().lookup("MainCamera");
engine.SetMainCamera(cam1);
}
if (input.IsKeyPressed(GLFW_KEY_2)) {
auto cam2 = engine.GetWorld().lookup("CinematicCamera");
engine.SetMainCamera(cam2);
}
}
Camera API
CameraUtils
The CameraUtils subsystem provides helper functions for building any camera behavior through scripts. Access it via engine.GetCameraUtils().
These operate on any Transform and are useful for positioning and rotating cameras:
Camera Helpers
Direction Vectors
Ray Struct
ScreenToWorld returns a Ray:
struct Ray {
glm::vec3 origin; // Near plane intersection
glm::vec3 direction; // Normalized direction into the scene
};
Writing Camera Behaviors
All camera behaviors are written as scripts using the Behavior base class and CameraUtils helpers. There is no built-in follow camera — you build exactly the behavior you need.
Third-Person Follow Camera
class ThirdPersonCamera : public Behavior {
public:
void OnStart(flecs::entity e, Engine &engine) override {
const auto *ns = e.get<NativeScript>();
if (ns) {
auto it = ns->properties.find("target");
if (it != ns->properties.end())
m_TargetName = it->second;
it = ns->properties.find("smoothing");
if (it != ns->properties.end())
m_Smoothing = std::atof(it->second.c_str());
}
}
void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
auto target = engine.GetWorld().lookup(m_TargetName.c_str());
if (!target.is_valid() || !target.has<Transform>())
return;
auto *cam = e.get_mut<Transform>();
const auto *tgt = target.get<Transform>();
auto &utils = engine.GetCameraUtils();
// Smoothly follow behind and above the target
glm::vec3 desired = tgt->position + glm::vec3(0.0f, 5.0f, 8.0f);
utils.SmoothFollow(*cam, desired, m_Smoothing, dt);
// Smoothly look at the target
utils.SmoothLookAt(*cam, tgt->position, m_Smoothing * 2.0f, dt);
}
private:
std::string m_TargetName = "Player";
float m_Smoothing = 5.0f;
};
Orbit Camera
class OrbitCamera : public Behavior {
public:
void OnStart(flecs::entity e, Engine &engine) override {
const auto *ns = e.get<NativeScript>();
if (ns) {
auto it = ns->properties.find("target");
if (it != ns->properties.end())
m_TargetName = it->second;
it = ns->properties.find("radius");
if (it != ns->properties.end())
m_Radius = std::atof(it->second.c_str());
}
}
void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
auto target = engine.GetWorld().lookup(m_TargetName.c_str());
if (!target.is_valid() || !target.has<Transform>())
return;
auto *cam = e.get_mut<Transform>();
const auto *tgt = target.get<Transform>();
auto &input = engine.GetInput();
auto &utils = engine.GetCameraUtils();
// Mouse controls orbit
glm::vec2 delta = input.GetMouseDelta();
m_Yaw += delta.x * 0.3f;
m_Pitch = glm::clamp(m_Pitch - delta.y * 0.3f, -80.0f, 80.0f);
// Scroll controls distance
m_Radius = glm::clamp(m_Radius - input.GetScrollDelta() * 2.0f,
2.0f, 50.0f);
utils.Orbit(*cam, tgt->position, m_Radius, m_Yaw, m_Pitch);
}
private:
std::string m_TargetName = "Player";
float m_Radius = 10.0f;
float m_Yaw = 0.0f;
float m_Pitch = 20.0f;
};
First-Person Camera
class FirstPersonCamera : public Behavior {
public:
void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
auto *cam = e.get_mut<Transform>();
auto &input = engine.GetInput();
auto &utils = engine.GetCameraUtils();
// Mouse look
glm::vec2 delta = input.GetMouseDelta();
cam->rotation.y += delta.x * m_Sensitivity;
cam->rotation.x = glm::clamp(cam->rotation.x - delta.y * m_Sensitivity,
-89.0f, 89.0f);
// WASD movement relative to camera direction
glm::vec3 forward = utils.GetForward(*cam);
glm::vec3 right = utils.GetRight(*cam);
glm::vec3 move(0.0f);
if (input.IsKeyDown(GLFW_KEY_W)) move += forward;
if (input.IsKeyDown(GLFW_KEY_S)) move -= forward;
if (input.IsKeyDown(GLFW_KEY_D)) move += right;
if (input.IsKeyDown(GLFW_KEY_A)) move -= right;
if (glm::length(move) > 0.001f)
cam->position += glm::normalize(move) * m_Speed * dt;
}
private:
float m_Speed = 5.0f;
float m_Sensitivity = 0.15f;
};
Camera Shake on Impact
class PlayerWithShake : public Behavior {
public:
void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
auto &utils = engine.GetCameraUtils();
// Trigger shake on landing or collision
if (m_JustLanded) {
utils.Shake(0.3f, 0.4f); // intensity 0.3, duration 0.4s
m_JustLanded = false;
}
}
private:
bool m_JustLanded = false;
};
Zoom / Scope Effect
void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
auto *cam = e.get_mut<Camera>();
auto &input = engine.GetInput();
auto &utils = engine.GetCameraUtils();
float targetFov = input.IsKeyDown(GLFW_KEY_Z) ? 20.0f : 45.0f;
utils.SetZoom(*cam, targetFov, 8.0f, dt);
}
Cinematic Camera Switcher
Switch between cameras on a timer for cutscenes:
class CinematicDirector : public Behavior {
public:
void OnStart(flecs::entity e, Engine &engine) override {
m_Timer = 0.0f;
auto cam1 = engine.GetWorld().lookup("CineCam1");
engine.SetMainCamera(cam1);
}
void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
m_Timer += dt;
if (m_Timer > 5.0f && m_CurrentCam == 0) {
auto cam2 = engine.GetWorld().lookup("CineCam2");
engine.SetMainCamera(cam2);
m_CurrentCam = 1;
}
if (m_Timer > 10.0f && m_CurrentCam == 1) {
auto cam3 = engine.GetWorld().lookup("CineCam3");
engine.SetMainCamera(cam3);
m_CurrentCam = 2;
}
}
private:
float m_Timer = 0.0f;
int m_CurrentCam = 0;
};
Editor Features
Frustum Gizmo
In edit mode, every camera entity displays a wireframe frustum showing its field of view, near/far planes, and forward direction. This helps you position cameras in the scene.
Inspector Preview
When you select a camera entity, the Inspector shows a live preview thumbnail of what that camera sees. This updates in real-time as you adjust the camera's Transform or properties.