3539 lines
110 KiB
C
3539 lines
110 KiB
C
P_Ctx P = Zi;
|
|
ThreadLocal P_ThreadLocalCtx P_tl = Zi;
|
|
|
|
Readonly P_Ent P_NilEnt = {
|
|
.xf = CompXformIdentity,
|
|
.control.look = { 1, 0 },
|
|
.health = 1,
|
|
.lifetime_seconds = Inf,
|
|
};
|
|
|
|
Readonly P_Constraint P_NilConstraint = {
|
|
0
|
|
};
|
|
|
|
Readonly P_Frame P_NilFrame = {
|
|
.next = &P_NilFrame,
|
|
.prev = &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) };
|
|
}
|
|
|
|
P_EntKey P_EntKeyFromU64(u64 v)
|
|
{
|
|
return (P_EntKey) { .v = v };
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ 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),
|
|
);
|
|
}
|
|
else if (ent->is_pickup)
|
|
{
|
|
result = P_ShapeFromDesc(
|
|
.mass = 10,
|
|
.count = 1,
|
|
.radius = 0.10,
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Status helpers
|
|
|
|
b32 P_IsEntRolling(P_Frame *frame, P_Ent *ent)
|
|
{
|
|
b32 result = (
|
|
ent->last_roll_ns > 0 &&
|
|
(frame->time_ns - ent->last_roll_ns) > 0 &&
|
|
(frame->time_ns - ent->last_roll_ns) < P_RollTimeNs
|
|
);
|
|
return result;
|
|
}
|
|
|
|
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.050);
|
|
i64 body_animation_time_ns = frame->time_ns;
|
|
i64 legs_animation_time_ns = frame->time_ns;
|
|
i64 wep_animation_time_ns = frame->time_ns;
|
|
{
|
|
b32 is_rolling = P_IsEntRolling(frame, ent);
|
|
// b32 is_shooting = ent->is_guy && ((frame->time_ns - MaxI64(wep->last_fire_ns, wep->last_alt_fire_ns)) < animation_rate_ns);
|
|
b32 is_shooting = ent->is_guy && ((frame->time_ns - MaxI64(wep->last_fire_ns, wep->last_alt_fire_ns)) < (animation_rate_ns * 2));
|
|
b32 is_walking = !is_rolling && Vec2LenSq(ent->control.move) > (0.01 * 0.01);
|
|
|
|
// is_shooting = 1;
|
|
|
|
{
|
|
result.body_span = SPR_SpanKeyFromName(Lit("idle"));
|
|
result.legs_span = SPR_SpanKeyFromName(Lit("idle"));
|
|
result.wep_span = SPR_SpanKeyFromName(Lit("idle"));
|
|
|
|
// if (is_shooting)
|
|
// {
|
|
// animation_rate_ns = NsFromSeconds(0.001);
|
|
// }
|
|
|
|
|
|
// Body
|
|
if (is_rolling)
|
|
{
|
|
result.body_span = SPR_SpanKeyFromName(Lit("roll"));
|
|
result.weapon_over = 1;
|
|
}
|
|
else if (is_shooting)
|
|
{
|
|
result.body_span = SPR_SpanKeyFromName(Lit("shoot"));
|
|
// body_animation_time_ns = SaturateF32(NsFromSeconds(1.0 / wep->last_fire_rate));
|
|
}
|
|
else if (is_walking)
|
|
{
|
|
result.body_span = SPR_SpanKeyFromName(Lit("walk"));
|
|
body_animation_time_ns = ent->walk_time_accum_ns;
|
|
}
|
|
|
|
// Legs
|
|
if (is_rolling)
|
|
{
|
|
result.legs_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("guy/legs.ase")));
|
|
result.legs_span = SPR_SpanKeyFromName(Lit("roll"));
|
|
// legs_animation_time_ns = ent->walk_time_accum_ns;
|
|
}
|
|
else if (is_walking)
|
|
{
|
|
result.legs_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("guy/legs.ase")));
|
|
result.legs_span = SPR_SpanKeyFromName(Lit("walk"));
|
|
legs_animation_time_ns = ent->walk_time_accum_ns;
|
|
}
|
|
|
|
// Weapon
|
|
if (is_shooting)
|
|
{
|
|
result.wep_span = SPR_SpanKeyFromName(Lit("shoot"));
|
|
// legs_animation_time_ns = SaturateF32(NsFromSeconds(1.0 / wep->last_fire_rate));
|
|
}
|
|
}
|
|
}
|
|
|
|
result.body_frame_seq = body_animation_time_ns / animation_rate_ns;
|
|
result.legs_frame_seq = legs_animation_time_ns / animation_rate_ns;
|
|
result.wep_frame_seq = wep_animation_time_ns / animation_rate_ns;
|
|
|
|
// TODO: Use prefab lookup
|
|
|
|
if (ent->is_guy)
|
|
{
|
|
result.body_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("guy/guy.ase")));
|
|
}
|
|
else if (ent->is_guy_spawn)
|
|
{
|
|
result.body_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("prefab/GuySpawn.ase")));
|
|
}
|
|
else if (ent->is_health_spawn)
|
|
{
|
|
result.body_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("prefab/HealthSpawn.ase")));
|
|
}
|
|
else if (ent->is_bomb)
|
|
{
|
|
result.body_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("misc/bomb.ase")));
|
|
}
|
|
else if (ent->is_health)
|
|
{
|
|
result.body_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("misc/health.ase")));
|
|
}
|
|
else if (ent->is_health)
|
|
{
|
|
result.body_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("misc/health.ase")));
|
|
}
|
|
|
|
if (wep->is_uzi)
|
|
{
|
|
result.wep_sheet = SPR_SheetKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("wep/uzi.ase")));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Vec4 P_ColorFromEnt(P_Ent *ent)
|
|
{
|
|
String ent_str = P_StringFromEnt(ent);
|
|
u64 color_seed = MixU64s(P_EntityColorBasis, HashString(ent_str));
|
|
f32 hue_offset = TweakFloat("Entity color hue offset", 135, 0, 360);
|
|
f32 h = (Norm16(color_seed >> 2) * 1) * 360 + hue_offset;
|
|
f32 s = TweakFloat("Entity color saturation", 0.7, 0, 1);
|
|
f32 v = TweakFloat("Entity color brightness", 1, 0, 1);
|
|
// f32 s = TweakFloat("Entity color saturation", 0.3, 0, 1);
|
|
// f32 v = TweakFloat("Entity color brightness", 0.6, 0, 1);
|
|
return SrgbFromHsv(HSV(h, s, v));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ 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;
|
|
}
|
|
}
|
|
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 || ent->is_pickup;
|
|
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 tile 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 tile 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;
|
|
}
|
|
}
|
|
|
|
//- Generate world edge walls
|
|
{
|
|
{
|
|
// Top
|
|
GenWall *wall = PushStruct(scratch.arena, GenWall);
|
|
SllStackPush(first_wall, wall);
|
|
wall->dir = WallDir_Down;
|
|
wall->start = VEC2I32(0, 0);
|
|
wall->end = VEC2I32(P_TilesPitch, 0);
|
|
}
|
|
// Bottom
|
|
{
|
|
GenWall *wall = PushStruct(scratch.arena, GenWall);
|
|
SllStackPush(first_wall, wall);
|
|
wall->dir = WallDir_Up;
|
|
wall->start = VEC2I32(0, P_TilesPitch);
|
|
wall->end = VEC2I32(P_TilesPitch, P_TilesPitch);
|
|
}
|
|
// Left
|
|
{
|
|
GenWall *wall = PushStruct(scratch.arena, GenWall);
|
|
SllStackPush(first_wall, wall);
|
|
wall->dir = WallDir_Right;
|
|
wall->start = VEC2I32(0, 0);
|
|
wall->end = VEC2I32(0, P_TilesPitch);
|
|
}
|
|
// Right
|
|
{
|
|
GenWall *wall = PushStruct(scratch.arena, GenWall);
|
|
SllStackPush(first_wall, wall);
|
|
wall->dir = WallDir_Left;
|
|
wall->start = VEC2I32(P_TilesPitch, 0);
|
|
wall->end = VEC2I32(P_TilesPitch, P_TilesPitch);
|
|
}
|
|
}
|
|
|
|
//- 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 = VEC2(AbsF32(inv_delta.x), AbsF32(inv_delta.y));
|
|
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);
|
|
if (IsInf(inv_delta.x))
|
|
{
|
|
t_max.x = inv_delta.x;
|
|
}
|
|
if (IsInf(inv_delta.y))
|
|
{
|
|
t_max.y = inv_delta.y;
|
|
}
|
|
|
|
Vec2I32 dda_pos = dda_start;
|
|
b32 done = 0;
|
|
u32 max_iterations = 512;
|
|
u32 iteration_idx = 0;
|
|
for (; iteration_idx < max_iterations && !done; ++iteration_idx)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (iteration_idx >= max_iterations)
|
|
{
|
|
// This should never happen unless we have a bug in the algorithm
|
|
Assert(0);
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- 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);
|
|
}
|
|
dst->net_state = src->net_state;
|
|
dst->created_at_ns = frame->time_ns;
|
|
dst->created_at_tick = frame->tick;
|
|
dst->sim = !P_tl.is_client;
|
|
++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->assembled_at_ns = src_frame->assembled_at_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();
|
|
|
|
ProfZoneDF("Simulate world")
|
|
{
|
|
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);
|
|
|
|
//////////////////////////////
|
|
//- Kill ents with expired lifetimes
|
|
|
|
{
|
|
for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
|
|
{
|
|
if (ent->lifetime_seconds != Inf && SecondsFromNs(frame->time_ns - ent->created_at_ns) >= ent->lifetime_seconds)
|
|
{
|
|
ent->exists = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Prune ents
|
|
|
|
{
|
|
// Mark dead child ents
|
|
for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
|
|
{
|
|
if (ent->exists <= 0)
|
|
{
|
|
P_Ent *weapon = P_EntFromKey(frame, ent->weapon);
|
|
if (!P_IsEntNil(weapon))
|
|
{
|
|
weapon->exists = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gather ents to prune
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Prune
|
|
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_client = P_tl.is_client;
|
|
P_Ent *local_player = P_EntFromKey(frame, P_tl.local_player);
|
|
P_Ent *local_guy = P_EntFromKey(frame, local_player->guy);
|
|
|
|
//////////////////////////////
|
|
//- Mark simulated ents
|
|
|
|
if (is_client)
|
|
{
|
|
P_EntKey local_player_key = local_player->key;
|
|
P_EntKey local_guy_key = local_guy->key;
|
|
P_EntKey local_weapon_key = local_guy->weapon;
|
|
|
|
// Mark player/guy/weapon ents
|
|
for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
|
|
{
|
|
P_EntKey key = ent->key;
|
|
b32 sim = 0;
|
|
if (
|
|
P_MatchEntKey(key, local_player_key) ||
|
|
P_MatchEntKey(key, local_guy_key) ||
|
|
P_MatchEntKey(key, local_weapon_key)
|
|
)
|
|
{
|
|
sim = 1;
|
|
}
|
|
ent->sim = sim;
|
|
}
|
|
|
|
// Mark child ents
|
|
for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
|
|
{
|
|
if (!ent->sim)
|
|
{
|
|
ent->sim = P_EntFromKey(frame, ent->source)->sim;
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Spawn pickups
|
|
|
|
if (!is_client)
|
|
{
|
|
P_EntList queued_ents = Zi;
|
|
for (P_Ent *spawner = P_FirstEnt(frame); !P_IsEntNil(spawner); spawner = P_NextEnt(spawner))
|
|
{
|
|
b32 is_spawner = spawner->is_health_spawn;
|
|
if (is_spawner)
|
|
{
|
|
if (!P_IsEntKeyNil(spawner->pickup))
|
|
{
|
|
P_Ent *pickup = P_EntFromKey(frame, spawner->pickup);
|
|
if (P_IsEntNil(pickup))
|
|
{
|
|
spawner->last_spawn_reset_ns = frame->time_ns;
|
|
spawner->pickup = P_NilEntKey;
|
|
}
|
|
}
|
|
}
|
|
if (is_spawner && P_IsEntKeyNil(spawner->pickup))
|
|
{
|
|
i64 spawn_interval_ns = 0;
|
|
if (spawner->is_health_spawn)
|
|
{
|
|
spawn_interval_ns = NsFromSeconds(1);
|
|
}
|
|
b32 should_spawn = spawner->last_spawn_reset_ns == 0 || spawner->last_spawn_reset_ns + spawn_interval_ns <= frame->time_ns;
|
|
if (should_spawn)
|
|
{
|
|
spawner->last_spawn_reset_ns = frame->time_ns;
|
|
P_Ent *pickup = P_PushTempEnt(scratch.arena, &queued_ents);
|
|
{
|
|
pickup->key = P_RandEntKey();
|
|
pickup->xf = spawner->xf;
|
|
pickup->is_pickup = 1;
|
|
pickup->source = spawner->key;
|
|
pickup->is_health = spawner->is_health_spawn;
|
|
}
|
|
spawner->pickup = pickup->key;
|
|
}
|
|
}
|
|
}
|
|
P_SpawnEntsFromList(frame, queued_ents);
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Spawn guys
|
|
|
|
if (!is_client)
|
|
{
|
|
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;
|
|
if (player->is_bot)
|
|
{
|
|
guy->xf.r = Vec2FromAngle(Norm24(RandU64FromState(&world->rand)) * Tau);
|
|
}
|
|
|
|
//- 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);
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Accumulate control times
|
|
|
|
for (P_Ent *guy = P_FirstEnt(frame); !P_IsEntNil(guy); guy = P_NextEnt(guy))
|
|
{
|
|
if (guy->is_guy)
|
|
{
|
|
// Roll
|
|
if (guy->control.downs[P_Button_Roll])
|
|
{
|
|
// TODO: Not like this
|
|
|
|
i64 roll_timeout_ns = P_RollTimeoutNs;
|
|
i64 roll_time_ns = P_RollTimeNs;
|
|
|
|
if (frame->time_ns - roll_timeout_ns - roll_time_ns > guy->last_roll_ns || guy->last_roll_ns == 0)
|
|
{
|
|
guy->last_roll_ns = frame->time_ns;
|
|
// guy->last_roll_dir = NormVec2(guy->control.move);
|
|
guy->last_roll_dir = NormVec2(guy->control.look);
|
|
}
|
|
}
|
|
// Walk
|
|
b32 is_moving = Vec2LenSq(guy->control.move) > (0.001 * 0.001);
|
|
if (is_moving)
|
|
{
|
|
if (!P_IsEntRolling(frame, guy))
|
|
{
|
|
guy->walk_time_accum_ns += sim_dt_ns;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
guy->walk_time_accum_ns = 0;
|
|
}
|
|
// Fire
|
|
if (guy->control.held[P_Button_PrimaryFire] || guy->control.held[P_Button_AltFire])
|
|
{
|
|
guy->fire_time_accum_ns += sim_dt_ns;
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Integrate guy control forces
|
|
|
|
for (P_Ent *guy = P_FirstEnt(frame); !P_IsEntNil(guy); guy = P_NextEnt(guy))
|
|
{
|
|
if (guy->is_guy && guy->sim)
|
|
{
|
|
P_Control control = guy->control;
|
|
|
|
// Dampen movement
|
|
{
|
|
if (Vec2Len(guy->v) > 0.001)
|
|
{
|
|
f32 damp_force = TweakFloat("Guy damp force", 50, 0, 100);
|
|
// f32 damp_force = TweakFloat("Guy damp force", 0, 0, 100);
|
|
Vec2 damp = MulVec2(NegVec2(guy->v), damp_force * sim_dt);
|
|
guy->v = AddVec2(guy->v, damp);
|
|
}
|
|
else
|
|
{
|
|
guy->v = VEC2(0, 0);
|
|
}
|
|
}
|
|
|
|
f32 turn_rate = TweakFloat("Guy turn rate", 1, 0, 1);
|
|
f32 move_force = TweakFloat("Guy move force", 400, 0, 400);
|
|
f32 max_speed = TweakFloat("Guy max speed", 10, 0, 20);
|
|
|
|
Vec2 look = control.look;
|
|
Vec2 move = control.move;
|
|
|
|
// FIXME: Roll timeout
|
|
b32 is_rolling = P_IsEntRolling(frame, guy);
|
|
if (is_rolling)
|
|
{
|
|
Vec2 roll_dir = NormVec2(guy->xf.r);
|
|
|
|
move = roll_dir;
|
|
// look = roll_dir;
|
|
|
|
f32 roll_factor = 1.3;
|
|
move_force *= roll_factor;
|
|
max_speed *= roll_factor;
|
|
|
|
{
|
|
turn_rate = 0.1;
|
|
}
|
|
}
|
|
|
|
// Integrate linear movement
|
|
{
|
|
Vec2 new_velocity = guy->v;
|
|
new_velocity = AddVec2(new_velocity, MulVec2(move, move_force * sim_dt));
|
|
if (Vec2Len(new_velocity) > max_speed)
|
|
{
|
|
new_velocity = Vec2WithLen(new_velocity, max_speed);
|
|
}
|
|
|
|
guy->v = new_velocity;
|
|
}
|
|
|
|
// Integrate look
|
|
{
|
|
f32 cur_angle = AngleFromVec2(guy->xf.r);
|
|
f32 desired_angle = AngleFromVec2(look);
|
|
f32 diff = UnwindAngleF32(desired_angle - cur_angle);
|
|
f32 look_force = 1.0 / (sim_dt * sim_dt) * turn_rate;
|
|
guy->w = diff * sim_dt * look_force;
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- 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);
|
|
|
|
//////////////////////////////
|
|
//- Setup constraint constants
|
|
|
|
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", 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", 0.5, 0, 50);
|
|
|
|
//////////////////////////////
|
|
//- 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 || !ent0->sim)
|
|
{
|
|
inv_m0 = 0;
|
|
inv_i0 = 0;
|
|
}
|
|
if (!ent1->is_guy || !ent1->sim)
|
|
{
|
|
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->v;
|
|
Vec2 v1 = ent1->v;
|
|
f32 w0 = ent0->w;
|
|
f32 w1 = ent1->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->v = v0;
|
|
ent0->w = w0;
|
|
}
|
|
if (!P_IsEntNil(ent1))
|
|
{
|
|
ent1->v = v1;
|
|
ent1->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->v;
|
|
Vec2 v1 = ent1->v;
|
|
f32 w0 = ent0->w;
|
|
f32 w1 = ent1->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->v = v0;
|
|
ent0->w = w0;
|
|
}
|
|
if (!P_IsEntNil(ent1))
|
|
{
|
|
ent1->v = v1;
|
|
ent1->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->v = v0;
|
|
ent0->w = w0;
|
|
}
|
|
if (!P_IsEntNil(ent1))
|
|
{
|
|
ent1->v = v1;
|
|
ent1->w = w1;
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Integrate velocities
|
|
|
|
for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
|
|
{
|
|
if (!ent->is_bullet && ent->sim)
|
|
{
|
|
Xform xf = ent->xf;
|
|
xf.t = AddVec2(xf.t, MulVec2(ent->v, solver_dt));
|
|
xf.r = RotateVec2Angle(xf.r, ent->w * solver_dt);
|
|
ent->xf = xf;
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Build post-solve space from ents
|
|
|
|
P_Space post_solve_ents_space = P_SpaceFromEnts(scratch.arena, frame);
|
|
|
|
//////////////////////////////
|
|
//- Spawn bullets
|
|
|
|
{
|
|
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)
|
|
{
|
|
for (P_Button button = 0; button < P_Button_COUNT; ++button)
|
|
{
|
|
f32 fire_rate = 1; // In bullets per second
|
|
f32 bullets_per_fire = 1;
|
|
b32 firing = 0;
|
|
b32 is_bomb = 0;
|
|
if (button == P_Button_PrimaryFire && (firer->control.held[button] || firer->control.downs[button]))
|
|
{
|
|
fire_rate = 25;
|
|
bullets_per_fire = 5;
|
|
// fire_rate = 50;
|
|
// fire_rate = 50;
|
|
// bullets_per_fire = 1;
|
|
firing = (weapon->last_fire_ns + NsFromSeconds(1.0 / fire_rate)) <= frame->time_ns;
|
|
if (firing)
|
|
{
|
|
weapon->last_fire_ns = frame->time_ns;
|
|
}
|
|
}
|
|
if (button == P_Button_AltFire && (firer->control.held[button] || firer->control.downs[button]))
|
|
{
|
|
fire_rate = 10;
|
|
bullets_per_fire = 4;
|
|
firing = (weapon->last_alt_fire_ns + NsFromSeconds(1.0 / fire_rate)) <= frame->time_ns;
|
|
is_bomb = 1;
|
|
if (firing)
|
|
{
|
|
weapon->last_alt_fire_ns = frame->time_ns;
|
|
}
|
|
}
|
|
if (firing)
|
|
{
|
|
i64 tick_bullets_count = bullets_per_fire;
|
|
if (tick_bullets_count > 0)
|
|
{
|
|
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;
|
|
|
|
// TDOO: More specific key with seed that only increments on player control (for less chance of misprediction)
|
|
bullet->key = P_EntKeyFromU64(P_RandU64FromEnt(firer));
|
|
bullet->is_bomb = is_bomb;
|
|
|
|
bullet->source = weapon->key;
|
|
bullet->damage_attribution_player = firer->source;
|
|
bullet->sim = weapon->sim;
|
|
}
|
|
}
|
|
weapon->last_fire_rate = fire_rate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
P_SpawnEntsFromList(frame, bullets_to_spawn);
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Update bullets
|
|
|
|
{
|
|
P_EntList ents_to_spawn = Zi;
|
|
for (P_Ent *bullet = P_FirstEnt(frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet))
|
|
{
|
|
if (bullet->is_bullet)
|
|
{
|
|
P_Ent *weapon = P_EntFromKey(frame, bullet->source);
|
|
P_Ent *firer = P_EntFromKey(frame, weapon->source);
|
|
P_Ent *damager = P_EntFromKey(frame, bullet->damage_attribution_player);
|
|
|
|
b32 is_first_bullet_tick = bullet->created_at_tick == frame->tick;
|
|
|
|
//////////////////////////////
|
|
//- Bullet properties
|
|
|
|
// f32 spread = Tau * 0.5;
|
|
f32 spread = Tau * 0.05;
|
|
// f32 spread = Tau * 0.2;
|
|
// f32 spread = Tau * 0.01;
|
|
// f32 spread = 0;
|
|
|
|
b32 should_ricochet = 0;
|
|
|
|
f32 initial_speed = 1;
|
|
f32 speed_falloff = 0;
|
|
if (bullet->is_bomb)
|
|
{
|
|
should_ricochet = 1;
|
|
initial_speed = 50;
|
|
// initial_speed = 100;
|
|
speed_falloff = 5;
|
|
}
|
|
else
|
|
{
|
|
initial_speed = TweakFloat("Bullet speed", 75, 1, 100);
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Initialize bullet
|
|
|
|
Struct(BulletPath)
|
|
{
|
|
BulletPath *next;
|
|
Vec2 start;
|
|
Vec2 end;
|
|
};
|
|
BulletPath *first_bullet_path = 0;
|
|
BulletPath *last_bullet_path = 0;
|
|
|
|
if (is_first_bullet_tick)
|
|
{
|
|
Vec2 fire_pos = Zi;
|
|
Vec2 fire_dir = Zi;
|
|
Vec2 fire_base0 = Zi;
|
|
Vec2 fire_base1 = Zi;
|
|
{
|
|
Vec2 look = firer->control.look;
|
|
P_Anim anim = P_AnimFromEnt(frame, firer);
|
|
SPR_Sprite body = SPR_SpriteFromSheet(anim.body_sheet, anim.body_span, anim.body_frame_seq);
|
|
SPR_Sprite wep = SPR_SpriteFromSheet(anim.wep_sheet, anim.wep_span, anim.wep_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 muzzle_ray = wep.rays[SPR_RayKind_Ap];
|
|
fire_pos = MulAffineVec2(wep_pix_to_world_af, muzzle_ray.pos);
|
|
fire_dir = NormVec2(MulAffineBasisVec2(wep_pix_to_world_af, muzzle_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);
|
|
|
|
Vec2 bullet_dir = RotateVec2Angle(fire_dir, spread * (Norm24(MixU64s(bullet->key.v, P_BulletSpreadBasis)) - 0.5));
|
|
|
|
bullet->xf.t = fire_pos;
|
|
bullet->xf.r = NormVec2(bullet_dir);
|
|
|
|
bullet->v = MulVec2(NormVec2(bullet_dir), initial_speed);
|
|
// bullet->v = AddVec2(bullet->v, firer->v);
|
|
}
|
|
|
|
// 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 = fire_base0;
|
|
path->end = fire_base1;
|
|
}
|
|
{
|
|
// Weapon chamber -> bullet start path
|
|
BulletPath *path = PushStruct(scratch.arena, BulletPath);
|
|
SllQueuePush(first_bullet_path, last_bullet_path, path);
|
|
path->start = fire_base1;
|
|
path->end = fire_pos;
|
|
}
|
|
}
|
|
|
|
Vec2 dir = MulVec2(bullet->v, sim_dt);
|
|
Vec2 p0 = bullet->xf.t;
|
|
Vec2 p1 = p0;
|
|
|
|
// Cur pos -> next pos path
|
|
// if (!is_first_bullet_tick)
|
|
{
|
|
p1 = AddVec2(p0, dir);
|
|
BulletPath *path = PushStruct(scratch.arena, BulletPath);
|
|
SllQueuePush(first_bullet_path, last_bullet_path, path);
|
|
path->start = p0;
|
|
path->end = p1;
|
|
}
|
|
|
|
P_EntKey victim_key = Zi;
|
|
P_RaycastResult victim_raycast = Zi;
|
|
b32 has_hit = 0;
|
|
{
|
|
for (BulletPath *path = first_bullet_path; path && !has_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 };
|
|
P_Ent *potential_victim = P_EntFromKey(frame, potential_victim_key);
|
|
b32 can_hit = (
|
|
P_IsEntNil(potential_victim) ||
|
|
(potential_victim->is_guy && (!P_MatchEntKey(potential_victim->key, firer->key) || P_IsEntKeyNil(firer->key)))
|
|
);
|
|
if (can_hit)
|
|
{
|
|
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;
|
|
has_hit = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
P_Ent *victim = P_EntFromKey(frame, victim_key);
|
|
|
|
// Create hit
|
|
Vec2 final_pos = p1;
|
|
if (has_hit)
|
|
{
|
|
Vec2 normal = victim_raycast.normal;
|
|
bullet->bullet_hits_count += 1;
|
|
|
|
P_Ent *hit = P_PushTempEnt(scratch.arena, &ents_to_spawn);
|
|
{
|
|
hit->key = P_EntKeyFromU64(MixU64s(bullet->key.v, P_BulletHitBasis + bullet->bullet_hits_count));
|
|
hit->is_hit = 1;
|
|
hit->hit_entry = victim_raycast.p;
|
|
hit->hit_entry_normal = normal;
|
|
hit->lifetime_seconds = P_ObservationDurationSeconds;
|
|
// FIXME: Use relative velocity at collision point
|
|
hit->hit_entry_velocity = bullet->v;
|
|
if (victim->is_guy)
|
|
{
|
|
hit->hit_material = P_MaterialKind_Flesh;
|
|
}
|
|
else
|
|
{
|
|
hit->hit_material = P_MaterialKind_Wall;
|
|
}
|
|
}
|
|
|
|
// Reflect velocity along normal
|
|
if (should_ricochet)
|
|
{
|
|
bullet->v = SubVec2(bullet->v, MulVec2(normal, 2 * DotVec2(bullet->v, normal)));
|
|
bullet->v = MulVec2(bullet->v, 0.5);
|
|
}
|
|
else
|
|
{
|
|
bullet->exists = 0;
|
|
}
|
|
|
|
f32 collision_offset = 0.01; // Tiny offset along normal to prevent collision with the victim during ricochets
|
|
final_pos = AddVec2(hit->hit_entry, MulVec2(hit->hit_entry_normal, collision_offset));
|
|
}
|
|
|
|
bullet->xf.t = final_pos;
|
|
if (Vec2LenSq(dir) > (0.001 * 0.001))
|
|
{
|
|
bullet->xf.r = NormVec2(dir);
|
|
}
|
|
bullet->v = MulVec2(bullet->v, 1.0 - SaturateF32(speed_falloff * sim_dt));
|
|
|
|
// Create trail
|
|
{
|
|
P_Ent *trail = P_PushTempEnt(scratch.arena, &ents_to_spawn);
|
|
trail->key = P_EntKeyFromU64(MixU64s(bullet->key.v, P_BulletTrailBasis + frame->tick));
|
|
trail->is_trail = 1;
|
|
trail->trail_p0 = p0;
|
|
trail->trail_p1 = bullet->xf.t;
|
|
trail->lifetime_seconds = 0;
|
|
if (is_first_bullet_tick)
|
|
{
|
|
trail->is_first_trail = 1;
|
|
}
|
|
}
|
|
|
|
// TODO: Remove this
|
|
if (!is_client && !P_IsEntNil(victim))
|
|
{
|
|
if (damager->is_player)
|
|
{
|
|
victim->damage_attribution_player = damager->key;
|
|
victim->damage_attribution_dir = SubVec2(bullet->xf.t, p0);
|
|
}
|
|
victim->health -= 0.5;
|
|
}
|
|
|
|
// 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->xf.t.x < bounds.p0.x || bullet->xf.t.y < bounds.p0.y ||
|
|
bullet->xf.t.x > bounds.p1.x || bullet->xf.t.y > bounds.p1.y
|
|
)
|
|
{
|
|
bullet->exists = 0;
|
|
}
|
|
}
|
|
}
|
|
P_SpawnEntsFromList(frame, ents_to_spawn);
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Kill guys
|
|
|
|
{
|
|
P_EntList ents_to_spawn = Zi;
|
|
for (P_Ent *guy = P_FirstEnt(frame); !P_IsEntNil(guy); guy = P_NextEnt(guy))
|
|
{
|
|
if (guy->is_guy && 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_player);
|
|
// Update kill info
|
|
{
|
|
if (player->is_player)
|
|
{
|
|
player->deaths += 1;
|
|
}
|
|
if (killer->is_player && !P_MatchEntKey(player->key, killer->key))
|
|
{
|
|
killer->kills += 1;
|
|
}
|
|
guy->exists = 0;
|
|
guy->continuity_gen += 1;
|
|
guy->health = 1;
|
|
}
|
|
// Push kill event
|
|
{
|
|
P_Ent *death = P_PushTempEnt(scratch.arena, &ents_to_spawn);
|
|
death->key = P_EntKeyFromU64(MixU64s(guy->key.v, P_DeathBasis + (u64)player->deaths));
|
|
death->death_pos = guy->xf.t;
|
|
death->death_dir = guy->damage_attribution_dir;
|
|
death->is_death = 1;
|
|
death->death_victim = player->key;
|
|
death->death_killer = killer->key;
|
|
death->lifetime_seconds = P_ObservationDurationSeconds;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
P_SpawnEntsFromList(frame, ents_to_spawn);
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Smooth values
|
|
|
|
for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
|
|
{
|
|
f32 lerp_rate = SaturateF32(25 * sim_dt);
|
|
ent->smoothed_move_dir = SlerpVec2(ent->smoothed_move_dir, NormVec2(ent->control.move), lerp_rate);
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Debug draw
|
|
|
|
P_DebugDrawFrame(frame);
|
|
|
|
//////////////////////////////
|
|
//- End frame
|
|
|
|
frame->time_ns += sim_dt_ns;
|
|
}
|
|
EndScratch(scratch);
|
|
}
|