#include "game.h" #include "sys.h" #include "util.h" #include "world.h" #include "sprite.h" #include "sound.h" #include "mixer.h" #include "math.h" #include "scratch.h" #include "atomic.h" #include "app.h" #include "log.h" #include "collider.h" struct contact_lookup_entry { u64 hash; struct entity_handle contact_ent_handle; struct contact_lookup_entry *next; struct contact_lookup_entry *prev; }; struct contact_lookup_bucket { struct contact_lookup_entry *first; struct contact_lookup_entry *last; }; struct contact_lookup { struct arena arena; struct contact_lookup_bucket buckets[4096]; struct contact_lookup_entry *first_free_entry; }; #if COLLIDER_DEBUG /* TODO: Remove this (debugging) */ struct collision_debug_lookup { struct arena arena; struct fixed_dict dict; }; #endif GLOBAL struct { struct atomic_i32 game_thread_shutdown; struct sys_thread game_thread; b32 paused; struct sprite_scope *sprite_frame_scope; /* For debugging */ struct v2 user_cursor; /* TODO: Remove this (testing) */ b32 extra_spawn; b32 should_reset_level; /* Game thread input */ struct sys_mutex game_cmds_mutex; struct arena game_cmds_arena; struct entity *root; /* Constants */ struct math_spring_result contact_softness; struct math_spring_result mouse_joint_linear_softness; struct math_spring_result mouse_joint_angular_softness; f32 mouse_joint_max_force; /* Bookkeeping structures */ struct contact_lookup contact_lookup; #if COLLIDER_DEBUG struct collision_debug_lookup collision_debug_lookup; #endif /* Ticks */ struct sys_mutex prev_tick_mutex; struct atomic_u64 prev_tick_id; struct atomic_u64 prev_tick_continuity_gen; struct world prev_tick; struct world tick; } G = ZI, DEBUG_ALIAS(G, G_game); /* ========================== * * Startup * ========================== */ INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(game_shutdown); INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(game_thread_entry_point, arg); INTERNAL void reset_world(void); struct game_startup_receipt game_startup(struct mixer_startup_receipt *mixer_sr, struct sprite_startup_receipt *sheet_sr, struct sound_startup_receipt *sound_sr) { (UNUSED)mixer_sr; (UNUSED)sheet_sr; (UNUSED)sound_sr; /* Initialize constants */ { const f32 substep_dt = (1.f / ((f32)GAME_FPS * (f32)GAME_PHYSICS_SUBSTEPS)); const f32 contact_frequency = (1.f / substep_dt) / 8.f; const f32 contact_damping_ratio = 10; G.contact_softness = math_spring(contact_frequency, contact_damping_ratio, substep_dt); const f32 mouse_joint_linear_frequency = 5.0; const f32 mouse_joint_linear_damping_ratio = 0.7; const f32 mouse_joint_angular_frequency = 0.5f; const f32 mouse_joint_angular_damping_ratio = 0.1f; G.mouse_joint_max_force = 1000; G.mouse_joint_linear_softness = math_spring(mouse_joint_linear_frequency, mouse_joint_linear_damping_ratio, substep_dt); G.mouse_joint_angular_softness = math_spring(mouse_joint_angular_frequency, mouse_joint_angular_damping_ratio, substep_dt); } /* Initialize game cmd storage */ G.game_cmds_mutex = sys_mutex_alloc(); G.game_cmds_arena = arena_alloc(GIGABYTE(64)); /* Initialize empty world */ reset_world(); /* Initialize prev tick */ world_alloc(&G.prev_tick); /* FIXME: Make the world struct itself readonly as well */ arena_set_readonly(&G.prev_tick.entity_store->arena); G.prev_tick_mutex = sys_mutex_alloc(); G.game_thread = sys_thread_alloc(&game_thread_entry_point, NULL, STR("[P2] Game thread")); app_register_exit_callback(&game_shutdown); return (struct game_startup_receipt) { 0 }; } INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(game_shutdown) { __prof; atomic_i32_eval_exchange(&G.game_thread_shutdown, true); sys_thread_wait_release(&G.game_thread); } /* ========================== * * Game cmd * ========================== */ INTERNAL void push_cmds(struct game_cmd_array cmd_array) { struct sys_lock lock = sys_mutex_lock_e(&G.game_cmds_mutex); struct game_cmd *cmds = arena_push_array(&G.game_cmds_arena, struct game_cmd, cmd_array.count); MEMCPY(cmds, cmd_array.cmds, cmd_array.count * sizeof(*cmds)); sys_mutex_unlock(&lock); } INTERNAL struct game_cmd_array pop_cmds(struct arena *arena) { struct game_cmd_array array = ZI; if (G.game_cmds_arena.pos > 0) { struct sys_lock lock = sys_mutex_lock_e(&G.game_cmds_mutex); struct buffer game_cmds_buff = arena_to_buffer(&G.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(&G.game_cmds_arena); sys_mutex_unlock(&lock); } return array; } /* ========================== * * Activate * ========================== */ INTERNAL void activate_now(struct entity *ent) { entity_enable_prop(ent, ENTITY_PROP_ACTIVE); ent->activation_tick = G.tick.tick_id; ++ent->continuity_gen; } /* ========================== * * Contact lookup * ========================== */ INTERNAL void contact_lookup_alloc(struct contact_lookup *l) { MEMZERO_STRUCT(l); l->arena = arena_alloc(GIGABYTE(64)); } INTERNAL void contact_lookup_release(struct contact_lookup *l) { arena_release(&l->arena); } INTERNAL u64 contact_lookup_hash_from_entities(struct entity_handle h0, struct entity_handle h1) { struct buffer b0 = BUFFER_FROM_STRUCT(&h0); struct buffer b1 = BUFFER_FROM_STRUCT(&h1); u64 hash = hash_fnv64(HASH_FNV64_BASIS, b0); hash = hash_fnv64(hash, b1); return hash; } INTERNAL struct contact_lookup_entry *contact_lookup_get(struct contact_lookup *l, u64 hash) { u64 index = hash % ARRAY_COUNT(l->buckets); struct contact_lookup_bucket *bucket = &l->buckets[index]; struct contact_lookup_entry *res = NULL; for (struct contact_lookup_entry *e = bucket->first; e; e = e->next) { if (e->hash == hash) { res = e; break; } } return res; } INTERNAL void contact_lookup_set(struct contact_lookup *l, u64 hash, struct entity_handle handle) { u64 index = hash % ARRAY_COUNT(l->buckets); struct contact_lookup_bucket *bucket = &l->buckets[index]; struct contact_lookup_entry *prev = NULL; struct contact_lookup_entry **slot = &bucket->first; while (*slot) { if ((*slot)->hash == hash) { break; } prev = *slot; slot = &(*slot)->next; } struct contact_lookup_entry *entry = *slot; if (entry) { /* Set existing entry */ entry->contact_ent_handle = handle; } else { /* Allocate entry */ if (l->first_free_entry) { entry = l->first_free_entry; l->first_free_entry->prev = NULL; l->first_free_entry = entry->next; } else { entry = arena_push(&l->arena, struct contact_lookup_entry); } MEMZERO_STRUCT(entry); entry->hash = hash; entry->contact_ent_handle = handle; if (prev) { entry->prev = prev; prev->next = entry; } bucket->last = entry; *slot = entry; } } INTERNAL void contact_lookup_remove(struct contact_lookup *l, struct contact_lookup_entry *entry) { struct contact_lookup_bucket *bucket = &l->buckets[entry->hash % ARRAY_COUNT(l->buckets)]; struct contact_lookup_entry *prev = entry->prev; struct contact_lookup_entry *next = entry->next; if (prev) { prev->next = next; } else { bucket->first = next; } if (next) { next->prev = prev; } else { bucket->last = prev; } if (l->first_free_entry) { l->first_free_entry->prev = entry; } entry->next = l->first_free_entry; entry->prev = NULL; l->first_free_entry = entry; } /* ========================== * * Reset * ========================== */ INTERNAL void reset_world(void) { if (G.tick.entity_store) { /* Release world */ world_release(&G.tick); /* Release bookkeeping */ #if COLLIDER_DEBUG arena_release(&G.collision_debug_lookup.arena); #endif contact_lookup_release(&G.contact_lookup); } /* Create bookkeeping */ contact_lookup_alloc(&G.contact_lookup); #if COLLIDER_DEBUG G.collision_debug_lookup.arena = arena_alloc(GIGABYTE(64)); G.collision_debug_lookup.dict = fixed_dict_init(&G.collision_debug_lookup.arena, 4096); #endif /* Re-create world */ world_alloc(&G.tick); G.tick.continuity_gen = atomic_u64_eval(&G.prev_tick_continuity_gen) + 1; G.tick.timescale = GAME_TIMESCALE; } /* ========================== * * Test * ========================== */ /* TODO: Remove this */ INTERNAL void spawn_test_entities(void) { struct entity *root = entity_from_handle(G.tick.entity_store, G.tick.entity_store->root); root->mass_unscaled = F32_INFINITY; root->inertia_unscaled = F32_INFINITY; /* Player */ struct entity *player_ent = entity_nil(); //if (!G.extra_spawn) { { struct entity *e = entity_alloc(root); struct v2 pos = V2(1, -1); //struct v2 size = V2(0.5, 0.5); //struct v2 size = V2(0.5, 0.25); struct v2 size = V2(1.0, 1.0); //f32 r = PI / 4; f32 r = 0; if (!G.extra_spawn) { entity_enable_prop(e, ENTITY_PROP_PLAYER_CONTROLLED); entity_enable_prop(e, ENTITY_PROP_TEST); e->sprite = sprite_tag_from_path(STR("res/graphics/tim.ase")); e->mass_unscaled = 10; e->inertia_unscaled = 5; } else { entity_enable_prop(e, ENTITY_PROP_TEST); e->sprite = sprite_tag_from_path(STR("res/graphics/tim.ase")); e->mass_unscaled = 10; e->inertia_unscaled = 5; #if 0 e->sprite = sprite_tag_from_path(STR("res/graphics/box.ase")); e->mass_unscaled = 100; e->inertia_unscaled = 100; #endif } //e->sprite = sprite_tag_from_path(STR("res/graphics/box_rounded.ase")); //e->sprite_span_name = STR("idle.unarmed"); //e->sprite_span_name = STR("idle.one_handed"); e->sprite_span_name = STR("idle.two_handed"); e->sprite_collider_slice = STR("shape"); struct xform xf = XFORM_TRS(.t = pos, .r = r, .s = size); //xf.bx.y = -1.f; entity_set_xform(e, xf); e->linear_ground_friction = 250; e->angular_ground_friction = 200; //e->control_force = 500; e->control_force = 500; //e->control_force_max_speed = 4; e->control_torque = 1000; e->control_force_max_speed = 4; entity_enable_prop(e, ENTITY_PROP_PHYSICAL_DYNAMIC); player_ent = e; } /* Enemy */ { struct entity *e = entity_alloc(root); struct v2 pos = V2(1, -2); f32 r = 0; struct v2 size = V2(1, 1); struct xform xf = XFORM_TRS(.t = pos, .r = r, .s = size); entity_set_xform(e, xf); e->sprite = sprite_tag_from_path(STR("res/graphics/tim.ase")); e->sprite_collider_slice = STR("shape"); entity_enable_prop(e, ENTITY_PROP_PHYSICAL_DYNAMIC); e->mass_unscaled = 10; e->inertia_unscaled = 10; e->linear_ground_friction = 250; e->angular_ground_friction = 200; } /* Box */ #if 0 { struct entity *e = entity_alloc(root); struct v2 pos = V2(1, -0.5); f32 r = 0; struct v2 size = V2(0.5, 0.25); struct xform xf = XFORM_TRS(.t = pos, .r = r, .s = size); entity_set_xform(e, xf); e->sprite = sprite_tag_from_path(STR("res/graphics/box.ase")); e->sprite_collider_slice = STR("shape"); entity_enable_prop(e, ENTITY_PROP_PHYSICAL_DYNAMIC); e->mass_unscaled = 10; e->inertia_unscaled = 10; e->linear_ground_friction = 250; e->angular_ground_friction = 5; } #endif /* Player weapon */ if (player_ent->valid) { struct entity *e = entity_alloc(player_ent); e->sprite = sprite_tag_from_path(STR("res/graphics/gun.ase")); entity_enable_prop(e, ENTITY_PROP_ATTACHED); e->attach_slice = STR("attach.wep"); entity_enable_prop(e, ENTITY_PROP_WEAPON); e->trigger_delay = 1.0f / 10.0f; player_ent->equipped = e->handle; } /* Camera */ if (!G.extra_spawn && player_ent->valid) { struct entity *e = entity_alloc(root); entity_set_xform(e, XFORM_IDENT); entity_enable_prop(e, ENTITY_PROP_CAMERA); entity_enable_prop(e, ENTITY_PROP_CAMERA_ACTIVE); e->camera_follow = player_ent->handle; f32 width = (f32)DEFAULT_CAMERA_WIDTH; f32 height = (f32)DEFAULT_CAMERA_HEIGHT; e->camera_quad_xform = XFORM_TRS(.s = V2(width, height)); } G.extra_spawn = true; } /* ========================== * * TESTING CONTACT CONSTRAINT * ========================== */ INTERNAL void create_contacts(void) { struct entity_store *store = G.tick.entity_store; struct entity *root = G.root; for (u64 check0_index = 0; check0_index < store->reserved; ++check0_index) { struct entity *check0 = &store->entities[check0_index]; if (!entity_is_valid_and_active(check0)) continue; if (!(entity_has_prop(check0, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(check0, ENTITY_PROP_PHYSICAL_KINEMATIC))) continue; if (check0->local_collider.count <= 0) continue; struct xform check0_xf = entity_get_xform(check0); struct collider_shape check0_collider = check0->local_collider; for (u64 check1_index = 0; check1_index < store->reserved; ++check1_index) { struct entity *check1 = &store->entities[check1_index]; if (check1 == check0) continue; if (!entity_is_valid_and_active(check1)) continue; if (!(entity_has_prop(check1, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(check1, ENTITY_PROP_PHYSICAL_KINEMATIC))) continue; if (check1->local_collider.count <= 0) continue; /* Deterministic entity order based on index */ struct entity *e0; struct entity *e1; struct xform e0_xf; struct xform e1_xf; struct collider_shape e0_collider; struct collider_shape e1_collider; if (check0_index < check1_index) { e0 = check0; e1 = check1; e0_xf = check0_xf; e1_xf = entity_get_xform(check1); e0_collider = check0_collider; e1_collider = check1->local_collider; } else { e0 = check1; e1 = check0; e0_xf = entity_get_xform(check1); e1_xf = check0_xf; e0_collider = check1->local_collider; e1_collider = check0_collider; } u64 lookup_hash = contact_lookup_hash_from_entities(e0->handle, e1->handle); struct contact_lookup_entry *entry = contact_lookup_get(&G.contact_lookup, lookup_hash); struct entity *constraint_ent = entity_nil(); if (entry) { constraint_ent = entity_from_handle(store, entry->contact_ent_handle); if (entity_is_valid_and_active(constraint_ent)) { if (constraint_ent->contact_constraint_data.last_iteration >= G.tick.tick_id) { /* Already processed constraint this iteration */ continue; } else { ++constraint_ent->contact_constraint_data.last_iteration; } } else { /* Constraint ent no longer valid, delete entry */ contact_lookup_remove(&G.contact_lookup, entry); entry = NULL; } } /* Calculate collision */ struct collider_collision_points_result collider_res = collider_collision_points(&e0_collider, &e1_collider, e0_xf, e1_xf); /* Parts of algorithm are hard-coded to support 2 contact points */ CT_ASSERT(ARRAY_COUNT(constraint_ent->contact_constraint_data.points) == 2); CT_ASSERT(ARRAY_COUNT(collider_res.points) == 2); struct contact_constraint *constraint = NULL; if (collider_res.num_points > 0) { if (!entity_is_valid_and_active(constraint_ent)) { /* Create hit event */ { struct entity *event = entity_alloc(root); entity_enable_prop(event, ENTITY_PROP_HIT_EVENT); event->hit_event.e0 = e0->handle; event->hit_event.e1 = e1->handle; event->hit_event.normal = collider_res.normal; entity_enable_prop(event, ENTITY_PROP_RELEASE_AT_END_OF_FRAME); entity_enable_prop(event, ENTITY_PROP_ACTIVE); /* Calculate point */ struct v2 point = collider_res.points[0].point; if (collider_res.num_points > 1) { point = v2_add(point, v2_mul(v2_sub(collider_res.points[1].point, point), 0.5f)); } event->hit_event.point = point; /* Calculate relative velocity */ struct v2 vrel; { struct v2 v0 = e0->linear_velocity; struct v2 v1 = e1->linear_velocity; f32 w0 = e0->angular_velocity; f32 w1 = e1->angular_velocity; struct v2 vcp0 = v2_sub(point, e0_xf.og); struct v2 vcp1 = v2_sub(point, e1_xf.og); struct v2 vel0 = v2_add(v0, v2_perp_mul(vcp0, w0)); struct v2 vel1 = v2_add(v1, v2_perp_mul(vcp1, w1)); vrel = v2_sub(vel0, vel1); } event->hit_event.vrel = vrel; } /* Create constraint */ { constraint_ent = entity_alloc(root); constraint_ent->contact_constraint_data.e1 = e1->handle; constraint_ent->contact_constraint_data.e0 = e0->handle; constraint_ent->contact_constraint_data.skip_solve = entity_has_prop(e0, ENTITY_PROP_SENSOR) || entity_has_prop(e1, ENTITY_PROP_SENSOR) || !(entity_has_prop(e0, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(e1, ENTITY_PROP_PHYSICAL_DYNAMIC)); entity_enable_prop(constraint_ent, ENTITY_PROP_ACTIVE); /* TODO: Should we recalculate normal as more contact points are added? */ entity_enable_prop(constraint_ent, ENTITY_PROP_CONTACT_CONSTRAINT); activate_now(constraint_ent); ASSERT(!entry); /* Existing entry should never be present here */ contact_lookup_set(&G.contact_lookup, lookup_hash, constraint_ent->handle); } } constraint = &constraint_ent->contact_constraint_data; constraint->normal = collider_res.normal; constraint->friction = math_sqrt(e0->friction * e1->friction); /* Delete old contacts that are no longer present */ for (u32 i = 0; i < constraint->num_points; ++i) { struct contact_point *old = &constraint->points[i]; u32 id = old->id; b32 found = false; for (u32 j = 0; j < collider_res.num_points; ++j) { if (collider_res.points[j].id == id) { found = true; break; } } if (!found) { /* Delete contact by replacing with last in array */ *old = constraint->points[--constraint->num_points]; --i; } } /* Update / insert returned contacts */ for (u32 i = 0; i < collider_res.num_points; ++i) { struct collider_collision_point *res_point = &collider_res.points[i]; struct v2 point = res_point->point; f32 sep = res_point->separation; u32 id = res_point->id; struct contact_point *contact = NULL; /* Match */ for (u32 j = 0; j < constraint->num_points; ++j) { struct contact_point *t = &constraint->points[j]; if (t->id == id) { contact = t; break; } } if (!contact) { /* Insert */ contact = &constraint->points[constraint->num_points++]; MEMZERO_STRUCT(contact); contact->id = id; constraint->softness = G.contact_softness; constraint->pushout_velocity = 3.0f; } /* Update points & separation */ contact->point_local_e0 = xform_invert_mul_v2(e0_xf, point); contact->point_local_e1 = xform_invert_mul_v2(e1_xf, point); contact->starting_separation = sep; #if COLLIDER_DEBUG contact->dbg_pt = point; #endif } } else if (constraint_ent->valid) { constraint_ent->contact_constraint_data.num_points = 0; } /* TODO: Remove this (debugging) */ #if COLLIDER_DEBUG { struct string fdkey = STRING_FROM_BUFFER(BUFFER_FROM_STRUCT(&lookup_hash)); struct entity_handle *dbg_ent_handle = fixed_dict_get(&G.collision_debug_lookup.dict, fdkey); if (!dbg_ent_handle) { /* FIXME: Handle never released */ dbg_ent_handle = arena_push_zero(&G.collision_debug_lookup.arena, struct entity_handle); } struct entity *dbg_ent = entity_from_handle(store, *dbg_ent_handle); if (!dbg_ent->valid) { /* FIXME: Entity never released */ dbg_ent = entity_alloc(root); entity_enable_prop(dbg_ent, ENTITY_PROP_COLLISION_DEBUG); *dbg_ent_handle = dbg_ent->handle; fixed_dict_set(&G.collision_debug_lookup.arena, &G.collision_debug_lookup.dict, fdkey, dbg_ent_handle); } struct collision_debug *dbg = &dbg_ent->collision_debug_data; if (dbg->res.num_points == 0) { if (collider_res.num_points > 0) { ++e0->colliding; ++e1->colliding; } } else { if (collider_res.num_points == 0) { --e0->colliding; --e1->colliding; } } dbg->e0 = e0->handle; dbg->e1 = e1->handle; dbg->res = collider_res; if (constraint) { MEMCPY(dbg->points, constraint->points, sizeof(dbg->points)); dbg->num_points = constraint->num_points; } else { dbg->num_points = 0; } dbg->xf0 = e0_xf; dbg->xf1 = e1_xf; /* Update closest points */ { struct collider_closest_points_result closest_points_res = collider_closest_points(&e0_collider, &e1_collider, e0_xf, e1_xf); dbg->closest0 = closest_points_res.p0; dbg->closest1 = closest_points_res.p1; } } #endif } } } INTERNAL void prepare_contacts(void) { struct entity_store *store = G.tick.entity_store; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *constraint_ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(constraint_ent)) continue; if (!entity_has_prop(constraint_ent, ENTITY_PROP_CONTACT_CONSTRAINT)) continue; struct contact_constraint *constraint = &constraint_ent->contact_constraint_data; u32 num_points = constraint->num_points; struct entity *e0 = entity_from_handle(store, constraint->e0); struct entity *e1 = entity_from_handle(store, constraint->e1); if (num_points > 0 && entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1)) { struct v2 normal = constraint->normal; struct v2 tangent = v2_perp(normal); struct xform e0_xf = entity_get_xform(e0); struct xform e1_xf = entity_get_xform(e1); /* TODO: Cache this */ /* Calculate masses */ f32 inv_m0; f32 inv_m1; f32 inv_i0; f32 inv_i1; { f32 scale0 = math_fabs(xform_get_determinant(e0_xf)); f32 scale1 = math_fabs(xform_get_determinant(e1_xf)); inv_m0 = 1.f / (e0->mass_unscaled * scale0); inv_m1 = 1.f / (e1->mass_unscaled * scale1); inv_i0 = 1.f / (e0->inertia_unscaled * scale0); inv_i1 = 1.f / (e1->inertia_unscaled * scale1); } constraint->inv_m0 = inv_m0; constraint->inv_m1 = inv_m1; constraint->inv_i0 = inv_i0; constraint->inv_i1 = inv_i1; if (entity_has_prop(e0, ENTITY_PROP_PHYSICAL_KINEMATIC)) { constraint->inv_m0 = 0; constraint->inv_i0 = 0; } if (entity_has_prop(e1, ENTITY_PROP_PHYSICAL_KINEMATIC)) { constraint->inv_m1 = 0; constraint->inv_i1 = 0; } /* Update / insert returned contacts */ for (u32 i = 0; i < num_points; ++i) { struct contact_point *contact = &constraint->points[i]; struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, contact->point_local_e0), e0_xf.og); struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, contact->point_local_e1), e1_xf.og); /* Normal mass */ { f32 vcp0_wedge = v2_wedge(vcp0, normal); f32 vcp1_wedge = v2_wedge(vcp1, normal); f32 k = (inv_m0 + inv_m1) + (inv_i0 * vcp0_wedge * vcp0_wedge) + (inv_i1 * vcp1_wedge * vcp1_wedge); contact->inv_normal_mass = k > 0.0f ? 1.0f / k : 0.0f; } /* Tangent mass */ { f32 vcp0_wedge = v2_wedge(vcp0, tangent); f32 vcp1_wedge = v2_wedge(vcp1, tangent); f32 k = (inv_m0 + inv_m1) + (inv_i0 * vcp0_wedge * vcp0_wedge) + (inv_i1 * vcp1_wedge * vcp1_wedge); contact->inv_tangent_mass = k > 0.0f ? 1.0f / k : 0.0f; } #if !GAME_PHYSICS_ENABLE_WARM_STARTING contact->normal_impulse = 0; contact->tangent_impulse = 0; #endif } } else { /* Mark constraint for removal */ entity_disable_prop(constraint_ent, ENTITY_PROP_ACTIVE); entity_enable_prop(constraint_ent, ENTITY_PROP_RELEASE_AT_END_OF_FRAME); /* Remove from lookup */ u64 hash = contact_lookup_hash_from_entities(constraint->e0, constraint->e1); struct contact_lookup_entry *entry = contact_lookup_get(&G.contact_lookup, hash); if (entry) { contact_lookup_remove(&G.contact_lookup, entry); } else { ASSERT(false); /* This should always exist */ } } } } INTERNAL void warm_start_contacts(void) { struct entity_store *store = G.tick.entity_store; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *constraint_ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(constraint_ent)) continue; if (!entity_has_prop(constraint_ent, ENTITY_PROP_CONTACT_CONSTRAINT)) continue; struct contact_constraint *constraint = &constraint_ent->contact_constraint_data; u32 num_points = constraint->num_points; struct entity *e0 = entity_from_handle(store, constraint->e0); struct entity *e1 = entity_from_handle(store, constraint->e1); if (num_points > 0 && entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1) && !constraint->skip_solve) { struct xform e0_xf = entity_get_xform(e0); struct xform e1_xf = entity_get_xform(e1); f32 inv_m0 = constraint->inv_m0; f32 inv_m1 = constraint->inv_m1; f32 inv_i0 = constraint->inv_i0; f32 inv_i1 = constraint->inv_i1; struct v2 v0 = e0->linear_velocity; struct v2 v1 = e1->linear_velocity; f32 w0 = e0->angular_velocity; f32 w1 = e1->angular_velocity; /* Warm start */ struct v2 normal = constraint->normal; struct v2 tangent = v2_perp(normal); f32 inv_num_points = 1.f / num_points; for (u32 i = 0; i < num_points; ++i) { struct contact_point *point = &constraint->points[i]; struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, point->point_local_e0), e0_xf.og); struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, point->point_local_e1), e1_xf.og); struct v2 impulse = v2_add(v2_mul(normal, point->normal_impulse), v2_mul(tangent, point->tangent_impulse)); impulse = v2_mul(impulse, inv_num_points); v0 = v2_sub(v0, v2_mul(impulse, inv_m0)); v1 = v2_add(v1, v2_mul(impulse, inv_m1)); w0 -= v2_wedge(vcp0, impulse) * inv_i0; w1 += v2_wedge(vcp1, impulse) * inv_i1; } e0->linear_velocity = v0; e0->angular_velocity = w0; e1->linear_velocity = v1; e1->angular_velocity = w1; } } } INTERNAL void solve_contacts(f32 dt, b32 apply_bias) { struct entity_store *store = G.tick.entity_store; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *constraint_ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(constraint_ent)) continue; if (!entity_has_prop(constraint_ent, ENTITY_PROP_CONTACT_CONSTRAINT)) continue; struct contact_constraint *constraint = &constraint_ent->contact_constraint_data; struct entity *e0 = entity_from_handle(store, constraint->e0); struct entity *e1 = entity_from_handle(store, constraint->e1); struct v2 v0 = e0->linear_velocity; struct v2 v1 = e1->linear_velocity; f32 w0 = e0->angular_velocity; f32 w1 = e1->angular_velocity; u32 num_points = constraint->num_points; if (num_points > 0 && entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1) && !constraint->skip_solve) { struct xform e0_xf = entity_get_xform(e0); struct xform e1_xf = entity_get_xform(e1); f32 inv_m0 = constraint->inv_m0; f32 inv_m1 = constraint->inv_m1; f32 inv_i0 = constraint->inv_i0; f32 inv_i1 = constraint->inv_i1; /* Normal impulse */ struct v2 normal = constraint->normal; for (u32 point_index = 0; point_index < num_points; ++point_index) { struct contact_point *point = &constraint->points[point_index]; struct v2 p0 = xform_mul_v2(e0_xf, point->point_local_e0); struct v2 p1 = xform_mul_v2(e1_xf, point->point_local_e1); struct v2 vcp0 = v2_sub(p0, e0_xf.og); struct v2 vcp1 = v2_sub(p1, e1_xf.og); f32 separation = v2_dot(v2_sub(p1, p0), normal) + point->starting_separation; f32 velocity_bias = 0.0f; f32 mass_scale = 1.0f; f32 impulse_scale = 0.0f; if (separation > 0.0f) { /* Speculative */ velocity_bias = separation / dt; } else if (apply_bias) { /* Soft constraint */ struct math_spring_result softness = constraint->softness; f32 pushout_velocity = constraint->pushout_velocity; mass_scale = softness.mass_scale; impulse_scale = softness.impulse_scale; velocity_bias = max_f32(softness.bias_rate * separation, -pushout_velocity); } struct v2 vel0 = v2_add(v0, v2_perp_mul(vcp0, w0)); struct v2 vel1 = v2_add(v1, v2_perp_mul(vcp1, w1)); struct v2 vrel = v2_sub(vel0, vel1); f32 k = point->inv_normal_mass; /* (to be applied along n) */ f32 vn = v2_dot(vrel, normal); f32 j = ((k * mass_scale) * (vn - velocity_bias)) - (point->normal_impulse * impulse_scale); f32 old_impulse = point->normal_impulse; f32 new_impulse = max_f32(old_impulse + j, 0); f32 delta = new_impulse - old_impulse; point->normal_impulse = new_impulse; struct v2 impulse = v2_mul(normal, delta); v0 = v2_sub(v0, v2_mul(impulse, inv_m0)); v1 = v2_add(v1, v2_mul(impulse, inv_m1)); w0 -= v2_wedge(vcp0, impulse) * inv_i0; w1 += v2_wedge(vcp1, impulse) * inv_i1; } /* Tangent impulse */ struct v2 tangent = v2_perp(normal); for (u32 point_index = 0; point_index < num_points; ++point_index) { struct contact_point *point = &constraint->points[point_index]; struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, point->point_local_e0), e0_xf.og); struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, point->point_local_e1), e1_xf.og); struct v2 vel0 = v2_add(v0, v2_perp_mul(vcp0, w0)); struct v2 vel1 = v2_add(v1, v2_perp_mul(vcp1, w1)); struct v2 vrel = v2_sub(vel0, vel1); f32 k = point->inv_tangent_mass; /* (to be applied along t) */ f32 vt = v2_dot(vrel, tangent); f32 j = vt * k; f32 max_friction = constraint->friction * point->normal_impulse; f32 old_impulse = point->tangent_impulse; f32 new_impulse = clamp_f32(old_impulse + j, -max_friction, max_friction); f32 delta = new_impulse - old_impulse; point->tangent_impulse = new_impulse; struct v2 impulse = v2_mul(tangent, delta); v0 = v2_sub(v0, v2_mul(impulse, inv_m0)); v1 = v2_add(v1, v2_mul(impulse, inv_m1)); w0 -= v2_wedge(vcp0, impulse) * inv_i0; w1 += v2_wedge(vcp1, impulse) * inv_i1; } e0->linear_velocity = v0; e0->angular_velocity = w0; e1->linear_velocity = v1; e1->angular_velocity = w1; } } } /* ========================== * * TESTING MOTOR JOINT * ========================== */ INTERNAL void prepare_motor_joints(void) { struct entity_store *store = G.tick.entity_store; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *joint_ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(joint_ent)) continue; if (!entity_has_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT)) continue; struct motor_joint *joint = &joint_ent->motor_joint_data; struct entity *e0 = entity_from_handle(store, joint->e0); struct entity *e1 = entity_from_handle(store, joint->e1); if (entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1)) { struct xform e0_xf = entity_get_xform(e0); struct xform e1_xf = entity_get_xform(e1); /* TODO: Cache this */ /* Calculate masses */ f32 inv_m0; f32 inv_m1; f32 inv_i0; f32 inv_i1; { f32 scale0 = math_fabs(xform_get_determinant(e0_xf)); f32 scale1 = math_fabs(xform_get_determinant(e1_xf)); inv_m0 = 1.f / (e0->mass_unscaled * scale0); inv_m1 = 1.f / (e1->mass_unscaled * scale1); inv_i0 = 1.f / (e0->inertia_unscaled * scale0); inv_i1 = 1.f / (e1->inertia_unscaled * scale1); } joint->inv_m0 = inv_m0; joint->inv_m1 = inv_m1; joint->inv_i0 = inv_i0; joint->inv_i1 = inv_i1; joint->point_local_e0 = V2(0, 0); joint->point_local_e1 = V2(0, 0); struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, joint->point_local_e0), e0_xf.og); struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, joint->point_local_e1), e1_xf.og); struct xform linear_mass_xf; linear_mass_xf.bx.x = inv_m0 + inv_m1 + vcp0.y * vcp0.y * inv_i0 + vcp1.y * vcp1.y * inv_i1; linear_mass_xf.bx.y = -vcp0.y * vcp0.x * inv_i0 - vcp1.y * vcp1.x * inv_i1; linear_mass_xf.by.x = linear_mass_xf.bx.y; linear_mass_xf.by.y = inv_m0 + inv_m1 + vcp0.x * vcp0.x * inv_i0 + vcp1.x * vcp1.x * inv_i1; joint->linear_mass_xf = xform_invert(linear_mass_xf); joint->angular_mass = 1.f / (inv_i0 + inv_i1); #if !GAME_PHYSICS_ENABLE_WARM_STARTING joint->linear_impulse = V2(0, 0); joint->angular_impulse = 0; #endif } else { /* Mark joint for removal */ entity_disable_prop(joint_ent, ENTITY_PROP_ACTIVE); entity_enable_prop(joint_ent, ENTITY_PROP_RELEASE_AT_END_OF_FRAME); } } } INTERNAL void warm_start_motor_joints(void) { struct entity_store *store = G.tick.entity_store; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *joint_ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(joint_ent)) continue; if (!entity_has_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT)) continue; struct motor_joint *joint = &joint_ent->motor_joint_data; struct entity *e0 = entity_from_handle(store, joint->e0); struct entity *e1 = entity_from_handle(store, joint->e1); struct xform e0_xf = entity_get_xform(e0); struct xform e1_xf = entity_get_xform(e1); f32 inv_m0 = joint->inv_m0; f32 inv_m1 = joint->inv_m1; f32 inv_i0 = joint->inv_i0; f32 inv_i1 = joint->inv_i1; struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, joint->point_local_e0), e0_xf.og); struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, joint->point_local_e1), e1_xf.og); e0->linear_velocity = v2_sub(e0->linear_velocity, v2_mul(joint->linear_impulse, inv_m0)); e1->linear_velocity = v2_add(e1->linear_velocity, v2_mul(joint->linear_impulse, inv_m1)); e0->angular_velocity -= (v2_wedge(vcp0, joint->linear_impulse) + joint->angular_impulse) * inv_i0; e1->angular_velocity += (v2_wedge(vcp1, joint->linear_impulse) + joint->angular_impulse) * inv_i1; } } INTERNAL void solve_motor_joints(f32 dt) { struct entity_store *store = G.tick.entity_store; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *joint_ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(joint_ent)) continue; if (!entity_has_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT)) continue; struct motor_joint *joint = &joint_ent->motor_joint_data; struct entity *e0 = entity_from_handle(store, joint->e0); struct entity *e1 = entity_from_handle(store, joint->e1); struct xform e0_xf = entity_get_xform(e0); struct xform e1_xf = entity_get_xform(e1); f32 inv_m0 = joint->inv_m0; f32 inv_m1 = joint->inv_m1; f32 inv_i0 = joint->inv_i0; f32 inv_i1 = joint->inv_i1; struct v2 v0 = e0->linear_velocity; struct v2 v1 = e1->linear_velocity; f32 w0 = e0->angular_velocity; f32 w1 = e1->angular_velocity; f32 correction_rate = joint->correction_rate / dt; /* Angular constraint */ { f32 max_impulse = joint->max_torque * dt; f32 angular_separation = math_unwind_angle(xform_get_rotation(e1_xf) - xform_get_rotation(e0_xf)); f32 angular_bias = angular_separation * correction_rate; f32 impulse = -joint->angular_mass * (w1 - w0 + angular_bias); f32 old_impulse = joint->angular_impulse; joint->angular_impulse = clamp_f32(joint->angular_impulse + impulse, -max_impulse, max_impulse); impulse = joint->angular_impulse - old_impulse; w0 -= impulse * inv_i0; w1 += impulse * inv_i1; } /* Linear constraint */ { struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, joint->point_local_e0), e0_xf.og); struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, joint->point_local_e1), e1_xf.og); f32 max_impulse = joint->max_force * dt; struct v2 linear_separation = v2_sub(v2_add(e1_xf.og, vcp1), v2_add(e0_xf.og, vcp0)); struct v2 linear_bias = v2_mul(linear_separation, correction_rate); struct v2 vrel = v2_sub(v2_add(v1, v2_perp_mul(vcp1, w1)), v2_add(v0, v2_perp_mul(vcp0, w0))); struct v2 impulse = v2_neg(xform_basis_mul_v2(joint->linear_mass_xf, v2_add(vrel, linear_bias))); struct v2 old_impulse = joint->linear_impulse; joint->linear_impulse = v2_clamp_len(v2_add(joint->linear_impulse, impulse), max_impulse); impulse = v2_sub(joint->linear_impulse, old_impulse); v0 = v2_sub(v0, v2_mul(impulse, inv_m0)); v1 = v2_add(v1, v2_mul(impulse, inv_m1)); w0 -= v2_wedge(vcp0, impulse) * inv_i0; w1 += v2_wedge(vcp1, impulse) * inv_i1; } e0->linear_velocity = v0; e0->angular_velocity = w0; e1->linear_velocity = v1; e1->angular_velocity = w1; } } #if 1 /* ========================== * * TESTING MOUSE JOINT * ========================== */ INTERNAL void create_mouse_joints(struct game_cmd_array game_cmds) { struct entity_store *store = G.tick.entity_store; struct entity *root = G.root; b32 start_dragging = false; b32 stop_dragging = false; for (u64 i = 0; i < game_cmds.count; ++i) { struct game_cmd cmd = game_cmds.cmds[i]; b32 start = cmd.state == GAME_CMD_STATE_START; b32 stop = cmd.state == GAME_CMD_STATE_STOP; switch (cmd.kind) { case GAME_CMD_KIND_DRAG_OBJECT: { if (start) { start_dragging = true; } else if (stop) { stop_dragging = true; } } break; default: break; } } struct v2 cursor = G.user_cursor; struct entity *joint_ent = entity_find_first_match_one(store, ENTITY_PROP_MOUSE_JOINT); struct entity *target_ent = entity_from_handle(store, joint_ent->mouse_joint_data.target); if (start_dragging) { struct xform mouse_xf = xform_from_pos(cursor); struct collider_shape mouse_shape = ZI; mouse_shape.points[0] = V2(0, 0); mouse_shape.count = 1; 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; if (!entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC)) continue; struct collider_shape ent_collider = ent->local_collider; if (ent_collider.count > 0) { struct xform ent_xf = entity_get_xform(ent); /* TODO: Can just use boolean GJK */ struct collider_collision_points_result res = collider_collision_points(&ent_collider, &mouse_shape, ent_xf, mouse_xf); if (res.num_points > 0) { target_ent = ent; break; } } } } else if (stop_dragging) { target_ent = entity_nil(); } if (entity_is_valid_and_active(target_ent)) { if (!entity_is_valid_and_active(joint_ent)) { joint_ent = entity_alloc(root); joint_ent->mass_unscaled = F32_INFINITY; joint_ent->inertia_unscaled = F32_INFINITY; entity_enable_prop(joint_ent, ENTITY_PROP_MOUSE_JOINT); entity_enable_prop(joint_ent, ENTITY_PROP_ACTIVE); } struct mouse_joint *joint = &joint_ent->mouse_joint_data; struct xform xf = entity_get_xform(target_ent); f32 mass = target_ent->mass_unscaled * math_fabs(xform_get_determinant(xf)); if (!entity_handle_eq(joint->target, target_ent->handle)) { joint->point_local_start = xform_invert_mul_v2(xf, cursor); joint->target = target_ent->handle; } joint->point_local_end = xform_invert_mul_v2(xf, cursor); joint->linear_softness = G.mouse_joint_linear_softness; joint->angular_softness = G.mouse_joint_angular_softness; joint->max_force = G.mouse_joint_max_force * mass; } else { if (entity_is_valid_and_active(joint_ent)) { joint_ent->mouse_joint_data.target = entity_nil_handle(); } } } INTERNAL void prepare_mouse_joints(void) { struct entity_store *store = G.tick.entity_store; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *joint_ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(joint_ent)) continue; if (!entity_has_prop(joint_ent, ENTITY_PROP_MOUSE_JOINT)) continue; struct mouse_joint *joint = &joint_ent->mouse_joint_data; struct entity *ent = entity_from_handle(store, joint->target); if (entity_is_valid_and_active(ent)) { struct xform xf = entity_get_xform(ent); /* TODO: Cache this */ /* Calculate masses */ f32 inv_m; f32 inv_i; { f32 scale = math_fabs(xform_get_determinant(xf)); inv_m = 1.f / (ent->mass_unscaled * scale); inv_i = 1.f / (ent->inertia_unscaled * scale); } joint->inv_m = inv_m; joint->inv_i = inv_i; struct v2 vcp = v2_sub(xform_mul_v2(xf, joint->point_local_start), xf.og); struct xform linear_mass_xf; linear_mass_xf.bx.x = inv_m + inv_i * vcp.y * vcp.y; linear_mass_xf.bx.y = -inv_i * vcp.x * vcp.y; linear_mass_xf.by.x = linear_mass_xf.bx.y; linear_mass_xf.by.y = inv_m + inv_i * vcp.x * vcp.x; joint->linear_mass_xf = xform_invert(linear_mass_xf); #if !GAME_PHYSICS_ENABLE_WARM_STARTING joint->linear_impulse = V2(0, 0); joint->angular_impulse = 0; #endif } else { /* Mark joint for removal */ entity_disable_prop(joint_ent, ENTITY_PROP_ACTIVE); entity_enable_prop(joint_ent, ENTITY_PROP_RELEASE_AT_END_OF_FRAME); } } } INTERNAL void warm_start_mouse_joints(void) { struct entity_store *store = G.tick.entity_store; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *joint_ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(joint_ent)) continue; if (!entity_has_prop(joint_ent, ENTITY_PROP_MOUSE_JOINT)) continue; struct mouse_joint *joint = &joint_ent->mouse_joint_data; struct entity *ent = entity_from_handle(store, joint->target); if (entity_is_valid_and_active(ent)) { f32 inv_m = joint->inv_m; f32 inv_i = joint->inv_i; struct xform xf = entity_get_xform(ent); struct v2 vcp = v2_sub(xform_mul_v2(xf, joint->point_local_start), xf.og); ent->linear_velocity = v2_add(ent->linear_velocity, v2_mul(joint->linear_impulse, inv_m)); ent->angular_velocity += (v2_wedge(vcp, joint->linear_impulse) + joint->angular_impulse) * inv_i; } } } INTERNAL void solve_mouse_joints(f32 dt) { struct entity_store *store = G.tick.entity_store; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *joint_ent = &store->entities[entity_index]; if (!entity_is_valid_and_active(joint_ent)) continue; if (!entity_has_prop(joint_ent, ENTITY_PROP_MOUSE_JOINT)) continue; struct mouse_joint *joint = &joint_ent->mouse_joint_data; struct entity *ent = entity_from_handle(store, joint->target); if (entity_is_valid_and_active(ent)) { struct v2 v = ent->linear_velocity; f32 w = ent->angular_velocity; f32 inv_m = joint->inv_m; f32 inv_i = joint->inv_i; /* Angular impulse */ { struct math_spring_result softness = joint->angular_softness; f32 mass_scale = softness.mass_scale; f32 impulse_scale = softness.impulse_scale; f32 impulse = mass_scale * (-w / inv_i) - impulse_scale * joint->angular_impulse; joint->angular_impulse += impulse; w += impulse * inv_i; } /* Linear impulse */ { f32 max_impulse = joint->max_force / dt; struct xform xf = entity_get_xform(ent); struct v2 point_start = xform_mul_v2(xf, joint->point_local_start); struct v2 point_end = xform_mul_v2(xf, joint->point_local_end); struct v2 vcp = v2_sub(point_start, xf.og); struct v2 separation = v2_sub(point_start, point_end); struct math_spring_result softness = joint->linear_softness; struct v2 bias = v2_mul(separation, softness.bias_rate); f32 mass_scale = softness.mass_scale; f32 impulse_scale = softness.impulse_scale; #if 0 struct v2 vel = v2_add(v, v2_perp_mul(vcp, w)); struct v2 b = v2_mul(xform_basis_mul_v2(xform_invert(joint->linear_mass_xf), v2_add(vel, bias)), mass_scale); struct v2 old_impulse = joint->linear_impulse; struct v2 impulse = v2_sub(v2_mul(old_impulse, -impulse_scale), b); joint->linear_impulse = v2_clamp_len(v2_add(joint->linear_impulse, impulse), max_impulse); impulse = v2_sub(joint->linear_impulse, old_impulse); #else struct v2 vel = v2_add(v, v2_perp_mul(vcp, w)); struct v2 b = xform_basis_mul_v2(joint->linear_mass_xf, v2_add(vel, bias)); struct v2 impulse; impulse.x = -mass_scale * b.x - impulse_scale * joint->linear_impulse.x; impulse.y = -mass_scale * b.y - impulse_scale * joint->linear_impulse.y; struct v2 old_impulse = joint->linear_impulse; joint->linear_impulse.x += impulse.x; joint->linear_impulse.y += impulse.y; joint->linear_impulse = v2_clamp_len(joint->linear_impulse, max_impulse); impulse.x = joint->linear_impulse.x - old_impulse.x; impulse.y = joint->linear_impulse.y - old_impulse.y; #endif v = v2_add(v, v2_mul(impulse, inv_m)); w += v2_wedge(vcp, impulse) * inv_i; } ent->linear_velocity = v; ent->angular_velocity = w; } } } #else INTERNAL void prepare_mouse_joints(void) { } INTERNAL void warm_start_mouse_joints(void) { } INTERNAL void solve_mouse_joints(f32 dt) { (UNUSED)dt; } #endif /* ========================== * * TESTING PHYSICS INTEGRATION * ========================== */ INTERNAL void integrate_velocities_from_forces(f32 dt) { struct entity_store *store = G.tick.entity_store; 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; if (!entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC) && !entity_has_prop(ent, ENTITY_PROP_PHYSICAL_KINEMATIC)) continue; struct xform xf = entity_get_xform(ent); f32 det_abs = math_fabs(xform_get_determinant(xf)); f32 mass = ent->mass_unscaled * det_abs; f32 inertia = ent->inertia_unscaled * det_abs; /* Determine force & torque acceleration */ struct v2 force_accel = v2_mul(v2_div(ent->force, mass), dt); f32 torque_accel = (ent->torque / inertia) * dt; /* Integrate & clamp */ ent->linear_velocity = v2_clamp_len(v2_add(ent->linear_velocity, force_accel), GAME_MAX_LINEAR_VELOCITY); ent->angular_velocity = clamp_f32(ent->angular_velocity + torque_accel, -GAME_MAX_ANGULAR_VELOCITY, GAME_MAX_ANGULAR_VELOCITY); /* Reset forces */ ent->force = V2(0, 0); ent->torque = 0; } } INTERNAL void integrate_positions_from_velocities(f32 dt) { struct entity_store *store = G.tick.entity_store; 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; if (!entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC) && !entity_has_prop(ent, ENTITY_PROP_PHYSICAL_KINEMATIC)) continue; /* Clamp velocities */ ent->linear_velocity = v2_clamp_len(ent->linear_velocity, GAME_MAX_LINEAR_VELOCITY); ent->angular_velocity = clamp_f32(ent->angular_velocity, -GAME_MAX_ANGULAR_VELOCITY, GAME_MAX_ANGULAR_VELOCITY); struct v2 tick_linear_velocity = v2_mul(ent->linear_velocity, dt); f32 tick_angular_velocity = ent->angular_velocity * dt; struct xform xf = entity_get_xform(ent); xf.og = v2_add(xf.og, tick_linear_velocity); xf = xform_basis_rotated_world(xf, tick_angular_velocity); entity_set_xform(ent, xf); } } /* ========================== * * TESTING TOI * ========================== */ /* Takes 2 shapes and their xforms at t=0 and t=1. * Returns time of impact in range [0, 1]. */ INTERNAL f32 toi(struct collider_shape *c0, struct collider_shape *c1, struct xform xf0_t0, struct xform xf1_t0, struct xform xf0_t1, struct xform xf1_t1, f32 tolerance) { /* Find direction p0 -> p1 at t=0 */ struct v2 dir; struct v2 dir_neg; { struct collider_closest_points_result closest_points_res = collider_closest_points(c0, c1, xf0_t0, xf1_t0); if (closest_points_res.colliding) { /* Shapes are penetrating at t=0 */ return 0; } dir = v2_norm(v2_sub(closest_points_res.p1, closest_points_res.p0)); dir_neg = v2_neg(dir); } /* Safety check that shapes penetrate at t=1 */ f32 sep; { struct v2 p0 = collider_support_point(c0, xf0_t1, dir); struct v2 p1 = collider_support_point(c1, xf1_t1, dir_neg); sep = v2_dot(dir, v2_sub(p1, p0)); if (sep > tolerance) { /* Shapes are not penetrating at t=1 */ return 1; } } /* Bisect until distance is within tolerance */ /* TODO: Implement false position method as well? (should speed up more linear cases) */ f32 t0 = 0.0; f32 t1 = 1.0; f32 t = 0.5f; while (math_fabs(sep) > tolerance) { struct xform xf0 = xform_lerp(xf0_t0, xf0_t1, t); struct xform xf1 = xform_lerp(xf1_t0, xf1_t1, t); struct v2 p0 = collider_support_point(c0, xf0, dir); struct v2 p1 = collider_support_point(c1, xf1, dir_neg); sep = v2_dot(dir, v2_sub(p1, p0)); /* Update bracket */ if (sep > 0) { t0 = t; } else { t1 = t; } t = (t1 + t0) / 2.0; } return t; } INTERNAL f32 determine_earliest_toi(f32 dt, f32 tolerance) { __prof; f32 smallest_t = 1; struct entity_store *store = G.tick.entity_store; //struct entity *root = G.root; for (u64 e0_index = 0; e0_index < store->reserved; ++e0_index) { struct entity *e0 = &store->entities[e0_index]; if (!entity_is_valid_and_active(e0)) continue; if (!(entity_has_prop(e0, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(e0, ENTITY_PROP_PHYSICAL_KINEMATIC))) continue; if (e0->local_collider.count <= 0) continue; struct collider_shape e0_collider = e0->local_collider; struct xform e0_xf_t0 = entity_get_xform(e0); struct xform e0_xf_t1 = e0_xf_t0; { /* Calculate xform at t=1 */ struct v2 linear_velocity = v2_clamp_len(e0->linear_velocity, GAME_MAX_LINEAR_VELOCITY); f32 angular_velocity = clamp_f32(e0->angular_velocity, -GAME_MAX_ANGULAR_VELOCITY, GAME_MAX_ANGULAR_VELOCITY); struct v2 tick_linear_velocity = v2_mul(linear_velocity, dt); f32 tick_angular_velocity = angular_velocity * dt; e0_xf_t1.og = v2_add(e0_xf_t1.og, tick_linear_velocity); e0_xf_t1 = xform_basis_rotated_world(e0_xf_t1, tick_angular_velocity); } /* Start e1 index at e0 index + 1 to prevent redundant checks */ for (u64 e1_index = e0_index + 1; e1_index < store->reserved; ++e1_index) { struct entity *e1 = &store->entities[e1_index]; if (e1 == e0) continue; if (!entity_is_valid_and_active(e1)) continue; if (!(entity_has_prop(e1, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(e1, ENTITY_PROP_PHYSICAL_KINEMATIC))) continue; if (e1->local_collider.count <= 0) continue; struct collider_shape e1_collider = e1->local_collider; struct xform e1_xf_t0 = entity_get_xform(e1); struct xform e1_xf_t1 = e1_xf_t0; { /* Calculate xform at t=1 */ struct v2 linear_velocity = v2_clamp_len(e1->linear_velocity, GAME_MAX_LINEAR_VELOCITY); f32 angular_velocity = clamp_f32(e1->angular_velocity, -GAME_MAX_ANGULAR_VELOCITY, GAME_MAX_ANGULAR_VELOCITY); struct v2 tick_linear_velocity = v2_mul(linear_velocity, dt); f32 tick_angular_velocity = angular_velocity * dt; e1_xf_t1.og = v2_add(e1_xf_t1.og, tick_linear_velocity); e1_xf_t1 = xform_basis_rotated_world(e1_xf_t1, tick_angular_velocity); } f32 t = toi(&e0_collider, &e1_collider, e0_xf_t0, e1_xf_t0, e0_xf_t1, e1_xf_t1, tolerance); if (t != 0 && t < smallest_t) { smallest_t = t; } } } return smallest_t; } /* ========================== * * Update * ========================== */ INTERNAL void publish_game_tick(void) { __prof; struct sys_lock lock = sys_mutex_lock_e(&G.prev_tick_mutex); arena_set_readwrite(&G.prev_tick.entity_store->arena); { world_copy_replace(&G.prev_tick, &G.tick); } arena_set_readonly(&G.prev_tick.entity_store->arena); atomic_u64_eval_exchange(&G.prev_tick_id, G.prev_tick.tick_id); atomic_u64_eval_exchange(&G.prev_tick_continuity_gen, G.prev_tick.continuity_gen); sys_mutex_unlock(&lock); } INTERNAL void game_update(struct game_cmd_array game_cmds) { __prof; struct temp_arena scratch = scratch_begin_no_conflict(); /* ========================== * * Reset level if necessary * ========================== */ if (G.should_reset_level) { logf_info("Clearing level"); G.should_reset_level = false; G.extra_spawn = false; reset_world(); } /* ========================== * * Begin frame * ========================== */ ++G.tick.tick_id; G.tick.tick_ts = sys_timestamp(); G.tick.dt = max_f64(0.0, (1.0 / GAME_FPS) * G.tick.timescale); G.tick.time += G.tick.dt; f64 dt = G.tick.dt; f64 time = G.tick.time; G.sprite_frame_scope = sprite_scope_begin(); G.root = entity_from_handle(G.tick.entity_store, G.tick.entity_store->root); struct entity *root = G.root; struct entity_store *store = G.tick.entity_store; struct sprite_scope *sprite_frame_scope = G.sprite_frame_scope; (UNUSED)dt; (UNUSED)time; /* ========================== * * Spawn test entities * ========================== */ /* TODO: remove this (testing) */ /* Initialize entities */ { static b32 run = 0; if (!run) { run = 1; spawn_test_entities(); } } /* ========================== * * Process global game cmds * ========================== */ for (u64 cmd_index = 0; cmd_index < game_cmds.count; ++cmd_index) { struct game_cmd cmd = game_cmds.cmds[cmd_index]; switch (cmd.kind) { /* Cursor */ case GAME_CMD_KIND_CURSOR_MOVE: { G.user_cursor = cmd.cursor_pos; } break; /* Clear level */ case GAME_CMD_KIND_CLEAR_ALL: { G.should_reset_level = true; } break; /* Spawn test */ case GAME_CMD_KIND_SPAWN_TEST: { logf_info("Spawning (test)"); spawn_test_entities(); } break; default: break; }; } /* ========================== * * Activate entities * ========================== */ for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *ent = &store->entities[entity_index]; if (!ent->valid) continue; if (!entity_has_prop(ent, ENTITY_PROP_ACTIVE)) { u64 atick = ent->activation_tick; if (atick != 0 || G.tick.tick_id >= atick) { activate_now(ent); } } } /* ========================== * * Reset triggered entities * ========================== */ 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; if (entity_has_prop(ent, ENTITY_PROP_TRIGGER_NEXT_TICK)) { entity_disable_prop(ent, ENTITY_PROP_TRIGGER_NEXT_TICK); entity_enable_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK); } else if (entity_has_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK)) { entity_disable_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK); } } /* ========================== * * Update entity from sprite * ========================== */ 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; if (sprite_tag_is_nil(ent->sprite)) continue; /* Update animation */ struct sprite_sheet *sheet = sprite_sheet_from_tag_await(sprite_frame_scope, ent->sprite); { struct sprite_sheet_span span = sprite_sheet_get_span(sheet, ent->sprite_span_name); f64 time_in_frame = ent->animation_time_in_frame + dt; u64 frame_index = ent->animation_frame; if (frame_index < span.start || frame_index > span.end) { frame_index = span.start; } struct sprite_sheet_frame frame = sprite_sheet_get_frame(sheet, frame_index); while (time_in_frame > frame.duration) { time_in_frame -= frame.duration; ++frame_index; if (frame_index > span.end) { /* Loop animation */ frame_index = span.start; } frame = sprite_sheet_get_frame(sheet, frame_index); } ent->animation_time_in_frame = time_in_frame; ent->animation_frame = frame_index; } /* Update sprite local xform */ { struct sprite_sheet_slice slice = sprite_sheet_get_slice(sheet, STR("pivot"), ent->animation_frame); struct v2 sprite_size = v2_div(sheet->frame_size, (f32)PIXELS_PER_UNIT); struct v2 dir = v2_mul_v2(sprite_size, slice.dir); f32 rot = v2_angle(dir) + PI / 2; struct xform xf = XFORM_IDENT; xf = xform_rotated(xf, -rot); xf = xform_scaled(xf, sprite_size); xf = xform_translated(xf, v2_neg(slice.center)); ent->sprite_local_xform = xf; } /* Update collider from sprite */ if (ent->sprite_collider_slice.len > 0) { struct xform cxf = ent->sprite_local_xform; #if 0 if (entity_has_prop(ent, ENTITY_PROP_TEST)) { f32 scale = 0.5; cxf = xform_scaled(cxf, V2(scale, scale)); } #endif struct sprite_sheet_slice slice = sprite_sheet_get_slice(sheet, ent->sprite_collider_slice, ent->animation_frame); ent->local_collider = collider_from_quad(xform_mul_quad(cxf, quad_from_rect(slice.rect))); #if 1 if (entity_has_prop(ent, ENTITY_PROP_TEST)) { //if ((true)) { #if 0 ent->local_collider.points[0] = V2(0, 0); ent->local_collider.count = 1; ent->local_collider.radius = 0.5; #elif 1 ent->local_collider.points[0] = v2_with_len(V2(0.08, 0.17), 0.15); ent->local_collider.points[1] = v2_with_len(V2(-0.07, -0.2), 0.15); ent->local_collider.count = 2; ent->local_collider.radius = 0.075; #elif 1 #if 0 /* "Bad" winding order */ ent->local_collider.points[0] = V2(-0.15, 0.15); ent->local_collider.points[1] = V2(0.15, 0.15); ent->local_collider.points[2] = V2(0, -0.15); #else ent->local_collider.points[0] = V2(0, -0.15); ent->local_collider.points[1] = V2(0.15, 0.15); ent->local_collider.points[2] = V2(-0.15, 0.15); #endif ent->local_collider.count = 3; ent->local_collider.radius = 0.25; //ent->local_collider.radius = math_fabs(math_sin(G.tick.time) / 3); #else //ent->local_collider.radius = 0.5; ent->local_collider.radius = 0.25; //ent->local_collider.radius = 0.; #endif } #endif } } /* ========================== * * Update attachments * ========================== */ 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; if (!entity_has_prop(ent, ENTITY_PROP_ATTACHED)) continue; struct entity *parent = entity_from_handle(store, ent->parent); struct sprite_tag parent_sprite = parent->sprite; struct sprite_sheet *parent_sheet = sprite_sheet_from_tag_await(sprite_frame_scope, parent_sprite); struct xform parent_sprite_xf = parent->sprite_local_xform; struct sprite_sheet_slice attach_slice = sprite_sheet_get_slice(parent_sheet, ent->attach_slice, parent->animation_frame); struct v2 attach_pos = xform_mul_v2(parent_sprite_xf, attach_slice.center); struct v2 attach_dir = xform_basis_mul_v2(parent_sprite_xf, attach_slice.dir); struct xform xf = entity_get_local_xform(ent); xf.og = attach_pos; xf = xform_basis_with_rotation_world(xf, v2_angle(attach_dir) + PI / 2); entity_set_local_xform(ent, xf); } /* ========================== * * Update control from player cmds * ========================== */ 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; if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { /* Process cmds */ struct v2 move = ent->control.move; struct v2 focus = ent->control.focus; b32 firing = entity_has_prop(ent, ENTITY_PROP_TRIGGERING_EQUIPPED); for (u64 i = 0; i < game_cmds.count; ++i) { struct game_cmd cmd = game_cmds.cmds[i]; b32 start = cmd.state == GAME_CMD_STATE_START; b32 stop = cmd.state == GAME_CMD_STATE_STOP; /* TODO: Combine movement from multiple inputs? E.G. a sudden * start and immediate stop cmd should still move the player a * tad. */ switch (cmd.kind) { case GAME_CMD_KIND_PLAYER_MOVE: { move = cmd.move_dir; focus = cmd.aim_dir; } break; case GAME_CMD_KIND_PLAYER_FIRE: { if (start) { firing = true; } else if (stop) { firing = false; } } break; default: break; } } /* Movement */ if (v2_len_sq(move) > 1) { /* Cap movement vector magnitude at 1 */ move = v2_norm(move); } ent->control.move = move; ent->control.focus = focus; /* Firing */ if (firing) { entity_enable_prop(ent, ENTITY_PROP_TRIGGERING_EQUIPPED); } else { entity_disable_prop(ent, ENTITY_PROP_TRIGGERING_EQUIPPED); } } } /* ========================== * * Test * ========================== */ #if 0 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; if (!entity_has_prop(ent, ENTITY_PROP_TEST)) continue; #if 0 if (!ent->test_initialized) { ent->test_initialized = true; ent->test_start_local_xform = entity_get_local_xform(ent); ent->test_start_sprite_xform = ent->sprite_local_xform; } f32 t = (f32)time; struct v2 og = v2_mul(V2(math_cos(t), math_sin(t)), 3); f32 r = t * 3; struct v2 s = V2(1 + (math_fabs(math_sin(t * 5)) * 3), 1); (UNUSED)og; (UNUSED)r; (UNUSED)s; og = v2_add(og, ent->test_start_local_xform.og); r += xform_get_rotation(ent->test_start_local_xform); s = v2_add(s, xform_get_scale(ent->test_start_local_xform)); struct xform xf = entity_get_local_xform(ent); xf.og = og; xf = xform_rotated_to(xf, r); xf = xform_scaled_to(xf, s); entity_set_local_xform(ent, xf); #else f32 t = (f32)time; struct v2 og = v2_mul(V2(math_cos(t), math_sin(t)), 3); f32 rot = t * PI / 3; struct v2 scale = V2(1 + (math_fabs(math_sin(t * 5)) * 3), 1); (UNUSED)og; (UNUSED)rot; (UNUSED)scale; struct xform xf = entity_get_local_xform(ent); xf = xform_rotated_to(xf, rot); xf = xform_scaled_to(xf, scale); entity_set_local_xform(ent, xf); #endif } #endif /* ========================== * * Trigger equipped * ========================== */ 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; if (entity_has_prop(ent, ENTITY_PROP_TRIGGERING_EQUIPPED)) { struct entity *eq = entity_from_handle(store, ent->equipped); if (entity_is_valid_and_active(eq)) { entity_enable_prop(eq, ENTITY_PROP_TRIGGERED_THIS_TICK); } } } /* ========================== * * Process triggered entities * ========================== */ 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; if (!entity_has_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK)) continue; if ((time - ent->last_triggered < ent->trigger_delay) && ent->last_triggered != 0) continue; ent->last_triggered = time; /* Fire weapon */ if (entity_has_prop(ent, ENTITY_PROP_WEAPON)) { struct sprite_tag sprite = ent->sprite; u32 animation_frame = ent->animation_frame; struct sprite_sheet *sheet = sprite_sheet_from_tag_await(sprite_frame_scope, sprite); struct xform sprite_local_xform = ent->sprite_local_xform; struct sprite_sheet_slice out_slice = sprite_sheet_get_slice(sheet, STR("out"), animation_frame); struct v2 rel_pos = xform_mul_v2(sprite_local_xform, out_slice.center); struct v2 rel_dir = xform_basis_mul_v2(sprite_local_xform, out_slice.dir); { /* Spawn bullet */ struct entity *bullet = entity_alloc(root); bullet->sprite = sprite_tag_from_path(STR("res/graphics/bullet.ase")); bullet->bullet_src = ent->handle; bullet->bullet_src_pos = rel_pos; bullet->bullet_src_dir = rel_dir; //bullet->bullet_impulse = 0.1f; //bullet->bullet_impulse = 0.25f; //bullet->bullet_impulse = 5.f; bullet->bullet_impulse = 100000.f; bullet->mass_unscaled = 0.04f; bullet->inertia_unscaled = 0.00001f; bullet->sprite_collider_slice = STR("shape"); entity_enable_prop(bullet, ENTITY_PROP_BULLET); entity_enable_prop(bullet, ENTITY_PROP_SENSOR); } } } /* ========================== * * Create forces from control move * ========================== */ #if 0 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; if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { struct v2 move = ent->control.move; struct v2 force = v2_mul(move, ent->control_force); entity_apply_force_to_center(ent, force); } } #else 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; if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { struct entity *joint_ent = entity_from_handle(store, ent->move_joint); if (!entity_is_valid_and_active(joint_ent)) { joint_ent = entity_alloc(root); joint_ent->mass_unscaled = F32_INFINITY; joint_ent->inertia_unscaled = F32_INFINITY; entity_enable_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT); entity_enable_prop(joint_ent, ENTITY_PROP_ACTIVE); ent->move_joint = joint_ent->handle; struct motor_joint_def def = ZI; def.e0 = joint_ent->handle; /* Re-using joint entity as e0 */ def.e1 = ent->handle; def.correction_rate = 0; def.max_force = ent->control_force; def.max_torque = 0; joint_ent->motor_joint_data = motor_joint_from_def(def); } entity_set_xform(joint_ent, XFORM_IDENT); /* Reset joint ent position */ joint_ent->linear_velocity = v2_mul(v2_clamp_len(ent->control.move, 1), ent->control_force_max_speed); } } #endif /* ========================== * * Create forces from control focus (aim) * ========================== */ #if GAME_PLAYER_AIM 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; if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { struct xform xf = entity_get_xform(ent); struct xform sprite_xf = xform_mul(xf, ent->sprite_local_xform); /* Retrieve / create aim joint */ struct entity *joint_ent = entity_from_handle(store, ent->aim_joint); if (!entity_is_valid_and_active(joint_ent)) { joint_ent = entity_alloc(root); joint_ent->mass_unscaled = F32_INFINITY; joint_ent->inertia_unscaled = F32_INFINITY; entity_enable_prop(joint_ent, ENTITY_PROP_PHYSICAL_KINEMATIC); /* Since we'll be setting velocity */ entity_enable_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT); entity_enable_prop(joint_ent, ENTITY_PROP_ACTIVE); ent->aim_joint = joint_ent->handle; struct motor_joint_def def = ZI; def.e0 = joint_ent->handle; /* Re-using joint entity as e0 */ def.e1 = ent->handle; def.correction_rate = 0.1; def.max_force = 0; def.max_torque = ent->control_torque; joint_ent->motor_joint_data = motor_joint_from_def(def); } /* Solve for final angle using law of sines */ f32 new_angle; { struct v2 ent_pos = xf.og; struct v2 focus_pos = v2_add(ent_pos, ent->control.focus); struct v2 sprite_hold_pos; struct v2 sprite_hold_dir; { struct sprite_sheet *sheet = sprite_sheet_from_tag_await(sprite_frame_scope, ent->sprite); struct sprite_sheet_slice slice = sprite_sheet_get_slice(sheet, STR("attach.wep"), ent->animation_frame); sprite_hold_pos = slice.center; sprite_hold_dir = slice.dir; } struct v2 hold_dir = xform_basis_mul_v2(sprite_xf, sprite_hold_dir); struct v2 hold_pos = xform_mul_v2(sprite_xf, sprite_hold_pos); if (v2_eq(hold_pos, ent_pos)) { /* If hold pos is same as origin (E.G if pivot is being used as hold pos), then move hold pos forward a tad to avoid issue */ sprite_hold_pos = v2_add(sprite_hold_pos, V2(0, -1)); hold_pos = xform_mul_v2(sprite_xf, sprite_hold_pos); } f32 forward_hold_angle_offset; { struct xform xf_unrotated = xform_basis_with_rotation_world(xf, 0); struct v2 hold_pos_unrotated = xform_mul_v2(xf_unrotated, xform_mul_v2(ent->sprite_local_xform, sprite_hold_pos)); forward_hold_angle_offset = v2_angle_from_dirs(V2(0, -1), v2_sub(hold_pos_unrotated, xf_unrotated.og)); } struct v2 hold_ent_dir = v2_sub(ent_pos, hold_pos); struct v2 focus_ent_dir = v2_sub(ent_pos, focus_pos); f32 hold_ent_len = v2_len(hold_ent_dir); f32 focus_ent_len = v2_len(focus_ent_dir); f32 final_hold_angle_btw_ent_and_focus = v2_angle_from_dirs(hold_ent_dir, hold_dir); f32 final_focus_angle_btw_ent_and_hold = math_asin((math_sin(final_hold_angle_btw_ent_and_focus) * hold_ent_len) / focus_ent_len); f32 final_ent_angle_btw_focus_and_hold = PI - (final_focus_angle_btw_ent_and_hold + final_hold_angle_btw_ent_and_focus); new_angle = math_unwind_angle(v2_angle_from_dirs(V2(0, -1), v2_sub(focus_pos, ent_pos)) + final_ent_angle_btw_focus_and_hold - forward_hold_angle_offset); } f32 new_vel = 0; if (!F32_IS_NAN(new_angle)) { const f32 angle_error_allowed = 0.001; struct xform joint_xf = entity_get_xform(joint_ent); f32 diff = math_unwind_angle(new_angle - xform_get_rotation(joint_xf)); if (math_fabs(diff) > angle_error_allowed) { new_vel = diff / dt; } } joint_ent->angular_velocity = new_vel; } } #endif /* ========================== * * Create ground friction force (gravity) * ========================== */ 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; if (!entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC)) continue; struct entity *joint_ent = entity_from_handle(store, ent->ground_friction_joint); struct motor_joint_def def = ZI; def.e0 = root->handle; def.e1 = ent->handle; def.correction_rate = 0; def.max_force = ent->linear_ground_friction; def.max_torque = ent->angular_ground_friction; if (joint_ent->motor_joint_data.max_force != def.max_force || joint_ent->motor_joint_data.max_torque != def.max_torque) { if (!entity_is_valid_and_active(joint_ent)) { joint_ent = entity_alloc(root); entity_enable_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT); entity_enable_prop(joint_ent, ENTITY_PROP_ACTIVE); joint_ent->motor_joint_data = motor_joint_from_def(def); ent->ground_friction_joint = joint_ent->handle; } } } /* ========================== * * Physics * ========================== */ (UNUSED)create_contacts; (UNUSED)prepare_contacts; (UNUSED)warm_start_contacts; (UNUSED)solve_contacts; (UNUSED)prepare_motor_joints; (UNUSED)warm_start_motor_joints; (UNUSED)solve_motor_joints; (UNUSED)create_mouse_joints; (UNUSED)prepare_mouse_joints; (UNUSED)warm_start_mouse_joints; (UNUSED)solve_mouse_joints; (UNUSED)integrate_velocities_from_forces; (UNUSED)integrate_positions_from_velocities; #if 1 { f32 remaining_dt = dt; while (remaining_dt > 0) { f32 min_toi = 0.000001f; f32 tolerance = 0.001f; f32 earliest_toi = max_f32(determine_earliest_toi(remaining_dt, tolerance), min_toi); f32 step_dt = remaining_dt * earliest_toi; integrate_velocities_from_forces(step_dt); create_contacts(); create_mouse_joints(game_cmds); prepare_contacts(); prepare_motor_joints(); prepare_mouse_joints(); f32 substep_dt = step_dt / GAME_PHYSICS_SUBSTEPS; for (u32 i = 0; i < GAME_PHYSICS_SUBSTEPS; ++i) { #if GAME_PHYSICS_ENABLE_WARM_STARTING warm_start_contacts(); warm_start_motor_joints(); warm_start_mouse_joints(); #endif #if GAME_PHYSICS_ENABLE_COLLISION solve_contacts(substep_dt, true); #endif solve_motor_joints(substep_dt); solve_mouse_joints(substep_dt); integrate_positions_from_velocities(substep_dt); #if GAME_PHYSICS_ENABLE_COLLISION && GAME_PHYSICS_ENABLE_RELAXATION solve_contacts(substep_dt, false); /* Relaxation */ #endif } remaining_dt -= step_dt; } } #endif /* ========================== * * Respond to hit events * ========================== */ 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; if (!entity_has_prop(ent, ENTITY_PROP_HIT_EVENT)) continue; struct hit_event *event = &ent->hit_event; struct entity *e0 = entity_from_handle(store, event->e0); struct entity *e1 = entity_from_handle(store, event->e1); if (entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1)) { /* Bullet hit entity */ if (entity_has_prop(e0, ENTITY_PROP_BULLET) || entity_has_prop(e1, ENTITY_PROP_BULLET)) { struct entity *bullet = entity_has_prop(e0, ENTITY_PROP_BULLET) ? e0 : e1; struct entity *target = e0 == bullet ? e1 : e0; (UNUSED)bullet; (UNUSED)target; entity_enable_prop(bullet, ENTITY_PROP_RELEASE_AT_END_OF_FRAME); /* Create test blood */ /* TODO: Remove this */ { struct xform xf = XFORM_TRS(.t = event->point); struct entity *decal = entity_alloc(root); decal->sprite = sprite_tag_from_path(STR("res/graphics/blood.ase")); entity_set_xform(decal, xf); entity_enable_prop(decal, ENTITY_PROP_PHYSICAL_KINEMATIC); decal->linear_velocity = v2_mul(v2_norm(event->normal), 0.5f); decal->angular_velocity = 1 - (((f32)sys_rand_u32() / (f32)U32_MAX) * 2); } } } } /* ========================== * * Initialize bullet kinematics from sources * ========================== */ 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; /* FIXME: Apply src entity velocity to bullet velocity */ if (entity_has_prop(ent, ENTITY_PROP_BULLET) && ent->activation_tick == G.tick.tick_id) { struct entity *src = entity_from_handle(store, ent->bullet_src); struct xform src_xf = entity_get_xform(src); struct v2 pos = xform_mul_v2(src_xf, ent->bullet_src_pos); struct v2 impulse = xform_basis_mul_v2(src_xf, ent->bullet_src_dir); impulse = v2_with_len(impulse, ent->bullet_impulse); struct xform xf = XFORM_TRS(.t = pos, .r = v2_angle(impulse) + PI / 2); entity_set_xform(ent, xf); entity_enable_prop(ent, ENTITY_PROP_PHYSICAL_KINEMATIC); entity_apply_linear_impulse_to_center(ent, impulse); } } /* ========================== * * Update camera position * ========================== */ 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; /* Camera follow */ if (entity_has_prop(ent, ENTITY_PROP_CAMERA)) { struct entity *follow = entity_from_handle(store, ent->camera_follow); struct xform xf = entity_get_xform(ent); f32 aspect_ratio = 1.0; { struct xform quad_xf = xform_mul(entity_get_xform(ent), ent->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 ratio_y = 0.33f; f32 ratio_x = ratio_y / aspect_ratio; struct v2 camera_focus_dir = v2_mul_v2(follow->control.focus, V2(ratio_x, ratio_y)); struct v2 camera_focus_pos = v2_add(entity_get_xform(follow).og, camera_focus_dir); ent->camera_xform_target = xf; ent->camera_xform_target.og = camera_focus_pos; /* Lerp camera */ if (ent->camera_applied_lerp_continuity_gen_plus_one == ent->camera_lerp_continuity_gen + 1) { f32 t = 1 - math_pow(2.f, -20.f * (f32)G.tick.dt); xf = xform_lerp(xf, ent->camera_xform_target, t); } else { /* Skip lerp */ xf = ent->camera_xform_target; } ent->camera_applied_lerp_continuity_gen_plus_one = ent->camera_lerp_continuity_gen + 1; entity_set_xform(ent, xf); } } /* ========================== * * Update sound emitters * ========================== */ /* TODO: Sound entities should be created by game thread, but played by the * user thread. This is so sounds play at the correct time on the user * thread regardless of interp delay. */ 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; if (entity_has_prop(ent, ENTITY_PROP_TEST_SOUND_EMITTER)) { struct mixer_desc desc = ent->sound_desc; desc.speed = G.tick.timescale; desc.pos = entity_get_xform(ent).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); } } } } /* ========================== * * Release entities * ========================== */ /* TODO: Breadth first iteration to only release parent entities (since * child entities will be released along with parent anyway) */ { struct temp_arena temp = arena_temp_begin(scratch.arena); struct entity **ents_to_release = arena_dry_push(temp.arena, struct entity *); u64 ents_to_release_count = 0; for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) { struct entity *ent = &store->entities[entity_index]; if (!ent->valid) continue; if (entity_has_prop(ent, ENTITY_PROP_RELEASE_AT_END_OF_FRAME)) { *arena_push(temp.arena, struct entity *) = ent; ++ents_to_release_count; } } for (u64 i = 0; i < ents_to_release_count; ++i) { struct entity *ent = ents_to_release[i]; if (ent->valid) { /* Release */ entity_release(store, ent); } } arena_temp_end(temp); } /* ========================== * * Publish tick * ========================== */ /* Publish tick */ publish_game_tick(); __profframe("Game"); /* ========================== * * End frame cache scopes * ========================== */ sprite_scope_end(sprite_frame_scope); scratch_end(scratch); } /* ========================== * * Game thread * ========================== */ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(game_thread_entry_point, arg) { struct temp_arena scratch = scratch_begin_no_conflict(); (UNUSED)arg; sys_timestamp_t last_frame_ts = 0; f64 target_dt = GAME_FPS > (0) ? (1.0 / GAME_FPS) : 0; while (!atomic_i32_eval(&G.game_thread_shutdown)) { __profscope(game_update_w_sleep); struct temp_arena temp = arena_temp_begin(scratch.arena); sleep_frame(last_frame_ts, target_dt); last_frame_ts = sys_timestamp(); { struct game_cmd_array game_cmds = pop_cmds(temp.arena); if (!G.paused) { game_update(game_cmds); } /* Check for pause / next frame cmds */ for (u64 i = 0; i < game_cmds.count; ++i) { struct game_cmd cmd = game_cmds.cmds[i]; switch (cmd.kind) { case GAME_CMD_KIND_PAUSE: { G.paused = !G.paused; } break; case GAME_CMD_KIND_STEP: { if (G.paused) { G.paused = false; game_update(game_cmds); G.paused = true; } } break; default: break; } } } arena_temp_end(temp); } scratch_end(scratch); } /* ========================== * * Interface * ========================== */ void game_get_latest_tick(struct world *dest) { __prof; struct sys_lock lock = sys_mutex_lock_s(&G.prev_tick_mutex); world_copy_replace(dest, &G.prev_tick); sys_mutex_unlock(&lock); } u64 game_get_latest_tick_id(void) { return atomic_u64_eval(&G.prev_tick_id); } u64 game_get_latest_tick_continuity_gen(void) { return atomic_u64_eval(&G.prev_tick_continuity_gen); } void game_push_cmds(struct game_cmd_array cmd_array) { push_cmds(cmd_array); }