Skip to content

Instantly share code, notes, and snippets.

@nahuakang
Forked from randyprime/entity_structure.odin
Created May 3, 2025 10:35
Show Gist options
  • Save nahuakang/20f22457fd3c7c6b57b71267f0083c48 to your computer and use it in GitHub Desktop.
Save nahuakang/20f22457fd3c7c6b57b71267f0083c48 to your computer and use it in GitHub Desktop.
ENTITY MEGASTRUCT
// the tl;dr -> https://storage.randy.gg/entity%20midwit.png
/*
ENTITY MEGASTRUCT
by randy.gg
This is an extremely simple and flexible entity structure for video games that doesn't make you want to
die when you're 20k lines deep in a project.
pros:
- you never have to think about the ideal entity structure again and can get back to working on things that
actually matter (actually using it to add new entities and making your game better, instead of overthinking)
- has all of the reuse power of an Entity Component System (ECS)
- doesn't have the complexity of an ECS
- you don't have to think about where to put that one new piece of data you need while in the middle of gameplay programming
- you can do easy serialisation by just copying the bytes over
cons:
- it seems "messy" and wasteful
- probably won't get you laid
If you're heavily memory constrained, you'll probs want to upgrade this into a discriminated union with a shared Entity base
structure. Or even dynamically allocate each new entity. But that comes with extra complexity. Don't pay it unless you have to.
I used it in these games:
https://store.steampowered.com/app/2571560/ARCANA/
https://store.steampowered.com/app/3309460/Demon_Knives/ (we used the more complicated variation I mentioned earlier,
except it was probably overkill in hindsight)
https://store.steampowered.com/app/3433610/Terrafactor/
It holds up incredibly well, even as you scale it up.
I got this idea from Ryan Fleury a few years ago. Been using it every single day ever since.
---
This is Odin style pseduo-code and won't actually compile.
There's a few missing pieces and extra things you need when scaling this up, but it's a good enough overview to
get the point across.
*/
//
// the data structures
//
// you can crank this however high you want. In Terrafactor I've got mine at 65,536 (with some extra things for
// looping over them properly)
MAX_ENTITIES :: 1024
game_state: Game_State
Game_State :: struct {
initialized: bool,
entities: [MAX_ENTITIES]Entity,
entity_id_gen: u64,
entity_top_count: u64,
world_name: string,
player_handle: Entity_Handle,
}
EntityKind :: enum {
nil,
player,
goblin,
ogre,
big_boss_goblin,
wood_spikes,
defense_wall,
}
Entity :: struct {
allocated: bool,
handle: Entity_Handle,
kind: EntityKind,
// could pack these into flags if you feel like it (not really needed though)
has_physics: bool,
damagable_by_player: bool,
is_mob: bool,
blocks_mobs: bool,
// just put whatever state you need in here to make the game...
position: Vector2,
velocity: Vector2,
acceleration: Vector2,
hitbox: Vector4,
hit_cooldown_end_time: float64,
health: int,
next_attack_time: float64,
sprite_id: SpriteID,
current_animation_frame: int,
// ...
// Constant Entity Data
//
// this is constant based on the kind of the entity
// you could put this somewhere else if you want, I like having it inside the entity for easy access though.
// the 'using' is Odin/Jai specific and just makes it so you can:
// 'entity.max_health' instead of 'entity.const_data.max_health'
//
using const_data: Const_Entity_Data,
}
Const_Entity_Data :: struct {
update: proc(^Entity),
draw: proc(^Entity),
icon_image: SpriteID,
max_health: int, // you could move this back into the Entity struct to make it dynamic, and no existing code would break
}
//
// creating / destroying
//
entity_create :: proc(kind: Entity_Kind) -> ^Entity {
// look through game_state.entities and grab the first one that isnt 'allocated'
// (could also use a free list, and use straight from entity_top_count when that's empty)
new_index := -1
new_entity: ^Entity = ......
for entity, index in game_state {
if !e.allocated {
new_entity = entity
new_index = index
break
}
}
if new_index == -1 {
log.error("out of entities, probably just double the MAX_ENTITIES")
return nil
}
game_state.entity_top_count += 1
// then set it up
new_entity.allocated = true
game_state.entity_id_gen += 1
new_entity.handle.id = game_state.entity_id_gen
new_entity.handle.index = new_index
// could add whatever defaults in here
new_entity.draw = default_draw_based_on_entity_data
switch kind {
case .player: setup_player(new_entity)
case .goblin: setup_goblin(new_entity)
case .wood_spikes: setup_wood_spikes(new_entity)
// ...
}
return new_entity
}
entity_destroy :: proc(entity: ^Entity) {
entity^ = {} // it's really that simple
}
//
// handles
//
// Use this for storing entities instead of pointers for any long-ish period of time.
// If you're holding a pointer, and the thing has a chance of being destroyed, use a handle
// instead so it doesn't kill your game.
Entity_Handle :: struct {
index: u64,
id: u64,
}
entity_to_handle :: proc(entity: ^Entity) -> Entity_Handle {
return entity.handle
}
//
// Having a zero return value (instead of using a null pointer) is a very useful concept for not having to
// deal with null pointer crashes.
// (more on this below)
//
@(rodata) // marks this as read-only data, crashes when you try to write.
zero_entity: Entity
handle_to_entity :: proc(handle: Entity_Handle) -> ^Entity {
if handle == {} {
return &zero_entity
}
entity := &game_state.entities[handle.index] // might wanna do some extra bounds checks on this first
if entity.handle.id == handle.id {
return entity
} else {
// the entity has been destroyed, and there's a new one in this slot
return &zero_entity
}
}
//
// When you get &zero_entity in a return value, instead of a null pointer, you can safely access it.
// Since the entire thing is zeroed, a lot of your logic / algorithms will just gracefully fail.
//
/* for example
handle_that_is_invalid := Entity_Handle{}
entity := handle_to_entity(handle_that_is_invalid)
if entity.allocated { // this won't crash, just read a zero and gracefully skip
do_something()
}
*/
//
// SETUP (where the content magic happens)
//
//
// The setup is designed to write into both the main dynamic Entity structure
// and the Const_Entity_Data structure.
//
// That way everything you need to add a new piece of content, ie - an enemy, build, item, etc
// ... is all localised in the one place.
//
// This becomes very important for the speed of adding new stuff in. What I usually do is just copy from the
// most similar existing entity as a starting point.
//
setup_player :: proc(entity: ^Entity) {
entity.kind = .player
entity.has_physics = true
entity.max_health = 100
// update function is also nice and localised here
entity.update = proc(entity: ^Entity) {
entity.health = entity.max_health
// overlap a hitbox and do hit stuff
if key_just_pressed(.LEFT_MOUSE) {
for against in game_state.entities {
if !against.allocated || !against.damageable_by_player do continue
// ... hitbox checking stuff
}
}
if entity.vel != {} {
entity.sprite_id = .running
} else {
entity.sprite_id = .idle
}
}
entity.draw = proc(entity: ^Entity) {
default_draw_based_on_entity_data(entity)
// could add extra stuff to the draw like a sword or other items in the player's hand
// ...
}
}
setup_goblin :: proc(entity: ^Entity) {
entity.kind = .goblin
entity.is_mob = true
entity.damageable_by_player = true
entity.max_health = 200
entity.update = proc(entity: ^Entity) {
// ... AI stuff (topic for another day)
}
}
setup_wood_spikes :: proc(entity: ^Entity) {
entity.kind = .wood_spikes
entity.max_health = 200
entity.sprite_id = .wood_spikes_sprite // just a static sprite
// entity.draw = ... // we can leave this blank to use the default
entity.update = proc(entity: ^Entity) {
// It's 100% fine in the early days to just loop through all entities like this.
// Make it faster later when you actually notice it become a problem. Not before.
for against in game_state.entities {
if !against.allocated do continue
// todo - check for overlapping mobs so we can damage them
}
}
}
//
// main entry, update, and rendering
//
// not a full example of a main loop
main :: proc() {
for {
update()
render()
}
}
update :: proc(delta_t: float64) {
for entity in entities {
if !entity.allocated do continue
// call the update function
entity.update(entity)
if entity.has_physics {
// do some epic physics stuff (topic for another day)
entity.vel += entity.acc * delta_t
entity.pos += entity.vel * delta_t
entity.acc = 0
// could do some collision resolution stuff in here...
}
// you might even want to split the update into pre-physics and post-physics
// entity.post_physics_update(entity)
}
// could do some other stuff out here
// Like if things every become slow inside an entity update, break them out and optimise
// a larger all-in-one pass
for entity in entitites {
// ... do some operation in bulk on them or something
}
}
// (a very incomplete example, just showing off the entity draw)
render :: proc() {
for entity in entities {
if !entity.allocated do continue
entity.draw(entity)
}
}
default_draw_based_on_entity_data :: proc(e: Entity) {
// draw_sprite stuff based on e.pos, e.sprite_id, etc ...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment