Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save geduardcatalindev/52203c69f58d24636bfacd1f065d0e13 to your computer and use it in GitHub Desktop.

Select an option

Save geduardcatalindev/52203c69f58d24636bfacd1f065d0e13 to your computer and use it in GitHub Desktop.

Revisions

  1. @randyprime randyprime revised this gist May 3, 2025. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions entity_structure.odin
    Original file line number Diff line number Diff line change
    @@ -32,6 +32,8 @@ 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.
  2. @randyprime randyprime revised this gist May 3, 2025. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions entity_structure.odin
    Original file line number Diff line number Diff line change
    @@ -3,6 +3,7 @@
    /*
    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.
  3. @randyprime randyprime revised this gist May 3, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion entity_structure.odin
    Original file line number Diff line number Diff line change
    @@ -45,7 +45,7 @@ get the point across.
    //

    // 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)
    // looping over them properly)
    MAX_ENTITIES :: 1024

    game_state: Game_State
  4. @randyprime randyprime revised this gist May 3, 2025. 1 changed file with 0 additions and 3 deletions.
    3 changes: 0 additions & 3 deletions entity_structure.odin
    Original file line number Diff line number Diff line change
    @@ -38,9 +38,6 @@ 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.
    If you want to see an example of what this looks like in a production codebase
    you can checkout my mentorship program -> https://learn.randy.gg
    */

    //
  5. @randyprime randyprime revised this gist May 1, 2025. 1 changed file with 17 additions and 9 deletions.
    26 changes: 17 additions & 9 deletions entity_structure.odin
    Original file line number Diff line number Diff line change
    @@ -4,10 +4,12 @@
    ENTITY MEGASTRUCT
    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.
    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)
    - 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
    @@ -17,12 +19,14 @@ 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.
    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/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.
    @@ -31,7 +35,8 @@ It holds up incredibly well, even as you scale it up.
    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.
    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.
    If you want to see an example of what this looks like in a production codebase
    you can checkout my mentorship program -> https://learn.randy.gg
    @@ -42,7 +47,8 @@ you can checkout my mentorship program -> https://learn.randy.gg
    // 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)
    // 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
    @@ -103,7 +109,7 @@ Const_Entity_Data :: struct {
    draw: proc(^Entity),

    icon_image: SpriteID,
    max_health: int, // you could easily move this back into the Entity struct to make it dynamic, and no existing code would break
    max_health: int, // you could move this back into the Entity struct to make it dynamic, and no existing code would break
    }

    //
    @@ -159,7 +165,8 @@ entity_destroy :: proc(entity: ^Entity) {
    //

    // 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.
    // 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,
    @@ -171,7 +178,8 @@ entity_to_handle :: proc(entity: ^Entity) -> 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.
    // 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.
  6. @randyprime randyprime revised this gist May 1, 2025. No changes.
  7. @randyprime randyprime revised this gist May 1, 2025. No changes.
  8. @randyprime randyprime created this gist May 1, 2025.
    339 changes: 339 additions & 0 deletions entity_structure.odin
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,339 @@
    // the tl;dr -> https://storage.randy.gg/entity%20midwit.png

    /*
    ENTITY MEGASTRUCT
    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.
    ---
    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.
    If you want to see an example of what this looks like in a production codebase
    you can checkout my mentorship program -> https://learn.randy.gg
    */

    //
    // 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 easily 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 ...
    }