#include "user.h" #include "renderer.h" #include "font.h" #include "texture.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" 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_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 { b32 shutdown; struct sys_thread user_thread; struct arena arena; struct sys_window *window; struct renderer_canvas *world_canvas; struct renderer_canvas *viewport_bg_canvas; struct renderer_canvas *viewport_canvas; struct xform world_view; 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 */ f64 time; f64 dt; struct v2 screen_size; struct v2 screen_cursor; struct v2 viewport_screen_offset; struct v2 viewport_size; struct v2 viewport_center; struct v2 viewport_cursor; struct v2 world_cursor; } G = { 0 }, 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, /* Testing */ [SYS_BTN_C] = USER_BIND_KIND_DEBUG_CLEAR, [SYS_BTN_F6] = USER_BIND_KIND_DEBUG_DRAW, [SYS_BTN_F7] = USER_BIND_KIND_DEBUG_CAMERA, [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 }; /* ========================== * * Window -> user communication * ========================== */ INTERNAL struct sys_event_array pop_sys_events(struct arena *arena) { struct sys_event_array array = { 0 }; sys_mutex_lock(&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(&G.sys_events_mutex); return array; } INTERNAL SYS_WINDOW_EVENT_CALLBACK_DEF(window_event_callback, event) { sys_mutex_lock(&G.sys_events_mutex); { *arena_push(&G.sys_events_arena, struct sys_event) = event; } sys_mutex_unlock(&G.sys_events_mutex); } /* ========================== * * 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(f64 blend_time) { __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; } } /* 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 */ struct world *from_tick = oldest_tick; struct world *to_tick = newest_tick; for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { f64 bt_time = sys_timestamp_seconds(bt->world.tick_ts); if (bt_time < blend_time && bt_time > sys_timestamp_seconds(from_tick->tick_ts)) { from_tick = &bt->world; } if (bt_time > blend_time && bt_time < sys_timestamp_seconds(to_tick->tick_ts)) { 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) { f64 bt_time = sys_timestamp_seconds(bt->world.tick_ts); if (bt_time < sys_timestamp_seconds(from_tick->tick_ts)) { *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); } return (struct interp_ticks) { .from_tick = from_tick, .to_tick = to_tick }; } /* ========================== * * User -> game communication * ========================== */ struct game_cmd_node { struct game_cmd cmd; struct game_cmd_node *next; }; struct game_cmd_list { struct arena *arena; struct game_cmd_node *first; struct game_cmd_node *last; }; INTERNAL void queue_game_cmd(struct game_cmd_list *list, struct game_cmd cmd) { struct game_cmd_node *node = arena_push_zero(list->arena, struct game_cmd_node); node->cmd = cmd; if (list->first) { list->last->next = node; } else { list->first = node; } list->last = node; } INTERNAL void pubilsh_game_cmds(struct game_cmd_list *list) { struct temp_arena scratch = scratch_begin(list->arena); /* Construct array */ struct game_cmd_array array = { .cmds = arena_dry_push(scratch.arena, struct game_cmd) }; for (struct game_cmd_node *node = list->first; node; node = node->next) { struct game_cmd *cmd = arena_push(scratch.arena, struct game_cmd); *cmd = node->cmd; ++array.count; } /* Push array to game thread */ if (array.count > 0) { game_push_cmds(array); } scratch_end(scratch); } /* ========================== * * Update * ========================== */ /* TODO: remove this (testing) */ INTERNAL void debug_draw_xform(struct xform xf) { f32 thickness = 2.f; f32 arrowhead_len = 15.f; u32 color = RGBA_F(0, 1, 1, 0.3); u32 color_x = RGBA_F(1, 0, 0, 0.3); u32 color_y = RGBA_F(0, 1, 0, 0.3); struct v2 pos = xform_mul_v2(G.world_view, xf.og); struct v2 x_ray = xform_basis_mul_v2(G.world_view, xform_get_right(xf)); struct v2 y_ray = xform_basis_mul_v2(G.world_view, xform_get_up(xf)); struct quad quad = quad_from_rect(RECT(0, 0, 1, -1)); quad = quad_mul_xform(quad_scale(quad, 0.075), xf); draw_solid_arrow_ray(G.viewport_canvas, pos, x_ray, thickness, arrowhead_len, color_x); draw_solid_arrow_ray(G.viewport_canvas, pos, y_ray, thickness, arrowhead_len, color_y); draw_solid_quad(G.viewport_canvas, 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_F(1, 0.5, 0, 1); u32 color_acc = RGBA_F(1, 1, 0.5, 1); struct v2 pos = xform_mul_v2(G.world_view, ent->world_xform.og); struct v2 vel_ray = xform_basis_mul_v2(G.world_view, ent->velocity); struct v2 acc_ray = xform_basis_mul_v2(G.world_view, ent->acceleration); draw_solid_arrow_ray(G.viewport_canvas, pos, vel_ray, thickness, arrow_len, color_vel); draw_solid_arrow_ray(G.viewport_canvas, pos, acc_ray, thickness, arrow_len, color_acc); } INTERNAL void user_update(void) { struct temp_arena scratch = scratch_begin_no_conflict(); struct game_cmd_list cmd_list = { .arena = scratch.arena }; /* Get time */ f64 cur_time = sys_timestamp_seconds(sys_timestamp()); G.dt = max_f64(0.0, cur_time - G.time); G.time += G.dt; G.screen_size = sys_window_get_size(G.window); /* ========================== * * Produce interpolated tick * ========================== */ b32 tick_is_first_frame = false; { __profscope(produce_interpolated_tick); #if USER_INTERP_ENABLED f64 blend_time_offset = (1.0 / GAME_FPS) * USER_INTERP_OFFSET_TICK_RATIO; f64 blend_time = G.time > blend_time_offset ? G.time - blend_time_offset : 0; /* Pull ticks */ struct interp_ticks interp_ticks = pull_ticks(blend_time); struct world *t0 = interp_ticks.from_tick; struct world *t1 = interp_ticks.to_tick; tick_is_first_frame = (t0->tick_id == 0 || t1->tick_id == 0); f32 tick_blend = 0; { f64 t0_time = sys_timestamp_seconds(t0->tick_ts); f64 t1_time = sys_timestamp_seconds(t1->tick_ts); if (t1_time > t0_time) { tick_blend = (f32)((blend_time - t0_time) / (t1_time - t0_time)); } tick_blend = clamp_f32(tick_blend, 0.0f, 1.0f); } world_copy_replace(&G.world, t1); /* Blend time */ G.world.time = math_lerp_f64(t0->time, t1->time, (f64)tick_blend); /* Blend entities */ struct entity_array t0_entities = entity_store_as_array(&t0->entity_store); struct entity_array t1_entities = entity_store_as_array(&t1->entity_store); struct entity_array world_entities = entity_store_as_array(&G.world.entity_store); u64 num_entities = min_u64(t0_entities.count, t1_entities.count); for (u64 i = 0; i < num_entities; ++i) { struct entity *e0 = &t0_entities.entities[i]; struct entity *e1 = &t1_entities.entities[i]; struct entity *e = &world_entities.entities[i]; if (e0->handle.gen == e1->handle.gen && e0->continuity_gen == e1->continuity_gen) { e->rel_xform = xform_lerp(e0->rel_xform, e1->rel_xform, tick_blend); e->world_xform = xform_lerp(e0->world_xform, e1->world_xform, tick_blend); e->acceleration = v2_lerp(e0->acceleration, e1->acceleration, tick_blend); e->velocity = v2_lerp(e0->velocity, e1->velocity, tick_blend); e->player_acceleration = math_lerp(e0->player_acceleration, e1->player_acceleration, tick_blend); e->player_aim = v2_lerp(e0->player_aim, e1->player_aim, tick_blend); e->sprite_quad_xform = xform_lerp(e0->sprite_quad_xform, e1->sprite_quad_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->camera_quad_xform = xform_lerp(e0->camera_quad_xform, e1->camera_quad_xform, tick_blend); e->camera_rel_xform_target = xform_lerp(e0->camera_rel_xform_target, e1->camera_rel_xform_target, 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 } struct entity_array entities_array = entity_store_as_array(&G.world.entity_store); /* ========================== * * Find important entities * ========================== */ struct entity *player = entity_nil(); struct entity *active_camera = entity_nil(); for (u64 entity_index = 0; entity_index < entities_array.count; ++entity_index) { struct entity *ent = &entities_array.entities[entity_index]; if (!ent->valid) continue; /* Player */ if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { player = ent; } /* Active camera */ if (entity_has_prop(ent, ENTITY_PROP_CAMERA) && entity_has_prop(ent, ENTITY_PROP_CAMERA_ACTIVE)) { active_camera = ent; } } /* ========================== * * Read sys events * ========================== */ struct sys_event_array events = pop_sys_events(scratch.arena); /* Reset bind states "was_pressed" */ 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_quit(); } if (event->kind == SYS_EVENT_KIND_BUTTON_UP) { #if DEVELOPER /* Escape quit */ if (event->button == SYS_BTN_ESC) { app_quit(); } #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) && !event->is_repeat) { 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; b32 out_of_bounds = button >= SYS_BTN_M1 && button <= SYS_BTN_M5 && (G.viewport_cursor.x < 0 || G.viewport_cursor.y < 0 || G.viewport_cursor.x > G.viewport_size.x || G.viewport_cursor.y > G.viewport_size.y); G.bind_states[bind].is_held = pressed && !out_of_bounds; if (pressed) { if (!out_of_bounds) { ++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 || state.is_held) { queue_game_cmd(&cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_CLEAR_ALL }); } } 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; } /* ========================== * * Update viewport * ========================== */ /* Calculate screen viewport dimensions */ if (G.debug_camera) { G.viewport_size = G.screen_size; G.viewport_screen_offset = V2(0, 0); } else { /* Determine viewport size by camera & window dimensions */ f32 aspect_ratio = 1.0; { struct xform quad_xf = xform_mul(active_camera->world_xform, active_camera->camera_quad_xform); struct v2 camera_size = xform_get_scale(quad_xf); if (!v2_eq(camera_size, V2(0, 0))) { 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 = (f32)math_ceil(width / aspect_ratio); } G.viewport_size = V2(width, height); /* Center viewport 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.viewport_screen_offset = V2(x, y); } G.viewport_center = v2_mul(G.viewport_size, 0.5); G.viewport_cursor = v2_sub(G.screen_cursor, G.viewport_screen_offset); /* ========================== * * Update view * ========================== */ if (G.debug_camera) { G.world_view = xform_with_rotation(G.world_view, 0); /* Pan view */ if (G.bind_states[USER_BIND_KIND_PAN].is_held) { if (!G.debug_camera_panning) { G.debug_camera_pan_start = xform_invert_mul_v2(G.world_view, G.viewport_cursor); } G.debug_camera_panning = true; struct v2 offset = v2_sub(G.debug_camera_pan_start, xform_invert_mul_v2(G.world_view, G.viewport_cursor)); G.world_view = xform_translate(G.world_view, v2_neg(offset)); G.debug_camera_pan_start = xform_invert_mul_v2(G.world_view, G.viewport_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); struct v2 world_cursor = xform_invert_mul_v2(G.world_view, G.viewport_cursor); G.world_view = xform_translate(G.world_view, world_cursor); G.world_view = xform_scale(G.world_view, V2(zoom, zoom)); G.world_view = xform_translate(G.world_view, v2_neg(world_cursor)); } } else { struct v2 center = active_camera->world_xform.og; f32 rot = xform_get_rotation(active_camera->world_xform); /* Scale view into viewport based on camera size */ struct v2 size = G.viewport_size; { struct xform quad_xf = xform_mul(active_camera->world_xform, active_camera->camera_quad_xform); struct v2 camera_size = xform_get_scale(quad_xf); if (!v2_eq(camera_size, V2(0, 0))) { size = v2_div_v2(size, camera_size); } } f32 scale = min_f32(size.x, size.y); struct trs trs = TRS( .t = v2_sub(G.viewport_center, center), .r = rot, .s = V2(scale, scale) ); struct v2 pivot = center; G.world_view = XFORM_IDENT; G.world_view = xform_translate(G.world_view, pivot); G.world_view = xform_trs_pivot_rs(G.world_view, trs, pivot); } G.world_cursor = xform_invert_mul_v2(G.world_view, G.viewport_cursor); /* ========================== * * Update listener * ========================== */ { struct v2 up = V2(0, -1); struct v2 listener_pos = xform_invert_mul_v2(G.world_view, G.viewport_center); struct v2 listener_dir = v2_norm(xform_basis_invert_mul_v2(G.world_view, up)); mixer_set_listener(listener_pos, listener_dir); } /* ========================== * * Draw test BG * ========================== */ { u32 color = RGBA_F(0.2f, 0.2f, 0.2f, 1.f); draw_solid_rect(G.viewport_bg_canvas, RECT(0, 0, G.viewport_size.x, G.viewport_size.y), color); } /* ========================== * * Draw test grid * ========================== */ { f32 thickness = 3.f; u32 color = RGBA(0x3f, 0x3f, 0x3f, 0xFF); u32 x_color = RGBA(0x3f, 0, 0, 0xFF); u32 y_color = RGBA(0, 0x3f, 0, 0xFF); i64 startx = -10; i64 starty = -10; i64 rows = 20; i64 cols = 20; /* Draw column lines */ struct v2 col_ray = xform_basis_mul_v2(G.world_view, V2(0, rows)); for (i64 col = starty; col <= (starty + cols); ++col) { u32 line_color = color; if (col == 0) { line_color = y_color; } struct v2 pos = xform_mul_v2(G.world_view, V2(col, starty)); draw_solid_ray(G.viewport_bg_canvas, pos, col_ray, thickness, line_color); } struct v2 row_ray = xform_basis_mul_v2(G.world_view, V2(cols, 0)); for (i64 row = startx; row <= (startx + rows); ++row) { u32 line_color = color; if (row == 0) { line_color = x_color; } struct v2 pos = xform_mul_v2(G.world_view, V2(startx, row)); draw_solid_ray(G.viewport_bg_canvas, pos, row_ray, thickness, line_color); } } /* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ /* ========================== * * Iterate entities * ========================== */ /* Iterate entities */ for (u64 entity_index = 0; entity_index < entities_array.count; ++entity_index) { __profscope(user_entity_iter); struct entity *ent = &entities_array.entities[entity_index]; if (!ent->valid) continue; b32 skip_debug_draw = !G.debug_camera && ent == active_camera; b32 skip_debug_draw_transform = ent == active_camera; /* Draw sprite */ if (ent->sprite_name.len > 0) { struct string tex_name = ent->sprite_name; /* Draw texture */ struct texture *texture = texture_load_async(tex_name); if (texture) { struct xform xf = xform_mul(ent->world_xform, ent->sprite_quad_xform); struct quad quad = quad_mul_xform(QUAD_UNIT_SQUARE_CENTERED, xf); u32 tint = ent->sprite_tint; struct draw_texture_params params = DRAW_TEXTURE_PARAMS(.texture = texture, .tint = tint); struct sheet *sheet = sheet_load(ent->sprite_name); if (sheet) { struct sheet_tag tag = sheet_get_tag(sheet, ent->sprite_tag_name); u64 frame_index = tag.start + ent->animation_frame; struct sheet_frame frame = sheet_get_frame(sheet, frame_index); if (entity_has_prop(ent, ENTITY_PROP_ANIMATING)) { f64 time_in_frame = ent->animation_time_in_frame; while (time_in_frame > frame.duration) { time_in_frame -= frame.duration; ++frame_index; if (frame_index > tag.end) { /* Loop animation */ frame_index = tag.start; } frame = sheet_get_frame(sheet, frame_index); } } params.clip = frame.clip; } draw_texture_quad(G.world_canvas, params, quad); #if 0 if (G.debug_draw && !skip_debug_draw) { /* Debug draw sprite quad */ { f32 thickness = 2.f; u32 color = RGBA_F(1, 1, 0, 0.25); draw_solid_quad_line(G.world_canvas, quad, (thickness / PIXELS_PER_UNIT / G.world_view.zoom), color); } /* Debug draw sprite transform */ { debug_draw_xform(xf); } /* Debug draw sprite pivot */ { u32 color = RGBA_F(1, 0, 0, 1); draw_solid_circle(G.world_canvas, ent->world_xform.og, 0.02, color, 20); } } #endif } } /* Debug draw info */ if (G.debug_draw && !skip_debug_draw) { struct temp_arena temp = arena_temp_begin(scratch.arena); #if 0 struct font *disp_font = font_load_async(STR("res/fonts/fixedsys.ttf"), 12.0f); if (disp_font) { struct xform xf = ent->world_xform; struct trs trs = trs_from_xform(xf); struct v2 velocity = ent->velocity; struct v2 acceleration = ent->acceleration; f32 offset = 1; struct v2 pos = v2_add(xf.og, v2_mul(V2(0, -1), offset)); pos = xform_mul_v2(G.world_view, pos); pos = v2_round(pos); struct string disp_name = ent->sprite_name; struct string fmt = STR( "sprite name: \"%F\"\n" "pos: (%F, %F)\n" "scale: (%F, %F)\n" "rot: %F\n" "velocity: (%F, %F)\n" "acceleration: (%F, %F)\n" ); struct string text = string_format(temp.arena, fmt, FMT_STR(disp_name), FMT_FLOAT((f64)trs.t.x), FMT_FLOAT((f64)trs.t.y), FMT_FLOAT((f64)trs.s.x), FMT_FLOAT((f64)trs.s.y), FMT_FLOAT((f64)trs.r), FMT_FLOAT((f64)velocity.x), FMT_FLOAT((f64)velocity.y), FMT_FLOAT((f64)acceleration.x), FMT_FLOAT((f64)acceleration.y) ); draw_text(G.viewport_canvas, disp_font, pos, text); } #endif debug_draw_movement(ent); if (!skip_debug_draw_transform) { debug_draw_xform(ent->world_xform); } /* Draw hierarchy */ struct entity *parent = entity_from_handle(&G.world.entity_store, ent->parent); if (parent->valid) { u32 color = RGBA_F(0.6, 0.6, 1, 0.75); f32 thickness = 5; f32 arrow_height = 15; struct v2 start = xform_mul_v2(G.world_view, ent->world_xform.og); struct v2 end = xform_mul_v2(G.world_view, parent->world_xform.og); draw_solid_arrow_line(G.viewport_canvas, start, end, thickness, arrow_height, color); } /* Draw aim */ if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { u32 color = RGBA_F(0.75, 0, 0.75, 0.5); f32 thickness = 3; f32 arrow_height = 10; struct v2 pos = xform_mul_v2(G.world_view, ent->world_xform.og); struct v2 aim_ray = xform_basis_mul_v2(G.world_view, ent->player_aim); draw_solid_arrow_ray(G.viewport_canvas, pos, aim_ray, thickness, arrow_height, color); } /* Draw camera rect */ if (entity_has_prop(ent, ENTITY_PROP_CAMERA)) { u32 color = ent == active_camera ? RGBA_F(1, 1, 1, 0.5) : RGBA_F(0, 0.75, 0, 0.5); f32 thickness = 3; struct xform quad_xf = xform_mul(ent->world_xform, ent->camera_quad_xform); struct quad quad = quad_mul_xform(QUAD_UNIT_SQUARE_CENTERED, quad_xf); quad = quad_mul_xform(quad, G.world_view); draw_solid_quad_line(G.viewport_canvas, quad, thickness, color); } arena_temp_end(temp); } } /* Draw crosshair or show cursor */ if (!G.debug_camera) { struct v2 crosshair_pos = G.viewport_cursor; u32 tint = RGBA_F(1, 1, 1, 1); struct v2 size = V2(0, 0); struct texture *t = texture_load_async(STR("res/graphics/crosshair.ase")); if (t) { size = t->size; struct xform xf = XFORM_TRS(.t = crosshair_pos, .s = size); struct quad quad = quad_mul_xform(QUAD_UNIT_SQUARE_CENTERED, xf); draw_texture_quad(G.viewport_canvas, DRAW_TEXTURE_PARAMS(.texture = t, .tint = tint), quad); } struct rect cursor_clip = RECT_FROM_V2(G.viewport_screen_offset, G.viewport_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 movement input * ========================== */ /* Movement */ struct v2 input_move_dir = { 0 }; { 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_view, input_move_dir); /* Make move dir relative to world view */ input_move_dir = v2_norm(input_move_dir); } /* Aim */ struct v2 input_aim = player->player_aim; if (!G.debug_camera) { input_aim = v2_sub(G.world_cursor, player->world_xform.og); } /* Queue cmd */ queue_game_cmd(&cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_PLAYER_MOVE, .move_dir = input_move_dir, .aim = input_aim }); /* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ /* Debug draw info */ if (G.debug_draw) { struct temp_arena temp = arena_temp_begin(scratch.arena); f32 spacing = 20; struct v2 pos = V2(10, 8); struct font *font = font_load(STR("res/fonts/fixedsys.ttf"), 12.0f); draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("time: %F"), FMT_FLOAT((f64)G.time))); pos.y += spacing; draw_text(G.viewport_canvas, 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.viewport_canvas, 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.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_screen_offset: (%F, %F)"), FMT_FLOAT((f64)G.viewport_screen_offset.x), FMT_FLOAT((f64)G.viewport_screen_offset.y))); pos.y += spacing; draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_size: (%F, %F)"), FMT_FLOAT((f64)G.viewport_size.x), FMT_FLOAT((f64)G.viewport_size.y))); pos.y += spacing; draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_center: (%F, %F)"), FMT_FLOAT((f64)G.viewport_center.x), FMT_FLOAT((f64)G.viewport_center.y))); pos.y += spacing; draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_cursor: (%F, %F)"), FMT_FLOAT((f64)G.viewport_cursor.x), FMT_FLOAT((f64)G.viewport_cursor.y))); pos.y += spacing; draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_view.og: (%F, %F)"), FMT_FLOAT((f64)G.world_view.og.x), FMT_FLOAT((f64)G.world_view.og.y))); pos.y += spacing; draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_view rotation: %F"), FMT_FLOAT((f64)xform_get_rotation(G.world_view)))); pos.y += spacing; draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_view scale: (%F, %F)"), FMT_FLOAT((f64)xform_get_scale(G.world_view).x), FMT_FLOAT((f64)xform_get_scale(G.world_view).x))); pos.y += spacing; draw_text(G.viewport_canvas, 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.viewport_canvas, font, pos, string_format(temp.arena, STR("debug_camera: %F"), FMT_STR(G.debug_camera ? STR("true") : STR("false")))); pos.y += spacing; arena_temp_end(temp); } /* Push game cmds */ pubilsh_game_cmds(&cmd_list); /* ========================== * * Present * ========================== */ /* Send canvases to GPU */ renderer_canvas_send_to_gpu(G.viewport_bg_canvas); renderer_canvas_send_to_gpu(G.world_canvas); renderer_canvas_send_to_gpu(G.viewport_canvas); /* Set canvas views before presenting */ renderer_canvas_set_view(G.viewport_bg_canvas, XFORM_IDENT); renderer_canvas_set_view(G.world_canvas, G.world_view); renderer_canvas_set_view(G.viewport_canvas, XFORM_IDENT); /* Present */ i32 vsync = VSYNC_ENABLED; struct renderer_canvas **canvases = arena_dry_push(scratch.arena, struct renderer_canvas *); u64 canvases_count = 0; { /* Viewport background canvas */ *arena_push(scratch.arena, struct renderer_canvas *) = G.viewport_bg_canvas; ++canvases_count; /* World canvas */ if (!tick_is_first_frame) { /* Only render world if not on first frame */ *arena_push(scratch.arena, struct renderer_canvas *) = G.world_canvas; ++canvases_count; } /* Viewport canvas */ *arena_push(scratch.arena, struct renderer_canvas *) = G.viewport_canvas; ++canvases_count; } renderer_canvas_present(canvases, canvases_count, G.screen_size, RECT_FROM_V2(G.viewport_screen_offset, G.viewport_size), vsync); scratch_end(scratch); } /* ========================== * * Startup * ========================== */ INTERNAL SYS_THREAD_FUNC_DEF(user_thread_entry_point, arg) { (UNUSED)arg; sys_timestamp_t last_frame_ts = 0; f64 target_dt = USER_FRAME_LIMIT > (0) ? (1.0 / USER_FRAME_LIMIT) : 0; while (!G.shutdown) { __profscope(user_update_w_sleep); sleep_frame(last_frame_ts, target_dt); last_frame_ts = sys_timestamp(); user_update(); } } 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 texture_startup_receipt *texture_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)texture_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_canvas = renderer_canvas_alloc(); G.world_view = XFORM_TRS(.t = V2(0, 0), .r = 0, .s = V2(PIXELS_PER_UNIT, PIXELS_PER_UNIT)); G.viewport_bg_canvas = renderer_canvas_alloc(); G.viewport_canvas = renderer_canvas_alloc(); G.window = window; sys_window_register_event_callback(G.window, &window_event_callback); G.user_thread = sys_thread_init(&user_thread_entry_point, NULL, STR("[P1] User thread")); return (struct user_startup_receipt) { 0 }; } void user_shutdown(void) { G.shutdown = true; sys_thread_join(&G.user_thread); }