From 7d1e5c1d61bb059df885ebfbdcd9d6b0ee667b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Madar=C3=A1sz?= Date: Sun, 9 May 2021 12:27:10 +0200 Subject: [PATCH] added basic prediction + pkt_dump_struct --- code/apps/client/CMakeLists.txt | 1 + code/apps/client/header/prediction.h | 5 +++ code/apps/client/source/game.c | 9 ++++-- code/apps/client/source/main.c | 4 ++- code/apps/client/source/platform_raylib.c | 37 ++++++++++++++++++++--- code/apps/client/source/prediction.c | 25 +++++++++++++++ code/apps/client/source/world_view.c | 8 +++++ code/common/entity_view.c | 8 +++-- code/common/entity_view.h | 12 +++++++- code/common/game.h | 1 + code/common/packet.c | 30 ++++++++++++++++++ code/common/packet_utils.h | 15 +++++---- code/common/world/world.c | 31 +++++++++++-------- code/common/world/world.h | 4 +++ code/modules/modules/general.h | 2 ++ 15 files changed, 163 insertions(+), 29 deletions(-) create mode 100644 code/apps/client/header/prediction.h create mode 100644 code/apps/client/source/prediction.c diff --git a/code/apps/client/CMakeLists.txt b/code/apps/client/CMakeLists.txt index bddd5a0..c5a5f4d 100644 --- a/code/apps/client/CMakeLists.txt +++ b/code/apps/client/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(client-common STATIC source/game.c source/camera.c source/world_view.c + source/prediction.c source/utils/options.c diff --git a/code/apps/client/header/prediction.h b/code/apps/client/header/prediction.h new file mode 100644 index 0000000..8c9a75a --- /dev/null +++ b/code/apps/client/header/prediction.h @@ -0,0 +1,5 @@ +#pragma once +#include "entity_view.h" + +float smooth_val(float cur, float tgt, float dt); +void predict_receive_update(entity_view *d, entity_view *data); \ No newline at end of file diff --git a/code/apps/client/source/game.c b/code/apps/client/source/game.c index 016caec..e367b80 100644 --- a/code/apps/client/source/game.c +++ b/code/apps/client/source/game.c @@ -86,6 +86,10 @@ void game_world_view_set_active_by_idx(uint16_t idx) { game_world_view_set_active(&world_viewers[idx]); } +void game_world_view_active_entity_map(void (*map_proc)(uint64_t key, entity_view value)) { + entity_view_map(&active_viewer->entities, map_proc); +} + void game_world_view_set_active(world_view *view) { active_viewer = view; camera_set_follow(view->owner_id); @@ -146,12 +150,13 @@ void game_input() { } void game_update() { - if (is_viewer_only) network_client_tick(); + if (is_viewer_only) { + network_client_tick(); + } else world_update(); } void game_render() { - camera_update(); platform_render(); } diff --git a/code/apps/client/source/main.c b/code/apps/client/source/main.c index 250b2cd..6f16a3c 100644 --- a/code/apps/client/source/main.c +++ b/code/apps/client/source/main.c @@ -33,6 +33,7 @@ int main(int argc, char** argv) zpl_opts_add(&opts, "bs", "block-size", "amount of units within a block (single axis)", ZPL_OPTS_INT); zpl_opts_add(&opts, "cs", "chunk-size", "amount of blocks within a chunk (single axis)", ZPL_OPTS_INT); zpl_opts_add(&opts, "ws", "world-size", "amount of chunks within a world (single axis)", ZPL_OPTS_INT); + zpl_opts_add(&opts, "n", "npc-count", "amount of demo npcs to spawn", ZPL_OPTS_INT); uint32_t ok = zpl_opts_compile(&opts, argc, argv); @@ -49,6 +50,7 @@ int main(int argc, char** argv) uint16_t block_size = zpl_opts_integer(&opts, "block-size", DEFAULT_BLOCK_SIZE); uint16_t chunk_size = zpl_opts_integer(&opts, "chunk-size", DEFAULT_CHUNK_SIZE); uint16_t world_size = zpl_opts_integer(&opts, "world-size", DEFAULT_WORLD_SIZE); + uint32_t npc_count = zpl_opts_integer(&opts, "npc-count", 100); if (zpl_opts_has_arg(&opts, "random-seed")) { zpl_random rnd={0}; @@ -72,7 +74,7 @@ int main(int argc, char** argv) ECS_IMPORT(world_ecs(), Controllers); ECS_IMPORT(world_ecs(), Physics); uint16_t half_world_dim = world_dim() / 2; - for (int i = 0; i < 100; i++) { + for (int i = 0; i < npc_count; i++) { uint64_t e = entity_spawn(NULL); ecs_add(world_ecs(), e, EcsDemoNPC); Position *pos = ecs_get_mut(world_ecs(), e, Position, NULL); diff --git a/code/apps/client/source/platform_raylib.c b/code/apps/client/source/platform_raylib.c index 7dbda1a..a0520ee 100644 --- a/code/apps/client/source/platform_raylib.c +++ b/code/apps/client/source/platform_raylib.c @@ -4,6 +4,7 @@ #include "network.h" #include "game.h" #include "entity_view.h" +#include "prediction.h" #include "camera.h" #include "math.h" @@ -51,7 +52,7 @@ void DrawRectangleEco(int posX, int posY, int width, int height, Color color) void platform_init() { InitWindow(screenWidth, screenHeight, "eco2d - client"); - SetWindowState(FLAG_WINDOW_UNDECORATED|FLAG_WINDOW_HIGHDPI|FLAG_WINDOW_MAXIMIZED|FLAG_WINDOW_RESIZABLE); + SetWindowState(FLAG_WINDOW_UNDECORATED|FLAG_WINDOW_MAXIMIZED|FLAG_WINDOW_RESIZABLE); SetTargetFPS(60); screenWidth = GetScreenWidth(); @@ -61,6 +62,17 @@ void platform_init() { render_camera.offset = (Vector2){screenWidth/2.0f, screenHeight/2.0f}; render_camera.rotation = 0.0f; render_camera.zoom = 4.0f/GFX_WORLD_SCALE; + + // NOTE(zaklaus): Paint the screen before we load the game + // TODO(zaklaus): Render a cool loading screen background maybe? :wink: :wink: + + BeginDrawing(); + ClearBackground(GetColor(0x222034)); + + char const *loading_text = "zpl.eco2d is loading..."; + int text_w = MeasureText(loading_text, 120); + DrawText(loading_text, GetScreenWidth()-text_w-15, GetScreenHeight()-135, 120, RAYWHITE); + EndDrawing(); } void platform_shutdown() { @@ -122,15 +134,20 @@ void display_conn_status(); void DEBUG_draw_entities(uint64_t key, entity_view data); void DEBUG_draw_ground(uint64_t key, entity_view data); +void lerp_entity_positions(uint64_t key, entity_view data); + void platform_render() { + game_world_view_active_entity_map(lerp_entity_positions); + camera_update(); + camera game_camera = camera_get(); render_camera.target = (Vector2){game_camera.x * GFX_WORLD_SCALE, game_camera.y * GFX_WORLD_SCALE}; BeginDrawing(); - ClearBackground(BLACK); + ClearBackground(GetColor(0x222034)); BeginMode2D(render_camera); - entity_view_map(&game_world_view_get_active()->entities, DEBUG_draw_ground); - entity_view_map(&game_world_view_get_active()->entities, DEBUG_draw_entities); + game_world_view_active_entity_map(DEBUG_draw_ground); + game_world_view_active_entity_map(DEBUG_draw_entities); EndMode2D(); display_conn_status(); EndDrawing(); @@ -164,6 +181,7 @@ void DEBUG_draw_ground(uint64_t key, entity_view data) { double x = data.x * size + offset; double y = data.y * size + offset; + DrawRectangleEco((int)x-offset, (int)y-offset, size+offset, size+offset, BLACK); DrawRectangleEco((int)x, (int)y, size-offset, size-offset, LIME); for (uint16_t i = 0; i < chunk_size*chunk_size; i++) { @@ -210,4 +228,15 @@ void DEBUG_draw_entities(uint64_t key, entity_view data) { }break; default:break; } +} + +void lerp_entity_positions(uint64_t key, entity_view data) { + world_view *view = game_world_view_get_active(); + + if (data.flag == EFLAG_INTERP) { + entity_view *e = entity_view_get(&view->entities, key); + + e->x = smooth_val(e->x, e->tx, 0); + e->y = smooth_val(e->y, e->ty, 0); + } } \ No newline at end of file diff --git a/code/apps/client/source/prediction.c b/code/apps/client/source/prediction.c new file mode 100644 index 0000000..3402876 --- /dev/null +++ b/code/apps/client/source/prediction.c @@ -0,0 +1,25 @@ +#include "zpl.h" +#include "prediction.h" +#include "world/world.h" + +#define PREDICT_SMOOTH_FACTOR_LO 0.8 +#define PREDICT_SMOOTH_FACTOR_HI 0.12 + +float smooth_val(float cur, float tgt, float dt) { +#if 0 + return zpl_lerp(cur, tgt, zpl_lerp(PREDICT_SMOOTH_FACTOR_HI, PREDICT_SMOOTH_FACTOR_LO, zpl_unlerp(dt, WORLD_TRACKER_UPDATE_FAST_MS, WORLD_TRACKER_UPDATE_SLOW_MS))); +#endif + return zpl_lerp(cur, tgt, PREDICT_SMOOTH_FACTOR_HI); +} + +void predict_receive_update(entity_view *d, entity_view *data) { + if (d && data->flag & EFLAG_INTERP) { + // NOTE(zaklaus): store target pos but keep x,y unchanged + float tx = data->x; + float ty = data->y; + data->x = d->x; + data->y = d->y; + data->tx = tx; + data->ty = ty; + } +} diff --git a/code/apps/client/source/world_view.c b/code/apps/client/source/world_view.c index 611eb67..58c2ece 100644 --- a/code/apps/client/source/world_view.c +++ b/code/apps/client/source/world_view.c @@ -1,4 +1,6 @@ #include "world_view.h" +#include "entity_view.h" +#include "prediction.h" #include "librg.h" #include "zpl.h" @@ -19,6 +21,8 @@ int32_t tracker_read_update(librg_world *w, librg_event *e) { world_view *view = (world_view*)librg_world_userdata_get(w); entity_view data = entity_view_unpack_struct(buffer, actual_length); + entity_view *d = entity_view_get(&view->entities, entity_id); + predict_receive_update(d, &data); entity_view_update_or_create(&view->entities, entity_id, data); return 0; } @@ -32,6 +36,10 @@ int32_t tracker_read_create(librg_world *w, librg_event *e) { world_view *view = (world_view*)librg_world_userdata_get(w); entity_view data = entity_view_unpack_struct(buffer, actual_length); + if (data.flag & EFLAG_INTERP) { + data.tx = data.x; + data.ty = data.y; + } entity_view_update_or_create(&view->entities, entity_id, data); return 0; } diff --git a/code/common/entity_view.c b/code/common/entity_view.c index 3fce6f9..2c34f3e 100644 --- a/code/common/entity_view.c +++ b/code/common/entity_view.c @@ -5,8 +5,11 @@ ZPL_TABLE_DEFINE(entity_view_tbl, entity_view_tbl_, entity_view); pkt_desc pkt_entity_view_desc[] = { { PKT_UINT(entity_view, kind) }, - { PKT_REAL(entity_view, x) }, - { PKT_REAL(entity_view, y) }, + { PKT_UINT(entity_view, flag) }, + { PKT_HALF(entity_view, x) }, + { PKT_HALF(entity_view, y) }, + { PKT_HALF(entity_view, vx) }, + { PKT_HALF(entity_view, vy) }, { PKT_END }, }; @@ -23,6 +26,7 @@ entity_view entity_view_unpack_struct(void *data, size_t len) { entity_view view = {0}; pkt_unpack_struct(&uc, pkt_entity_view_desc, PKT_STRUCT_PTR(&view)); + return view; } diff --git a/code/common/entity_view.h b/code/common/entity_view.h index e27145d..dfbca83 100644 --- a/code/common/entity_view.h +++ b/code/common/entity_view.h @@ -7,16 +7,26 @@ #include "packet_utils.h" typedef enum { - EKIND_PLAYER, + EKIND_PLAYER = 0, EKIND_THING, EKIND_CHUNK, FORCE_EKIND_UINT16 = UINT16_MAX } entity_kind; +typedef enum { + EFLAG_INTERP = (1 << 0), + FORCE_EFLAG_UINT16 = UINT16_MAX +} entity_flag; + typedef struct entity_view { entity_kind kind; + entity_flag flag; float x; float y; + float vx; + float vy; + float tx; + float ty; } entity_view; ZPL_TABLE_DECLARE(, entity_view_tbl, entity_view_tbl_, entity_view); diff --git a/code/common/game.h b/code/common/game.h index 2bf0de0..c2e25f7 100644 --- a/code/common/game.h +++ b/code/common/game.h @@ -18,6 +18,7 @@ world_view *game_world_view_get(uint16_t idx); void game_world_view_set_active_by_idx(uint16_t idx); void game_world_view_set_active(world_view *view); void game_world_view_cycle_active(uint8_t dir); +void game_world_view_active_entity_map(void (*map_proc)(uint64_t key, entity_view value)); //~ NOTE(zaklaus): viewer -> host actions void game_action_send_keystate(float x, float y, uint8_t use, uint8_t sprint); \ No newline at end of file diff --git a/code/common/packet.c b/code/common/packet.c index 80124a5..d31de97 100644 --- a/code/common/packet.c +++ b/code/common/packet.c @@ -135,3 +135,33 @@ int32_t pkt_pack_struct(cw_pack_context *pc, pkt_desc *desc, void *raw_blob, uin return 0; } + +void pkt_dump_struct(pkt_desc *desc, void* raw_blob, uint32_t blob_size) { + uint8_t *blob = (uint8_t*)raw_blob; + zpl_printf("{\n"); + for (pkt_desc *field = desc; field->type != CWP_NOT_AN_ITEM; ++field) { + zpl_printf(" \"%s\": ", field->name); + switch (field->type) { + case CWP_ITEM_BIN: { + // TODO(zaklaus): print memory dump as array of hex bytes + }break; + case CWP_ITEM_POSITIVE_INTEGER: { + zpl_printf("%u\n", *(uint64_t*)(blob + field->offset)); + }break; + case CWP_ITEM_NEGATIVE_INTEGER: { + zpl_printf("%d\n", *(int64_t*)(blob + field->offset)); + }break; + case CWP_ITEM_FLOAT: { + zpl_printf("%f\n", *(float*)(blob + field->offset)); + }break; + case CWP_ITEM_DOUBLE: { + zpl_printf("%f\n", *(double*)(blob + field->offset)); + }break; + default: { + zpl_printf("[WARN] unsupported pkt field type %lld !\n", field->type); + return; // unsupported field + }break; + } + } + zpl_printf("}\n"); +} \ No newline at end of file diff --git a/code/common/packet_utils.h b/code/common/packet_utils.h index eae54a8..d9c5976 100644 --- a/code/common/packet_utils.h +++ b/code/common/packet_utils.h @@ -74,27 +74,27 @@ static inline int32_t pkt_world_write(pkt_messages id, size_t pkt_size, int8_t i #endif #ifndef PKT_FIELD -#define PKT_FIELD(k, t, a) .type = k, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a) +#define PKT_FIELD(k, t, a) .type = k, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a), .name = #a #endif #ifndef PKT_UINT -#define PKT_UINT(t, a) .type = CWP_ITEM_POSITIVE_INTEGER, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a) +#define PKT_UINT(t, a) .type = CWP_ITEM_POSITIVE_INTEGER, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a), .name = #a #endif #ifndef PKT_SINT -#define PKT_SINT(t, a) .type = CWP_ITEM_NEGATIVE_INTEGER, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a) +#define PKT_SINT(t, a) .type = CWP_ITEM_NEGATIVE_INTEGER, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a), .name = #a #endif #ifndef PKT_REAL -#define PKT_REAL(t, a) .type = CWP_ITEM_DOUBLE, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a) +#define PKT_REAL(t, a) .type = CWP_ITEM_DOUBLE, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a), .name = #a #endif #ifndef PKT_HALF -#define PKT_HALF(t, a) .type = CWP_ITEM_FLOAT, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a) +#define PKT_HALF(t, a) .type = CWP_ITEM_FLOAT, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a), .name = #a #endif #ifndef PKT_ARRAY -#define PKT_ARRAY(t, a) .type = CWP_ITEM_BIN, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a[0]) +#define PKT_ARRAY(t, a) .type = CWP_ITEM_BIN, .offset = PKT_OFFSETOF(t, a), .size = PKT_FIELD_SIZEOF(t,a), .it_size = PKT_FIELD_SIZEOF(t,a[0]), .name = #a #endif #ifndef PKT_END @@ -106,6 +106,7 @@ static inline int32_t pkt_world_write(pkt_messages id, size_t pkt_size, int8_t i #endif typedef struct pkt_desc { + const char *name; cwpack_item_types type; size_t offset; size_t size; @@ -127,3 +128,5 @@ static inline size_t pkt_pack_desc_args(pkt_desc *desc) { for (pkt_desc *field = desc; field->type != CWP_NOT_AN_ITEM; ++field, ++cnt) {} return cnt; } + +void pkt_dump_struct(pkt_desc *desc, void* raw_blob, uint32_t blob_size); \ No newline at end of file diff --git a/code/common/world/world.c b/code/common/world/world.c index 99b8f20..90c4893 100644 --- a/code/common/world/world.c +++ b/code/common/world/world.c @@ -2,6 +2,7 @@ #include "librg.h" #include "modules/general.h" #include "modules/net.h" +#include "modules/physics.h" #include "world/world.h" #include "entity_view.h" @@ -24,14 +25,11 @@ typedef struct { static world_data world = {0}; -#define WORLD_TRACKER_UPDATE_FAST_MS 10 -#define WORLD_TRACKER_UPDATE_NORMAL_MS 100 -#define WORLD_TRACKER_UPDATE_SLOW_MS 800 - int32_t world_gen(); entity_view world_build_entity_view(int64_t e) { ECS_IMPORT(world_ecs(), General); + ECS_IMPORT(world_ecs(), Physics); ECS_IMPORT(world_ecs(), Net); entity_view view = {0}; @@ -41,7 +39,13 @@ entity_view world_build_entity_view(int64_t e) { view.kind = ecs_has(world_ecs(), e, EcsClient) ? EKIND_PLAYER : EKIND_THING; view.x = pos->x; view.y = pos->y; - return view; + } + + const Velocity *vel = ecs_get(world_ecs(), e, Velocity); + if (vel) { + view.flag |= EFLAG_INTERP; + view.vx = vel->x; + view.vy = vel->y; } const Chunk *chpos = ecs_get(world_ecs(), e, Chunk); @@ -49,7 +53,6 @@ entity_view world_build_entity_view(int64_t e) { view.kind = EKIND_CHUNK; view.x = chpos->x; view.y = chpos->y; - return view; } return view; @@ -91,7 +94,7 @@ int32_t world_init(int32_t seed, uint16_t block_size, uint16_t chunk_size, uint1 world.chunk_amount = chunk_amount; world.block_size = block_size; - world.dim = (world.chunk_size * world.chunk_amount);; + world.dim = (world.chunk_size * world.chunk_amount); world.size = world.dim * world.dim; if (world.tracker == NULL) { @@ -104,8 +107,8 @@ int32_t world_init(int32_t seed, uint16_t block_size, uint16_t chunk_size, uint1 } /* config our world grid */ - librg_config_chunksize_set(world.tracker, block_size * chunk_size, block_size * chunk_size, 0); - librg_config_chunkamount_set(world.tracker, chunk_amount, chunk_amount, 0); + librg_config_chunksize_set(world.tracker, block_size * world.chunk_size, block_size * world.chunk_size, 0); + librg_config_chunkamount_set(world.tracker, world.chunk_amount, world.chunk_amount, 0); librg_config_chunkoffset_set(world.tracker, LIBRG_OFFSET_BEG, LIBRG_OFFSET_BEG, 0); librg_event_set(world.tracker, LIBRG_WRITE_CREATE, tracker_write_create); @@ -120,10 +123,12 @@ int32_t world_init(int32_t seed, uint16_t block_size, uint16_t chunk_size, uint1 world.ecs = ecs_init(); ecs_set_entity_range(world.ecs, 0, UINT32_MAX); + int32_t world_build_status = world_gen(); + ZPL_ASSERT(world_build_status >= 0); ECS_IMPORT(world.ecs, General); - for (int i = 0; i < chunk_amount * chunk_amount; ++i) { + for (int i = 0; i < world.chunk_amount * world.chunk_amount; ++i) { ecs_entity_t e = ecs_new(world.ecs, 0); Chunk *chunk = ecs_get_mut(world.ecs, e, Chunk, NULL); librg_entity_track(world.tracker, e); @@ -133,7 +138,7 @@ int32_t world_init(int32_t seed, uint16_t block_size, uint16_t chunk_size, uint1 zpl_printf("[INFO] Created a new server world\n"); - return world_gen(); + return world_build_status; } int32_t world_destroy(void) { @@ -181,8 +186,8 @@ static void world_tracker_update(uint8_t ticker, uint8_t freq, uint8_t radius) { int32_t world_update() { ecs_progress(world.ecs, 0); - world_tracker_update(0, WORLD_TRACKER_UPDATE_FAST_MS, 2); - //world_tracker_update(1, WORLD_TRACKER_UPDATE_NORMAL_MS, 4); + //world_tracker_update(0, WORLD_TRACKER_UPDATE_FAST_MS, 2); + world_tracker_update(1, WORLD_TRACKER_UPDATE_NORMAL_MS, 4); //world_tracker_update(2, WORLD_TRACKER_UPDATE_SLOW_MS, 6); return 0; } diff --git a/code/common/world/world.h b/code/common/world/world.h index 54c1523..2c028f1 100644 --- a/code/common/world/world.h +++ b/code/common/world/world.h @@ -11,6 +11,10 @@ #define WORLD_ERROR_INVALID_BUFFER -0x0004 #define WORLD_ERROR_TRACKER_FAILED -0x0005 +#define WORLD_TRACKER_UPDATE_FAST_MS 10 +#define WORLD_TRACKER_UPDATE_NORMAL_MS 100 +#define WORLD_TRACKER_UPDATE_SLOW_MS 800 + #define WORLD_PKT_READER(name) int32_t name(void* data, uint32_t datalen, void *udata) typedef WORLD_PKT_READER(world_pkt_reader_proc); diff --git a/code/modules/modules/general.h b/code/modules/modules/general.h index 0365fdd..d03e0c2 100644 --- a/code/modules/modules/general.h +++ b/code/modules/modules/general.h @@ -1,6 +1,7 @@ #pragma once #include "flecs/flecs.h" #include "flecs/flecs_meta.h" +#include "world/world.h" ECS_STRUCT(Vector2D, { float x; @@ -10,6 +11,7 @@ ECS_STRUCT(Vector2D, { ECS_STRUCT(Chunk, { int16_t x; int16_t y; + ecs_vector(uint8_t) blocks; }); ECS_STRUCT(Drawable, {