trueno/src/entities.jai
2026-10-02 20:07:49 +03:00

374 lines
14 KiB
Plaintext

// Entity types are plain structs declared in game code, registered with an
// @Entity note the way console commands register with @Command. The metaprogram
// (src/meta/entity_types.jai) collects them into ENTITY_TYPES, and spawning,
// dispatch, editor UI and saving all come from that. Marking a struct @Entity
// without embedding Entity, or embedding Entity without the note, is an error.
//
// Foo :: struct {
// #as using base : Entity;
// base.type = Foo;
// PARTS :: Entity_Part.[...]; // visuals, may be empty
// EDITOR_MARKER :: "pack.animation"; // optional, editor-only billboard
// } @Entity
// entity_tick :: (using foo: *Foo, dt: float) {} // optional
// entity_draw :: (foo: *Foo, view: View_Id) {} // optional
//
// Fields of type float, s32, bool, string or Vector3 are editable and saved;
// @Slider / @Color notes pick the editor widget.
//
// Each frame the game calls tick_entities, then tick_entity_emitters, then
// draw_entities. The level editor calls the last two only: it shows
// entities and their particles without running their game logic.
#scope_export
Entity :: struct {
type : Type;
id : u32;
position : Vector3;
orientation : u8; // 0..23, see orientation.jai
emitters : [..]Particle_Emitter_Instance; // built from PARTS, not saved
}
Entity_Part_Kind :: enum {
TRILE;
BILLBOARD;
EMITTER;
}
Entity_Part :: struct {
kind : Entity_Part_Kind;
trile : string; // TRILE
orientation : u8; // TRILE, 0..23
animation : string; // BILLBOARD, e.g. "game_core.ball"
emitter : string; // EMITTER, a particle definition name
offset : Vector3;
}
Entity_Type_Info :: struct {
name : string;
type : Type;
parts : []Entity_Part;
editor_marker : string;
create : () -> *Entity;
}
#insert #run,stallable gen_entity_type_table();
get_entity_type_info :: (type: Type) -> *Entity_Type_Info {
for * ENTITY_TYPE_TABLE if it.type == type return it;
return null;
}
get_entity_parts :: (type: Type) -> []Entity_Part {
info := get_entity_type_info(type);
return ifx info then info.parts else .[];
}
get_entity_editor_marker :: (type: Type) -> string {
info := get_entity_type_info(type);
return ifx info then info.editor_marker else "";
}
entity_type_name :: (type: Type) -> string {
return (cast(*Type_Info_Struct) type).name;
}
// Entities turn about the middle of their own cell, so a rotated entity stays
// on the block it was placed on.
ENTITY_PIVOT :: Vector3.{0.5, 0.5, 0.5};
entity_point :: (e: *Entity, offset: Vector3) -> Vector3 {
return e.position + ENTITY_PIVOT + rotate_by_orientation(e.orientation, offset - ENTITY_PIVOT);
}
// A trile part fills a cell rather than being a point, so the pivot cancels out.
entity_trile_point :: (e: *Entity, offset: Vector3) -> Vector3 {
return e.position + rotate_by_orientation(e.orientation, offset);
}
// Orientation is taken up front rather than assigned afterwards because the
// emitters are placed relative to it.
spawn_entity :: (world: *World, type_name: string, position: Vector3, orientation: u8 = 0) -> *Entity {
for ENTITY_TYPE_TABLE {
if it.name != type_name continue;
e := it.create();
e.position = position;
e.orientation = orientation;
e.id = world.next_entity_id;
world.next_entity_id += 1;
init_entity_emitters(e);
array_add(*world.entities, e);
return e;
}
log_error("Unknown entity type: %", type_name);
return null;
}
get_first_entity :: (world: *World, type: Type) -> *Entity {
for e: world.entities if e.type == type return e;
return null;
}
remove_entity :: (world: *World, e: *Entity) {
for other, i: world.entities {
if other != e continue;
array_ordered_remove_by_index(*world.entities, i);
free_entity(e);
return;
}
}
free_entity :: (e: *Entity) {
array_free(e.emitters);
free(e);
}
free_all_entities :: (world: *World) {
for e: world.entities free_entity(e);
array_reset(*world.entities);
}
// Concrete per-type overloads in game code take precedence over these.
entity_tick :: (e: *$T/Entity, dt: float) {}
entity_draw :: (e: *$T/Entity, view: View_Id) { draw_entity_parts(e, view); }
tick_entities :: (world: *World, dt: float) {
for e: world.entities entity_dispatch_tick(e, dt);
}
entity_dispatch_tick :: (e: *Entity, dt: float) {
#insert #run,stallable gen_entity_dispatch("entity_tick(cast(*%)e, dt)");
}
entity_dispatch_draw :: (e: *Entity, view: View_Id) {
#insert #run,stallable gen_entity_dispatch("entity_draw(cast(*%)e, view)");
}
// Dispatches a call on the entity's concrete type. '%' in 'call' becomes the type
// name. Also used by the editor to run autoedit on an entity's own fields.
gen_entity_dispatch :: (call: string) -> string {
builder : String_Builder;
for ENTITY_TYPES {
name := (cast(*Type_Info_Struct) it).name;
print_to_builder(*builder, "if e.type == % { %; return; }\n", name, tprint(call, name));
}
return builder_to_string(*builder);
}
init_entity_emitters :: (e: *Entity) {
for part: get_entity_parts(e.type) {
if part.kind != .EMITTER continue;
inst : Particle_Emitter_Instance;
inst.definition_name = part.emitter;
inst.definition = get_emitter_def(part.emitter);
inst.offset = part.offset;
inst.position = entity_point(e, part.offset);
inst.active = true;
array_add(*e.emitters, inst);
}
}
tick_entity_emitters :: (world: *World, dt: float) {
for e: world.entities {
for *inst: e.emitters {
if inst.definition == null {
inst.definition = get_emitter_def(inst.definition_name);
if inst.definition == null continue;
}
inst.position = entity_point(e, inst.offset);
tick_emitter_instance(inst, dt);
}
}
}
// Once per frame, from the game and from the editor.
draw_entities :: (view: View_Id, world: *World) {
for e: world.entities entity_dispatch_draw(e, view);
}
entity_draw_trile :: (e: *Entity, view: View_Id, trile: string, offset: Vector3, orientation: u8 = 0) {
mesh, ok := get_trile_mesh(trile);
if !ok return;
draw_trile(view, mesh, entity_trile_point(e, offset), compose_orientations(e.orientation, orientation));
}
entity_draw_billboard :: (e: *Entity, view: View_Id, animation: string, offset: Vector3, frame: s32 = 0) {
anim := get_animation_from_string(animation);
if anim == null return;
draw_animation_frame(view, anim, frame, entity_point(e, offset));
}
draw_entity_parts :: (e: *Entity, view: View_Id) {
for part: get_entity_parts(e.type) {
if part.kind == {
case .TRILE; entity_draw_trile(e, view, part.trile, part.offset, part.orientation);
case .BILLBOARD; entity_draw_billboard(e, view, part.animation, part.offset);
case .EMITTER; // tick_entity_emitters handles these
}
}
}
// Fields are saved as name/value string pairs rather than typed JSON, so adding,
// removing or renaming one never breaks loading an older world.
Entity_Field :: struct {
name : string;
value : string;
}
entity_fields_to_strings :: (e: *Entity) -> []Entity_Field {
result : [..]Entity_Field;
result.allocator = temp;
for member: entity_saved_members(e.type) {
ptr := (cast(*u8) e) + member.offset_in_bytes;
value : string;
if member.type == type_info(float) value = tprint("%", (cast(*float) ptr).*);
else if member.type == type_info(s32) value = tprint("%", (cast(*s32) ptr).*);
else if member.type == type_info(bool) value = tprint("%", (cast(*bool) ptr).*);
else if member.type == type_info(string) value = (cast(*string) ptr).*;
else if member.type == type_info(Vector3) {
v := (cast(*Vector3) ptr).*;
value = tprint("% % %", v.x, v.y, v.z);
} else continue;
array_add(*result, .{member.name, value});
}
return result;
}
entity_apply_field :: (e: *Entity, name: string, value: string) {
for member: entity_saved_members(e.type) {
if member.name != name continue;
ptr := (cast(*u8) e) + member.offset_in_bytes;
if member.type == type_info(float) {
v, ok := string_to_float(value);
if ok { (cast(*float) ptr).* = v; }
} else if member.type == type_info(s32) {
v, ok := string_to_int(value);
if ok { (cast(*s32) ptr).* = cast(s32) v; }
} else if member.type == type_info(bool) {
(cast(*bool) ptr).* = (value == "true");
} else if member.type == type_info(string) {
(cast(*string) ptr).* = copy_string(value);
} else if member.type == type_info(Vector3) {
parts := split(value, " ",, temp);
if parts.count != 3 return;
v : Vector3;
x, okx := string_to_float(parts[0]);
y, oky := string_to_float(parts[1]);
z, okz := string_to_float(parts[2]);
if okx && oky && okz { (cast(*Vector3) ptr).* = .{x, y, z}; }
}
return;
}
log_warn("Entity type % has no field '%', skipping", entity_type_name(e.type), name);
}
#scope_file
#import "String";
// The members an entity type declares itself: constants and the Entity base are
// neither editable nor saved.
entity_saved_members :: (type: Type) -> []Type_Info_Struct_Member {
result : [..]Type_Info_Struct_Member;
result.allocator = temp;
for (cast(*Type_Info_Struct) type).members {
if it.flags & (.CONSTANT | .USING) continue;
if array_find(it.notes, "NoSave") continue;
array_add(*result, it);
}
return result;
}
#if FLAG_TEST_ENGINE {
// The two placement helpers use different pivots, and getting them the wrong
// way round only shows up as parts drifting off a rotated entity, so both are
// pinned to the invariant they exist for.
test_entity_part_placement :: () {
s := begin_suite("entity part placement");
e : Entity;
e.position = .{10, 3, -5};
// A trile part on the entity's own cell has to stay on that cell however
// the entity is turned, or rotating would walk it off the block it was
// placed on.
on_its_block := true;
for ori: 0..ORIENTATION_COUNT-1 {
e.orientation = cast(u8) ori;
if entity_trile_point(*e, .{0, 0, 0}) != e.position on_its_block = false;
}
check(*s, "a trile part on the entity's own cell never moves", on_its_block);
// Point parts turn about the middle of that same cell, so a part sitting
// at the middle is the fixed point of all 24 rotations.
at_the_middle := true;
for ori: 0..ORIENTATION_COUNT-1 {
e.orientation = cast(u8) ori;
if entity_point(*e, ENTITY_PIVOT) != e.position + ENTITY_PIVOT at_the_middle = false;
}
check(*s, "a point part at the middle never moves", at_the_middle);
e.orientation = 0;
check(*s, "unturned point parts sit at their authored offset",
entity_point(*e, .{1, 2, 3}) == e.position + Vector3.{1, 2, 3});
check(*s, "unturned trile parts sit at their authored offset",
entity_trile_point(*e, .{1, 2, 3}) == e.position + Vector3.{1, 2, 3});
// Orientation 1 is the quarter twist about Y, which sends X to Z.
e.orientation = 1;
check(*s, "a turned entity carries its parts around with it",
entity_trile_point(*e, .{1, 0, 0}) == e.position + Vector3.{0, 0, 1});
end_suite(s);
}
// Every declared type must reach the table, or it would be unspawnable and
// unsaveable with no diagnostic anywhere.
test_entity_type_table :: () {
s := begin_suite("entity type table");
check(*s, "the table covers every declared type", ENTITY_TYPE_TABLE.count == ENTITY_TYPES.count);
found_all := true;
for type: ENTITY_TYPES if get_entity_type_info(type) == null found_all = false;
check(*s, "every declared type is findable by type", found_all);
names_match := true;
for info: ENTITY_TYPE_TABLE if info.name != entity_type_name(info.type) names_match = false;
check(*s, "table names match the type names saves use", names_match);
// Entity itself is never a declared type, so it stands in for one that
// this build does not have.
check(*s, "an unknown type has no info", get_entity_type_info(Entity) == null);
check(*s, "an unknown type has no parts", get_entity_parts(Entity).count == 0);
end_suite(s);
}
// Stallable because the type table is: ENTITY_TYPES only exists once the
// metaprogram has seen every @Entity in the program.
#run,stallable {
test_entity_part_placement();
test_entity_type_table();
}
}
gen_entity_type_table :: () -> string {
builder : String_Builder;
append(*builder, "ENTITY_TYPE_TABLE :: Entity_Type_Info.[\n");
for ENTITY_TYPES {
ti := cast(*Type_Info_Struct) it;
marker := "\"\"";
for member: ti.members {
if member.name == "EDITOR_MARKER" && (member.flags & .CONSTANT) {
marker = tprint("%.EDITOR_MARKER", ti.name);
break;
}
}
print_to_builder(*builder,
" .{name = \"%\", type = %, parts = %.PARTS, editor_marker = %, create = () -> *Entity { return New(%); }},\n",
ti.name, ti.name, ti.name, marker, ti.name);
}
append(*builder, "];\n");
return builder_to_string(*builder);
}