I’ve been thinking about this for quite a while and I have narrowed it down to two approaches that can scale to larger games. I wrote a simplified example showing both. Feel free to give me feedback:
// nested struct component approach
// easy to reuse procedures
Transform :: struct {
translation: [3]f32,
orientation: quaternion128,
scale: [3]f32,
}
Frustum :: struct {
planes: [6][4]f32,
focal_length: f32,
near: f32,
far: f32,
}
Camera :: struct {
transform: Transform,
frustum: Frustum,
render_target: rawptr,
}
Light :: struct {
camera: Camera,
intensity: f32,
}
Static_Mesh :: struct {
transform: Transform,
aabb: [2][3]f32,
texture: rawptr,
// add mesh data here
}
apply_translation :: proc(self: ^[3]f32, translation: [3]f32) {
self^ += translation
// super contrived example, don't need a proc for this
}
apply_transform :: proc(self: ^Transform, transform: Transform) {
// put you fancy matrix math here
}
update_frustum :: proc(frustum: ^Frustum, transform: Transform) {
// compute new frustum planes using transform
}
set_intensity :: proc(light: ^Light, intensity: f32) {
scale := intensity / light.intensity
light.intensity = intensity
c := light.camera
c.frustum.far *= scale
update_frustum(&c.frustum, c.transform)
}
transform_light :: proc(light: ^Light, transform: Transform) {
transform_camera(&light.camera, transform)
}
transform_camera :: proc(camera: ^Camera, transform: Transform) {
apply_transform(&camera.transform, transform)
update_frustum(&camera.frustum, camera.transform)
}
transform_mesh :: proc(mesh: ^Static_Mesh, transform: Transform) {
apply_transform(&mesh.transform, transform)
// do mesh stuff here like recalculate AABB
}
transform :: proc {
transform_camera,
transform_light,
transform_mesh,
}
update_example :: proc() {
cameras: [4]Camera
lights: [12]Light
meshes: [64]Static_Mesh
set_intensity(&lights[0], 35)
add_transform := Transform{}
for &camera in cameras do transform(&camera, add_transform)
for &light in lights do transform(&light, add_transform)
for &mesh in meshes do transform(&mesh, add_transform)
}
// Megastruct-like approach
// Using batches gives free compression
// Similar entities can be put in the same batch
// Any unused component slices can be nil
Entity_Batch :: struct {
// Meta
components: Component_Set,
// Transform
translation: [][3]f32,
orientation: []quaternion128,
scale: [][3]f32,
// Frustum
planes: [][6][4]f32,
focal_length: []f32,
near: []f32,
far: []f32,
//Camera
render_target: []rawptr,
// Light
intensity: []f32,
// Static Mesh
aabb: [][2][3]f32,
texture: []rawptr,
}
Component :: enum {
Transform,
Frustum,
Camera,
Light,
Static_Mesh,
}
Component_Set :: bit_set[Component;u64]
BATCH_SIZE :: 64
new_batch :: proc(components: Component_Set) -> ^Entity_Batch {
batch := new(Entity_Batch)
batch.components = components
if .Transform in components {
batch.translation = make([][3]f32, BATCH_SIZE)
batch.orientation = make([]quaternion128, BATCH_SIZE)
batch.scale = make([][3]f32, BATCH_SIZE)
}
if .Frustum in components {
batch.planes = make([][6][4]f32, BATCH_SIZE)
batch.focal_length = make([]f32, BATCH_SIZE)
batch.near = make([]f32, BATCH_SIZE)
batch.far = make([]f32, BATCH_SIZE)
}
if .Camera in components {
batch.render_target = make([]rawptr, BATCH_SIZE)
}
if .Light in components {
batch.intensity = make([]f32, BATCH_SIZE)
}
if .Static_Mesh in components {
batch.aabb = make([][2][3]f32, BATCH_SIZE)
batch.texture = make([]rawptr, BATCH_SIZE)
}
return batch
}
update_batch :: proc(batch: ^Entity_Batch) {
camera_set := Component_Set{.Transform, .Frustum, .Camera}
light_set := Component_Set{.Transform, .Frustum, .Camera, .Light}
mesh_set := Component_Set{.Transform, .Static_Mesh}
if camera_set <= batch.components && .Light not_in batch.components {
// update camera
}
if light_set <= batch.components {
// update light
}
if mesh_set <= batch.components {
// update static mesh
}
}