#include "user.h" #include "app.h" #include "renderer.h" #include "font.h" #include "sprite.h" #include "draw.h" #include "intrinsics.h" #include "app.h" #include "game.h" #include "asset_cache.h" #include "string.h" #include "scratch.h" #include "math.h" #include "sys.h" #include "world.h" #include "entity.h" #include "mixer.h" #include "atomic.h" #include "collider.h" #include "rng.h" #include "log.h" struct bind_state { b32 is_held; /* Is this bind held down this frame */ u32 num_presses; /* How many times was this bind pressed since last frame */ u32 num_presses_and_repeats; /* Same as `num_presses` but includes key repeats as well */ u32 num_releases; /* How many times was this bind released since last frame */ }; struct blend_tick { struct blend_tick *next; struct blend_tick *prev; struct world world; }; GLOBAL struct { struct atomic_i32 user_thread_shutdown; struct sys_thread user_thread; struct arena arena; struct sys_window *window; /* Render targets */ struct renderer_texture final_texture; struct renderer_texture world_texture; struct renderer_texture ui_texture; struct renderer_texture backbuffer_texture; struct renderer_cmd_buffer *world_cmd_buffer; struct renderer_cmd_buffer *ui_cmd_buffer; struct renderer_cmd_buffer *final_cmd_buffer; struct renderer_cmd_buffer *backbuffer_cmd_buffer; struct xform world_to_ui_xf; struct blend_tick *head_free_blend_tick; struct blend_tick *head_blend_tick; struct world world; struct bind_state bind_states[USER_BIND_KIND_COUNT]; b32 debug_camera; b32 debug_camera_panning; struct v2 debug_camera_pan_start; b32 debug_draw; /* User thread input */ struct sys_mutex sys_events_mutex; struct arena sys_events_arena; /* Per-frame */ i64 time_ns; i64 dt_ns; struct v2 screen_size; struct v2 screen_cursor; struct v2 ui_screen_offset; struct v2 ui_size; struct v2 ui_center; struct v2 ui_cursor; struct v2 world_cursor; } G = ZI, DEBUG_ALIAS(G, G_user); /* ========================== * * Bind state * ========================== */ /* TODO: Remove this */ GLOBAL READONLY enum user_bind_kind g_binds[SYS_BTN_COUNT] = { [SYS_BTN_W] = USER_BIND_KIND_MOVE_UP, [SYS_BTN_S] = USER_BIND_KIND_MOVE_DOWN, [SYS_BTN_A] = USER_BIND_KIND_MOVE_LEFT, [SYS_BTN_D] = USER_BIND_KIND_MOVE_RIGHT, [SYS_BTN_ALT] = USER_BIND_KIND_WALK, [SYS_BTN_M1] = USER_BIND_KIND_FIRE, /* Testing */ [SYS_BTN_M2] = USER_BIND_KIND_DEBUG_DRAG, [SYS_BTN_C] = USER_BIND_KIND_DEBUG_CLEAR, [SYS_BTN_V] = USER_BIND_KIND_DEBUG_SPAWN, [SYS_BTN_N] = USER_BIND_KIND_DEBUG_STEP, [SYS_BTN_F1] = USER_BIND_KIND_DEBUG_PAUSE, [SYS_BTN_F2] = USER_BIND_KIND_DEBUG_CAMERA, [SYS_BTN_F3] = USER_BIND_KIND_DEBUG_DRAW, [SYS_BTN_F11] = USER_BIND_KIND_FULLSCREEN, [SYS_BTN_MWHEELUP] = USER_BIND_KIND_ZOOM_IN, [SYS_BTN_MWHEELDOWN] = USER_BIND_KIND_ZOOM_OUT, [SYS_BTN_M3] = USER_BIND_KIND_PAN, [SYS_BTN_CTRL] = USER_BIND_KIND_CTRL_TEST, #if RTC /* Debug */ [SYS_BTN_FORWARD_SLASH] = USER_BIND_KIND_RESET_COLLIDER_GJK_STEPS, [SYS_BTN_COMMA] = USER_BIND_KIND_DECR_COLLIDER_GJK_STEPS, [SYS_BTN_PERIOD] = USER_BIND_KIND_INCR_COLLIDER_GJK_STEPS #endif }; /* ========================== * * Startup * ========================== */ INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(user_shutdown); INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(user_thread_entry_point, arg); INTERNAL SYS_WINDOW_EVENT_CALLBACK_FUNC_DEF(window_event_callback, event); struct user_startup_receipt user_startup(struct work_startup_receipt *work_sr, struct renderer_startup_receipt *renderer_sr, struct font_startup_receipt *font_sr, struct sprite_startup_receipt *sprite_sr, struct draw_startup_receipt *draw_sr, struct game_startup_receipt *game_sr, struct asset_cache_startup_receipt *asset_cache_sr, struct mixer_startup_receipt *mixer_sr, struct sys_window *window) { (UNUSED)work_sr; (UNUSED)renderer_sr; (UNUSED)font_sr; (UNUSED)sprite_sr; (UNUSED)draw_sr; (UNUSED)game_sr; (UNUSED)asset_cache_sr; (UNUSED)mixer_sr; G.arena = arena_alloc(GIGABYTE(64)); G.sys_events_mutex = sys_mutex_alloc(); G.sys_events_arena = arena_alloc(GIGABYTE(64)); world_alloc(&G.world); G.world_to_ui_xf = XFORM_IDENT; G.world_cmd_buffer = renderer_cmd_buffer_alloc(); G.ui_cmd_buffer = renderer_cmd_buffer_alloc(); G.final_cmd_buffer = renderer_cmd_buffer_alloc(); G.backbuffer_cmd_buffer = renderer_cmd_buffer_alloc(); G.window = window; sys_window_register_event_callback(G.window, &window_event_callback); G.debug_draw = true; G.user_thread = sys_thread_alloc(&user_thread_entry_point, NULL, STR("[P1] User thread")); app_register_exit_callback(&user_shutdown); return (struct user_startup_receipt) { 0 }; } INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(user_shutdown) { __prof; atomic_i32_eval_exchange(&G.user_thread_shutdown, true); sys_thread_wait_release(&G.user_thread); } /* ========================== * * Window -> user communication * ========================== */ INTERNAL struct sys_event_array pop_sys_events(struct arena *arena) { struct sys_event_array array = ZI; struct sys_lock lock = sys_mutex_lock_e(&G.sys_events_mutex); { struct buffer events_buff = arena_to_buffer(&G.sys_events_arena); arena_align(arena, alignof(struct sys_event)); array.events = (struct sys_event *)arena_push_array(arena, u8, events_buff.size); array.count = events_buff.size / sizeof(struct sys_event); MEMCPY(array.events, events_buff.data, events_buff.size); arena_reset(&G.sys_events_arena); } sys_mutex_unlock(&lock); return array; } INTERNAL SYS_WINDOW_EVENT_CALLBACK_FUNC_DEF(window_event_callback, event) { struct sys_lock lock = sys_mutex_lock_e(&G.sys_events_mutex); { *arena_push(&G.sys_events_arena, struct sys_event) = event; } sys_mutex_unlock(&lock); } /* ========================== * * Game -> user communication * ========================== */ INTERNAL struct blend_tick *blend_tick_alloc(void) { struct blend_tick *bt = NULL; if (G.head_free_blend_tick) { bt = G.head_free_blend_tick; G.head_free_blend_tick = bt->next; *bt = (struct blend_tick) { .world = bt->world }; } else { bt = arena_push_zero(&G.arena, struct blend_tick); world_alloc(&bt->world); } if (G.head_blend_tick) { bt->next = G.head_blend_tick; G.head_blend_tick->prev = bt; } G.head_blend_tick = bt; return bt; } INTERNAL void blend_tick_release(struct blend_tick *bt) { struct blend_tick *next = bt->next; struct blend_tick *prev = bt->prev; /* Remove from list */ if (next) { next->prev = prev; } if (prev) { prev->next = next; } if (bt == G.head_blend_tick) { G.head_blend_tick = next; } /* Add to free list */ bt->next = G.head_free_blend_tick; bt->prev = NULL; G.head_free_blend_tick = bt; } struct interp_ticks { struct world *from_tick; struct world *to_tick; }; INTERNAL struct interp_ticks pull_ticks(i64 blend_time_ns) { __prof; /* Find newest stored tick */ struct world *newest_tick = NULL; for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { if (!newest_tick || bt->world.tick_id > newest_tick->tick_id) { newest_tick = &bt->world; } } struct world *from_tick; struct world *to_tick; if (newest_tick && game_get_latest_tick_continuity_gen() == newest_tick->continuity_gen) { /* Pull new tick from game thread if necessary */ if (!newest_tick || game_get_latest_tick_id() > newest_tick->tick_id) { struct blend_tick *latest_bt = blend_tick_alloc(); newest_tick = &latest_bt->world; game_get_latest_tick(newest_tick); } /* Find oldest tick */ struct world *oldest_tick = NULL; for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { if (!oldest_tick || bt->world.tick_id < oldest_tick->tick_id) { oldest_tick = &bt->world; } } /* Find closest ticks to blend time */ from_tick = oldest_tick; to_tick = newest_tick; for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { i64 bt_time_ns = bt->world.publishtime_ns; if (bt_time_ns < blend_time_ns && bt_time_ns > from_tick->publishtime_ns) { from_tick = &bt->world; } if (bt_time_ns > blend_time_ns && bt_time_ns < to_tick->publishtime_ns) { to_tick = &bt->world; } } /* Free any unused old ticks */ { struct temp_arena scratch = scratch_begin_no_conflict(); struct blend_tick **bts_to_free = arena_dry_push(scratch.arena, struct blend_tick *); u64 bts_to_free_count = 0; for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { i64 bt_time_ns = bt->world.publishtime_ns; if (bt_time_ns < from_tick->publishtime_ns) { *arena_push(scratch.arena, struct blend_tick *) = bt; ++bts_to_free_count; } } for (u64 i = 0; i < bts_to_free_count; ++i) { blend_tick_release(bts_to_free[i]); } scratch_end(scratch); } } else { /* Latest tick is discontinuous */ /* Free all blend ticks */ { struct temp_arena scratch = scratch_begin_no_conflict(); struct blend_tick **bts_to_free = arena_dry_push(scratch.arena, struct blend_tick *); u64 bts_to_free_count = 0; for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { *arena_push(scratch.arena, struct blend_tick *) = bt; ++bts_to_free_count; } for (u64 i = 0; i < bts_to_free_count; ++i) { blend_tick_release(bts_to_free[i]); } scratch_end(scratch); } /* Allocate new blend tick */ struct blend_tick *bt = blend_tick_alloc(); struct world *tick = &bt->world; game_get_latest_tick(tick); from_tick = tick; to_tick = tick; } return (struct interp_ticks) { .from_tick = from_tick, .to_tick = to_tick }; } /* ========================== * * User -> game communication * ========================== */ INTERNAL void queue_game_cmd(struct arena *arena, struct game_cmd_list *list, struct game_cmd src) { struct game_cmd *cmd = arena_push(arena, struct game_cmd); *cmd = src; if (list->last) { list->last->next = cmd; } else { list->first = cmd; } list->last = cmd; } INTERNAL void pubilsh_game_cmds(struct game_cmd_list *list) { struct temp_arena scratch = scratch_begin_no_conflict(); struct buffer buff = game_string_from_cmds(scratch.arena, list); game_push_cmds_string(buff); scratch_end(scratch); } /* ========================== * * Debug draw * ========================== */ /* TODO: remove this (testing) */ INTERNAL void debug_draw_xform(struct xform xf, u32 color_x, u32 color_y) { f32 thickness = 2.f; f32 arrowhead_len = 15.f; struct v2 pos = xform_mul_v2(G.world_to_ui_xf, xf.og); struct v2 x_ray = xform_basis_mul_v2(G.world_to_ui_xf, xform_get_right(xf)); struct v2 y_ray = xform_basis_mul_v2(G.world_to_ui_xf, xform_get_up(xf)); f32 ray_scale = 1; x_ray = v2_mul(x_ray, ray_scale); y_ray = v2_mul(y_ray, ray_scale); draw_arrow_ray(G.ui_cmd_buffer, pos, x_ray, thickness, arrowhead_len, color_x); draw_arrow_ray(G.ui_cmd_buffer, pos, y_ray, thickness, arrowhead_len, color_y); //u32 color_quad = RGBA_32_F(0, 1, 1, 0.3); //struct quad quad = quad_from_rect(RECT(0, 0, 1, -1)); //quad = xform_mul_quad(xf, quad_scale(quad, 0.075f)); //draw_quad(G.ui_cmd_buffer, quad, color); } /* TODO: remove this (testing) */ INTERNAL void debug_draw_movement(struct entity *ent) { f32 thickness = 2.f; f32 arrow_len = 15.f; u32 color_vel = RGBA_32_F(1, 0.5, 0, 1); struct xform xf = entity_get_xform(ent); struct v2 velocity = ent->linear_velocity; struct v2 pos = xform_mul_v2(G.world_to_ui_xf, xf.og); struct v2 vel_ray = xform_basis_mul_v2(G.world_to_ui_xf, velocity); if (v2_len(vel_ray) > 0.00001) { draw_arrow_ray(G.ui_cmd_buffer, pos, vel_ray, thickness, arrow_len, color_vel); } } /* ========================== * * Sort entities * ========================== */ INTERNAL SORT_COMPARE_FUNC_DEF(entity_draw_order_cmp, arg_a, arg_b, udata) { (UNUSED)udata; struct entity *a = *(struct entity **)arg_a; struct entity *b = *(struct entity **)arg_b; i32 res = 0; if (res == 0) { /* Sort by layer */ i32 a_cmp = a->layer; i32 b_cmp = b->layer; res = (a_cmp < b_cmp) - (a_cmp > b_cmp); } if (res == 0) { /* Sort by sprite */ u128 a_cmp = a->sprite.hash; u128 b_cmp = b->sprite.hash; res = u128_lt(a_cmp, b_cmp) - u128_gt(a_cmp, b_cmp); } if (res == 0) { /* Sort by activation */ u64 a_cmp = a->activation_tick; u64 b_cmp = b->activation_tick; res = (a_cmp < b_cmp) - (a_cmp > b_cmp); } return res; } /* ========================== * * Update * ========================== */ INTERNAL void user_update(void) { struct temp_arena scratch = scratch_begin_no_conflict(); /* ========================== * * Begin frame * ========================== */ i64 now_ns = sys_time_ns(); G.dt_ns = max_f64(0.0, now_ns - G.time_ns); G.time_ns += G.dt_ns; G.screen_size = sys_window_get_size(G.window); struct entity_store *store = G.world.entity_store; struct sprite_scope *sprite_frame_scope = sprite_scope_begin(); struct game_cmd_list cmd_list = ZI; /* ========================== * * Interpolate between game ticks * ========================== */ { __profscope(interpolate_ticks); #if USER_INTERP_ENABLED /* TODO: Use actual fps of game thread (will differ from GAME_FPS if game thread is lagging) to hide lag with slow-motion? */ i64 blend_time_offset_ns = NS_FROM_SECONDS((1.0 / GAME_FPS) * USER_INTERP_OFFSET_TICK_RATIO); i64 blend_time_ns = G.time_ns > blend_time_offset_ns ? G.time_ns - blend_time_offset_ns : 0; /* Pull ticks */ struct interp_ticks interp_ticks = pull_ticks(blend_time_ns); struct world *t0 = interp_ticks.from_tick; struct world *t1 = interp_ticks.to_tick; f32 tick_blend = 0; { i64 t0_time_ns = t0->publishtime_ns; i64 t1_time_ns = t1->publishtime_ns; if (t1_time_ns > t0_time_ns) { f64 t0_t1_elapsed = SECONDS_FROM_NS(t1_time_ns - t0_time_ns); f64 t0_blend_elapsed = SECONDS_FROM_NS(blend_time_ns - t0_time_ns); tick_blend = t0_blend_elapsed / t0_t1_elapsed; } tick_blend = clamp_f32(tick_blend, 0.0f, 1.0f); } world_copy_replace(&G.world, t0); /* Blend world globals */ G.world.time_ns = math_lerp_i64(t0->time_ns, t1->time_ns, (f64)tick_blend); G.world.dt_ns = math_lerp_i64(t0->dt_ns, t1->dt_ns, (f64)tick_blend); /* Blend entities */ u64 num_entities = min_u64(t0->entity_store->reserved, t1->entity_store->reserved); { __profscope(tick_blending); for (u64 i = 0; i < num_entities; ++i) { struct entity *e = &store->entities[i]; struct entity *e0 = &t0->entity_store->entities[i]; struct entity *e1 = &t1->entity_store->entities[i]; if (e0->valid && e1->valid && entity_has_prop(e0, ENTITY_PROP_ACTIVE) && entity_has_prop(e1, ENTITY_PROP_ACTIVE) && e0->handle.gen == e1->handle.gen && e0->continuity_gen == e1->continuity_gen) { e->local_xform = xform_lerp(e0->local_xform, e1->local_xform, tick_blend); if (e->is_top) { /* TODO: Cache parent & child xforms in game thread */ struct xform e0_xf = entity_get_xform(e0); struct xform e1_xf = entity_get_xform(e1); entity_set_xform(e, xform_lerp(e0_xf, e1_xf, tick_blend)); } e->control_force = math_lerp_f32(e0->control_force, e1->control_force, tick_blend); e->control_torque = math_lerp_f32(e0->control_torque, e1->control_torque, tick_blend); e->linear_velocity = v2_lerp(e0->linear_velocity, e1->linear_velocity, tick_blend); e->angular_velocity = math_lerp_angle(e0->angular_velocity, e1->angular_velocity, tick_blend); e->control.move = v2_lerp(e0->control.move, e1->control.move, tick_blend); e->control.focus = v2_lerp(e0->control.focus, e1->control.focus, tick_blend); e->sprite_local_xform = xform_lerp(e0->sprite_local_xform, e1->sprite_local_xform, tick_blend); e->animation_time_in_frame = math_lerp_f64(e0->animation_time_in_frame, e1->animation_time_in_frame, (f64)tick_blend); e->animation_frame = (u32)math_round_to_int(math_lerp_f32(e0->animation_frame, e1->animation_frame, tick_blend)); e->camera_quad_xform = xform_lerp(e0->camera_quad_xform, e1->camera_quad_xform, tick_blend); e->camera_xform_target = xform_lerp(e0->camera_xform_target, e1->camera_xform_target, tick_blend); e->shake = math_lerp_f32(e0->shake, e1->shake, tick_blend); e->tracer_gradient_start = v2_lerp(e0->tracer_gradient_start, e1->tracer_gradient_start, tick_blend); e->tracer_gradient_end = v2_lerp(e0->tracer_gradient_end, e1->tracer_gradient_end, tick_blend); } } } #else struct interp_ticks interp_ticks = pull_ticks(G.time); world_copy_replace(&G.world, interp_ticks.to_tick); tick_is_first_frame = G.world.tick_id == 0; #endif } /* ========================== * * Find important entities * ========================== */ struct entity *active_camera; { enum entity_prop props[] = { ENTITY_PROP_CAMERA, ENTITY_PROP_CAMERA_ACTIVE }; active_camera = entity_find_first_match_all(store, (struct entity_prop_array) { .count = ARRAY_COUNT(props), .props = props }); } struct entity *active_player; { enum entity_prop props[] = { ENTITY_PROP_PLAYER_CONTROLLED, ENTITY_PROP_ACTIVE }; active_player = entity_find_first_match_all(store, (struct entity_prop_array) { .count = ARRAY_COUNT(props), .props = props }); } /* ========================== * * Read sys events * ========================== */ struct sys_event_array events = pop_sys_events(scratch.arena); /* Reset bind pressed / released states */ for (u32 i = 0; i < ARRAY_COUNT(G.bind_states); ++i) { G.bind_states[i] = (struct bind_state) { .is_held = G.bind_states[i].is_held }; } for (u64 entity_index = 0; entity_index < events.count; ++entity_index) { struct sys_event *event = &events.events[entity_index]; if (event->kind == SYS_EVENT_KIND_QUIT) { app_exit(); } if (event->kind == SYS_EVENT_KIND_BUTTON_UP) { #if DEVELOPER /* Escape quit */ if (event->button == SYS_BTN_ESC) { app_exit(); } #endif } /* Update mouse pos */ if (event->kind == SYS_EVENT_KIND_CURSOR_MOVE) { G.screen_cursor = event->cursor_position; } /* Update bind states */ if ((event->kind == SYS_EVENT_KIND_BUTTON_DOWN || event->kind == SYS_EVENT_KIND_BUTTON_UP)) { enum sys_btn button = event->button; button = button >= SYS_BTN_COUNT ? SYS_BTN_NONE : button; enum user_bind_kind bind = g_binds[button]; if (bind) { b32 pressed = event->kind == SYS_EVENT_KIND_BUTTON_DOWN; #if 0 b32 out_of_bounds = button >= SYS_BTN_M1 && button <= SYS_BTN_M5 && (G.ui_cursor.x < 0 || G.ui_cursor.y < 0 || G.ui_cursor.x > G.ui_size.x || G.ui_cursor.y > G.ui_size.y); #else b32 out_of_bounds = false; #endif G.bind_states[bind].is_held = pressed && !out_of_bounds; if (pressed) { if (!out_of_bounds) { ++G.bind_states[bind].num_presses_and_repeats; if (!event->is_repeat) { ++G.bind_states[bind].num_presses; } } } else { ++G.bind_states[bind].num_releases; } } } } /* ========================== * * Debug commands * ========================== */ /* Test fullscreen */ { struct bind_state state = G.bind_states[USER_BIND_KIND_FULLSCREEN]; if (state.num_presses) { struct sys_window_settings settings = sys_window_get_settings(G.window); settings.flags ^= SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN; sys_window_update_settings(G.window, &settings); } } /* Test clear world */ { struct bind_state state = G.bind_states[USER_BIND_KIND_DEBUG_CLEAR]; if (state.num_presses) { queue_game_cmd(scratch.arena, &cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_CLEAR_ALL }); } } /* Test pause */ { struct bind_state state = G.bind_states[USER_BIND_KIND_DEBUG_PAUSE]; if (state.num_presses) { queue_game_cmd(scratch.arena, &cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_PAUSE }); } } /* Test step */ { struct bind_state state = G.bind_states[USER_BIND_KIND_DEBUG_STEP]; for (u32 i = 0; i < state.num_presses_and_repeats; ++i) { queue_game_cmd(scratch.arena, &cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_STEP }); } } /* Test spawn */ { struct bind_state state = G.bind_states[USER_BIND_KIND_DEBUG_SPAWN]; if (state.num_presses) { queue_game_cmd(scratch.arena, &cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_SPAWN_TEST }); } } if (G.bind_states[USER_BIND_KIND_DEBUG_DRAW].num_presses > 0) { G.debug_draw = !G.debug_draw; } if (G.bind_states[USER_BIND_KIND_DEBUG_CAMERA].num_presses > 0) { G.debug_camera = !G.debug_camera; } /* ========================== * * Apply shake * ========================== */ for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(ent)) continue; /* How much time between camera shakes */ i64 frequency_ns = NS_FROM_SECONDS(0.01f); f32 shake = ent->shake; if (shake > 0) { u64 basis = hash_fnv64(HASH_FNV64_BASIS, BUFFER_FROM_STRUCT(&ent->handle)); u64 angle_seed0 = basis + (u64)(G.world.time_ns / frequency_ns); u64 angle_seed1 = angle_seed0 + 1; f32 angle0 = rng_noise_f32(angle_seed0, 0, TAU); f32 angle1 = rng_noise_f32(angle_seed1, 0, TAU); struct v2 vec0 = v2_with_len(v2_from_angle(angle0), shake); /* NOTE: vec1 not completely accurate since shake can change between frames, just a prediction */ struct v2 vec1 = v2_with_len(v2_from_angle(angle1), shake); /* TODO: Cubic interp? */ f32 blend = (f32)(G.world.time_ns % frequency_ns) / (f32)frequency_ns; struct v2 vec = v2_lerp(vec0, vec1, blend); struct xform xf = entity_get_xform(ent); xf.og = v2_add(xf.og, v2_mul(vec, shake)); entity_set_xform(ent, xf); } } /* ========================== * * Update ui from camera * ========================== */ /* Calculate ui dimensions */ if (G.debug_camera) { G.ui_size = G.screen_size; G.ui_screen_offset = V2(0, 0); } else { /* Determine ui size by camera & window dimensions */ f32 aspect_ratio = 1.0; { struct xform quad_xf = xform_mul(entity_get_xform(active_camera), active_camera->camera_quad_xform); struct v2 camera_size = xform_get_scale(quad_xf); if (!v2_is_zero(camera_size)) { aspect_ratio = camera_size.x / camera_size.y; } } f32 width = G.screen_size.x; f32 height = G.screen_size.y; if (width / height > aspect_ratio) { width = height * aspect_ratio; } else { height = math_ceil(width / aspect_ratio); } G.ui_size = V2(width, height); /* Center ui in window */ f32 x = 0; f32 y = 0; x = math_round(G.screen_size.x / 2 - width / 2); y = math_round(G.screen_size.y / 2 - height / 2); G.ui_screen_offset = V2(x, y); } G.ui_center = v2_mul(G.ui_size, 0.5); G.ui_cursor = v2_sub(G.screen_cursor, G.ui_screen_offset); /* ========================== * * Update view from camera * ========================== */ if (G.debug_camera) { G.world_to_ui_xf = xform_basis_with_rotation_world(G.world_to_ui_xf, 0); struct v2 world_cursor = xform_invert_mul_v2(G.world_to_ui_xf, G.ui_cursor); /* Pan view */ if (G.bind_states[USER_BIND_KIND_PAN].is_held) { if (!G.debug_camera_panning) { G.debug_camera_pan_start = world_cursor; G.debug_camera_panning = true; } struct v2 offset = v2_neg(v2_sub(G.debug_camera_pan_start, world_cursor)); G.world_to_ui_xf = xform_translated(G.world_to_ui_xf, offset); world_cursor = xform_invert_mul_v2(G.world_to_ui_xf, G.ui_cursor); G.debug_camera_pan_start = world_cursor; } else { G.debug_camera_panning = false; } /* Zoom view */ i32 input_zooms = G.bind_states[USER_BIND_KIND_ZOOM_IN].num_presses - G.bind_states[USER_BIND_KIND_ZOOM_OUT].num_presses; if (input_zooms != 0) { /* Zoom to cursor */ f32 zoom_rate = 2; f32 zoom = math_pow(zoom_rate, input_zooms); G.world_to_ui_xf = xform_translated(G.world_to_ui_xf, world_cursor); G.world_to_ui_xf = xform_scaled(G.world_to_ui_xf, V2(zoom, zoom)); G.world_to_ui_xf = xform_translated(G.world_to_ui_xf, v2_neg(world_cursor)); } } else { struct xform xf = entity_get_xform(active_camera); struct v2 center = xf.og; f32 rot = xform_get_rotation(xf); /* Scale view into viewport based on camera size */ struct v2 size = G.ui_size; { struct xform quad_xf = xform_mul(xf, active_camera->camera_quad_xform); struct v2 camera_size = xform_get_scale(quad_xf); if (!v2_is_zero(camera_size)) { size = v2_div_v2(size, camera_size); } } f32 scale_ui = min_f32(size.x, size.y); struct trs trs = TRS(.t = v2_sub(G.ui_center, center), .r = rot, .s = V2(scale_ui, scale_ui)); struct v2 pivot = center; G.world_to_ui_xf = XFORM_IDENT; G.world_to_ui_xf = xform_translated(G.world_to_ui_xf, pivot); G.world_to_ui_xf = xform_translated(G.world_to_ui_xf, trs.t); G.world_to_ui_xf = xform_rotated(G.world_to_ui_xf, trs.r); G.world_to_ui_xf = xform_scaled(G.world_to_ui_xf, trs.s); G.world_to_ui_xf = xform_translated(G.world_to_ui_xf, v2_neg(pivot)); } G.world_cursor = xform_invert_mul_v2(G.world_to_ui_xf, G.ui_cursor); /* ========================== * * Update listener from view * ========================== */ { struct v2 up = V2(0, -1); struct v2 listener_pos = xform_invert_mul_v2(G.world_to_ui_xf, G.ui_center); struct v2 listener_dir = v2_norm(xform_basis_invert_mul_v2(G.world_to_ui_xf, up)); mixer_set_listener(listener_pos, listener_dir); } /* ========================== * * Draw test grid * ========================== */ { f32 thickness = 2; u32 color = RGBA_32(0x3f, 0x3f, 0x3f, 0xFF); struct v2 offset = v2_neg(xform_mul_v2(G.world_to_ui_xf, V2(0, 0))); f32 spacing = xform_get_scale(G.world_to_ui_xf).x; struct v2 pos = xform_invert_mul_v2(G.world_to_ui_xf, V2(0, 0)); struct v2 size = xform_basis_invert_mul_v2(G.world_to_ui_xf, G.ui_size); draw_grid(G.world_cmd_buffer, RECT_FROM_V2(pos, size), color, thickness, spacing, offset); } /* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ /* ========================== * * Sort entities * ========================== */ struct entity **sorted = arena_dry_push(scratch.arena, struct entity *); u64 sorted_count = 0; { /* Copy valid entities */ { __profscope(copy_sprites_for_sorting); for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *ent = &store->entities[entity_index]; if (entity_is_valid_and_active(ent)) { *arena_push(scratch.arena, struct entity *) = ent; ++sorted_count; } } } /* Sort */ { __profscope(sort_sprites); merge_sort(sorted, sorted_count, sizeof(*sorted), entity_draw_order_cmp, NULL); } } /* ========================== * * Draw entities * ========================== */ { __profscope(draw_entities); for (u64 sorted_index = 0; sorted_index < sorted_count; ++sorted_index) { struct entity *ent = sorted[sorted_index]; if (!entity_is_valid_and_active(ent)) continue; //if (sprite_tag_is_nil(ent->sprite)) continue; struct sprite_tag sprite = ent->sprite; struct entity *parent = entity_from_handle(store, ent->parent); struct xform xf = entity_get_xform(ent); struct xform parent_xf = entity_get_xform(parent); b32 skip_debug_draw = !G.debug_camera && ent == active_camera; b32 skip_debug_draw_transform = entity_has_prop(ent, ENTITY_PROP_CAMERA); skip_debug_draw_transform = true; struct xform sprite_xform = xf; /* Draw tracer */ if (entity_has_prop(ent, ENTITY_PROP_TRACER)) { struct v2 velocity = ent->tracer_start_velocity; struct v2 a = ent->tracer_start; struct v2 b = xf.og; struct v2 c = ent->tracer_gradient_start; struct v2 d = ent->tracer_gradient_end; struct v2 vcd = v2_sub(d, c); struct v2 vca = v2_sub(a, c); struct v2 vdb = v2_sub(b, d); struct v2 vdc = v2_neg(vcd); f32 opacity_a = 1; if (v2_dot(velocity, vca) < 0) { a = c; opacity_a = 0; } else { opacity_a = v2_dot(vcd, vca) / v2_len_sq(vcd); } f32 opacity_b = clamp_f32(1.f - (v2_dot(vdc, vdb) / v2_len_sq(vdc)), 0, 1); f32 thickness = 0.01f; u32 color_start = RGBA_32_F(1, 0.5, 0, opacity_a); u32 color_end = RGBA_32_F(1, 0.8, 0.4, opacity_b); if (opacity_b > 0.99f) { draw_circle(G.world_cmd_buffer, b, thickness / 2, color_end, 20); } draw_gradient_line(G.world_cmd_buffer, a, b, thickness, color_start, color_end); } /* Draw sprite */ //if ((false)) { if (!sprite_tag_is_nil(sprite)) { /* Calculate sprite xform */ sprite_xform = xform_mul(xf, ent->sprite_local_xform); /* Async load */ struct sprite_sheet *sheet = sprite_sheet_from_tag_async(sprite_frame_scope, sprite); struct sprite_texture *texture = sprite_texture_from_tag_async(sprite_frame_scope, sprite); (UNUSED)texture; /* TODO: Fade in placeholder if texture isn't loaded */ if (sheet->loaded) { struct sprite_sheet_frame frame = sprite_sheet_get_frame(sheet, ent->animation_frame); struct quad quad = xform_mul_quad(sprite_xform, QUAD_UNIT_SQUARE_CENTERED); struct draw_texture_params params = DRAW_TEXTURE_PARAMS(.sprite = sprite, .tint = ent->sprite_tint, .clip = frame.clip); draw_quad_texture(G.world_cmd_buffer, params, quad); } } /* Debug draw entity info */ if (G.debug_draw && !skip_debug_draw) { struct temp_arena temp = arena_temp_begin(scratch.arena); if (entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(ent, ENTITY_PROP_PHYSICAL_KINEMATIC)) { debug_draw_movement(ent); } /* Draw xform */ if (!skip_debug_draw_transform) { u32 color_x = RGBA_32_F(1, 0, 0, 0.5); u32 color_y = RGBA_32_F(0, 1, 0, 0.5); debug_draw_xform(xf, color_x, color_y); } /* Draw AABB */ if (ent->local_collider.count > 0) { struct aabb aabb = collider_aabb_from_collider(&ent->local_collider, xf); f32 thickness = 1; u32 color = RGBA_32_F(1, 0, 1, 0.5); struct quad quad = quad_from_aabb(aabb); quad = xform_mul_quad(G.world_to_ui_xf, quad); draw_quad_line(G.ui_cmd_buffer, quad, thickness, color); } /* Draw focus arrow */ if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { struct sprite_sheet *sheet = sprite_sheet_from_tag_async(sprite_frame_scope, ent->sprite); struct sprite_sheet_slice slice = sprite_sheet_get_slice(sheet, STR("attach.wep"), ent->animation_frame); struct v2 start = xform_mul_v2(sprite_xform, slice.center); start = xform_mul_v2(G.world_to_ui_xf, start); struct v2 end = v2_add(xf.og, ent->control.focus); end = xform_mul_v2(G.world_to_ui_xf, end); draw_arrow_line(G.ui_cmd_buffer, start, end, 3, 10, RGBA_32_F(1, 1, 1, 0.5)); } #if 0 /* Draw slices */ if (!sprite_tag_is_nil(ent->sprite)) { struct sprite_sheet *sheet = sprite_sheet_from_tag_async(sprite_frame_scope, sprite); u32 quad_color = RGBA_32_F(1, 0, 0.5, 1); u32 point_color = RGBA_32_F(1, 0, 0, 1); u32 ray_color = RGBA_32_F(1, 0, 0.5, 1); if (colliding) { quad_color = RGBA_32_F(1, 1, 1, 1); } for (u64 i = 0; i < sheet->slice_groups_count; ++i) { struct sprite_sheet_slice_group *group = &sheet->slice_groups[i]; if (string_ends_with(group->name, STR(".ray"))) continue; for (u32 j = 0; j < group->per_frame_count; ++j) { struct sprite_sheet_slice slice = group->frame_slices[(ent->animation_frame * group->per_frame_count) + j]; struct v2 center = xform_mul_v2(sprite_xform, slice.center); center = xform_mul_v2(G.world_to_ui_xf, center); if (!slice.has_ray) { struct quad quad = quad_from_rect(slice.rect); quad = xform_mul_quad(sprite_xform, quad); quad = xform_mul_quad(G.world_to_ui_xf, quad); draw_quad_line(G.ui_cmd_buffer, quad, 2, quad_color); } draw_circle(G.ui_cmd_buffer, center, 3, point_color, 20); if (slice.has_ray) { struct v2 ray = xform_basis_mul_v2(sprite_xform, slice.dir); ray = xform_basis_mul_v2(G.world_to_ui_xf, ray); ray = v2_with_len(ray, 25); draw_arrow_ray(G.ui_cmd_buffer, center, ray, 2, 10, ray_color); } } } } #endif /* Draw collider */ if (entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC)) { struct collider_shape collider = ent->local_collider; u32 color = RGBA_32_F(1, 1, 0, 0.5); f32 thickness = 2; { /* Draw collider using support points */ u32 detail = 32; //u32 detail = 64; //u32 detail = 512; draw_collider_line(G.ui_cmd_buffer, G.world_to_ui_xf, collider, xf, thickness, color, detail); } { /* Draw collider shape points */ for (u32 i = 0; i < collider.count; ++i) { struct v2 p = xform_mul_v2(xform_mul(G.world_to_ui_xf, xf), collider.points[i]); draw_circle(G.ui_cmd_buffer, p, 3, COLOR_BLUE, 10); } } if (collider.count == 1 && collider.radius > 0) { /* Draw upwards line for circle */ struct v2 start = xf.og; struct v2 end = collider_get_support_point(&collider, xf, v2_neg(xf.by)).p; start = xform_mul_v2(G.world_to_ui_xf, start); end = xform_mul_v2(G.world_to_ui_xf, end); draw_line(G.ui_cmd_buffer, start, end, thickness, color); } #if 0 /* Draw support point at focus dir */ { struct v2 p = collider_support_point(&collider, xf, ent->control.focus); p = xform_mul_v2(G.world_to_ui_xf, p); draw_circle(G.ui_cmd_buffer, p, 3, COLOR_RED, 10); } #endif } /* Draw contact constraint */ if (entity_has_prop(ent, ENTITY_PROP_CONTACT_CONSTRAINT)) { struct phys_contact_constraint *data = &ent->contact_constraint_data; struct entity *e0 = entity_from_handle(store, data->e0); struct entity *e1 = entity_from_handle(store, data->e1); (UNUSED)e0; (UNUSED)e1; #if 1 /* Draw contact points */ { f32 radius = 5; for (u32 i = 0; i < data->num_points; ++i) { struct phys_contact_point point = data->points[i]; #if 0 struct v2 p0 = xform_mul_v2(e0_xf, contact.point_local_e0); struct v2 p1 = xform_mul_v2(e1_xf, contact.point_local_e1); struct v2 point = v2_add(p0, v2_mul(v2_sub(p1, p0), 0.5f)); #else struct v2 dbg_pt = point.dbg_pt; #endif /* Draw point */ { //u32 color = contact.persisted ? RGBA_32_F(1, 1, 0, 0.50) : RGBA_32_F(1, 0, 0, 0.50); u32 color = RGBA_32_F(1, 1, 0, 0.50); //struct v2 point = xform_mul_v2(e0_xf, contact.p0_local); //struct v2 point = contact.p0_initial_world; draw_circle(G.ui_cmd_buffer, xform_mul_v2(G.world_to_ui_xf, dbg_pt), radius, color, 10); } /* Draw normal */ { u32 color = COLOR_WHITE; f32 len = 0.1f; f32 arrow_thickness = 2; f32 arrow_height = 5; struct v2 start = xform_mul_v2(G.world_to_ui_xf, dbg_pt); struct v2 end = xform_mul_v2(G.world_to_ui_xf, v2_add(dbg_pt, v2_mul(v2_norm(data->normal), len))); draw_arrow_line(G.ui_cmd_buffer, start, end, arrow_thickness, arrow_height, color); } #if 0 /* Draw contact info */ { struct font *disp_font = font_load_async(STR("res/fonts/fixedsys.ttf"), 12.0f); if (disp_font) { f32 offset_px = 10; struct string fmt = STR( "e0 index: %F\n" "e1 index: %F\n" "id: 0x%F\n" "impulse (n): %F\n" "impulse (t): %F\n" "separation: %F\n" "normal: (%F, %F)\n" "num contacts: %F" ); struct string text = string_format(temp.arena, fmt, FMT_UINT(e0->handle.idx), FMT_UINT(e1->handle.idx), FMT_HEX(point.id), FMT_FLOAT_P(point.normal_impulse, 3), FMT_FLOAT_P(point.tangent_impulse, 3), FMT_FLOAT_P(point.starting_separation, 6), FMT_FLOAT_P(data->normal.x, 6), FMT_FLOAT_P(data->normal.y, 6), FMT_UINT(data->num_points)); draw_text(G.ui_cmd_buffer, disp_font, v2_add(v2_round(xform_mul_v2(G.world_to_ui_xf, dbg_pt)), V2(0, offset_px)), text); } } #endif } } } /* Draw collision debug */ #if COLLIDER_DEBUG if (entity_has_prop(ent, ENTITY_PROP_COLLISION_DEBUG)) { struct phys_collision_debug *data = &ent->collision_debug_data; struct collider_collision_points_result collider_res = data->res; struct entity *e0 = entity_from_handle(store, data->e0); struct entity *e1 = entity_from_handle(store, data->e1); struct collider_shape e0_collider = e0->local_collider; struct collider_shape e1_collider = e1->local_collider; (UNUSED)e0_collider; (UNUSED)e1_collider; /* Draw closest points */ #if 0 { f32 radius = 4; u32 color = RGBA_32_F(1, 1, 0, 0.5); struct v2 a = xform_mul_v2(G.world_to_ui_xf, data->closest0); struct v2 b = xform_mul_v2(G.world_to_ui_xf, data->closest1); draw_circle(G.ui_cmd_buffer, a, radius, color, 10); draw_circle(G.ui_cmd_buffer, b, radius, color, 10); } #endif /* Draw clipping */ { f32 thickness = 2; f32 radius = 4; u32 color_line = RGBA_32_F(1, 0, 1, 0.25); u32 color_a = RGBA_32_F(1, 0, 0, 0.25); u32 color_b = RGBA_32_F(0, 1, 0, 0.25); u32 color_line_clipped = RGBA_32_F(1, 0, 1, 1); u32 color_a_clipped = RGBA_32_F(1, 0, 0, 1); u32 color_b_clipped = RGBA_32_F(0, 1, 0, 1); { struct v2 a = xform_mul_v2(G.world_to_ui_xf, collider_res.a0); struct v2 b = xform_mul_v2(G.world_to_ui_xf, collider_res.b0); draw_line(G.ui_cmd_buffer, a, b, thickness, color_line); draw_circle(G.ui_cmd_buffer, a, radius, color_a, 10); draw_circle(G.ui_cmd_buffer, b, radius, color_b, 10); struct v2 a_clipped = xform_mul_v2(G.world_to_ui_xf, collider_res.a0_clipped); struct v2 b_clipped = xform_mul_v2(G.world_to_ui_xf, collider_res.b0_clipped); draw_line(G.ui_cmd_buffer, a_clipped, b_clipped, thickness, color_line_clipped); draw_circle(G.ui_cmd_buffer, a_clipped, radius, color_a_clipped, 10); draw_circle(G.ui_cmd_buffer, b_clipped, radius, color_b_clipped, 10); } { struct v2 a = xform_mul_v2(G.world_to_ui_xf, collider_res.a1); struct v2 b = xform_mul_v2(G.world_to_ui_xf, collider_res.b1); draw_line(G.ui_cmd_buffer, a, b, thickness, color_line); draw_circle(G.ui_cmd_buffer, a, radius, color_a, 10); draw_circle(G.ui_cmd_buffer, b, radius, color_b, 10); struct v2 a_clipped = xform_mul_v2(G.world_to_ui_xf, collider_res.a1_clipped); struct v2 b_clipped = xform_mul_v2(G.world_to_ui_xf, collider_res.b1_clipped); draw_line(G.ui_cmd_buffer, a_clipped, b_clipped, thickness, color_line_clipped); draw_circle(G.ui_cmd_buffer, a_clipped, radius, color_a_clipped, 10); draw_circle(G.ui_cmd_buffer, b_clipped, radius, color_b_clipped, 10); } } #if COLLIDER_DEBUG_DETAILED_DRAW_MENKOWSKI struct xform e0_xf = data->xf0; struct xform e1_xf = data->xf1; #if 0 /* Only draw points with large separation */ b32 should_draw = false; for (u32 i = 0; i < data->num_points; ++i) { if (data->points[i].starting_separation < -0.1) { should_draw = true; break; } } #else b32 should_draw = true; #endif if (should_draw) { #if 0 /* Test info */ { struct font *disp_font = font_load_async(STR("res/fonts/fixedsys.ttf"), 12.0f); if (disp_font) { f32 offset_px = 10; struct string fmt = STR( "e0 pos: (%F, %F)\n" "e0 rot: %F\n" "e1 pos: (%F, %F)\n" "e1 rot: %F\n" ); struct string text = string_format(temp.arena, fmt, FMT_FLOAT_P(e0_xf.og.x, 24), FMT_FLOAT_P(e0_xf.og.y, 24), FMT_FLOAT_P(xform_get_rotation(e0_xf), 24), FMT_FLOAT_P(e1_xf.og.x, 24), FMT_FLOAT_P(e1_xf.og.y, 24), FMT_FLOAT_P(xform_get_rotation(e1_xf), 24)); draw_text(G.ui_cmd_buffer, disp_font, v2_add(v2_round(xform_mul_v2(G.world_to_ui_xf, V2(0, 0))), V2(0, offset_px)), text); } } #endif /* Draw menkowski */ { u32 color = collider_res.solved ? RGBA_32_F(0, 0, 0.25, 1) : RGBA_32_F(0, 0.25, 0.25, 1); f32 thickness = 2; u32 detail = 512; (UNUSED)thickness; struct v2_array m = menkowski(temp.arena, &e0_collider, &e1_collider, e0_xf, e1_xf, detail); for (u64 i = 0; i < m.count; ++i) m.points[i] = xform_mul_v2(G.world_to_ui_xf, m.points[i]); draw_poly_line(G.ui_cmd_buffer, m, true, thickness, color); //draw_poly(G.ui_cmd_buffer, m, color); } /* Draw cloud */ { u32 color = RGBA_32_F(1, 1, 1, 1); f32 radius = 2; struct v2_array m = cloud(temp.arena, &e0_collider, &e1_collider, e0_xf, e1_xf); for (u64 i = 0; i < m.count; ++i) { struct v2 p = xform_mul_v2(G.world_to_ui_xf, m.points[i]); draw_circle(G.ui_cmd_buffer, p, radius, color, 10); } } /* Draw normal */ { u32 color = COLOR_WHITE; f32 len = 0.1f; f32 arrow_thickness = 2; f32 arrow_height = 5; struct v2 start = xform_mul_v2(G.world_to_ui_xf, V2(0, 0)); struct v2 end = xform_mul_v2(G.world_to_ui_xf, v2_mul(v2_norm(collider_res.normal), len)); draw_arrow_line(G.ui_cmd_buffer, start, end, arrow_thickness, arrow_height, color); } /* Draw prototype */ { f32 thickness = 2; u32 color = RGBA_32_F(1, 1, 1, 0.25); struct v2_array m = { .points = collider_res.prototype.points, .count = collider_res.prototype.len }; for (u64 i = 0; i < m.count; ++i) m.points[i] = xform_mul_v2(G.world_to_ui_xf, m.points[i]); draw_poly_line(G.ui_cmd_buffer, m, true, thickness, color); for (u64 i = 0; i < m.count; ++i) draw_circle(G.ui_cmd_buffer, m.points[i], 10, color, 10); } /* Draw simplex */ { f32 thickness = 2; u32 line_color = COLOR_YELLOW; u32 color_first = RGBA_32_F(1, 0, 0, 0.75); u32 color_second = RGBA_32_F(0, 1, 0, 0.75); u32 color_third = RGBA_32_F(0, 0, 1, 0.75); struct collider_menkowski_simplex simplex = collider_res.simplex; struct v2 simplex_points[] = { simplex.a.p, simplex.b.p, simplex.c.p }; for (u64 i = 0; i < ARRAY_COUNT(simplex_points); ++i) simplex_points[i] = xform_mul_v2(G.world_to_ui_xf, simplex_points[i]); struct v2_array simplex_array = { .count = simplex.len, .points = simplex_points }; if (simplex.len >= 1) { u32 color = simplex.len == 1 ? color_first : (simplex.len == 2 ? color_second : color_third); draw_circle(G.ui_cmd_buffer, simplex_array.points[0], thickness * 3, color, 10); } if (simplex.len >= 2) { u32 color = simplex.len == 2 ? color_first : color_second; draw_circle(G.ui_cmd_buffer, simplex_array.points[1], thickness * 3, color, 10); } if (simplex.len >= 3) { u32 color = color_first; draw_circle(G.ui_cmd_buffer, simplex_array.points[2], thickness * 3, color, 10); } if (simplex.len >= 2) { draw_poly_line(G.ui_cmd_buffer, simplex_array, simplex.len > 2, thickness, line_color); } } } #endif } #endif #endif /* Draw hierarchy */ if (entity_has_prop(parent, ENTITY_PROP_ACTIVE) && !parent->is_root) { u32 color = RGBA_32_F(0.6, 0.6, 1, 0.75); f32 thickness = 2; f32 arrow_height = 15; struct v2 start = xform_mul_v2(G.world_to_ui_xf, xf.og); struct v2 end = xform_mul_v2(G.world_to_ui_xf, parent_xf.og); draw_arrow_line(G.ui_cmd_buffer, start, end, thickness, arrow_height, color); } /* Draw camera rect */ if (entity_has_prop(ent, ENTITY_PROP_CAMERA)) { u32 color = ent == active_camera ? RGBA_32_F(1, 1, 1, 0.5) : RGBA_32_F(0, 0.75, 0, 0.5); f32 thickness = 3; struct xform quad_xf = xform_mul(xf, ent->camera_quad_xform); struct quad quad = xform_mul_quad(quad_xf, QUAD_UNIT_SQUARE_CENTERED); quad = xform_mul_quad(G.world_to_ui_xf, quad); draw_quad_line(G.ui_cmd_buffer, quad, thickness, color); } arena_temp_end(temp); } } } /* Draw crosshair or show cursor */ if (!G.debug_camera) { struct v2 crosshair_pos = G.ui_cursor; u32 tint = RGBA_32_F(1, 1, 1, 1); struct sprite_tag crosshair_tag = sprite_tag_from_path(STR("res/graphics/crosshair.ase")); struct sprite_texture *t = sprite_texture_from_tag_async(sprite_frame_scope, crosshair_tag); struct v2 size = V2(t->width, t->height); struct xform xf = XFORM_TRS(.t = crosshair_pos, .s = size); struct quad quad = xform_mul_quad(xf, QUAD_UNIT_SQUARE_CENTERED); draw_quad_texture(G.ui_cmd_buffer, DRAW_TEXTURE_PARAMS(.sprite = crosshair_tag, .tint = tint), quad); struct rect cursor_clip = RECT_FROM_V2(G.ui_screen_offset, G.ui_size); cursor_clip.pos = v2_add(cursor_clip.pos, v2_mul(size, 0.5f)); cursor_clip.pos = v2_add(cursor_clip.pos, V2(1, 1)); cursor_clip.size = v2_sub(cursor_clip.size, size); sys_window_cursor_hide(G.window); sys_window_cursor_enable_clip(G.window, cursor_clip); } else { sys_window_cursor_disable_clip(G.window); sys_window_cursor_show(G.window); } /* ========================== * * Construct player control cmd * ========================== */ { /* Queue player move cmd */ f32 move_speed = 1.0f; if (G.bind_states[USER_BIND_KIND_WALK].is_held) { //const f32 walk_ratio = 0.25f; const f32 walk_ratio = 0.05f; move_speed *= walk_ratio; } struct v2 input_move_dir = ZI; { for (enum user_bind_kind bind = 0; bind < (i32)ARRAY_COUNT(G.bind_states); ++bind) { struct bind_state state = G.bind_states[bind]; if (!state.is_held && state.num_presses <= 0) { continue; } switch (bind) { /* Movement */ case USER_BIND_KIND_MOVE_UP: { input_move_dir.y -= 1; } break; case USER_BIND_KIND_MOVE_DOWN: { input_move_dir.y += 1; } break; case USER_BIND_KIND_MOVE_LEFT: { input_move_dir.x -= 1; } break; case USER_BIND_KIND_MOVE_RIGHT: { input_move_dir.x += 1; } break; default: break; } } input_move_dir = xform_basis_invert_mul_v2(G.world_to_ui_xf, input_move_dir); /* Make move dir relative to world view */ input_move_dir = v2_mul(v2_norm(input_move_dir), move_speed); } struct v2 input_aim_dir = v2_sub(G.world_cursor, entity_get_xform(active_player).og); /* Queue cursor move cmd */ queue_game_cmd(scratch.arena, &cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_CURSOR_MOVE, .cursor_pos = G.world_cursor }); /* Queue player input cmd */ if (!G.debug_camera) { queue_game_cmd(scratch.arena, &cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_PLAYER_MOVE, .move_dir = input_move_dir, .aim_dir = input_aim_dir }); } /* Queue player fire cmd */ i32 fire_presses = G.bind_states[USER_BIND_KIND_FIRE].num_presses - G.bind_states[USER_BIND_KIND_FIRE].num_releases; if (!G.debug_camera && fire_presses) { queue_game_cmd(scratch.arena, &cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_PLAYER_FIRE, .state = fire_presses > 0 ? GAME_CMD_STATE_START : GAME_CMD_STATE_STOP }); } /* Queue physics drag cmd */ i32 drag_presses = G.bind_states[USER_BIND_KIND_DEBUG_DRAG].num_presses - G.bind_states[USER_BIND_KIND_DEBUG_DRAG].num_releases; if (drag_presses) { queue_game_cmd(scratch.arena, &cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_DRAG_OBJECT, .state = drag_presses > 0 ? GAME_CMD_STATE_START : GAME_CMD_STATE_STOP }); } #if COLLIDER_DEBUG /* Gjk steps */ { i64 new_steps = collider_debug_steps; new_steps += G.bind_states[USER_BIND_KIND_INCR_COLLIDER_GJK_STEPS].num_presses_and_repeats; new_steps -= G.bind_states[USER_BIND_KIND_DECR_COLLIDER_GJK_STEPS].num_presses_and_repeats; if (G.bind_states[USER_BIND_KIND_RESET_COLLIDER_GJK_STEPS].num_presses_and_repeats > 0) new_steps = 0; collider_debug_steps = (u32)clamp_i64(new_steps, 0, U32_MAX); } #endif } /* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ /* Debug draw info */ if (G.debug_draw) { f32 spacing = 20; struct v2 pos = V2(10, 8); struct font *font = font_load_async(STR("res/fonts/fixedsys.ttf"), 12.0f); if (font) { struct temp_arena temp = arena_temp_begin(scratch.arena); draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("time: %F"), FMT_FLOAT(SECONDS_FROM_NS(G.time_ns)))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("world time: %F"), FMT_FLOAT(SECONDS_FROM_NS(G.world.time_ns)))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("entities: %F/%F"), FMT_UINT(G.world.entity_store->allocated), FMT_UINT(G.world.entity_store->reserved))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("screen_size: (%F, %F)"), FMT_FLOAT((f64)G.screen_size.x), FMT_FLOAT((f64)G.screen_size.y))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("screen_cursor: (%F, %F)"), FMT_FLOAT((f64)G.screen_cursor.x), FMT_FLOAT((f64)G.screen_cursor.y))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("ui_screen_offset: (%F, %F)"), FMT_FLOAT((f64)G.ui_screen_offset.x), FMT_FLOAT((f64)G.ui_screen_offset.y))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("ui_size: (%F, %F)"), FMT_FLOAT((f64)G.ui_size.x), FMT_FLOAT((f64)G.ui_size.y))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("ui_center: (%F, %F)"), FMT_FLOAT((f64)G.ui_center.x), FMT_FLOAT((f64)G.ui_center.y))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("ui_cursor: (%F, %F)"), FMT_FLOAT((f64)G.ui_cursor.x), FMT_FLOAT((f64)G.ui_cursor.y))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("world_to_ui_xf.og: (%F, %F)"), FMT_FLOAT((f64)G.world_to_ui_xf.og.x), FMT_FLOAT((f64)G.world_to_ui_xf.og.y))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("world_to_ui_xf rotation: %F"), FMT_FLOAT((f64)xform_get_rotation(G.world_to_ui_xf)))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("world_to_ui_xf scale: (%F, %F)"), FMT_FLOAT((f64)xform_get_scale(G.world_to_ui_xf).x), FMT_FLOAT((f64)xform_get_scale(G.world_to_ui_xf).x))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("world_cursor: (%F, %F)"), FMT_FLOAT((f64)G.world_cursor.x), FMT_FLOAT((f64)G.world_cursor.y))); pos.y += spacing; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("debug_camera: %F"), FMT_STR(G.debug_camera ? STR("true") : STR("false")))); pos.y += spacing; struct v2 player_linear_vel = entity_find_first_match_one(store, ENTITY_PROP_PLAYER_CONTROLLED)->linear_velocity; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("player linear velocity: (%F, %F)"), FMT_FLOAT_P((f64)player_linear_vel.x, 12), FMT_FLOAT_P((f64)player_linear_vel.y, 12))); pos.y += spacing; f32 player_angular_vel = entity_find_first_match_one(store, ENTITY_PROP_PLAYER_CONTROLLED)->angular_velocity; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("player angular velocity: %F"), FMT_FLOAT_P((f64)player_angular_vel, 12))); pos.y += spacing; struct v2 player_pos = entity_get_xform(entity_find_first_match_one(store, ENTITY_PROP_PLAYER_CONTROLLED)).og; draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("player pos: (%F, %F)"), FMT_FLOAT_P((f64)player_pos.x, 12), FMT_FLOAT_P((f64)player_pos.y, 12))); pos.y += spacing; #if COLLIDER_DEBUG draw_text(G.ui_cmd_buffer, font, pos, string_format(temp.arena, STR("collider gjk steps: %F"), FMT_UINT(collider_debug_steps))); pos.y += spacing; #endif arena_temp_end(temp); } } /* Push game cmds */ pubilsh_game_cmds(&cmd_list); /* ========================== * * Render * ========================== */ { __profscope(render); struct rect ui_viewport = RECT_FROM_V2(V2(0, 0), G.ui_size); struct rect backbuffer_viewport = RECT_FROM_V2(V2(0, 0), G.screen_size); /* Allocate render textures */ struct v2i32 ui_resolution = v2_round_to_int(ui_viewport.size); struct v2i32 backbuffer_resolution = v2_round_to_int(backbuffer_viewport.size); struct v2i32 world_resolution = ui_resolution; { /* World texture */ if (!G.world_texture.handle || !v2i32_eq(renderer_texture_get_size(G.world_texture), world_resolution)) { if (G.world_texture.handle) { renderer_texture_release(G.world_texture); } G.world_texture = renderer_texture_alloc(RENDERER_TEXTURE_FORMAT_R8G8B8A8_UNORM, RENDERER_TEXTURE_FLAG_TARGET, world_resolution, NULL); } /* Ui texture */ if (!G.ui_texture.handle || !v2i32_eq(renderer_texture_get_size(G.ui_texture), ui_resolution)) { if (G.ui_texture.handle) { renderer_texture_release(G.ui_texture); } G.ui_texture = renderer_texture_alloc(RENDERER_TEXTURE_FORMAT_R8G8B8A8_UNORM, RENDERER_TEXTURE_FLAG_TARGET, ui_resolution, NULL); } /* Final texture */ if (!G.final_texture.handle || !v2i32_eq(renderer_texture_get_size(G.final_texture), ui_resolution)) { if (G.final_texture.handle) { renderer_texture_release(G.final_texture); } G.final_texture = renderer_texture_alloc(RENDERER_TEXTURE_FORMAT_R8G8B8A8_UNORM, RENDERER_TEXTURE_FLAG_TARGET, ui_resolution, NULL); } /* Backbuffer texture */ if (!G.backbuffer_texture.handle || !v2i32_eq(renderer_texture_get_size(G.backbuffer_texture), backbuffer_resolution)) { G.backbuffer_texture = renderer_backbuffer_recreate(backbuffer_resolution);; } } /* Combine render textures w/ quad draw cmds */ { /* Draw world texture to final */ { struct draw_texture_params params = DRAW_TEXTURE_PARAMS(.texture = G.world_texture); struct quad quad = quad_from_rect(RECT_FROM_V2(V2(0, 0), V2_FROM_V2I32(ui_resolution))); draw_quad_texture(G.final_cmd_buffer, params, quad); } /* Draw ui texture to final */ { struct draw_texture_params params = DRAW_TEXTURE_PARAMS(.texture = G.ui_texture); struct quad quad = quad_from_rect(RECT_FROM_V2(V2(0, 0), V2_FROM_V2I32(ui_resolution))); draw_quad_texture(G.final_cmd_buffer, params, quad); } /* Draw final texture to backbuffer */ { struct draw_texture_params params = DRAW_TEXTURE_PARAMS(.texture = G.final_texture); struct quad quad = quad_from_rect(RECT_FROM_V2(G.ui_screen_offset, V2_FROM_V2I32(G.ui_size))); draw_quad_texture(G.backbuffer_cmd_buffer, params, quad); } } /* Send cmd buffers to GPU */ renderer_cmd_buffer_send_to_gpu(G.world_cmd_buffer); renderer_cmd_buffer_send_to_gpu(G.ui_cmd_buffer); renderer_cmd_buffer_send_to_gpu(G.final_cmd_buffer); renderer_cmd_buffer_send_to_gpu(G.backbuffer_cmd_buffer); /* Execute render cmds */ { /* Clear textures */ renderer_texture_clear(G.world_texture, RGBA_32_F(0.2f, 0.2f, 0.2f, 1.f)); renderer_texture_clear(G.ui_texture, RGBA_32_F(0, 0, 0, 0)); renderer_texture_clear(G.final_texture, RGBA_32_F(0, 0, 0, 0)); renderer_texture_clear(G.backbuffer_texture, RGBA_32_F(0, 0, 0, 1)); /* Render to world texture */ renderer_texture_render(G.world_texture, G.world_cmd_buffer, G.world_to_ui_xf, ui_viewport, sprite_frame_scope); /* Render to UI texture */ renderer_texture_render(G.ui_texture, G.ui_cmd_buffer, XFORM_IDENT, ui_viewport, sprite_frame_scope); /* Render to final texture */ renderer_texture_render(G.final_texture, G.final_cmd_buffer, XFORM_IDENT, ui_viewport, sprite_frame_scope); /* Render to backbuffer */ renderer_texture_render(G.backbuffer_texture, G.backbuffer_cmd_buffer, XFORM_IDENT, backbuffer_viewport, sprite_frame_scope); } /* Present */ renderer_backbuffer_present(VSYNC_ENABLED); } /* ========================== * * End frame cache scopes * ========================== */ sprite_scope_end(sprite_frame_scope); scratch_end(scratch); } /* ========================== * * User thread entry point * ========================== */ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(user_thread_entry_point, arg) { (UNUSED)arg; i64 last_frame_ns = 0; i64 target_dt_ns = NS_FROM_SECONDS(USER_FRAME_LIMIT > (0) ? (1.0 / USER_FRAME_LIMIT) : 0); while (!atomic_i32_eval(&G.user_thread_shutdown)) { __profscope(user_update_w_sleep); sleep_frame(last_frame_ns, target_dt_ns); last_frame_ns = sys_time_ns(); user_update(); } }