Skip to content

rezich/Entity_Storage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Entity_Storage

this is a simple entity storage system for your video game or whatever.

it does not contain:

  • a component system
  • any sort of hierarchical/relational system
  • any kind of serialization/deserialization system

however, it would be trivial to build any of that on top of this system.

this README is currently kind of rambly and confusing. check out examples/game to see this module in action.

what it is / how it works

this entity framework allows you to derive your "entity" structs either from the included Base_Entity struct, or from an Entity struct you define yourself. an Entity_Storage struct will be automatically generated for all the entity structs in your code, containing an Entity_Substorage (which contains a Bucket_Array) for each one, with the same name as the struct but preceded by an underscore. the size of the buckets can be defined using a @bucket_size=100 note on the entity struct declaration.

an instance of this Entity_Storage struct is added to the context as context.entity_storage, and this lets you write code that iterates over all instances of a given entity type very easily:

Physics_Object :: struct {
    using base: Base_Entity;
    mass: float;
    // ...
}

simulate :: (using physics_object: *Physics_Object, dt: float) {
    // ...
}

using context.entity_storage;
for _Physics_Object simulate(it, dt);

sometimes you don't want to iterate over all instances of a given entity type, but only a subset that meets some criteria. you can do this too:

can_be_picked_up :: (using physics_object: *Physics_Object) -> bool {
    return mass < 50;
}

for each(Physics_Object, where=can_be_picked_up) {
    // ...
}

each() returns an Each_Entity(T) struct whose for_expansion iterates through all of that type of entity, runs the where= procedure on it, and if that returns true, then the code block is executed for that entity.

you might ask, well, isn't that just the same as the following code:

using context.entity_storage;
for _Physics_Object {
    if !can_be_picked_up(it) continue;
    // ...
}

and you'd be correct. they do exactly the same thing under the hood. each() is a bit more consise when doing one-off things, and the other way is better for main loops—like in the example above—where you're probably going to be writing one simulate() or update() or present() or draw() (or whatever) line for each entity type. (this sort of thing intentionally isn't automatically generated for you, in order to give you full control over the order in which things are executed.)

if that's not enough, there's a third valid syntactic way to do the exact same thing:

EACH_PICKUPABLE_OBJECT :: Each_Entity(Physics_Object).{where=can_be_picked_up};

for EACH_PICKUPABLE_OBJECT {
    // ...
}

I dunno, maybe that's useful for someone.

speaking of useful, another useful tool is select_all():

pickupable_objects := select_all(Physics_Object, where=can_be_picked_up);
print("there's % pickupable objects\n", pickupable_objects.entities.count);
for pickupable_objects {
    // ...
}

this has the exact same syntax as each(), except select_all() returns an Entity_Selection(T) struct, which is a wrapper around an array of Handle(T). we'll get to what Handles are in a second, but the important thing to know about select_all() is that it is slower to iterate through than any of those three ways of doing the same thing above, because of indirection. the tradeoff is the Entity_Selection(T) that select_all() returns is something that can safely persist across procedure calls and frames (more on this in the next section).

as an added bonus, every time you iterate over an Entity_Selection(T), any entities in the selection that have since been despawned will be automatically removed from the selection.

Handles instead of pointers

pointers are awesome but not great for remembering entities across frame boundaries or even between procedure calls within a frame, because the location in memory that an entity pointer points to could contain a completely different entity than you expect, if, say, the entity was despawned and another entity was spawned in its place in memory since you last checked. or it could have simply despawned, and a pointer to its location in memory would now point at something that shouldn't exist anymore (until a new entity of the same type is spawned to take its place).

for this reason, entities are assigned a unique id when they are spawned, and you can call get_handle(ptr) on an entity pointer to get a Handle(T) that contains both the entity pointer and the id of the entity. then, later, you can use ent, gone := from_handle(hnd) to get the entity pointer back, along with a gone boolean that's true if the entity has been despawned (deactivated or cleaned up), or if the entity that's now in that pointer has a different id. basically, if the entity is gone. both return parameters are enforced with #must, forcing you to consume gone, whether you like it or not, because it's for your own good.

long story short, you want to use Handle(T)s (which is what spawn() returns, anyway) to remember entities across frame boudaries and procedure calls, unless you know what you're doing (i.e. passing a pointer to procedure calls that you are 100% sure will not despawn the entity). then when you're actually using the entity pointer to do some stuff with it, you use from_handle(), and the compiler reminds you to check to see if it's gone since you last checked.

as mentioned above, this is how you spawn entities:

{ obj: Physics_Object; obj.mass = 100; spawn(obj); }

player: Handle(Player);
{ p: Player; player = spawn(p); }

to despawn() entities, you can pass either a pointer or Handle:

despawn(player);

simulate :: (using obj: *Physics_Object, dt: float) {
    if mass == 0 despawn(obj);
}