How can I design a basic game object structure the "Odin" way

Hellope!

I’m new to Odin and procedural programming. Like many others, I have been trained to think in a OOP way since when I started learning how to program. I want to ask this to help me understand the procedural way of designing and thinking. So in a typically OOP game engine I will have a class structure similar to this.

class GameObject {
    void Start() { ... }
    void Update() { ... }
}

class Character : GameObject {
    void Start() override { ... }
    void Update() override { ... }
}

class Vehicle : GameObject {
    void Start() override { ... }
    void Update() override { ... }
}

Then in a engine system I can call the Start and Update of all gameobjects this way

class GameplaySystem {
    void Gameloop {
        for obj in GetAllGameObjects() {
            if first_frame {
                obj.Start();
            }
            obj.Update();
        }
    }
}

This is over simplified of course. The good thing about this structure is that when I want to add a new type of GameObject in my game I can just inherent from GameObject base or any child class without needing to do much else, which makes the coding gameplay pretty straightforward.

I know I can use “using” keyword in Odin to get a similar functionally like the OOP example above but should I do that? Is there a better way of thinking this kind of problem from a more procedural way thinking?

I have also tried ECS and it feels a bit overkill for a really small indie game since I won’t have tons of objects in the level and it makes things a bit complicated to setup.

So any ideas would be great to hear since mainly I want to brainstorm and widen my horizon a bit.

Thanks.

3 Likes

The simpler option is to just keep them separate and not try to unify them.

So have a character, and then an array of vehicles. And iterate over those separate arrays.

It is unlikely that your game actually needs a very generic way of iterating over the entities/objects, and you really only have some basic things. This will make things a lot easier to maintain, and stop you trying to “abstract” away before it is necessary.

Doing the dumb thing allows you to see the actual patterns rather than applying premade “patterns” that might not work for your actual task at hand.

12 Likes

I recommend you to watch Casey Muoratori video on sparse entity system, you will undestand what inheritance is and many other stuff with that one video. Spoiler: Inheritance is just a compression algorithm. That video is a must watch at the level of Mike Acton Data oriente programming

8 Likes

Prefer composition over inheritance. You can also use the ECS (Entity-Component-Systems) pattern if you want (good for bigger projects especially with multiplayer).

If you want inheritance, Google how to do it in C because you can do the same in Odin. Inheritance in C++ or other languages is a hidden (implicit) composition on the data level, making it less flexible than explicit composition.

You can easily do OOP in C and Odin if you want. I enjoy OOP in C and Odin more than in OOP languages because of how everything is explicit and clear.

3 Likes

That makes a lot sense, thanks.

Yeah I should actually work work on the real gameplay features instead of focusing on a “one size fit all” solution.

1 Like

Thanks, will watch it, Casey is the one who made me realized that there is other ways of programming other than OOP in the first place.

1 Like

Interesting to know that people are doing OOP in C. Will take a look, thanks

1 Like

if you still want a “one size fits all” then you could use an uberstruct

Which is a struct that contains all the fields that the object might need. But some are left untouched based on a type tag.

That lets you put them all in a single array still.

1 Like

I see, sort of like the sparse entity system that Casey explained in the video above. I wonder if I can use the union type in Odin to make the flat struct a bit more compact. But maybe that will introduce an extra union type check and a dereference.

Don’t worry about making the struct compact, you’d get more flexibility without the union,

chances are there’s a bunch of (component-like) things that can be shared between objects that don’t mirror any kind of hierarchy. It’s better if those things are just shared without needing to special case each type

1 Like

That’s a really good point, yeah I can’t share variables between union variations which limits the flexibility.

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
	}

}
4 Likes