#include "game.h" #include "app.h" #include "sys.h" #include "util.h" #include "entity.h" #include "sheet.h" #include "tick.h" #include "sound.h" #include "mixer.h" #include "math.h" #include "scratch.h" #define GAME_FPS 30 GLOBAL struct { b32 shutdown; struct sys_thread game_thread; f64 timescale; struct entity *free_entity_head; struct tick tick; /* Game thread input */ struct sys_mutex game_cmds_mutex; struct arena game_cmds_arena; /* Game thread output */ struct sys_mutex published_tick_mutex; struct tick published_tick; } L = { 0 } DEBUG_LVAR(L_game); /* ========================== * * Entity allocation * ========================== */ INTERNAL struct entity *entity_alloc(void) { struct entity *entity = NULL; if (L.free_entity_head) { /* Reuse from free list */ entity = L.free_entity_head; L.free_entity_head = entity->next_free; *entity = (struct entity) { .handle = entity->handle }; } else { /* Make new */ if (L.tick.entities_count >= MAX_ENTITIES) { sys_panic(STR("MAX_ENTITIES reached")); } u64 idx = L.tick.entities_count++; entity = &L.tick.entities[idx]; *entity = (struct entity) { .handle = { .gen = 1, .idx = idx } }; } return entity; } #if 0 INTERNAL void entity_release(struct entity *entity) { entity->next_free = L.free_entity_head; L.free_entity_head = entity; *entity = (struct entity) { .gen = entity->gen + 1 }; } #endif /* Returns a valid entity or nil entity. Always safe to read result, need to check to write. */ INTERNAL struct entity *entity_from_handle(struct entity_handle eh) { if (eh.idx < L.tick.entities_count) { struct entity *entity = &L.tick.entities[eh.idx]; if (entity->handle.gen == eh.gen) { return entity; } } return entity_nil(); } INTERNAL void entity_tree_attach(struct entity *parent, struct entity *child) { struct entity *first_child = entity_from_handle(parent->first); struct entity *last_child = entity_from_handle(parent->last); child->prev = last_child->handle; child->parent = parent->handle; if (last_child->valid) { last_child->next = child->handle; } parent->last = child->handle; if (!first_child->valid) { parent->first = child->handle; } } /* ========================== * * Game cmd * ========================== */ INTERNAL void push_cmds(struct game_cmd_array cmd_array) { sys_mutex_lock(&L.game_cmds_mutex); { for (u64 i = 0; i < cmd_array.count; ++i) { struct game_cmd *write_cmd = arena_push(&L.game_cmds_arena, struct game_cmd); *write_cmd = cmd_array.cmds[i]; } } sys_mutex_unlock(&L.game_cmds_mutex); } INTERNAL struct game_cmd_array pop_cmds(struct arena *arena) { struct game_cmd_array array = { 0 }; if (L.game_cmds_arena.pos > 0) { sys_mutex_lock(&L.game_cmds_mutex); { struct buffer game_cmds_buff = arena_to_buffer(&L.game_cmds_arena); arena_align(arena, ALIGNOF(struct game_cmd)); array.cmds = (struct game_cmd *)arena_push_array(arena, u8, game_cmds_buff.size); array.count = game_cmds_buff.size / sizeof(struct game_cmd); MEMCPY(array.cmds, game_cmds_buff.data, game_cmds_buff.size); arena_reset(&L.game_cmds_arena); } sys_mutex_unlock(&L.game_cmds_mutex); } return array; } /* ========================== * * Update * ========================== */ INTERNAL void publish_game_tick(void) { __prof; sys_mutex_lock(&L.published_tick_mutex); L.tick.published_ts = sys_timestamp(); tick_cpy(&L.published_tick, &L.tick); sys_mutex_unlock(&L.published_tick_mutex); } INTERNAL void game_update(void) { __prof; struct temp_arena scratch = scratch_begin_no_conflict(); ++L.tick.id; L.tick.dt = max_f64(0.0, (1.0 / GAME_FPS) * L.timescale); L.tick.time += L.tick.dt; /* TODO: remove this (testing) */ /* Initialize entities */ static b32 run = 0; if (!run) { run = 1; (UNUSED)entity_tree_attach; /* Player ent */ struct entity *player_ent; { struct v2 pos = V2(0, 0); struct v2 size = V2(1, 1); f32 r = 0; struct entity *e = entity_alloc(); e->valid = true; e->rel_xform = XFORM_TRS(.t = pos, .r = r, .s = size); struct string sprite_name = STR("res/graphics/tim.ase"); struct string sprite_tag_name = STR("UNARMED"); struct sheet *sheet = sheet_load(sprite_name); f32 meters_width = sheet->frame_size.x / PIXELS_PER_UNIT; f32 meters_height = sheet->frame_size.y / PIXELS_PER_UNIT; struct v2 sprite_pos = V2(0, 0); f32 sprite_rot = 0; struct v2 sprite_size = V2(meters_width, meters_height); struct v2 sprite_pivot; { struct v2 sprite_pivot_norm = V2(0, 0.5); /* Pivot x & y are each normalized about sprite dimensions. <0, 0> is center, <1, 1> is bottom right corner, etc. */ struct v2 half_size = v2_mul(sprite_size, 0.5f); sprite_pivot = v2_mul_v2(sprite_pivot_norm, half_size); } struct xform sprite_xf = XFORM_POS(sprite_pos); sprite_xf = xform_rotate(sprite_xf, sprite_rot); sprite_xf = xform_translate(sprite_xf, v2_neg(sprite_pivot)); sprite_xf = xform_scale(sprite_xf, sprite_size); e->sprite_xform = sprite_xf; e->sprite_name = sprite_name; e->sprite_tag_name = sprite_tag_name; e->sprite_tint = COLOR_WHITE; entity_enable_prop(e, ENTITY_PROP_PLAYER_CONTROLLED); e->player_max_speed = 5.f; e->player_acceleration = 15.0f; entity_enable_prop(e, ENTITY_PROP_ANIMATING); e->animation_looping = true; player_ent = e; //entity_enable_prop(e, ENTITY_PROP_TEST); //entity_enable_prop(e, ENTITY_PROP_TEST_FOLLOW_MOUSE); } /* Child ent */ { struct v2 pos = V2(0, 0.25); struct v2 size = V2(1, 1); f32 r = 0; struct entity *e = entity_alloc(); e->valid = true; e->rel_xform = XFORM_TRS(.t = pos, .r = r, .s = size); struct string sprite_name = STR("res/graphics/tim.ase"); struct string sprite_tag_name = STR("UNARMED"); struct sheet *sheet = sheet_load(sprite_name); f32 meters_width = sheet->frame_size.x / PIXELS_PER_UNIT; f32 meters_height = sheet->frame_size.y / PIXELS_PER_UNIT; struct v2 sprite_pos = V2(0, 0); f32 sprite_rot = 0; struct v2 sprite_size = V2(meters_width, meters_height); struct v2 sprite_pivot; { struct v2 sprite_pivot_norm = V2(0, 0.5); /* Pivot x & y are each normalized about sprite dimensions. <0, 0> is center, <1, 1> is bottom right corner, etc. */ struct v2 half_size = v2_mul(sprite_size, 0.5f); sprite_pivot = v2_mul_v2(sprite_pivot_norm, half_size); } struct xform sprite_xf = XFORM_POS(sprite_pos); sprite_xf = xform_rotate(sprite_xf, sprite_rot); sprite_xf = xform_translate(sprite_xf, v2_neg(sprite_pivot)); sprite_xf = xform_scale(sprite_xf, sprite_size); e->sprite_xform = sprite_xf; e->sprite_name = sprite_name; e->sprite_tag_name = sprite_tag_name; e->sprite_tint = RGBA_F(0.3, 0.3, 0.3, 0.3); //entity_enable_prop(e, ENTITY_PROP_PLAYER_CONTROLLED); //e->player_max_speed = 5.f; //e->player_acceleration = 15.0f; entity_enable_prop(e, ENTITY_PROP_ANIMATING); e->animation_looping = true; //entity_enable_prop(e, ENTITY_PROP_TEST); //entity_enable_prop(e, ENTITY_PROP_TEST_FOLLOW_MOUSE); entity_tree_attach(player_ent, e); } /* Camera ent */ { struct entity *e = entity_alloc(); e->valid = true; e->rel_xform = XFORM_IDENT; entity_enable_prop(e, ENTITY_PROP_CAMERA); e->camera_active = true; e->camera_follow = player_ent->handle; e->camera_zoom = 1; } } /* ========================== * * Process game cmds * ========================== */ L.tick.player_move_dir = V2(0, 0); struct game_cmd_array game_cmds = pop_cmds(scratch.arena); for (u64 i = 0; i < game_cmds.count; ++i) { struct game_cmd cmd = game_cmds.cmds[i]; switch (cmd.kind) { /* Movement */ case GAME_CMD_KIND_PLAYER_MOVE: { struct v2 dir = cmd.dir; L.tick.player_move_dir = v2_add(L.tick.player_move_dir, dir); } break; /* Focus */ case GAME_CMD_KIND_PLAYER_FOCUS: { L.tick.player_focus = cmd.pos; } break; default: break; }; } if (v2_len(L.tick.player_move_dir) > 1.f) { /* Clamp movement magnitude */ L.tick.player_move_dir = v2_norm(L.tick.player_move_dir); } /* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ /* ========================== * * Update entities pre-physics * ========================== */ for (u64 entity_index = 0; entity_index < ARRAY_COUNT(L.tick.entities); ++entity_index) { struct entity *ent = &L.tick.entities[entity_index]; if (!ent->valid) continue; /* ========================== * * Initialize * ========================== */ /* ENTITY_PROP_TEST */ if (entity_has_prop(ent, ENTITY_PROP_TEST) && !ent->test_initialized) { ent->test_initialized = true; ent->test_start_rel_xform = ent->rel_xform; ent->test_start_sprite_xform = ent->sprite_xform; } /* ENTITY_PROP_ANIMATING */ if (entity_has_prop(ent, ENTITY_PROP_ANIMATING) && ent->animation_start_time == 0) { ent->animation_start_time = L.tick.time; } /* ========================== * * Update animation * ========================== */ if (ent->animation_start_time > 0) { /* Stop animation if past duration and not looping */ if (!ent->animation_looping) { f64 time_in_anim = L.tick.time - ent->animation_start_time; struct sheet *sheet = sheet_load(ent->sprite_name); if (sheet) { struct sheet_tag tag = sheet_get_tag(sheet, ent->sprite_tag_name); struct sheet_frame frame = { 0 }; u64 frame_index = tag.start; while (time_in_anim > 0) { frame = sheet_get_frame(sheet, frame_index); if (frame_index > tag.end) { entity_disable_prop(ent, ENTITY_PROP_ANIMATING); ent->animation_start_time = 0; break; } time_in_anim -= frame.duration; ++frame_index; } } else { entity_disable_prop(ent, ENTITY_PROP_ANIMATING); ent->animation_start_time = 0; } } } /* ========================== * * Test * ========================== */ /* ENTITY_PROP_TEST */ if (entity_has_prop(ent, ENTITY_PROP_TEST)) { f32 t = ((f32)L.tick.time); f32 r = t * 2.f; f32 s = 1 + (math_fabs(math_sin(t * 5)) * 3); (UNUSED)r; (UNUSED)s; ent->rel_xform = xform_with_rotation(ent->rel_xform, r); ent->rel_xform = xform_with_scale(ent->rel_xform, V2(s, 1)); } } /* ========================== * * Update entity physics * ========================== */ for (u64 entity_index = 0; entity_index < ARRAY_COUNT(L.tick.entities); ++entity_index) { struct entity *ent = &L.tick.entities[entity_index]; if (!ent->valid) continue; if (ent->parent.gen) continue; /* Only update parent entities */ /* ========================== * * Player movement * ========================== */ if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { f32 max_speed = ent->player_max_speed; f32 acceleration_rate = ent->player_acceleration; acceleration_rate = clamp_f32(acceleration_rate, 0, GAME_FPS); /* Can't integrate acceleration rate higher than FPS */ struct v2 target_velocity = v2_mul(L.tick.player_move_dir, max_speed); struct v2 target_acceleration = v2_sub(target_velocity, ent->velocity); ent->acceleration = v2_mul(target_acceleration, acceleration_rate); } /* ========================== * * Integrate acceleration & velocity * ========================== */ { f32 dt = (f32)L.tick.dt; /* Apply acceleration to velocity */ struct v2 a = v2_mul(ent->acceleration, dt); ent->velocity = v2_add(ent->velocity, a); /* Apply velocity to position */ ent->rel_xform.og = v2_add(ent->rel_xform.og, v2_mul(ent->velocity, dt)); } /* ========================== * * Player look direction * ========================== */ if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { struct v2 ent_pos = ent->rel_xform.og; struct v2 look_pos = L.tick.player_focus; f32 r = v2_angle_to_point(ent_pos, look_pos) + PI / 2; ent->rel_xform = xform_with_rotation(ent->rel_xform, r); } /* ========================== * * Update position from mouse * ========================== */ /* ENTITY_PROP_TEST_FOLLOW_MOUSE */ if (entity_has_prop(ent, ENTITY_PROP_TEST_FOLLOW_MOUSE)) { ent->rel_xform.og = L.tick.player_focus; ent->test_start_rel_xform.og = L.tick.player_focus; } /* ========================== * * Calculate xforms * ========================== */ ent->world_xform = ent->rel_xform; struct entity *child = entity_from_handle(ent->first); struct xform parent_xform = ent->world_xform; while (child->valid) { child->world_xform = xform_mul(parent_xform, child->rel_xform); /* Depth first iteration */ if (child->first.gen) { /* Next child */ parent_xform = child->world_xform; child = entity_from_handle(child->first); } else if (child->next.gen) { /* Next sibling */ child = entity_from_handle(child->next); } else if (child->parent.gen && entity_from_handle(child->parent)->next.gen) { /* Next parent sibling */ struct entity *parent = entity_from_handle(child->parent); struct entity *grandparent = entity_from_handle(parent->parent); parent_xform = grandparent->world_xform; child = entity_from_handle(parent->next); } else { child = entity_nil(); } } } /* ========================== * * Update entities post-physics * ========================== */ for (u64 entity_index = 0; entity_index < ARRAY_COUNT(L.tick.entities); ++entity_index) { struct entity *ent = &L.tick.entities[entity_index]; if (!ent->valid) continue; /* ========================== * * Update camera position * ========================== */ if (entity_has_prop(ent, ENTITY_PROP_CAMERA)) { struct entity *follow = entity_from_handle(ent->camera_follow); ent->world_xform = follow->world_xform; ent->world_xform = xform_with_rotation(ent->world_xform, 0); ent->world_xform = xform_with_scale(ent->world_xform, V2(1, 1)); } /* ========================== * * Update sound emitters * ========================== */ if (entity_has_prop(ent, ENTITY_PROP_TEST_SOUND_EMITTER)) { struct mixer_desc desc = ent->sound_desc; desc.speed = L.timescale; desc.pos = ent->world_xform.og; struct sound *sound = sound_load_async(ent->sound_name, 0); b32 played = ent->sound_handle.gen != 0; if (sound) { if (!played) { ent->sound_handle = mixer_play_ex(sound, desc); } else { mixer_track_set(ent->sound_handle, desc); } } } } /* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ /* Publish tick */ publish_game_tick(); __profframe("Game"); scratch_end(scratch); } /* ========================== * * Startup * ========================== */ INTERNAL void game_thread_entry_point(void *arg) { (UNUSED)arg; sys_timestamp_t last_frame_ts = 0; f64 target_dt = GAME_FPS > 0 ? (1.0 / GAME_FPS) : 0; while (!L.shutdown) { __profscope(game_update_w_sleep); sleep_frame(last_frame_ts, target_dt); last_frame_ts = sys_timestamp(); game_update(); } } void game_startup(void) { /* Initialize game cmd storage */ L.game_cmds_mutex = sys_mutex_alloc(); L.game_cmds_arena = arena_alloc(GIGABYTE(64)); /* Initialize tick storage */ L.published_tick_mutex = sys_mutex_alloc(); L.timescale = 1.0; L.game_thread = sys_thread_init(&game_thread_entry_point, NULL, STR("[P2] Game thread")); } void game_shutdown(void) { L.shutdown = true; sys_thread_join(&L.game_thread); } /* ========================== * * Interface * ========================== */ void game_get_latest_tick(struct tick *dest) { sys_mutex_lock(&L.published_tick_mutex); tick_cpy(dest, &L.published_tick); sys_mutex_unlock(&L.published_tick_mutex); } u64 game_get_latest_tick_id(void) { return L.published_tick.id; } void game_push_cmds(struct game_cmd_array cmd_array) { push_cmds(cmd_array); }