How to Implement Custom Camera Behaviors Within the Camera_System in Equilibrium Engine
You implement custom camera behaviors within the camera_system by defining new ECS components to store behavior parameters and creating systems that run in the PostUpdate phase to modify the Camera component's position and view matrices after the default input handling completes.
The Equilibrium Engine leverages a data-oriented Entity-Component-System (ECS) architecture via flecs, allowing you to implement custom camera behaviors within the camera_system without modifying core engine code. Because the default logic lives entirely inside equilibrium/systems/scene/camera_system.c and operates on raw data components, you can layer orbital controls, smooth follow mechanics, or cinematic cuts by introducing new components and scheduling additional systems.
Understanding the Camera System Architecture
Before extending the system, you need to understand how the default implementation processes camera data each frame.
Component Registration and Data Layout
The Camera component is defined as plain data in equilibrium/components/scene/scene_components.h (lines 9–12). It stores position, rotation, FOV, and view/projection matrices. The SceneComponentsImport function registers this component with the ECS world using ECS_COMPONENT_DECLARE(Camera), making it queryable by any system.
System Phases and Execution Order
In equilibrium/systems/scene/camera_system.c (lines 77–90), the CameraSystemImport function registers two critical elements:
- An observer called
InitializeCamerathat triggers onEcsOnSetwhen an entity has bothAppWindowandCameracomponents (lines 14–60) - A system called
UpdateCamerathat runs on theOnInputphase and processes keyboard/mouse input to update rotation and position (lines 62–174)
The ECS resolves execution order by phase tags. Because UpdateCamera runs on OnInput, you can implement custom camera behaviors within the camera_system by creating systems that run on PostUpdate (or use after = UpdateCamera dependencies), ensuring your logic executes after the default fly-around controller.
The Default Update Loop
The built-in UpdateCamera system queries [in] gui.components.AppWindow, scene.components.Camera alongside Input components. It writes to camera->position and camera->rotation, then relies on downstream renderer logic (or a dedicated matrix update system) to recalculate camera->view and camera->proj. This separation means any custom system simply needs to overwrite these fields before the renderer consumes them.
Step-by-Step Implementation Guide
To implement custom camera behaviors within the camera_system, follow this pattern of component definition and system registration.
Step 1: Define the Behavior Component
Create a struct that stores parameters specific to your behavior. For example, a third-person follow camera needs a target entity and offset distances.
/* file: equilibrium/components/scene/scene_components.h */
typedef struct FollowTarget {
ecs_entity_t target; /* Entity whose Transform to follow */
float distance; /* Distance behind the target */
float height; /* Vertical offset */
} FollowTarget;
/* Declare the component handle */
EQUILIBRIUM_API extern ECS_COMPONENT_DECLARE(FollowTarget);
Place this definition near the existing Camera struct (around line 9) to maintain consistency with the engine's component organization.
Step 2: Register the Component
Inside the SceneComponentsImport function in the same file, register the new component so the ECS recognizes it:
void SceneComponentsImport(ecs_world_t *world) {
ECS_MODULE(world, SceneComponents);
/* Existing registrations */
ECS_COMPONENT_DECLARE(Camera);
/* New registration */
ECS_COMPONENT_DECLARE(FollowTarget);
/* ... rest of imports ... */
}
Step 3: Create the Custom System
Create a new file (e.g., equilibrium/systems/scene/follow_camera_system.c) that implements your logic. The system queries the Camera component alongside your custom component and any required dependencies like Transform.
/* file: equilibrium/systems/scene/follow_camera_system.c */
#include "components/scene/scene_components.h"
#include "components/transform.h"
#include <cglm/cglm.h>
static void FollowCamera(ecs_iter_t *it) {
Camera *cam = ecs_field(it, Camera, 2);
FollowTarget *ft = ecs_field(it, FollowTarget, 3);
Transform *tr = ecs_field(it, Transform, 4); /* Target's transform */
for (int i = 0; i < it->count; i++) {
/* Calculate position behind target using target's rotation */
vec3 forward;
glm_quat_rotatev(tr[i].rotation, GLM_ZUP, forward);
vec3 offset;
glm_vec3_scale(forward, -ft[i].distance, offset);
offset[1] = ft[i].height;
glm_vec3_add(tr[i].position, offset, cam[i].position);
/* Orient camera to look at target */
glm_lookat(cam[i].position, tr[i].position, GLM_YUP, cam[i].view);
}
}
Step 4: Schedule System Execution
Register the system in a phase that runs after the default camera update. Using PostUpdate ensures the follow behavior overrides the default fly-around input handling from camera_system.c.
void FollowCameraSystemImport(ecs_world_t *world) {
ECS_MODULE(world, FollowCameraSystem);
ECS_IMPORT(world, SceneComponents);
ECS_IMPORT(world, TransformComponents);
/* Run in PostUpdate to override UpdateCamera's results */
ECS_SYSTEM(world, FollowCamera, PostUpdate,
[in] gui.components.AppWindow,
scene.components.Camera,
scene.components.FollowTarget,
[in] scene.components.Transform);
}
Step 5: Import the System
In your main entry point (e.g., equilibrium/main.c), import your custom system after CameraSystemImport to ensure all component types are registered before the system queries are built.
/* file: equilibrium/main.c */
int main(int argc, char *argv[]) {
ecs_world_t *world = ecs_init_w_args(argc, argv);
CameraSystemImport(world); /* Default system (lines 77-90) */
FollowCameraSystemImport(world); /* Custom behavior */
while (ecs_progress(world, 0)) { }
return ecs_fini(world);
}
Complete Working Example: Third-Person Follow Camera
Here is the complete integration showing entity setup. This example attaches a follow camera to a player entity:
/* Spawn player with Transform */
ecs_entity_t player = ecs_new(world);
ecs_set(world, player, Transform, {
.position = {0.0f, 0.0f, 0.0f},
.rotation = {0.0f, 0.0f, 0.0f, 1.0f}
});
/* Create camera entity with both Camera and FollowTarget */
ecs_entity_t cam = ecs_new(world);
ecs_set(world, cam, Camera, {
.fov = 60.0f,
.near = 0.1f,
.far = 1000.0f
});
ecs_set(world, cam, FollowTarget, {
.target = player,
.distance = 5.0f,
.height = 2.0f
});
/* Optional: Establish hierarchy */
ecs_add_pair(world, cam, EcsChildOf, player);
Now the camera automatically stays 5 units behind the player at a height of 2 units. Because the FollowCamera system runs in PostUpdate, it completely overrides the default input-driven movement from UpdateCamera (lines 62–174) while preserving the ability to react to window events via the AppWindow component.
Summary
- Component-based architecture: The Equilibrium Engine stores camera data in plain C structs (
Camerainscene_components.h) separate from behavior logic, enabling easy extension. - Phase scheduling: Implement custom camera behaviors within the camera_system by registering systems in the
PostUpdatephase (or usingafter = UpdateCamera) to ensure they execute after the default input handling incamera_system.c(lines 77–90). - No core modifications required: You never need to edit
camera_system.c; simply define new components, query them alongsideCamera, and write tocamera->positionandcamera->viewdirectly. - Matrix recalculation: Always recompute the view matrix (using
glm_lookator similar) after modifying position/rotation, as the renderer readscamera->viewandcamera->projduring the render phase.
Frequently Asked Questions
How do I disable the default fly-around controls for specific cameras?
Add your custom system with an after = UpdateCamera dependency in the OnInput phase, or simply run in PostUpdate and overwrite the position/rotation values. Because the default UpdateCamera (lines 62–174) only writes data without side effects, your subsequent system can completely replace the results without "disabling" the original system globally.
Can I combine multiple custom behaviors on the same camera entity?
Yes. You can attach multiple custom components (e.g., FollowTarget, CameraShake, ZoomConstraint) to a single camera entity and create separate systems for each. Use explicit before/after dependencies in the ECS_SYSTEM macro to define the order of operations (e.g., apply follow, then shake, then clamp).
Where should I place my custom system files?
Follow the existing directory convention: place component definitions in equilibrium/components/scene/ and system implementations in equilibrium/systems/scene/. Mirror the pattern seen in camera_system.c and camera_system.h to maintain consistency with the engine's modular architecture.
Do I need to recompute the projection matrix in my custom system?
No, unless you are modifying fov, near, or far planes. The projection matrix (camera->proj) typically remains static and is calculated during initialization (or by a dedicated matrix-update system). Your custom behavior only needs to update camera->position, camera->rotation, and camera->view using functions like glm_lookat from cglm.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →