P_Ctx P = Zi; ThreadLocal P_ThreadLocalCtx P_tl = Zi; Readonly P_Ent P_NilEnt = { .xf = CompXformIdentity, .control.look = { 1, 0 }, .health = 1, }; Readonly P_Constraint P_NilConstraint = { 0 }; Readonly P_Frame P_NilFrame = { .first_ent = &P_NilEnt, .last_ent = &P_NilEnt, }; //////////////////////////////////////////////////////////// //~ Bootstrap void P_Bootstrap(void) { P.s2v.arena = AcquireArena(Gibi(64)); } //////////////////////////////////////////////////////////// //~ Nil helpers b32 P_IsEntKeyNil(P_EntKey key) { return key.v == 0; } b32 P_IsConstraintKeyNil(P_ConstraintKey key) { return key.v == 0; } b32 P_IsEntNil(P_Ent *ent) { return ent == 0 || ent == &P_NilEnt; } b32 P_IsConstraintNil(P_Constraint *constraint) { return constraint == 0 || constraint == &P_NilConstraint; } b32 P_IsFrameNil(P_Frame *frame) { return frame == 0 || frame == &P_NilFrame; } //////////////////////////////////////////////////////////// //~ Key helpers b32 P_MatchEntKey(P_EntKey a, P_EntKey b) { return a.v == b.v; } b32 P_MatchConstraintKey(P_ConstraintKey a, P_ConstraintKey b) { return a.v == b.v; } P_ConstraintKey P_ConstraintKeyFromU64s(u64 a, u64 b) { return (P_ConstraintKey) { .v = MixU64s(a, b) }; } //////////////////////////////////////////////////////////// //~ Rand helpers P_EntKey P_RandEntKey(void) { // TODO: Don't use true randomness for entity keys. It's overkill & non-deterministic. P_EntKey result = Zi; TrueRand(StringFromStruct(&result)); return result; } u64 P_RandU64FromEnt(P_Ent *ent) { u64 result = MixU64s(ent->key.v, ent->rand_seq); if (!P_IsEntNil(ent)) { ent->rand_seq += 1; } return result; } //////////////////////////////////////////////////////////// //~ String helpers String P_StringFromEnt(P_Ent *ent) { String result = Zi; result.len = ent->string_len; result.text = ent->string_text; return result; } void P_SetEntString(P_Ent *ent, String str) { i64 len = MinI64(countof(ent->string_text), str.len); CopyBytes(ent->string_text, str.text, len); ent->string_len = len; } //////////////////////////////////////////////////////////// //~ Tile helpers String P_NameFromTileKind(P_TileKind kind) { // Tile names array PERSIST Readonly String names[P_TileKind_COUNT] = { #define X(name, ...) [P_TileKind_##name] = CompLit(#name), P_TilesXList(X) #undef X }; String result = Zi; if (kind >= 0 && kind < countof(names)) { result = names[kind]; } return result; } //////////////////////////////////////////////////////////// //~ Prefab helpers String P_NameFromPrefabKind(P_PrefabKind kind) { PERSIST Readonly String names[P_PrefabKind_COUNT] = { #define X(name, ...) [P_PrefabKind_##name] = CompLit(#name), P_PrefabsXList(X) #undef X }; String result = Zi; if (kind >= 0 && kind < countof(names)) { result = names[kind]; } return result; } //////////////////////////////////////////////////////////// //~ Shape helpers P_Shape P_ShapeFromDescEx(P_ShapeDesc desc) { desc.count = MaxI32(desc.count, 1); P_Shape result = Zi; { result.points_count = desc.count; CopyStructs(result.points, desc.points, result.points_count); Vec2 accum = Zi; for (i32 p_idx = 0; p_idx < result.points_count; ++p_idx) { accum = AddVec2(accum, result.points[p_idx]); } result.centroid = DivVec2(accum, result.points_count); result.center_of_mass = result.centroid; result.radius = desc.radius; result.mass = desc.mass; } return result; } P_Shape P_MulXformShape(Xform xf, P_Shape shape) { P_Shape result = shape; for (i32 i = 0; i < shape.points_count; ++i) { result.points[i] = MulXformVec2(xf, shape.points[i]); } result.centroid = MulXformVec2(xf, shape.centroid); result.center_of_mass = MulXformVec2(xf, shape.center_of_mass); return result; } Rng2 P_BoundingBoxFromShape(P_Shape shape) { f32 skin = 0.01; Vec2 left = P_SupportPointFromShape(shape, VEC2(-1, 0)).p; Vec2 top = P_SupportPointFromShape(shape, VEC2(0, -1)).p; Vec2 right = P_SupportPointFromShape(shape, VEC2(1, 0)).p; Vec2 bottom = P_SupportPointFromShape(shape, VEC2(0, 1)).p; Rng2 result = Zi; result.p0 = VEC2(left.x - skin, top.y - skin); result.p1 = VEC2(right.x + skin, bottom.y + skin); return result; } P_Shape P_LocalShapeFromEnt(P_Ent *ent) { P_Shape result = Zi; // TODO: This is a temporary hack. We should eventually switch to using a prefab lookup table. if (ent->is_guy) { result = P_ShapeFromDesc( .mass = 10, .count = 1, .radius = TweakFloat("Guy radius", 0.25, 0, 1), ); // f32 guy_width = 0.75; // f32 guy_height = 0.4; // result = P_ShapeFromDesc( // .mass = 10, // .count = 2, // .points = { VEC2(0, -guy_width / 2 + (guy_height / 2)), VEC2(0, guy_width / 2 - (guy_height / 2)) }, // .radius = guy_height / 2, // ); // Rng2 test_rect = Zi; // test_rect.p0 = VEC2(-1, -1); // test_rect.p1 = VEC2(1, 1); // result = P_ShapeFromDesc( // // .radius = 0.5, // .radius = 0, // .count = 4, // .points[0] = VEC2(test_rect.p0.x, test_rect.p0.y), // .points[1] = VEC2(test_rect.p1.x, test_rect.p0.y), // .points[2] = VEC2(test_rect.p1.x, test_rect.p1.y), // .points[3] = VEC2(test_rect.p0.x, test_rect.p1.y), // ); } return result; } P_Shape P_WorldShapeFromEnt(P_Ent *ent) { P_Shape local = P_LocalShapeFromEnt(ent); P_Shape world = P_MulXformShape(ent->xf, local); return world; } //////////////////////////////////////////////////////////// //~ Animation helpers P_Anim P_AnimFromEnt(P_Frame *frame, P_Ent *ent) { P_Anim result = Zi; P_Ent *wep = P_EntFromKey(frame, ent->weapon); // TODO: Determine animation dynamically i64 animation_rate_ns = NsFromSeconds(0.100); { i64 walk_duration_ns = frame->time_ns; result.frame_seq = walk_duration_ns / animation_rate_ns; // result.span = SPR_SpanKeyFromName(Lit("test")); result.span = SPR_SpanKeyFromName(Lit("walk")); } // TODO: Use prefab lookup if (ent->is_guy) { result.sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("guy/guy.ase"))); } if (ent->is_guy_spawn) { result.sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("prefab/GuySpawn.ase"))); } if (wep->is_uzi) { result.wep_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("wep/uzi.ase"))); } if (wep->is_launcher) { result.wep_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("wep/launcher.ase"))); } return result; } //////////////////////////////////////////////////////////// //~ Collision // NOTE: Everything here is pretty much copied directly from the old prototype. // The techniques are slow and do more than what we need. For example we should // probably just switch from GJK to SAT for shape collision testing. P_SupportPoint P_SupportPointFromShapeEx(P_Shape shape, Vec2 dir, i32 ignore_idx) { P_SupportPoint result = Zi; Vec2 dir_norm = NormVec2(dir); f32 max_dot = -Inf; if (shape.points_count == 1) { // Don't ignore for single-point colliders ignore_idx = -1; } for (i32 point_idx = 0; point_idx < shape.points_count; ++point_idx) { if (point_idx != ignore_idx) { Vec2 p = shape.points[point_idx]; f32 dot = DotVec2(p, dir_norm); if (dot > max_dot) { max_dot = dot; result.p = p; result.id = point_idx; } } } result.p = AddVec2(result.p, MulVec2(dir_norm, shape.radius)); return result; } P_SupportPoint P_SupportPointFromShape(P_Shape shape, Vec2 dir) { return P_SupportPointFromShapeEx(shape, dir, -1); } P_MenkowskiPoint P_MenkowskiPointFromShapes(P_Shape shape0, P_Shape shape1, Vec2 dir) { P_MenkowskiPoint result = Zi; result.s0 = P_SupportPointFromShape(shape0, dir); result.s1 = P_SupportPointFromShape(shape1, NegVec2(dir)); result.p = SubVec2(result.s0.p, result.s1.p); return result; } P_ClippedLine P_ClipLineToLine(Vec2 a0, Vec2 b0, Vec2 a1, Vec2 b1, Vec2 normal) { Vec2 vab0 = SubVec2(b0, a0); Vec2 vab1 = SubVec2(b1, a1); Vec2 va0a1 = SubVec2(a1, a0); Vec2 vb0b1 = SubVec2(b1, b0); f32 vab0_w = WedgeVec2(vab0, normal); f32 vab1_w = WedgeVec2(vab1, normal); f32 va0a1_w = WedgeVec2(va0a1, normal); f32 vb0b1_w = WedgeVec2(vb0b1, normal); // FIXME: Handle 0 denominator f32 a0t; f32 b0t; { f32 w = 1 / vab0_w; a0t = ClampF32(va0a1_w * w, 0, 1); b0t = ClampF32(vb0b1_w * -w, 0, 1); } f32 a1t; f32 b1t; { f32 w = 1 / vab1_w; a1t = ClampF32(-va0a1_w * w, 0, 1); b1t = ClampF32(-vb0b1_w * -w, 0, 1); } P_ClippedLine result = Zi; result.a0_clipped = AddVec2(a0, MulVec2(vab0, a0t)); result.a1_clipped = AddVec2(a1, MulVec2(vab1, a1t)); result.b0_clipped = AddVec2(b0, MulVec2(vab0, -b0t)); result.b1_clipped = AddVec2(b1, MulVec2(vab1, -b1t)); return result; } Vec2 P_ClipPointToLine(Vec2 a, Vec2 b, Vec2 p, Vec2 normal) { Vec2 vab = SubVec2(b, a); Vec2 vap = SubVec2(p, a); f32 vab_w = WedgeVec2(vab, normal); f32 vap_w = WedgeVec2(vap, normal); f32 w = 1 / vab_w; f32 t = ClampF32(vap_w * w, 0, 1); Vec2 result = AddVec2(a, MulVec2(vab, t)); return result; } P_CollisionResult P_CollisionResultFromShapes(P_Shape shape0, P_Shape shape1) { P_CollisionResult result = Zi; TempArena scratch = BeginScratchNoConflict(); f32 tolerance = 0.05f; // How close can non-overlapping shapes be before collision is considered f32 min_unique_pt_dist_sq = (0.001f * 0.001f); // NOTE: Should always be less than tolerance, since colliding = 1 if origin is within this distance. u32 max_iterations = 64; // To prevent extremely large prototypes when origin is in exact center of rounded feature ////////////////////////////// //- GJK P_MenkowskiSimplex simplex = Zi; Vec2 non_overlapping_dir = Zi; b32 is_overlapping = 0; { P_MenkowskiPoint m = Zi; // First point is support point in shape's general directions to eachother Vec2 dir = SubVec2(shape1.centroid, shape0.centroid); if (IsVec2Zero(dir)) dir = VEC2(1, 0); simplex.a = P_MenkowskiPointFromShapes(shape0, shape1, dir); simplex.count = 1; Vec2 removed_a = Zi; Vec2 removed_b = Zi; u32 removed_count = 0; for (;;) { ////////////////////////////// //- Find initial points in simplex if (simplex.count == 1) { // Second point is support point towards origin dir = NegVec2(simplex.a.p); m = P_MenkowskiPointFromShapes(shape0, shape1, dir); // Check that new point is far enough away from existing point if (Vec2LenSq(SubVec2(m.p, simplex.a.p)) < min_unique_pt_dist_sq) { is_overlapping = 0; break; } simplex.b = simplex.a; simplex.a = m; simplex.count = 2; // Third point is support point in direction of line normal towards origin dir = PerpVec2TowardsDir(SubVec2(simplex.b.p, simplex.a.p), NegVec2(simplex.a.p)); } ////////////////////////////// //- Find third point in simplex { m = P_MenkowskiPointFromShapes(shape0, shape1, dir); // Check that new point is far enough away from existing points if ( Vec2LenSq(SubVec2(m.p, simplex.a.p)) < min_unique_pt_dist_sq || Vec2LenSq(SubVec2(m.p, simplex.b.p)) < min_unique_pt_dist_sq || ( (removed_count >= 1) && ( (Vec2LenSq(SubVec2(m.p, removed_a)) < min_unique_pt_dist_sq) || (removed_count >= 2 && Vec2LenSq(SubVec2(m.p, removed_b)) < min_unique_pt_dist_sq) ) ) || AbsF32(WedgeVec2(SubVec2(simplex.b.p, simplex.a.p), SubVec2(m.p, simplex.a.p))) < min_unique_pt_dist_sq ) { is_overlapping = 0; break; } simplex.c = simplex.b; simplex.b = simplex.a; simplex.a = m; simplex.count = 3; if ( (AbsF32(WedgeVec2(SubVec2(simplex.b.p, simplex.a.p), NegVec2(simplex.a.p))) <= min_unique_pt_dist_sq) || (AbsF32(WedgeVec2(SubVec2(simplex.c.p, simplex.b.p), NegVec2(simplex.b.p))) <= min_unique_pt_dist_sq) || (AbsF32(WedgeVec2(SubVec2(simplex.c.p, simplex.a.p), NegVec2(simplex.a.p))) <= min_unique_pt_dist_sq) ) { // Simplex lies on origin is_overlapping = 1; break; } } ////////////////////////////// //- Determine origin region Vec2 vab = SubVec2(simplex.b.p, simplex.a.p); Vec2 vac = SubVec2(simplex.c.p, simplex.a.p); Vec2 vbc = SubVec2(simplex.c.p, simplex.b.p); Vec2 rab_dir = PerpVec2TowardsDir(vab, NegVec2(vac)); Vec2 rac_dir = PerpVec2TowardsDir(vac, NegVec2(vab)); Vec2 rbc_dir = PerpVec2TowardsDir(vbc, vab); f32 rab_dot = DotVec2(rab_dir, NegVec2(simplex.a.p)); f32 rac_dot = DotVec2(rac_dir, NegVec2(simplex.a.p)); f32 rbc_dot = DotVec2(rbc_dir, NegVec2(simplex.b.p)); f32 vab_dot = DotVec2(vab, NegVec2(simplex.a.p)) / Vec2LenSq(vab); f32 vac_dot = DotVec2(vac, NegVec2(simplex.a.p)) / Vec2LenSq(vac); f32 vbc_dot = DotVec2(vbc, NegVec2(simplex.b.p)) / Vec2LenSq(vbc); if (rab_dot >= 0 && vab_dot >= 0 && vab_dot <= 1) { // Region ab, remove c removed_count = 1; removed_a = simplex.c.p; simplex.count = 2; dir = rab_dir; // Next third point is in direction of region ab } else if (rac_dot >= 0 && vac_dot >= 0 && vac_dot <= 1) { // Region ac, remove b removed_count = 1; removed_a = simplex.b.p; simplex.count = 2; simplex.b = simplex.c; dir = rac_dir; // Next third point is in direction of region ac } else if (rbc_dot >= 0 && vbc_dot >= 0 && vbc_dot <= 1) { // Region bc, remove a removed_count = 1; removed_a = simplex.a.p; simplex.count = 2; simplex.a = simplex.b; simplex.b = simplex.c; dir = rbc_dir; // Next third point is in direction of region bc } else if (vab_dot <= 0 && vac_dot <= 0) { // Region a, remove bc removed_count = 2; removed_a = simplex.b.p; removed_b = simplex.c.p; simplex.count = 1; } else if (vab_dot >= 1 && vbc_dot <= 0) { // Region b, remove ac removed_count = 2; removed_a = simplex.a.p; removed_b = simplex.c.p; simplex.count = 1; simplex.a = simplex.b; } else if (vac_dot >= 1 && vbc_dot >= 1) { // Region c, remove ab removed_count = 2; removed_a = simplex.a.p; removed_b = simplex.b.p; simplex.count = 1; simplex.a = simplex.c; } else { // No region, must be in simplex is_overlapping = 1; break; } } if (!is_overlapping) { non_overlapping_dir = dir; } } ////////////////////////////// //- EPA // Find dir from origin to closest edge Vec2 normal = Zi; P_MenkowskiSimplex closest_feature = Zi; { P_MenkowskiPoint *proto = 0; if (is_overlapping) { u32 proto_count = 0; proto = ArenaNext(scratch.arena, P_MenkowskiPoint); { Assert(simplex.count == 3); P_MenkowskiPoint *tmp = PushStructsNoZero(scratch.arena, P_MenkowskiPoint, 3); tmp[0] = simplex.a; tmp[1] = simplex.b; tmp[2] = simplex.c; proto_count = 3; } i32 winding = WindingFromVec2(SubVec2(simplex.c.p, simplex.a.p), SubVec2(simplex.b.p, simplex.a.p)); u32 epa_iterations = 0; for (;;) { ++epa_iterations; // FIXME: Winding order of ps & pe index P_MenkowskiPoint closest_a = Zi; P_MenkowskiPoint closest_b = Zi; u32 closest_b_index = 0; { // Find edge segment on prototype closest to the origin f32 closest_len_sq = Inf; for (u32 i = 0; i < proto_count; ++i) { u32 a_index = i; u32 b_index = (i < proto_count - 1) ? (i + 1) : 0; P_MenkowskiPoint a = proto[a_index]; P_MenkowskiPoint b = proto[b_index]; Vec2 vab = SubVec2(b.p, a.p); Vec2 vao = NegVec2(a.p); f32 proj_ratio = ClampF32(DotVec2(vao, vab) / Vec2LenSq(vab), 0, 1); Vec2 proj = AddVec2(a.p, MulVec2(vab, proj_ratio)); f32 proj_len_sq = Vec2LenSq(proj); if (proj_len_sq < closest_len_sq - min_unique_pt_dist_sq) { closest_a = a; closest_b = b; closest_b_index = b_index; closest_len_sq = proj_len_sq; } } } Vec2 vab = SubVec2(closest_b.p, closest_a.p); // Find new point in dir Vec2 dir = MulVec2(PerpVec2(vab), winding); P_MenkowskiPoint m = P_MenkowskiPointFromShapes(shape0, shape1, dir); // Check validity of new point { b32 valid = 1; { // NOTE: Changing this value affects how stable normals are for rounded colliders //const f32 validity_epsilon = min_unique_pt_dist_sq; // Arbitrary //const f32 validity_epsilon = 0.00000000001f; // Arbitrary const f32 validity_epsilon = min_unique_pt_dist_sq; // Arbitrary Vec2 vam = SubVec2(m.p, closest_a.p); Vec2 vbm = SubVec2(closest_b.p, closest_a.p); f32 dot = DotVec2(vab, vam) / Vec2LenSq(vab); if (dot >= -validity_epsilon && dot <= 1 - validity_epsilon && (WedgeVec2(vab, vam) * -winding) >= -validity_epsilon) { // New point is not between edge valid = 0; } else if (Vec2LenSq(vam) < min_unique_pt_dist_sq || Vec2LenSq(vbm) < min_unique_pt_dist_sq) { // New point is too close to existing valid = 0; } } if (!valid || epa_iterations >= max_iterations) { normal = NormVec2(dir); closest_feature.a = closest_a; closest_feature.b = closest_b; closest_feature.count = 2; break; } } // Expand prototype PushStructNoZero(scratch.arena, P_MenkowskiPoint); ++proto_count; // Shift points in prototype to make room for (u32 i = proto_count - 1; i > closest_b_index; --i) { u32 shift_from = (i > 0) ? i - 1 : proto_count - 1; u32 shift_to = i; proto[shift_to] = proto[shift_from]; } // Insert new point into prototype proto[closest_b_index] = m; } // Debug draw // { // P_DebugDrawPoint(simplex.a.p, VEC4(1, 0, 0, 0.5)); // P_DebugDrawPoint(simplex.b.p, VEC4(0, 1, 0, 0.5)); // P_DebugDrawPoint(simplex.c.p, VEC4(0, 0, 1, 0.5)); // P_DebugDrawLine(simplex.a.p, simplex.b.p, Color_Yellow); // P_DebugDrawLine(simplex.b.p, simplex.c.p, Color_Yellow); // P_DebugDrawLine(simplex.c.p, simplex.a.p, Color_Yellow); // if (proto_count > 0) // { // for (i64 i = 0; i < proto_count; ++i) // { // i64 p1_idx = i + 1; // if (p1_idx == proto_count) // { // p1_idx = 0; // } // Vec2 p0 = proto[i].p; // Vec2 p1 = proto[p1_idx].p; // P_DebugDrawLine(p0, p1, VEC4(0, 1, 0, 0.5)); // } // } // } } else { normal = NormVec2(non_overlapping_dir); closest_feature.count = simplex.count; closest_feature.a = simplex.a; closest_feature.b = simplex.b; } } ////////////////////////////// //- Determine collision b32 is_colliding = 0; { if (is_overlapping) { is_colliding = 1; } else { // Shapes not overlapping, determine if distance between shapes within tolerance if (closest_feature.count == 1) { Vec2 p = NegVec2(closest_feature.a.p); if (Vec2LenSq(p) <= (tolerance * tolerance)) { is_colliding = 1; } } else { // Project origin to determine if distance is within tolerance. Assert(closest_feature.count == 2); Vec2 vab = SubVec2(closest_feature.b.p, closest_feature.a.p); Vec2 vao = NegVec2(closest_feature.a.p); f32 ratio = ClampF32(DotVec2(vab, vao) / DotVec2(vab, vab), 0, 1); Vec2 p = AddVec2(closest_feature.a.p, MulVec2(vab, ratio)); if (Vec2LenSq(p) <= (tolerance * tolerance)) { is_colliding = 1; } } } } ////////////////////////////// //- Compute collision points // Clip to determine final points i32 collision_points_count = 0; P_CollisionPoint collision_points[2] = Zi; if (is_colliding) { // Max vertices must be < 16 to fit in 4 bit ids StaticAssert(countof(shape0.points) <= 16); { b32 collapse0 = 0; b32 collapse1 = 0; P_SupportPoint a0 = closest_feature.a.s0; P_SupportPoint a1 = closest_feature.a.s1; P_SupportPoint b0 = closest_feature.b.s0; P_SupportPoint b1 = closest_feature.b.s1; // FIXME: Manually account for shapes w/ 1 & 2 points if (closest_feature.count == 2) { if (a0.id == b0.id) { if (shape0.points_count > 1) { b0 = P_SupportPointFromShapeEx(shape0, normal, b0.id); } else { collapse0 = 1; b0 = a0; } } if (a1.id == b1.id) { if (shape1.points_count > 1) { b1 = P_SupportPointFromShapeEx(shape1, NegVec2(normal), b1.id); } else { collapse1 = 1; b1 = a1; } } } else { collapse0 = 1; collapse1 = 1; b0 = a0; b1 = a1; } Vec2 vab0 = SubVec2(b0.p, a0.p); Vec2 vab1 = SubVec2(b1.p, a1.p); Vec2 vab0_norm = NormVec2(vab0); Vec2 vab1_norm = NormVec2(vab1); // Swap points based on normal direction for consistent clipping if (WedgeVec2(normal, vab0) < 0) { P_SupportPoint tmp = a0; a0 = b0; b0 = tmp; vab0 = NegVec2(vab0); } if (WedgeVec2(normal, vab1) < 0) { P_SupportPoint tmp = a1; a1 = b1; b1 = tmp; vab1 = NegVec2(vab1); } // Collapse lines that are too far in the direction of the normal to be accurately clipped f32 collapse_epsilon = 0.05f; collapse0 = collapse0 || AbsF32(WedgeVec2(normal, vab0_norm)) < collapse_epsilon; collapse1 = collapse1 || AbsF32(WedgeVec2(normal, vab1_norm)) < collapse_epsilon; // Collapse lines into deepest point if (collapse0) { if (DotVec2(normal, vab0) > 0) { a0 = b0; } else { // TODO: Remove this (debugging) b0 = a0; } } if (collapse1) { if (DotVec2(normal, vab1) < 0) { a1 = b1; } else { // TODO: Remove this (debugging) b1 = a1; } } f32 a_sep = Inf; f32 b_sep = Inf; Vec2 a_midpoint = Zi; Vec2 b_midpoint = Zi; b32 ignore_a = 1; b32 ignore_b = 1; if (!collapse0 && !collapse1) { // Clip line to line P_ClippedLine clip_result = P_ClipLineToLine(a0.p, b0.p, a1.p, b1.p, normal); Vec2 a0_clipped = clip_result.a0_clipped; Vec2 a1_clipped = clip_result.a1_clipped; Vec2 b0_clipped = clip_result.b0_clipped; Vec2 b1_clipped = clip_result.b1_clipped; // Calc midpoint between clipped a & b Vec2 va0a1_clipped = SubVec2(a1_clipped, a0_clipped); Vec2 vb0b1_clipped = SubVec2(b1_clipped, b0_clipped); a_sep = DotVec2(va0a1_clipped, normal); b_sep = DotVec2(vb0b1_clipped, normal); a_midpoint = AddVec2(a0_clipped, MulVec2(va0a1_clipped, 0.5f)); b_midpoint = AddVec2(b0_clipped, MulVec2(vb0b1_clipped, 0.5f)); ignore_a = 0; ignore_b = 0; Vec2 vfin = SubVec2(b_midpoint, a_midpoint); if (Vec2LenSq(vfin) < (0.005 * 0.005)) { if (a_sep > b_sep) { ignore_a = 1; } else { ignore_b = 1; } } } else { Vec2 p0 = a0.p; Vec2 p1 = a1.p; // TODO: Choose ID based on closest clipped point if (collapse1 && !collapse0) { // Project a1 onto vab0 p0 = P_ClipPointToLine(a0.p, b0.p, a1.p, normal); } if (collapse0 && !collapse1) { // Project a0 onto vab1 p1 = P_ClipPointToLine(a1.p, b1.p, a0.p, normal); } // Calc midpoint Vec2 vsep = SubVec2(p1, p0); a_midpoint = AddVec2(p0, MulVec2(vsep, 0.5f)); a_sep = DotVec2(normal, p1) - DotVec2(normal, p0); ignore_a = 0; } // Insert points if (!ignore_a && a_sep < tolerance) { P_CollisionPoint *point = &collision_points[collision_points_count++]; point->id = a0.id | (a1.id << 4); point->separation = a_sep; point->p = a_midpoint; } if (!ignore_b && b_sep < tolerance) { P_CollisionPoint *point = &collision_points[collision_points_count++]; point->id = b0.id | (b1.id << 4); point->separation = b_sep; point->p = b_midpoint; } } } ////////////////////////////// //- Compute closest points Vec2 closest_p0 = Zi; Vec2 closest_p1 = Zi; if (closest_feature.count == 1) { closest_p0 = closest_feature.a.s0.p; closest_p1 = closest_feature.a.s1.p; } else { Assert(closest_feature.count == 2); // FIXME: Winding order dependent? f32 ratio = 0; { // Determine ratio between edge a & b that projected origin lies Vec2 vab = SubVec2(closest_feature.b.p, closest_feature.a.p); Vec2 vao = NegVec2(closest_feature.a.p); ratio = ClampF32(DotVec2(vab, vao) / DotVec2(vab, vab), 0, 1); } // Shape 0 closest_p0 = SubVec2(closest_feature.b.s0.p, closest_feature.a.s0.p); closest_p0 = MulVec2(closest_p0, ratio); closest_p0 = AddVec2(closest_p0, closest_feature.a.s0.p); // Shape 1 closest_p1 = SubVec2(closest_feature.b.s1.p, closest_feature.a.s1.p); closest_p1 = MulVec2(closest_p1, ratio); closest_p1 = AddVec2(closest_p1, closest_feature.a.s1.p); } CopyStructs(result.collision_points, collision_points, countof(collision_points)); result.collision_points_count = collision_points_count; result.collision_normal = normal; result.closest_p0 = closest_p0; result.closest_p1 = closest_p1; EndScratch(scratch); return result; } P_RaycastResult P_RaycastShape(P_Shape shape, Vec2 ray_start, Vec2 ray_dir) { f32 radius = shape.radius; f32 ray_len = Vec2Len(ray_dir); Vec2 ray_dir_norm = DivVec2(ray_dir, ray_len); Vec2 s = ray_start; Vec2 e = AddVec2(ray_start, ray_dir_norm); Vec2 vse = ray_dir_norm; Vec2 isect = Zi; Vec2 isect_normal = Zi; b32 isect_is_round = 0; b32 isect_found = 0; if (shape.points_count > 1) { // Find expanded line intersections with ray for (i32 p_idx = 0; p_idx < shape.points_count && !isect_found; ++p_idx) { Vec2 a = Zi; Vec2 b = Zi; Vec2 vab = Zi; Vec2 normal = Zi; { i32 a_idx = p_idx; i32 b_idx = a_idx + 1; if (b_idx >= shape.points_count) { b_idx = 0; } Vec2 a_orig = shape.points[a_idx]; Vec2 b_orig = shape.points[b_idx]; vab = SubVec2(b_orig, a_orig); normal = NegVec2(PerpVec2(NormVec2(vab))); Vec2 radius_add = MulVec2(normal, radius); a = AddVec2(a_orig, radius_add); b = AddVec2(b_orig, radius_add); } Vec2 vsa = SubVec2(a, s); Vec2 vsb = SubVec2(b, s); f32 wa = WedgeVec2(vse, vsa); f32 wb = WedgeVec2(vse, vsb); if (wa > 0 && wb < 0) { f32 t = -wa / (wb - wa); isect = AddVec2(a, MulVec2(vab, t)); isect_normal = normal; isect_found = 1; } } // Find closest rounded corner if (!isect_found && radius != 0) { isect_is_round = 1; for (i32 f_idx = 0; f_idx < shape.points_count && !isect_found; ++f_idx) { Vec2 f_orig = shape.points[f_idx]; Vec2 a = Zi; Vec2 b = Zi; Vec2 vab = Zi; { i32 prev_idx = f_idx - 1; i32 next_idx = f_idx + 1; if (prev_idx < 0) { prev_idx = shape.points_count - 1; } if (next_idx >= shape.points_count) { next_idx = 0; } Vec2 prev_orig = shape.points[prev_idx]; Vec2 next_orig = shape.points[next_idx]; Vec2 vpf = SubVec2(f_orig, prev_orig); Vec2 vfn = SubVec2(next_orig, f_orig); Vec2 vpf_norm = NormVec2(vpf); Vec2 vfn_norm = NormVec2(vfn); Vec2 radius_add_a = MulVec2(PerpVec2(vpf_norm), -radius); Vec2 radius_add_b = MulVec2(PerpVec2(vfn_norm), -radius); a = AddVec2(f_orig, radius_add_a); b = AddVec2(f_orig, radius_add_b); } Vec2 vsa = SubVec2(a, s); Vec2 vsb = SubVec2(b, s); f32 wa = WedgeVec2(vse, vsa); f32 wb = WedgeVec2(vse, vsb); if (wa > 0 && wb < 0) { isect = f_orig; isect_found = 1; } } } // Find closest corner if (!isect_found) { f32 min_dist = Inf; for (i32 p_idx = 0; p_idx < shape.points_count && !isect_found; ++p_idx) { Vec2 p = shape.points[p_idx]; f32 dist = AbsF32(WedgeVec2(vse, SubVec2(p, s))); if (dist < min_dist) { isect = p; min_dist = dist; } } } } else if (shape.points_count == 1 && radius != 0) { isect = shape.points[0]; isect_is_round = 1; } // Find round intersection b32 is_intersecting = 0; if (isect_is_round || !isect_found) { Vec2 vsi = SubVec2(isect, s); f32 dot = DotVec2(vse, vsi); f32 wedge = WedgeVec2(vse, vsi); is_intersecting = AbsF32(wedge) < radius; if (is_intersecting) { f32 diff = SqrtF32(radius * radius - wedge * wedge); f32 entrance_t = dot - diff; f32 exit_t = dot + diff; { Vec2 old_isect = isect; isect = AddVec2(s, MulVec2(vse, entrance_t)); isect_normal = NormVec2(SubVec2(isect, old_isect)); } } } else { is_intersecting = isect_found; } P_RaycastResult result = Zi; result.is_intersecting = is_intersecting; if (is_intersecting) { result.p = isect; result.normal = isect_normal; } return result; } Vec2 P_EdgePointFromShape(P_Shape shape, Vec2 dir) { Vec2 result = shape.centroid; P_RaycastResult raycast = P_RaycastShape(shape, shape.centroid, NegVec2(dir)); if (raycast.is_intersecting) { result = raycast.p; } return result; } //////////////////////////////////////////////////////////// //~ Lookup helpers P_Ent *P_EntFromKey(P_Frame *frame, P_EntKey key) { P_Ent *result = &P_NilEnt; P_World *world = frame->world; if (!P_IsEntKeyNil(key) && frame->tick > 0 && frame->ents_count > 0 && frame->ent_bins_count > 0) { i64 tick = frame->tick; P_EntBin *bin = &frame->ent_bins[key.v % frame->ent_bins_count]; for (P_Ent *e = bin->first; e; e = e->next_in_bin) { if (e->key.v == key.v) { result = e; break; } } } return result; } P_Ent *P_SourcePlayerFromEnt(P_Frame *frame, P_Ent *ent) { P_Ent *result = ent; while (!P_IsEntNil(result) && !result->is_player) { result = P_EntFromKey(frame, result->source); } return result; } P_Constraint *P_ConstraintFromKey(P_Frame *frame, P_ConstraintKey key) { P_Constraint *result = &P_NilConstraint; P_World *world = frame->world; if (!P_IsConstraintKeyNil(key) && frame->tick > 0 && frame->constraints_count > 0 && frame->constraint_bins_count > 0) { i64 tick = frame->tick; P_ConstraintBin *bin = &frame->constraint_bins[key.v % frame->constraint_bins_count]; for (P_Constraint *c = bin->first; c; c = c->next_in_bin) { if (c->key.v == key.v) { result = c; break; } } } return result; } //////////////////////////////////////////////////////////// //~ Iteration helpers P_Ent *P_FirstEnt(P_Frame *frame) { P_Ent *result = &P_NilEnt; if (!P_IsEntNil(frame->first_ent)) { result = frame->first_ent; } return result; } P_Ent *P_NextEnt(P_Ent *e) { P_Ent *result = &P_NilEnt; if (!P_IsEntNil(e) && !P_IsEntNil(e->next)) { result = e->next; } return result; } P_Constraint *P_FirstConstraint(P_Frame *frame) { P_Constraint *result = &P_NilConstraint; if (!P_IsConstraintNil(frame->first_constraint)) { result = frame->first_constraint; } return result; } P_Constraint *P_NextConstraint(P_Constraint *c) { P_Constraint *result = &P_NilConstraint; if (!P_IsConstraintNil(c) && !P_IsConstraintNil(c->next)) { result = c->next; } return result; } //////////////////////////////////////////////////////////// //~ Space P_Space P_SpaceFromEnts(Arena *arena, P_Frame *frame) { P_Space space = Zi; TempArena scratch = BeginScratch(arena); P_World *world = frame->world; space.dims = VEC2I32(P_WorldPitch, P_WorldPitch); i64 cells_count = P_WorldPitch * P_WorldPitch; space.cells = PushStructs(arena, P_SpaceCell, cells_count); Rng2 space_aabb = RNG2( VEC2(0, 0), VEC2(P_WorldPitch, P_WorldPitch) ); //- Insert entity shapes for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent)) { b32 should_insert = ent->is_guy; if (should_insert) { P_Shape shape = P_WorldShapeFromEnt(ent); Rng2 aabb = P_BoundingBoxFromShape(shape); aabb = AddRng2Vec2(aabb, VEC2(P_WorldPitch / 2.0, P_WorldPitch / 2.0)); aabb.p0 = FloorVec2(aabb.p0); aabb.p1 = CeilVec2(aabb.p1); aabb.p1.x = MaxF32(aabb.p1.x, aabb.p0.x + 1); aabb.p1.y = MaxF32(aabb.p1.y, aabb.p0.y + 1); aabb = IntersectRng2(aabb, space_aabb); if (!IsRng2Empty(aabb)) { ++space.unique_entries_count; for (i32 y = aabb.p0.y; y < aabb.p1.y; ++y) { for (i32 x = aabb.p0.x; x < aabb.p1.x; ++x) { i64 cell_idx = y * P_WorldPitch + x; P_SpaceCell *cell = &space.cells[cell_idx]; P_SpaceEntryNode *entry_node = PushStruct(arena, P_SpaceEntryNode); SllStackPush(cell->first, entry_node); entry_node->entry.shape = shape; entry_node->entry.shape_id = ent->key.v; ++space.entries_count; } } } } } EndScratch(scratch); return space; } P_Space P_SpaceFromWalls(Arena *arena, P_Frame *frame) { P_Space space = Zi; TempArena scratch = BeginScratch(arena); P_World *world = frame->world; space.dims = VEC2I32(P_WorldPitch, P_WorldPitch); i64 cells_count = P_WorldPitch * P_WorldPitch; space.cells = PushStructs(arena, P_SpaceCell, cells_count); Rng2 space_aabb = RNG2( VEC2(0, 0), VEC2(P_WorldPitch, P_WorldPitch) ); Enum(WallDir) { WallDir_None, WallDir_Up, WallDir_Right, WallDir_Down, WallDir_Left, }; Struct(GenWall) { GenWall *next; WallDir dir; Vec2I32 start; Vec2I32 end; }; GenWall *first_wall = 0; //- Generate horizontal walls for (i32 tile_y = 0; tile_y < P_TilesPitch + 1; ++tile_y) { i32 wall_start = -1; WallDir prev_wall_dir = WallDir_None; for (i32 tile_x = 0; tile_x < P_TilesPitch + 1; ++tile_x) { P_TileKind tile = P_TileKind_Empty; P_TileKind tile_t = P_TileKind_Empty; if (tile_x >= 0 && tile_x < P_TilesPitch) { if (tile_y >= 0 && tile_y < P_TilesPitch) { tile = world->tiles[tile_y * (i32)P_TilesPitch + tile_x]; } if ((tile_y - 1) >= 0 && (tile_y - 1) < P_TilesPitch) { tile_t = world->tiles[(tile_y - 1) * (i32)P_TilesPitch + tile_x]; } } WallDir dir = WallDir_None; if (tile == P_TileKind_Wall && tile_t != P_TileKind_Wall) { dir = WallDir_Up; } else if (tile != P_TileKind_Wall && tile_t == P_TileKind_Wall) { dir = WallDir_Down; } if (dir != prev_wall_dir) { if (prev_wall_dir != WallDir_None) { GenWall *wall = PushStruct(scratch.arena, GenWall); SllStackPush(first_wall, wall); wall->dir = prev_wall_dir; wall->start = VEC2I32(wall_start, tile_y); wall->end = VEC2I32(tile_x, tile_y); } wall_start = tile_x; } prev_wall_dir = dir; } } //- Generate vertical walls for (i32 tile_x = 0; tile_x < P_TilesPitch + 1; ++tile_x) { i32 wall_start = -1; WallDir prev_wall_dir = WallDir_None; for (i32 tile_y = 0; tile_y < P_TilesPitch + 1; ++tile_y) { P_TileKind tile = P_TileKind_Empty; P_TileKind tile_l = P_TileKind_Empty; if (tile_y >= 0 && tile_y < P_TilesPitch) { if (tile_x >= 0 && tile_x < P_TilesPitch) { tile = world->tiles[tile_y * (i32)P_TilesPitch + tile_x]; } if ((tile_x - 1) >= 0 && (tile_x - 1) < P_TilesPitch) { tile_l = world->tiles[tile_y * (i32)P_TilesPitch + (tile_x - 1)]; } } WallDir dir = WallDir_None; if (tile == P_TileKind_Wall && tile_l != P_TileKind_Wall) { dir = WallDir_Left; } else if (tile != P_TileKind_Wall && tile_l == P_TileKind_Wall) { dir = WallDir_Right; } if (dir != prev_wall_dir) { if (prev_wall_dir != WallDir_None) { GenWall *wall = PushStruct(scratch.arena, GenWall); SllStackPush(first_wall, wall); wall->dir = prev_wall_dir; wall->start = VEC2I32(tile_x, wall_start); wall->end = VEC2I32(tile_x, tile_y); } wall_start = tile_y; } prev_wall_dir = dir; } } //- Push walls to space for (GenWall *wall = first_wall; wall; wall = wall->next) { Vec2 p0 = VEC2(wall->start.x / P_TilesPerMeter - P_WorldPitch / 2, wall->start.y / P_TilesPerMeter - P_WorldPitch / 2); Vec2 p1 = VEC2(wall->end.x / P_TilesPerMeter - P_WorldPitch / 2, wall->end.y / P_TilesPerMeter - P_WorldPitch / 2); // P_Shape shape = P_ShapeFromDesc(.count = 2, .points = { p0, p1 }, .radius = 0.01 ); P_Shape shape = P_ShapeFromDesc(.count = 2, .points = { p0, p1 }, .radius = 0.0 ); Rng2 aabb = P_BoundingBoxFromShape(shape); aabb = AddRng2Vec2(aabb, VEC2(P_WorldPitch / 2.0, P_WorldPitch / 2.0)); aabb.p0 = FloorVec2(aabb.p0); aabb.p1 = CeilVec2(aabb.p1); aabb.p1.x = MaxF32(aabb.p1.x, aabb.p0.x + 1); aabb.p1.y = MaxF32(aabb.p1.y, aabb.p0.y + 1); aabb = IntersectRng2(aabb, space_aabb); u64 id = P_WallShapeIDBasis ^ wall->dir; id = MixU64s(id, (((u64)wall->start.x) | ((u64)wall->start.y << 32))); id = MixU64s(id, (((u64)wall->end.x) | ((u64)wall->end.y << 32))); ++space.unique_entries_count; for (i32 y = aabb.p0.y; y < aabb.p1.y; ++y) { for (i32 x = aabb.p0.x; x < aabb.p1.x; ++x) { i64 cell_idx = y * P_WorldPitch + x; P_SpaceCell *cell = &space.cells[cell_idx]; P_SpaceEntryNode *entry_node = PushStruct(arena, P_SpaceEntryNode); SllStackPush(cell->first, entry_node); entry_node->entry.shape = shape; entry_node->entry.shape_id = id; entry_node->entry.dir.x = (wall->dir == WallDir_Right) + ((wall->dir == WallDir_Left) * -1); entry_node->entry.dir.y = (wall->dir == WallDir_Down) + ((wall->dir == WallDir_Up) * -1); ++space.entries_count; } } } EndScratch(scratch); return space; } P_SpaceCell P_SpaceCellFromPos(P_Space *space, Vec2 pos) { P_SpaceCell result = Zi; pos = AddVec2(pos, VEC2(P_WorldPitch / 2.0, P_WorldPitch / 2.0)); if (pos.x >= 0 && pos.x < space->dims.x && pos.y >= 0 && pos.y < space->dims.y) { i64 cell_idx = FloorF32(pos.y) * P_WorldPitch + FloorF32(pos.x); result = space->cells[cell_idx]; } return result; } // TODO: Parameterized bin-count void P_UniqueSpaceEntriesFromRay(Arena *arena, P_SpaceEntryList *result, i32 spaces_count, P_Space **spaces, Vec2 ray_p0, Vec2 ray_p1) { TempArena scratch = BeginScratch(arena); { Struct(BinEntry) { BinEntry *next; u64 shape_id; }; u64 bins_count = 256; BinEntry **bins = PushStructs(scratch.arena, BinEntry *, bins_count); P_DebugDrawLine(ray_p0, ray_p1, Color_Red); // TODO: Clip to avoid unnecessary iterations outside of world bounds ray_p0 = AddVec2(ray_p0, VEC2(P_WorldPitch / 2.0, P_WorldPitch / 2.0)); ray_p1 = AddVec2(ray_p1, VEC2(P_WorldPitch / 2.0, P_WorldPitch / 2.0)); Vec2I32 dda_start = Vec2I32FromVec(FloorVec2(ray_p0)); Vec2I32 dda_end = Vec2I32FromVec(FloorVec2(ray_p1)); Vec2 delta = SubVec2(ray_p1, ray_p0); Vec2 inv_delta = RecipVec2(delta); Vec2 step_dir = VEC2((delta.x > 0) - (delta.x < 0), (delta.y > 0) - (delta.y < 0)); Vec2 t_delta = MulVec2Vec2(step_dir, inv_delta); Vec2 t_max = SubVec2(Vec2FromVec(dda_start), ray_p0); t_max.x += step_dir.x > 0; t_max.y += step_dir.y > 0; t_max = MulVec2Vec2(t_max, inv_delta); Vec2I32 dda_pos = dda_start; b32 done = 0; while (!done) { if (dda_pos.x >= 0 && dda_pos.y >= 0 && dda_pos.x < P_WorldPitch && dda_pos.y < P_WorldPitch) { if (P_tl.debug_draw_enabled) { Vec2 world_pos = Vec2FromVec(dda_pos); world_pos = SubVec2(world_pos, VEC2(P_WorldPitch / 2.0, P_WorldPitch / 2.0)); P_DebugDrawRect(RNG2(world_pos, AddVec2(world_pos, VEC2(1, 1))), VEC4(0.85, 0.5, 0.75, 0.75)); } for (i32 space_idx = 0; space_idx < spaces_count; ++space_idx) { P_Space *space = spaces[space_idx]; if (dda_pos.x < space->dims.x && dda_pos.y < space->dims.y) { i64 cell_idx = dda_pos.y * P_WorldPitch + dda_pos.x; P_SpaceCell cell = space->cells[cell_idx]; for ( P_SpaceEntryNode *src_space_entry_node = cell.first; src_space_entry_node; src_space_entry_node = src_space_entry_node->next ) { P_SpaceEntry *src_space_entry = &src_space_entry_node->entry; BinEntry **bin = &bins[src_space_entry->shape_id % bins_count]; BinEntry *bin_entry = *bin; for (; bin_entry; bin_entry = bin_entry->next) { if (bin_entry->shape_id == src_space_entry->shape_id) { break; } } if (!bin_entry) { // Entry is unique { bin_entry = PushStruct(scratch.arena, BinEntry); bin_entry->shape_id = src_space_entry->shape_id; SllStackPush(*bin, bin_entry); } { P_SpaceEntryNode *dst = PushStruct(arena, P_SpaceEntryNode); dst->entry = *src_space_entry; SllQueuePush(result->first, result->last, dst); ++result->count; } } } } } } if (dda_pos.x == dda_end.x && dda_pos.y == dda_end.y) { done = 1; } else if (t_max.x < t_max.y) { dda_pos.x += step_dir.x; t_max.x += t_delta.x; } else { dda_pos.y += step_dir.y; t_max.y += t_delta.y; } } } EndScratch(scratch); } //////////////////////////////////////////////////////////// //~ List helpers P_Ent *P_PushTempEnt(Arena *arena, P_EntList *list) { P_EntListNode *n = PushStruct(arena, P_EntListNode); SllQueuePush(list->first, list->last, n); ++list->count; P_Ent *ent = &n->ent; *ent = P_NilEnt; ent->exists = 1; return ent; } //////////////////////////////////////////////////////////// //~ Debug draw void P_DebugDrawPoint(Vec2 p, Vec4 srgb) { if (P_tl.debug_draw_enabled) { P_DebugDrawNode *n = PushStruct(P_tl.debug_arena, P_DebugDrawNode); { n->kind = P_DebugDrawKind_Point; n->srgb32 = U32FromVec4(srgb); n->point.p = p; } SllQueuePush(P_tl.first_debug_draw_node, P_tl.last_debug_draw_node, n); P_tl.debug_draw_nodes_count += 1; } } void P_DebugDrawLine(Vec2 p0, Vec2 p1, Vec4 srgb) { if (P_tl.debug_draw_enabled) { P_DebugDrawNode *n = PushStruct(P_tl.debug_arena, P_DebugDrawNode); { n->kind = P_DebugDrawKind_Line; n->srgb32 = U32FromVec4(srgb); n->line.p0 = p0; n->line.p1 = p1; } SllQueuePush(P_tl.first_debug_draw_node, P_tl.last_debug_draw_node, n); P_tl.debug_draw_nodes_count += 1; } } void P_DebugDrawRect(Rng2 rect, Vec4 srgb) { if (P_tl.debug_draw_enabled) { P_DebugDrawNode *n = PushStruct(P_tl.debug_arena, P_DebugDrawNode); { n->kind = P_DebugDrawKind_Rect; n->srgb32 = U32FromVec4(srgb); n->rect = rect; } SllQueuePush(P_tl.first_debug_draw_node, P_tl.last_debug_draw_node, n); P_tl.debug_draw_nodes_count += 1; } } void P_DebugDrawShape(P_Shape shape, Vec4 srgb) { if (P_tl.debug_draw_enabled) { P_DebugDrawNode *n = PushStruct(P_tl.debug_arena, P_DebugDrawNode); { n->kind = P_DebugDrawKind_Shape; n->srgb32 = U32FromVec4(srgb); n->shape = shape; } SllQueuePush(P_tl.first_debug_draw_node, P_tl.last_debug_draw_node, n); P_tl.debug_draw_nodes_count += 1; } } void P_DebugDrawFrame(P_Frame *frame) { if (P_tl.debug_draw_enabled) { P_World *world = frame->world; ////////////////////////////// //- Draw walls { TempArena scratch = BeginScratchNoConflict(); { Struct(BinEntry) { BinEntry *next; u64 shape_id; }; u64 bins_count = NextPow2U64(world->walls_space.unique_entries_count * 4); BinEntry **bins = PushStructs(scratch.arena, BinEntry *, bins_count); i64 cells_count = world->walls_space.dims.x * world->walls_space.dims.y; for (i64 cell_idx = 0; cell_idx < cells_count; ++cell_idx) { P_SpaceCell *cell = &world->walls_space.cells[cell_idx]; for (P_SpaceEntryNode *space_entry_node = cell->first; space_entry_node; space_entry_node = space_entry_node->next) { P_SpaceEntry *space_entry = &space_entry_node->entry; BinEntry **bin = &bins[space_entry->shape_id % bins_count]; BinEntry *bin_entry = *bin; for (; bin_entry; bin_entry = bin_entry->next) { if (bin_entry->shape_id == space_entry->shape_id) { break; } } if (!bin_entry) { // Draw unique wall { bin_entry = PushStruct(scratch.arena, BinEntry); bin_entry->shape_id = space_entry->shape_id; SllStackPush(*bin, bin_entry); } { Vec4 color = VEC4(0.5, 0.75, 0.5, 0.75); P_DebugDrawShape(space_entry->shape, color); } } } } } EndScratch(scratch); } for (i32 cell_y = 0; cell_y < world->walls_space.dims.y; ++cell_y) { for (i32 cell_x = 0; cell_x < world->walls_space.dims.x; ++cell_x) { } } ////////////////////////////// //- Draw entities for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent)) { P_Shape world_shape = P_WorldShapeFromEnt(ent); // Draw aabb { Vec4 color = VEC4(0.4, 0.2, 0.2, 1); Rng2 bb = P_BoundingBoxFromShape(world_shape); P_DebugDrawRect(bb, color); } // Draw shape { // Vec4 color = Color_Cyan; // Vec4 color = VEC4(0.2, 0.4, 0.2, 1); Vec4 color = MulVec4Vec4(VEC4(1, 1, 1, 1), P_tl.debug_tint); P_DebugDrawShape(world_shape, color); } // Draw rot { Vec4 color = VEC4(0.8, 0.8, 0.8, 1); Vec2 p0 = world_shape.centroid; Vec2 p1 = P_EdgePointFromShape(world_shape, ent->xf.r); P_DebugDrawLine(p0, p1, color); } // Draw look { Vec4 color = VEC4(0.4, 0.8, 0.4, 1); Vec2 p0 = world_shape.centroid; Vec2 p1 = P_EdgePointFromShape(world_shape, ent->control.look); P_DebugDrawLine(p0, p1, color); } } ////////////////////////////// //- Draw constraints for (P_Constraint *constraint = P_FirstConstraint(frame); !P_IsConstraintNil(constraint); constraint = P_NextConstraint(constraint)) { P_Ent *ent0 = P_EntFromKey(frame, constraint->ent0); P_Ent *ent1 = P_EntFromKey(frame, constraint->ent1); Vec2 normal = constraint->normal; Vec2 center0 = constraint->static_center0; Vec2 center1 = constraint->static_center1; if (!P_IsEntNil(ent0)) { center0 = P_WorldShapeFromEnt(ent0).center_of_mass; } if (!P_IsEntNil(ent1)) { center1 = P_WorldShapeFromEnt(ent1).center_of_mass; } for (i32 contact_idx = 0; contact_idx < constraint->points_count; ++contact_idx) { P_ContactPoint *contact = &constraint->points[contact_idx]; Vec2 p0 = AddVec2(center0, contact->vcp0); Vec2 p1 = AddVec2(center1, contact->vcp1); P_DebugDrawPoint(p0, Color_Cyan); P_DebugDrawLine(p0, AddVec2(p0, normal), Color_White); } } } } //////////////////////////////////////////////////////////// //~ Msg P_Msg *P_PushMsg(P_MsgKind kind, String data) { P_MsgNode *msg_node = PushStruct(P_tl.out_msgs_arena, P_MsgNode); P_Msg *msg = &msg_node->msg; msg->kind = kind; msg->data = PushString(P_tl.out_msgs_arena, data); msg->xf = XformIdentity; DllQueuePush(P_tl.out_msgs.first, P_tl.out_msgs.last, msg_node); ++P_tl.out_msgs.count; return msg; } //////////////////////////////////////////////////////////// //~ World P_World *P_AcquireWorld(void) { P_World *world = 0; { Arena *arena = AcquireArena(Gibi(64)); world = PushStruct(arena, P_World); world->arena = arena; } world->frames_arena = AcquireArena(Gibi(64)); world->bake_arena = AcquireArena(Gibi(64)); world->first_frame = &P_NilFrame; world->last_frame = &P_NilFrame; world->frame_bins_count = Kibi(16); world->frame_bins = PushStructs(world->arena, P_FrameBin, world->frame_bins_count); // TODO world->tiles = PushStructs(world->arena, u8, P_TilesCount); TrueRand(StringFromStruct(&world->seed)); return world; } void P_SpawnEntsFromList(P_Frame *frame, P_EntList ents) { P_World *world = frame->world; for (P_EntListNode *n = ents.first; n; n = n->next) { P_Ent *src = &n->ent; P_EntKey key = src->key; if (!P_IsEntKeyNil(src->key)) { P_EntBin *bin = &frame->ent_bins[key.v % frame->ent_bins_count]; P_Ent *dst = bin->first; for (; dst; dst = dst->next_in_bin) { if (dst->key.v == key.v) { break; } } if (!dst) { dst = world->first_free_ent; if (dst) { SllStackPop(world->first_free_ent); } else { dst = PushStructNoZero(world->frames_arena, P_Ent); } DllQueuePushNPZ(&P_NilEnt, frame->first_ent, frame->last_ent, dst, next, prev); DllQueuePushNP(bin->first, bin->last, dst, next_in_bin, prev_in_bin); } P_Ent *old_next = dst->next; P_Ent *old_prev = dst->prev; P_Ent *old_next_in_bin = dst->next_in_bin; P_Ent *old_prev_in_bin = dst->prev_in_bin; { *dst = *src; dst->xf = NormXform(dst->xf); } dst->next = old_next; dst->prev = old_prev; dst->next_in_bin = old_next_in_bin; dst->prev_in_bin = old_prev_in_bin; dst->created_at_ns = frame->time_ns; dst->created_at_tick = frame->tick; ++frame->ents_count; } } } P_Constraint *P_PushConstraint(P_Frame *frame, P_ConstraintKey key) { P_World *world = frame->world; P_Constraint *constraint = world->first_free_constraint; if (constraint) { SllStackPop(world->first_free_constraint); } else { constraint = PushStructNoZero(world->frames_arena, P_Constraint); } *constraint = P_NilConstraint; constraint->key = key; P_ConstraintBin *bin = &frame->constraint_bins[key.v % frame->constraint_bins_count]; DllQueuePushNPZ(&P_NilConstraint, frame->first_constraint, frame->last_constraint, constraint, next, prev); DllQueuePushNP(bin->first, bin->last, constraint, next_in_bin, prev_in_bin); ++frame->constraints_count; return constraint; } P_Frame *P_FrameFromTick(P_World *world, i64 tick) { P_Frame *result = &P_NilFrame; if (world->frame_bins_count > 0) { u64 hash = MixU64(tick); P_FrameBin *bin = &world->frame_bins[hash % world->frame_bins_count]; for (P_Frame *frame = bin->first; frame; frame = frame->next_in_bin) { if (frame->tick == tick) { result = frame; break; } } } return result; } void P_ClearFrames(P_World *world, i64 tick_min, i64 tick_max) { // TODO: Fast path for when range encompasses all frames in the world // TODO: Don't need linear search P_Frame *frame = world->first_frame; while (!P_IsFrameNil(frame)) { P_Frame *next_frame = frame->next; if (frame->tick >= tick_min && frame->tick <= tick_max) { // Free ents if (!P_IsEntNil(frame->first_ent)) { frame->last_ent->next = world->first_free_ent; world->first_free_ent = frame->first_ent; } // Free constraints if (!P_IsConstraintNil(frame->first_constraint)) { frame->last_constraint->next = world->first_free_constraint; world->first_free_constraint = frame->first_constraint; } // Free frame u64 hash = MixU64(frame->tick); P_FrameBin *bin = &world->frame_bins[hash % world->frame_bins_count]; DllQueueRemoveNPZ(&P_NilFrame, world->first_frame, world->last_frame, frame, next, prev); DllQueueRemoveNPZ(0, bin->first, bin->last, frame, next_in_bin, prev_in_bin); SllStackPush(world->first_free_frame, frame); } else { break; } frame = next_frame; } } P_Frame *P_PushFrame(P_World *world, P_Frame *src_frame, i64 tick) { P_Frame *frame = world->first_free_frame; if (frame) { SllStackPop(world->first_free_frame); i64 old_ent_bins_count = frame->ent_bins_count; P_EntBin *old_ent_bins = frame->ent_bins; i64 old_constraint_bins_count = frame->constraint_bins_count; P_ConstraintBin *old_constraint_bins = frame->constraint_bins; { ZeroStruct(frame); } frame->ent_bins_count = old_ent_bins_count; frame->ent_bins = old_ent_bins; ZeroStructs(frame->ent_bins, frame->ent_bins_count); frame->constraint_bins_count = old_constraint_bins_count; frame->constraint_bins = old_constraint_bins; ZeroStructs(frame->constraint_bins, frame->constraint_bins_count); } else { frame = PushStruct(world->frames_arena, P_Frame); } { frame->world = world; frame->tick = tick; frame->time_ns = src_frame->time_ns; frame->first_ent = &P_NilEnt; frame->last_ent = &P_NilEnt; if (!frame->ent_bins) { frame->ent_bins_count = Kibi(16); frame->ent_bins = PushStructs(world->frames_arena, P_EntBin, frame->ent_bins_count); } frame->first_constraint = &P_NilConstraint; frame->last_constraint = &P_NilConstraint; if (!frame->constraint_bins) { frame->constraint_bins_count = Kibi(1); frame->constraint_bins = PushStructs(world->frames_arena, P_ConstraintBin, frame->constraint_bins_count); } } // Copy ents for (P_Ent *src = P_FirstEnt(src_frame); !P_IsEntNil(src); src = P_NextEnt(src)) { P_Ent *dst = world->first_free_ent; if (dst) { SllStackPop(world->first_free_ent); } else { dst = PushStructNoZero(world->frames_arena, P_Ent); } *dst = *src; P_EntBin *bin = &frame->ent_bins[src->key.v % frame->ent_bins_count]; DllQueuePushNPZ(&P_NilEnt, frame->first_ent, frame->last_ent, dst, next, prev); DllQueuePushNP(bin->first, bin->last, dst, next_in_bin, prev_in_bin); ++frame->ents_count; } // Copy constraints for (P_Constraint *src = P_FirstConstraint(src_frame); !P_IsConstraintNil(src); src = P_NextConstraint(src)) { P_Constraint *dst = world->first_free_constraint; if (dst) { SllStackPop(world->first_free_constraint); } else { dst = PushStructNoZero(world->frames_arena, P_Constraint); } *dst = *src; P_ConstraintBin *bin = &frame->constraint_bins[src->key.v % frame->constraint_bins_count]; DllQueuePushNPZ(&P_NilConstraint, frame->first_constraint, frame->last_constraint, dst, next, prev); DllQueuePushNP(bin->first, bin->last, dst, next_in_bin, prev_in_bin); ++frame->constraints_count; } // Clear frames P_ClearFrames(world, tick, I64Max); // Insert frame { u64 hash = MixU64(tick); P_FrameBin *bin = &world->frame_bins[hash % world->frame_bins_count]; DllQueuePushNPZ(&P_NilFrame, world->first_frame, world->last_frame, frame, next, prev); DllQueuePushNPZ(0, bin->first, bin->last, frame, next_in_bin, prev_in_bin); } return frame; } //////////////////////////////////////////////////////////// //~ Step void P_StepFrame(P_Frame *frame) { TempArena scratch = BeginScratchNoConflict(); P_World *world = frame->world; P_Frame *prev_frame = frame->prev; i64 sim_dt_ns = SIM_TICK_INTERVAL_NS; f64 sim_dt = SecondsFromNs(sim_dt_ns); ////////////////////////////// //- Prune ents { i64 ents_to_prune_count = 0; P_EntKey *ents_to_prune = PushStructsNoZero(scratch.arena, P_EntKey, frame->ents_count); for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent)) { if (ent->exists <= 0) { ents_to_prune[ents_to_prune_count++] = ent->key; if (!P_IsEntKeyNil(ent->weapon)) { ents_to_prune[ents_to_prune_count++] = ent->weapon; } } } for (i64 prune_idx = 0; prune_idx < ents_to_prune_count; ++prune_idx) { // FIXME: Ensure sure prunes are received by clients P_EntKey key = ents_to_prune[prune_idx]; P_Ent *ent = P_EntFromKey(frame, key); if (!P_IsEntNil(ent)) { P_EntBin *bin = &frame->ent_bins[ent->key.v % frame->ent_bins_count]; DllQueueRemoveNP(bin->first, bin->last, ent, next_in_bin, prev_in_bin); DllQueueRemoveNPZ(&P_NilEnt, frame->first_ent, frame->last_ent, ent, next, prev); frame->ents_count -= 1; SllStackPush(world->first_free_ent, ent); } } } ////////////////////////////// //- Query ents b32 is_predicting = P_tl.is_predicting; P_Ent *local_player = P_EntFromKey(frame, P_tl.local_player); P_Ent *local_guy = P_EntFromKey(frame, local_player->guy); ////////////////////////////// //- Spawn guys if (!is_predicting) { P_EntList queued_ents = Zi; for (P_Ent *player = P_FirstEnt(frame); !P_IsEntNil(player); player = P_NextEnt(player)) { if (player->is_player) { if (P_IsEntKeyNil(player->guy)) { player->guy = P_RandEntKey(); } P_Ent *guy = P_EntFromKey(frame, player->guy); if (P_IsEntNil(guy)) { guy = P_PushTempEnt(scratch.arena, &queued_ents); guy->is_guy = 1; guy->key = player->guy; guy->source = player->key; //- Choose guy spawn point { P_Ent *highest_scoring_spawn = &P_NilEnt; { Struct(SpawnNode) { SpawnNode *next; P_Ent *ent; f32 score; }; i64 spawns_count = 0; SpawnNode *first_spawn = 0; SpawnNode *last_spawn = 0; // Push spawns for (P_Ent *spawn_ent = P_FirstEnt(frame); !P_IsEntNil(spawn_ent); spawn_ent = P_NextEnt(spawn_ent)) { if (spawn_ent->is_guy_spawn) { SpawnNode *spawn = PushStruct(scratch.arena, SpawnNode); SllQueuePush(first_spawn, last_spawn, spawn); spawn->ent = spawn_ent; spawn->score = P_WorldPitch * 1000; ++spawns_count; } } // Score spawns for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent)) { b32 should_avoid = 0; if (ent->is_guy) { should_avoid = 1; } if (P_MatchEntKey(ent->key, player->spawn)) { // Avoid old spawn should_avoid = 1; } if (should_avoid) { for (SpawnNode *spawn = first_spawn; spawn; spawn = spawn->next) { // TODO: Something better than linear distance for scoring f32 score = Vec2Len(SubVec2(ent->xf.t, spawn->ent->xf.t)); spawn->score = MinF32(spawn->score, score); } } } // Find highest scoring spawn i64 highest_score = -Inf; for (SpawnNode *spawn = first_spawn; spawn; spawn = spawn->next) { f32 rand_score_spread = 10; f32 virtual_score = spawn->score + rand_score_spread * Norm24(RandU64FromState(&world->rand)); if (virtual_score > highest_score) { highest_score = virtual_score; highest_scoring_spawn = spawn->ent; } } } guy->xf = highest_scoring_spawn->xf; player->spawn = highest_scoring_spawn->key; } } } } P_SpawnEntsFromList(frame, queued_ents); } ////////////////////////////// //- Equip spawned guys // TODO: Remove this (weapon testing) { P_EntList queued_ents = Zi; for (P_Ent *guy = P_FirstEnt(frame); !P_IsEntNil(guy); guy = P_NextEnt(guy)) { if (guy->is_guy && guy->created_at_tick == frame->tick) { P_Ent *weapon = P_EntFromKey(frame, guy->weapon); if (!weapon->is_weapon) { weapon = P_PushTempEnt(scratch.arena, &queued_ents); weapon->is_weapon = 1; weapon->key = P_RandEntKey(); weapon->source = guy->key; // weapon->is_uzi = 1; weapon->is_launcher = 1; guy->weapon = weapon->key; } } } P_SpawnEntsFromList(frame, queued_ents); } ////////////////////////////// //- Update guy controls from player controls { for (P_Ent *guy = P_FirstEnt(frame); !P_IsEntNil(guy); guy = P_NextEnt(guy)) { if (guy->is_guy) { ZeroStruct(&guy->control); } } for (P_Ent *player = P_FirstEnt(frame); !P_IsEntNil(player); player = P_NextEnt(player)) { if (player->is_player) { P_Ent *guy = P_EntFromKey(frame, player->guy); if (!P_IsEntNil(guy)) { guy->control = player->control; } } } // Normalize controls for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent)) { ent->control.move = ClampVec2Len(ent->control.move, 0, 1); } } ////////////////////////////// //- Integrate guy control forces for (P_Ent *guy = P_FirstEnt(frame); !P_IsEntNil(guy); guy = P_NextEnt(guy)) { if (guy->is_guy && (!is_predicting || guy == local_guy)) { P_Control control = guy->control; // Dampen movement { if (Vec2Len(guy->solved_v) > 0.001) { f32 damp_force = TweakFloat("Guy damp force", 50, 0, 100); Vec2 damp = MulVec2(NegVec2(guy->solved_v), damp_force * sim_dt); guy->solved_v = AddVec2(guy->solved_v, damp); } else { guy->solved_v = VEC2(0, 0); } } // Integrate linear movement { f32 move_force = TweakFloat("Guy move force", 400, 0, 400); f32 max_speed = TweakFloat("Guy max speed", 10, 0, 20); Vec2 new_velocity = guy->solved_v; new_velocity = AddVec2(new_velocity, MulVec2(control.move, move_force * sim_dt)); if (Vec2Len(new_velocity) > max_speed) { new_velocity = Vec2WithLen(new_velocity, max_speed); } guy->solved_v = new_velocity; } // Integrate look { f32 turn_rate = TweakFloat("Guy turn rate", 1, 0, 1); f32 cur_angle = AngleFromVec2(guy->xf.r); f32 desired_angle = AngleFromVec2(control.look); f32 diff = UnwindAngleF32(desired_angle - cur_angle); f32 look_force = 1.0 / (sim_dt * sim_dt) * turn_rate; guy->solved_w = diff * sim_dt * look_force; } } } ////////////////////////////// //- Setup constraints store i32 solver_steps_count = SIM_PHYSICS_SUBSTEPS; f32 solver_dt = sim_dt / solver_steps_count; // Solid params // SoftSpring solid_spring = MakeSpring(TweakFloat("Contact spring hz", 25, 5, 200), TweakFloat("Contact spring damp", 10, 5, 100), solver_dt); SoftSpring solid_spring = MakeSpring(TweakFloat("Contact spring hz", 100, 5, 200), TweakFloat("Contact spring damp", 10, 5, 100), solver_dt); f32 solid_pushout_velocity = TweakFloat("Contact spring pushout", 3, 0, 50); // Gentle params // f32 gentle_pushout_factor = TweakFloat("Gentle pushout factor", 10, 0, 50); f32 gentle_pushout_factor = TweakFloat("Gentle pushout factor", 0.5, 0, 50); ////////////////////////////// //- Bake world { u64 desired_bake_hash = world->tiles_hash; if (desired_bake_hash != world->baked_hash) { LogDebugF("Bake step"); ResetArena(world->bake_arena); world->walls_space = P_SpaceFromWalls(world->bake_arena, frame); world->baked_hash = desired_bake_hash; } } ////////////////////////////// //- Build pre-solve space from ents P_Space pre_solve_ents_space = P_SpaceFromEnts(scratch.arena, frame); ////////////////////////////// //- Generate guy constraints for (P_Ent *ent0 = P_FirstEnt(frame); !P_IsEntNil(ent0); ent0 = P_NextEnt(ent0)) { if (ent0->is_guy) { P_Shape shape0 = P_WorldShapeFromEnt(ent0); Rng2 aabb0 = P_BoundingBoxFromShape(shape0); Rng2 query_rect = Zi; query_rect.p0 = FloorVec2(aabb0.p0); query_rect.p1 = CeilVec2(aabb0.p1); query_rect.p1.x = MaxF32(query_rect.p1.x, query_rect.p0.x + 1); query_rect.p1.y = MaxF32(query_rect.p1.y, query_rect.p0.y + 1); for (i32 query_y = query_rect.p0.y; query_y < query_rect.p1.y; ++query_y) { for (i32 query_x = query_rect.p0.x; query_x < query_rect.p1.x; ++query_x) { P_SpaceCell cells[] = { P_SpaceCellFromPos(&pre_solve_ents_space, VEC2(query_x, query_y)), P_SpaceCellFromPos(&world->walls_space, VEC2(query_x, query_y)), }; for (i64 cell_idx = 0; cell_idx < countof(cells); ++cell_idx) { P_SpaceCell cell = cells[cell_idx]; for (P_SpaceEntryNode *space_entry_node = cell.first; space_entry_node; space_entry_node = space_entry_node->next) { P_SpaceEntry *space_entry = &space_entry_node->entry; Rng2 aabb1 = P_BoundingBoxFromShape(space_entry->shape); P_Ent *ent1 = P_EntFromKey(frame, (P_EntKey) { .v = space_entry->shape_id }); if (!P_MatchEntKey(ent0->key, ent1->key) && IsIntersectingRng2(aabb0, aabb1)) { P_Shape shape1 = space_entry->shape; b32 is_static_collision = P_IsEntNil(ent1); b32 is_guy_on_guy_collision = ent0->is_guy && ent1->is_guy; if (is_static_collision || is_guy_on_guy_collision) { P_ConstraintKey constraint_key = Zi; { // Deterministic shape ID order for consistent constraint lookup u64 shape_id0 = ent0->key.v; u64 shape_id1 = space_entry->shape_id; if (shape_id0 > shape_id1) { u64 tmp = shape_id0; shape_id0 = shape_id1; shape_id1 = tmp; } constraint_key = P_ConstraintKeyFromU64s(shape_id0, shape_id1); } P_Constraint *constraint = P_ConstraintFromKey(frame, constraint_key); if (constraint->last_touched_tick < frame->tick) { P_CollisionResult collision = P_CollisionResultFromShapes(shape0, shape1); b32 skip_collision = 0; skip_collision = skip_collision || collision.collision_points_count <= 0; if (!skip_collision && !IsVec2Zero(space_entry->dir)) { // Skip collision if normal violates one-way direction // f32 threshold = 0.5; f32 threshold = 0; skip_collision = DotVec2(space_entry->dir, collision.collision_normal) >= threshold; } if (!skip_collision) { if (P_IsConstraintNil(constraint)) { constraint = P_PushConstraint(frame, constraint_key); } constraint->last_touched_tick = frame->tick; constraint->normal = collision.collision_normal; // constraint->friction = SqrtF32(ent0->friction * ent1->friction); constraint->friction = 0; if (is_static_collision) { constraint->flags |= P_ConstraintFlag_Solid; } else if (is_guy_on_guy_collision) { constraint->flags |= P_ConstraintFlag_Gentle; // constraint->flags |= P_ConstraintFlag_NoWarmStart; } // TODO: Real masses f32 inv_m0 = 10; f32 inv_m1 = 10; f32 inv_i0 = 0; f32 inv_i1 = 0; // Treat statics / non-predicted ents as infinite-mass if (!ent0->is_guy || (is_predicting && !P_MatchEntKey(ent0->key, local_guy->key))) { inv_m0 = 0; inv_i0 = 0; } if (!ent1->is_guy || (is_predicting && !P_MatchEntKey(ent1->key, local_guy->key))) { inv_m1 = 0; inv_i1 = 0; } constraint->ent0 = ent0->key; constraint->ent1 = ent1->key; constraint->static_center0 = shape0.center_of_mass; constraint->static_center1 = shape1.center_of_mass; constraint->inv_m0 = inv_m0; constraint->inv_m1 = inv_m1; constraint->inv_i0 = inv_i0; constraint->inv_i1 = inv_i1; // Delete old contacts that are no longer present for (i32 contact_point_idx = 0; contact_point_idx < constraint->points_count; ++contact_point_idx) { P_ContactPoint *contact = &constraint->points[contact_point_idx]; u32 id = contact->id; b32 match = 0; for (i32 collision_point_idx = 0; collision_point_idx < collision.collision_points_count; ++collision_point_idx) { if (collision.collision_points[collision_point_idx].id == id) { match = 1; break; } } if (!match) { // Delete contact by replacing with last in array *contact = constraint->points[constraint->points_count - 1]; constraint->points_count -= 1; contact_point_idx -= 1; } } // Create / update contacts from collision for (i32 collision_point_idx = 0; collision_point_idx < collision.collision_points_count; ++collision_point_idx) { P_CollisionPoint collision_point = collision.collision_points[collision_point_idx]; u32 id = collision_point.id; P_ContactPoint *contact = 0; { for (i32 contact_point_idx = 0; contact_point_idx < constraint->points_count; ++contact_point_idx) { P_ContactPoint *tmp = &constraint->points[contact_point_idx]; if (tmp->id == id) { contact = tmp; break; } } if (!contact) { contact = &constraint->points[constraint->points_count]; constraint->points_count += 1; ZeroStruct(contact); } } contact->id = id; Vec2 vcp0 = SubVec2(collision_point.p, shape0.center_of_mass); Vec2 vcp1 = SubVec2(collision_point.p, shape1.center_of_mass); contact->vcp0 = vcp0; contact->vcp1 = vcp1; contact->starting_separation = collision_point.separation; } } } } } } } } } } } ////////////////////////////// //- Prune constraints { i64 prune_constraints_count = 0; P_Constraint **prune_constraints = PushStructsNoZero(scratch.arena, P_Constraint *, frame->constraints_count); for (P_Constraint *constraint = P_FirstConstraint(frame); !P_IsConstraintNil(constraint); constraint = P_NextConstraint(constraint)) { b32 prune = 1; if (constraint->last_touched_tick == frame->tick) { prune = 0; } if (prune) { prune_constraints[prune_constraints_count] = constraint; prune_constraints_count += 1; } } for (i64 prune_idx = 0; prune_idx < prune_constraints_count; ++prune_idx) { P_Constraint *constraint = prune_constraints[prune_idx]; P_ConstraintBin *bin = &frame->constraint_bins[constraint->key.v % frame->constraint_bins_count]; DllQueueRemoveNP(bin->first, bin->last, constraint, next_in_bin, prev_in_bin); DllQueueRemoveNPZ(&P_NilConstraint, frame->first_constraint, frame->last_constraint, constraint, next, prev); frame->constraints_count -= 1; SllStackPush(world->first_free_constraint, constraint); } } ////////////////////////////// //- Run solver steps for (i32 solver_step_idx = 0; solver_step_idx < solver_steps_count; ++solver_step_idx) { ////////////////////////////// //- Prepare constraints for (P_Constraint *constraint = P_FirstConstraint(frame); !P_IsConstraintNil(constraint); constraint = P_NextConstraint(constraint)) { Vec2 normal = constraint->normal; Vec2 tangent = PerpVec2(normal); f32 inv_m0 = constraint->inv_m0; f32 inv_m1 = constraint->inv_m1; f32 inv_i0 = constraint->inv_i0; f32 inv_i1 = constraint->inv_i1; for (i32 contact_idx = 0; contact_idx < constraint->points_count; ++contact_idx) { P_ContactPoint *contact = &constraint->points[contact_idx]; Vec2 vcp0 = contact->vcp0; Vec2 vcp1 = contact->vcp1; // Compute normal mass { f32 vcp0_wedge = WedgeVec2(vcp0, normal); f32 vcp1_wedge = WedgeVec2(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; } // Compute tangent mass { f32 vcp0_wedge = WedgeVec2(vcp0, tangent); f32 vcp1_wedge = WedgeVec2(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; } } } ////////////////////////////// //- Warm start constraints for (P_Constraint *constraint = P_FirstConstraint(frame); !P_IsConstraintNil(constraint); constraint = P_NextConstraint(constraint)) { if (!(constraint->flags & P_ConstraintFlag_NoWarmStart)) { P_Ent *ent0 = P_EntFromKey(frame, constraint->ent0); P_Ent *ent1 = P_EntFromKey(frame, constraint->ent1); Vec2 v0 = ent0->solved_v; Vec2 v1 = ent1->solved_v; f32 w0 = ent0->solved_w; f32 w1 = ent1->solved_w; Vec2 normal = constraint->normal; Vec2 tangent = PerpVec2(normal); for (i32 contact_idx = 0; contact_idx < constraint->points_count; ++contact_idx) { P_ContactPoint *contact = &constraint->points[contact_idx]; Vec2 vcp0 = contact->vcp0; Vec2 vcp1 = contact->vcp1; Vec2 impulse = AddVec2(MulVec2(normal, contact->solved_normal_impulse), MulVec2(tangent, contact->solved_tangent_impulse)); // impulse = MulVec2(impulse, inv_num_points); v0 = SubVec2(v0, MulVec2(impulse, constraint->inv_m0)); v1 = AddVec2(v1, MulVec2(impulse, constraint->inv_m1)); w0 -= WedgeVec2(vcp0, impulse) * constraint->inv_i0; w1 += WedgeVec2(vcp1, impulse) * constraint->inv_i1; } if (!P_IsEntNil(ent0)) { ent0->solved_v = v0; ent0->solved_w = w0; } if (!P_IsEntNil(ent1)) { ent1->solved_v = v1; ent1->solved_w = w1; } } } ////////////////////////////// //- Solve constraints // TODO: Solve wall constraints last for (P_Constraint *constraint = P_FirstConstraint(frame); !P_IsConstraintNil(constraint); constraint = P_NextConstraint(constraint)) { P_Ent *ent0 = P_EntFromKey(frame, constraint->ent0); P_Ent *ent1 = P_EntFromKey(frame, constraint->ent1); Vec2 normal = constraint->normal; Vec2 tangent = PerpVec2(normal); f32 inv_m0 = constraint->inv_m0; f32 inv_m1 = constraint->inv_m1; f32 inv_i0 = constraint->inv_i0; f32 inv_i1 = constraint->inv_i1; Vec2 v0 = ent0->solved_v; Vec2 v1 = ent1->solved_v; f32 w0 = ent0->solved_w; f32 w1 = ent1->solved_w; Vec2 center0 = constraint->static_center0; Vec2 center1 = constraint->static_center1; if (!P_IsEntNil(ent0)) { center0 = P_WorldShapeFromEnt(ent0).center_of_mass; } if (!P_IsEntNil(ent1)) { center1 = P_WorldShapeFromEnt(ent1).center_of_mass; } //- Solve solid constraint if (constraint->flags & P_ConstraintFlag_Solid) { // Normal impulse for (i32 contact_idx = 0; contact_idx < constraint->points_count; ++contact_idx) { P_ContactPoint *contact = &constraint->points[contact_idx]; Vec2 vcp0 = contact->vcp0; Vec2 vcp1 = contact->vcp1; Vec2 p0 = AddVec2(center0, vcp0); Vec2 p1 = AddVec2(center1, vcp1); f32 separation = DotVec2(SubVec2(p1, p0), normal) + contact->starting_separation; f32 velocity_bias = 0.0; f32 mass_scale = 1.0; f32 impulse_scale = 0.0; // TODO: Do a relaxation pass without bias b32 apply_bias = 1; if (separation > 0.0) { // Speculative velocity_bias = separation / solver_dt; } else if (apply_bias) { // Soft constraint SoftSpring softness = solid_spring; f32 pushout_velocity = solid_pushout_velocity; mass_scale = softness.mass_scale; impulse_scale = softness.impulse_scale; velocity_bias = MaxF32(softness.bias_rate * separation, -pushout_velocity); } Vec2 vel0 = AddVec2(v0, MulPerpVec2(vcp0, w0)); Vec2 vel1 = AddVec2(v1, MulPerpVec2(vcp1, w1)); Vec2 vrel = SubVec2(vel0, vel1); f32 k = contact->inv_normal_mass; // To be applied along normal f32 vn = DotVec2(vrel, normal); f32 j = ((k * mass_scale) * (vn - velocity_bias)) - (contact->solved_normal_impulse * impulse_scale); f32 old_impulse = contact->solved_normal_impulse; f32 new_impulse = MaxF32(old_impulse + j, 0); f32 delta = new_impulse - old_impulse; contact->solved_normal_impulse = new_impulse; Vec2 impulse = MulVec2(normal, delta); v0 = SubVec2(v0, MulVec2(impulse, inv_m0)); v1 = AddVec2(v1, MulVec2(impulse, inv_m1)); w0 -= WedgeVec2(vcp0, impulse) * inv_i0; w1 += WedgeVec2(vcp1, impulse) * inv_i1; } // Tangent impulse for (i32 contact_idx = 0; contact_idx < constraint->points_count; ++contact_idx) { P_ContactPoint *contact = &constraint->points[contact_idx]; Vec2 vcp0 = contact->vcp0; Vec2 vcp1 = contact->vcp1; Vec2 vel0 = AddVec2(v0, MulPerpVec2(vcp0, w0)); Vec2 vel1 = AddVec2(v1, MulPerpVec2(vcp1, w1)); Vec2 vrel = SubVec2(vel0, vel1); f32 k = contact->inv_tangent_mass; // To be applied along tangent f32 vt = DotVec2(vrel, tangent); f32 j = vt * k; f32 max_friction = constraint->friction * contact->solved_normal_impulse; f32 old_impulse = contact->solved_tangent_impulse; f32 new_impulse = ClampF32(old_impulse + j, -max_friction, max_friction); f32 delta = new_impulse - old_impulse; contact->solved_tangent_impulse = new_impulse; Vec2 impulse = MulVec2(tangent, delta); v0 = SubVec2(v0, MulVec2(impulse, inv_m0)); v1 = AddVec2(v1, MulVec2(impulse, inv_m1)); w0 -= WedgeVec2(vcp0, impulse) * inv_i0; w1 += WedgeVec2(vcp1, impulse) * inv_i1; } if (!P_IsEntNil(ent0)) { ent0->solved_v = v0; ent0->solved_w = w0; } if (!P_IsEntNil(ent1)) { ent1->solved_v = v1; ent1->solved_w = w1; } } //- Solve gentle constraint if (constraint->flags & P_ConstraintFlag_Gentle) { // Normal impulse for (i32 contact_idx = 0; contact_idx < constraint->points_count; ++contact_idx) { P_ContactPoint *contact = &constraint->points[contact_idx]; Vec2 vcp0 = contact->vcp0; Vec2 vcp1 = contact->vcp1; Vec2 p0 = AddVec2(center0, vcp0); Vec2 p1 = AddVec2(center1, vcp1); f32 separation = DotVec2(SubVec2(p1, p0), normal) + contact->starting_separation; f32 j = -separation * solver_dt * gentle_pushout_factor; f32 old_impulse = contact->solved_normal_impulse; f32 new_impulse = MaxF32(old_impulse + j, 0); f32 delta = new_impulse - old_impulse; contact->solved_normal_impulse = new_impulse; Vec2 impulse = MulVec2(normal, delta); v0 = SubVec2(v0, MulVec2(impulse, inv_m0)); v1 = AddVec2(v1, MulVec2(impulse, inv_m1)); w0 -= WedgeVec2(vcp0, impulse) * inv_i0; w1 += WedgeVec2(vcp1, impulse) * inv_i1; } if (!P_IsEntNil(ent0)) { ent0->solved_v = v0; ent0->solved_w = w0; } if (!P_IsEntNil(ent1)) { ent1->solved_v = v1; ent1->solved_w = w1; } } } ////////////////////////////// //- Integrate velocities for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent)) { if (!is_predicting || ent == local_guy) { Xform xf = ent->xf; xf.t = AddVec2(xf.t, MulVec2(ent->solved_v, solver_dt)); xf.r = RotateVec2Angle(xf.r, ent->solved_w * solver_dt); ent->xf = xf; } } } ////////////////////////////// //- Build post-solve space from ents P_Space post_solve_ents_space = P_SpaceFromEnts(scratch.arena, frame); ////////////////////////////// //- Move bullets for (P_Ent *bullet = P_FirstEnt(frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet)) { if (bullet->is_bullet) { Vec2 start = bullet->bullet_start; Vec2 end = bullet->bullet_end; Vec2 vel = SubVec2(end, start); bullet->bullet_start = end; bullet->bullet_end = AddVec2(end, vel); } } ////////////////////////////// //- Fire bullets // TODO: Remove this { P_EntList bullets_to_spawn = Zi; for (P_Ent *firer = P_FirstEnt(frame); !P_IsEntNil(firer); firer = P_NextEnt(firer)) { P_Ent *weapon = P_EntFromKey(frame, firer->weapon); if (weapon->is_weapon && firer->control.fire_held) // if (weapon->is_weapon && firer->control.fire_presses) { // i64 fire_delta_ns = frame->time_ns - firer->last_fire_ns; // i64 single_bullet_delta_ns = NsFromSeconds(1) / firer->fire_rate; // i64 tick_bullets_count = sim_dt * firer->fire_rate; f32 fire_rate = 50; f32 bullets_per_fire = 1; f32 spread = Tau * 0.05; // f32 spread = Tau * 0.01; f32 tweak_speed = TweakFloat("Bullet speed", 100, 1, 100); // f32 tweak_speed = TweakFloat("Bullet speed", 1, 1, 100); b32 can_fire = (firer->last_fire_ns + NsFromSeconds(1.0 / fire_rate)) <= frame->time_ns; Vec2 fire_pos = Zi; Vec2 fire_dir = Zi; Vec2 fire_base0 = Zi; Vec2 fire_base1 = Zi; if (can_fire) { Vec2 look = firer->control.look; P_Anim anim = P_AnimFromEnt(frame, firer); SPR_Sprite body = SPR_SpriteFromSheet(anim.sheet, anim.span, anim.frame_seq); SPR_Sprite wep = SPR_SpriteFromSheet(anim.wep_sheet, anim.span, anim.frame_seq); //- Compute sprite transforms Affine ent_to_world_af = MulAffineXform(AffineIdentity, firer->xf); Affine body_pix_to_world_af = AffineIdentity; Affine wep_pix_to_world_af = AffineIdentity; { Vec2 pix_scale = VEC2(1.0 / P_CellsPerMeter, 1.0 / P_CellsPerMeter); //- Compute body transform Affine body_pix_to_ent_af = AffineIdentity; { body_pix_to_ent_af = ScaleAffine(body_pix_to_ent_af, pix_scale); SPR_Ray anchor_ray = body.rays[SPR_RayKind_Anchor]; body_pix_to_ent_af = RotateAffine(body_pix_to_ent_af, InvertRot(anchor_ray.dir)); body_pix_to_ent_af = TranslateAffine(body_pix_to_ent_af, NegVec2(anchor_ray.pos)); } //- Compute weapon transform Affine wep_pix_to_ent_af = AffineIdentity; { wep_pix_to_ent_af = ScaleAffine(wep_pix_to_ent_af, pix_scale); SPR_Ray body_anchor_ray = body.rays[SPR_RayKind_Anchor]; SPR_Ray body_ap_ray = body.rays[SPR_RayKind_Ap]; wep_pix_to_ent_af = RotateAffine(wep_pix_to_ent_af, InvertRot(body_anchor_ray.dir)); wep_pix_to_ent_af = TranslateAffine(wep_pix_to_ent_af, SubVec2(body_ap_ray.pos, body_anchor_ray.pos)); wep_pix_to_ent_af = RotateAffine(wep_pix_to_ent_af, NegVec2(body_ap_ray.dir)); SPR_Ray anchor_ray = wep.rays[SPR_RayKind_Anchor]; wep_pix_to_ent_af = RotateAffine(wep_pix_to_ent_af, anchor_ray.dir); wep_pix_to_ent_af = TranslateAffine(wep_pix_to_ent_af, NegVec2(anchor_ray.pos)); } body_pix_to_world_af = MulAffine(ent_to_world_af, body_pix_to_ent_af); wep_pix_to_world_af = MulAffine(ent_to_world_af, wep_pix_to_ent_af); } SPR_Ray fire_ray = wep.rays[SPR_RayKind_Ap]; fire_pos = MulAffineVec2(wep_pix_to_world_af, fire_ray.pos); fire_dir = NormRot(MulAffineBasisVec2(wep_pix_to_world_af, fire_ray.dir)); fire_base0 = MulVec2(PerpVec2(NegVec2(firer->xf.r)), WedgeVec2(firer->xf.r, SubVec2(firer->xf.t, fire_pos))); fire_base0 = AddVec2(fire_base0, firer->xf.t); Vec2 chamber_pos = MulAffineVec2(body_pix_to_world_af, body.rays[SPR_RayKind_Ap].pos); fire_base1 = MulVec2(PerpVec2(fire_dir), WedgeVec2(fire_dir, SubVec2(fire_pos, chamber_pos))); fire_base1 = AddVec2(fire_base1, chamber_pos); P_DebugDrawLine(fire_base0, fire_base1, Color_Yellow); P_DebugDrawLine(fire_base1, fire_pos, Color_Green); P_DebugDrawPoint(fire_base0, Color_Yellow); P_DebugDrawPoint(fire_base1, Color_Green); P_DebugDrawPoint(fire_pos, Color_Red); } // FIXME: Prevent obstructed weapons from firing through walls if (can_fire) { i64 tick_bullets_count = bullets_per_fire; if (tick_bullets_count > 0) { P_Shape firer_world_shape = P_WorldShapeFromEnt(firer); // Vec2 fire_pos = P_EdgePointFromShape(firer_world_shape, firer->control.look); // Vec2 fire_dir = firer->control.look; for (i64 bullet_idx = 0; bullet_idx < tick_bullets_count; ++bullet_idx) { P_Ent *bullet = P_PushTempEnt(scratch.arena, &bullets_to_spawn); bullet->is_bullet = 1; bullet->key = P_RandEntKey(); f32 rand_speed = Norm24(P_RandU64FromEnt(firer)) - 0.5; f32 rand_angle = Norm24(P_RandU64FromEnt(firer)) - 0.5; f32 speed = tweak_speed * sim_dt; f32 angle = AngleFromVec2(fire_dir); speed += (speed * 0.5) * rand_speed; angle += rand_angle * spread; Vec2 dir = Vec2FromAngle(angle); bullet->bullet_base0 = fire_base0; bullet->bullet_base1 = fire_base1; bullet->bullet_start = fire_pos; bullet->bullet_end = AddVec2(bullet->bullet_start, MulVec2(dir, speed)); bullet->source = weapon->key; bullet->damage_attribution = firer->source; } } firer->last_fire_ns = frame->time_ns; } } } P_SpawnEntsFromList(frame, bullets_to_spawn); } ////////////////////////////// //- Update bullet hits // TODO: Separate 'hits' from bullets, so that bullets can have multiple hits for (P_Ent *bullet = P_FirstEnt(frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet)) { if (bullet->is_bullet) { P_Ent *bullet_weapon = P_EntFromKey(frame, bullet->source); P_Ent *bullet_guy = P_EntFromKey(frame, bullet_weapon->source); P_Ent *bullet_damager = P_EntFromKey(frame, bullet->damage_attribution); bullet->has_hit = 0; Struct(BulletPath) { BulletPath *next; Vec2 start; Vec2 end; }; BulletPath *first_bullet_path = 0; BulletPath *last_bullet_path = 0; if (bullet->created_at_tick == frame->tick) { // On bullet's first tick, we want to ensure that the firer/weapon // wasn't obstructed (e.g. to prevent shooting through walls), so we // insert a path from the bullet's base to its starting position before // its actual firing path { // Firer origin -> weapon chamber path BulletPath *path = PushStruct(scratch.arena, BulletPath); SllQueuePush(first_bullet_path, last_bullet_path, path); path->start = bullet->bullet_base0; path->end = bullet->bullet_base1; } { // Weapon chamber -> bullet start path BulletPath *path = PushStruct(scratch.arena, BulletPath); SllQueuePush(first_bullet_path, last_bullet_path, path); path->start = bullet->bullet_base1; path->end = bullet->bullet_start; } } { BulletPath *path = PushStruct(scratch.arena, BulletPath); SllQueuePush(first_bullet_path, last_bullet_path, path); path->start = bullet->bullet_start; path->end = bullet->bullet_end; } P_EntKey victim_key = Zi; P_RaycastResult victim_raycast = Zi; b32 hit = 0; { for (BulletPath *path = first_bullet_path; path && !hit; path = path->next) { Vec2 path_dir = SubVec2(path->end, path->start); P_Space *cast_spaces[] = { &world->walls_space, &post_solve_ents_space, }; P_SpaceEntryList cast_entries = Zi; P_UniqueSpaceEntriesFromRay( scratch.arena, &cast_entries, countof(cast_spaces), cast_spaces, path->start, path->end ); f32 closest_len_sq = Inf; for (P_SpaceEntryNode *entry_node = cast_entries.first; entry_node; entry_node = entry_node->next) { P_SpaceEntry *entry = &entry_node->entry; P_EntKey potential_victim_key = (P_EntKey) { .v = entry->shape_id }; if (!P_MatchEntKey(potential_victim_key, bullet_guy->key) || P_IsEntKeyNil(bullet_guy->key)) { P_Shape potential_victim_shape = entry->shape; P_RaycastResult entrance_raycast = P_RaycastShape(potential_victim_shape, path->start, path_dir); Vec2 entrance = entrance_raycast.p; if (entrance_raycast.is_intersecting) { P_RaycastResult exit_raycast = P_RaycastShape(potential_victim_shape, path->start, NegVec2(path_dir)); Vec2 exit = exit_raycast.p; f32 da = DotVec2(path_dir, SubVec2(entrance, path->start)); f32 db = DotVec2(path_dir, SubVec2(exit, path->start)); if (db > 0 && (da <= Vec2LenSq(path_dir) || da <= 0)) { f32 len_sq = Vec2LenSq(SubVec2(entrance_raycast.p, path->start)); if (len_sq < closest_len_sq) { closest_len_sq = len_sq; victim_key = potential_victim_key; victim_raycast = entrance_raycast; hit = 1; } } } } } } } P_Ent *victim = P_EntFromKey(frame, victim_key); // TODO: Truncate bullet trail if (hit) { bullet->has_hit = 1; bullet->hit_entry = victim_raycast.p; bullet->hit_entry_normal = victim_raycast.normal; // bullet->bullet_end = bullet->hit_entry; bullet->exists = 0; if (victim->is_guy) { bullet->hit_material = P_MaterialKind_Flesh; } else { bullet->hit_material = P_MaterialKind_Wall; } } // TODO: Remove this if (!P_IsEntNil(victim)) { if (bullet_damager->is_player) { victim->damage_attribution = bullet_damager->key; } victim->health -= 0.25; } // Prune out of bounds bullet Rng2 bounds = Zi; bounds.p0 = VEC2(-P_WorldPitch / 2, -P_WorldPitch / 2); bounds.p1 = VEC2(P_WorldPitch / 2, P_WorldPitch / 2); if ( bullet->bullet_start.x < bounds.p0.x || bullet->bullet_start.y < bounds.p0.y || bullet->bullet_start.x > bounds.p1.x || bullet->bullet_start.y > bounds.p1.y ) { bullet->exists = 0; } } } ////////////////////////////// //- Kill guys for (P_Ent *guy = P_FirstEnt(frame); !P_IsEntNil(guy); guy = P_NextEnt(guy)) { if (guy->is_guy) { if (guy->health <= 0) { P_Ent *old_guy = P_EntFromKey(prev_frame, guy->key); if (old_guy->health > 0) { P_Ent *player = P_EntFromKey(frame, guy->source); P_Ent *killer = P_EntFromKey(frame, guy->damage_attribution); if (player->is_player) { player->deaths += 1; } if (killer->is_player && !P_MatchEntKey(player->key, killer->key)) { killer->kills += 1; } guy->exists = 0; } } } } ////////////////////////////// //- Debug draw P_DebugDrawFrame(frame); ////////////////////////////// //- End frame frame->time_ns += sim_dt_ns; EndScratch(scratch); }