374 lines
14 KiB
Plaintext
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);
|
|
}
|