diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8cfba2d4c..a1f3d29e6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -64,6 +64,8 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32 p_tick.c p_user.c p_slopes.c + p_sweep.cpp + p_test.cpp tables.c r_bsp.cpp r_data.c diff --git a/src/cvars.cpp b/src/cvars.cpp index 4b0897dcf..9336becb2 100644 --- a/src/cvars.cpp +++ b/src/cvars.cpp @@ -806,6 +806,7 @@ consvar_t cv_numlaps = OnlineCheat("numlaps", "Map default").values(numlaps_cons consvar_t cv_restrictskinchange = OnlineCheat("restrictskinchange", "Yes").yes_no().description("Don't let players change their skin in the middle of gameplay"); consvar_t cv_spbtest = OnlineCheat("spbtest", "Off").on_off().description("SPB can never target a player"); +consvar_t cv_showgremlins = OnlineCheat("showgremlins", "No").yes_no().description("Show line collision errors"); consvar_t cv_timescale = OnlineCheat(cvlist_timer)("timescale", "1.0").floating_point().min_max(FRACUNIT/20, 20*FRACUNIT).description("Overclock or slow down the game"); consvar_t cv_ufo_follow = OnlineCheat("ufo_follow", "0").min_max(0, MAXPLAYERS).description("Make UFO Catcher folow this player"); consvar_t cv_ufo_health = OnlineCheat("ufo_health", "-1").min_max(-1, 100).description("Override UFO Catcher health -- applied at spawn or when value is changed"); diff --git a/src/math/fixed.hpp b/src/math/fixed.hpp new file mode 100644 index 000000000..0e5cae4c1 --- /dev/null +++ b/src/math/fixed.hpp @@ -0,0 +1,107 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef math_fixed_hpp +#define math_fixed_hpp + +#include + +#include "traits.hpp" + +#include "../m_fixed.h" + +namespace srb2::math +{ + +struct Fixed +{ + static Fixed copysign(fixed_t x, fixed_t y) { return (x < 0) != (y < 0) ? -x : x; } + static Fixed hypot(fixed_t x, fixed_t y) { return FixedHypot(x, y); } + + constexpr Fixed() : val_(0) {} + constexpr Fixed(fixed_t val) : val_(val) {} + + template , bool> = true> + Fixed(T val) : val_(FloatToFixed(val)) {} + + Fixed(const Fixed& b) = default; + Fixed& operator=(const Fixed& b) = default; + + fixed_t value() const { return val_; } + int sign() const { return val_ < 0 ? -1 : 1; } + + operator fixed_t() const { return val_; } + explicit operator float() const { return FixedToFloat(val_); } + + Fixed& operator+=(const Fixed& b) + { + val_ += b.val_; + return *this; + } + + Fixed& operator-=(const Fixed& b) + { + val_ -= b.val_; + return *this; + } + + Fixed& operator*=(const Fixed& b) + { + val_ = FixedMul(val_, b.val_); + return *this; + } + + Fixed& operator/=(const Fixed& b) + { + val_ = FixedDiv(val_, b.val_); + return *this; + } + + Fixed operator-() const { return -val_; } + +#define X(op) \ + template \ + Fixed operator op(const T& b) const { return val_ op b; } \ + Fixed operator op(const Fixed& b) const \ + { \ + Fixed f{val_};\ + f op##= b;\ + return f;\ + } \ + template \ + Fixed& operator op##=(const T& b) \ + { \ + val_ op##= b; \ + return *this; \ + } + + X(+) + X(-) + X(*) + X(/) + +#undef X + +private: + fixed_t val_; +}; + +template <> +struct Traits +{ + static constexpr Fixed kZero = 0; + static constexpr Fixed kUnit = FRACUNIT; + + static constexpr auto copysign = Fixed::copysign; + static constexpr auto hypot = Fixed::hypot; +}; + +}; // namespace srb2::math + +#endif/*math_fixed_hpp*/ diff --git a/src/math/line_equation.hpp b/src/math/line_equation.hpp new file mode 100644 index 000000000..ec4b69d52 --- /dev/null +++ b/src/math/line_equation.hpp @@ -0,0 +1,102 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef math_line_equation_hpp +#define math_line_equation_hpp + +#include "fixed.hpp" +#include "line_segment.hpp" +#include "vec.hpp" + +namespace srb2::math +{ + +template +struct LineEquation +{ + using vec2 = Vec2; + using line_segment = LineSegment; + + // Fixed-point: shift value by this amount during + // multiplications and divisions to avoid overflows. + static constexpr std::enable_if_t, fixed_t> kF = 1024; // fixed_t, not Fixed + + LineEquation() {} + LineEquation(const vec2& p, const vec2& d) : d_(d), m_(d.y / d.x), b_(p.y - (p.x * m())) {} + LineEquation(const line_segment& l) : LineEquation(l.a, l.b - l.a) {} + + const vec2& d() const { return d_; } + T m() const { return m_; } + T b() const { return b_; } + T y(T x) const { return (m() * x) + b(); } + + vec2 intersect(const LineEquation& q) const + { + T x = (b() - q.b()) / (q.m() - m()); + return {x, y(x)}; + } + +protected: + vec2 d_{}; + T m_{}, b_{}; +}; + +template <> +inline LineEquation::LineEquation(const vec2& p, const vec2& d) : + d_(d), m_((d.y / d.x) / kF), b_((p.y / kF) - (p.x * m_)) +{ +} + +template <> +inline Fixed LineEquation::m() const +{ + return m_ * kF; +} + +template <> +inline Fixed LineEquation::b() const +{ + return b_ * kF; +} + +template <> +inline Fixed LineEquation::y(Fixed x) const +{ + return ((m_ * x) + b_) * kF; +} + +template <> +inline LineEquation::vec2 LineEquation::intersect(const LineEquation& q) const +{ + Fixed x = ((b_ - q.b_) / ((q.m_ - m_) * kF)) * kF; + return {x, y(x)}; +} + +template +struct LineEquationX : LineEquation +{ + T x(T y) const { return (y - LineEquation::b()) / LineEquation::m(); } +}; + +template <> +struct LineEquationX : LineEquation +{ + LineEquationX() {} + LineEquationX(const vec2& p, const vec2& d) : LineEquation(p, d), w_((d.x / d.y) / kF), a_((p.x / kF) - (p.y * w_)) {} + LineEquationX(const line_segment& l) : LineEquationX(l.a, l.b - l.a) {} + + Fixed x(Fixed y) const { return ((w_ * y) + a_) * kF; } + +protected: + Fixed w_{}, a_{}; +}; + +}; // namespace srb2::math + +#endif/*math_line_equation_hpp*/ diff --git a/src/math/line_segment.hpp b/src/math/line_segment.hpp new file mode 100644 index 000000000..0094acf60 --- /dev/null +++ b/src/math/line_segment.hpp @@ -0,0 +1,43 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef math_line_segment_hpp +#define math_line_segment_hpp + +#include +#include + +#include "vec.hpp" + +namespace srb2::math +{ + +template +struct LineSegment +{ + using vec2 = Vec2; + using view = std::pair; + + vec2 a, b; + + LineSegment(vec2 a_, vec2 b_) : a(a_), b(b_) {} + + template + LineSegment(const LineSegment& b) : LineSegment(b.a, b.b) {} + + bool horizontal() const { return a.y == b.y; } + bool vertical() const { return a.x == b.x; } + + view by_x() const { return std::minmax(a, b, [](auto& a, auto& b) { return a.x < b.x; }); } + view by_y() const { return std::minmax(a, b, [](auto& a, auto& b) { return a.y < b.y; }); } +}; + +}; // namespace srb2 + +#endif/*math_line_segment_hpp*/ diff --git a/src/math/traits.hpp b/src/math/traits.hpp new file mode 100644 index 000000000..fbb6aaa5b --- /dev/null +++ b/src/math/traits.hpp @@ -0,0 +1,34 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef math_traits_hpp +#define math_traits_hpp + +#include +#include + +namespace srb2::math +{ + +template +struct Traits; + +template +struct Traits>> +{ + static constexpr T kZero = 0.0; + static constexpr T kUnit = 1.0; + + static T copysign(T x, T y) { return std::copysign(x, y); } + static T hypot(T x, T y) { return std::hypot(x, y); } +}; + +}; // namespace srb2::math + +#endif/*math_traits_hpp*/ diff --git a/src/math/vec.hpp b/src/math/vec.hpp new file mode 100644 index 000000000..670a3677c --- /dev/null +++ b/src/math/vec.hpp @@ -0,0 +1,84 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef math_vec_hpp +#define math_vec_hpp + +#include + +#include "traits.hpp" + +namespace srb2::math +{ + +template +struct Vec2 +{ + T x, y; + + Vec2() : x{}, y{} {} + Vec2(T x_, T y_) : x(x_), y(y_) {} + Vec2(T z) : x(z), y(z) {} + + template + Vec2(const Vec2& b) : Vec2(b.x, b.y) {} + + T magnitude() const { return Traits::hypot(x, y); } + Vec2 normal() const { return {-y, x}; } + +#define X(op) \ + Vec2& operator op##=(const Vec2& b) \ + { \ + x op##= b.x; \ + y op##= b.y; \ + return *this; \ + } \ + Vec2 operator op(const Vec2& b) const { return Vec2(x op b.x, y op b.y); } \ + + X(+) + X(-) + X(*) + X(/) + +#undef X + + Vec2 operator-() const { return Vec2(-x, -y); } +}; + +template +struct is_vec2 : std::false_type {}; + +template +struct is_vec2> : std::true_type {}; + +template +inline constexpr bool is_vec2_v = is_vec2::value; + +#define X(op) \ + template , bool> = true> \ + Vec2 operator op(const T& a, const Vec2& b) \ + { \ + return Vec2 {a} op Vec2 {b}; \ + } \ + template , bool> = true> \ + Vec2 operator op(const Vec2& a, const U& b) \ + { \ + return Vec2 {a} op Vec2 {b}; \ + } \ + +X(+) +X(-) +X(*) +X(/) + +#undef X + +}; // namespace srb2::math + +#endif/*math_vec_hpp*/ diff --git a/src/p_local.h b/src/p_local.h index 0e0a3fb23..045a59472 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -387,9 +387,16 @@ struct tm_t // so missiles don't explode against sky hack walls line_t *ceilingline; - // set by PIT_CheckLine() for any line that stopped the PIT_CheckLine() - // that is, for any line which is 'solid' - line_t *blockingline; + // P_CheckPosition: this position blocks movement + boolean blocking; + + // P_CheckPosition: set this before each call to + // P_CheckPosition to enable a line sweep on collided + // lines + boolean sweep; + + // sweep: max step up at tm.x, tm.y + fixed_t maxstep; }; extern tm_t tm; @@ -415,6 +422,7 @@ struct TryMoveResult_t boolean success; line_t *line; mobj_t *mo; + vector2_t normal; }; boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y, TryMoveResult_t *result); @@ -422,6 +430,10 @@ boolean P_CheckMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff, T boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff, TryMoveResult_t *result); boolean P_SceneryTryMove(mobj_t *thing, fixed_t x, fixed_t y, TryMoveResult_t *result); +void P_TestLine(line_t *ld); +void P_ClearTestLines(void); +line_t *P_SweepTestLines(fixed_t ax, fixed_t ay, fixed_t bx, fixed_t by, fixed_t r, vector2_t *return_normal); + boolean P_IsLineBlocking(const line_t *ld, const mobj_t *thing); boolean P_IsLineTripWire(const line_t *ld); boolean P_CheckCameraPosition(fixed_t x, fixed_t y, camera_t *thiscam); diff --git a/src/p_map.c b/src/p_map.c index 79b9ba59e..683dd3c5d 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -1770,7 +1770,6 @@ static BlockItReturn_t PIT_CheckCameraLine(line_t *ld) // could be crossed in either order. // this line is out of the if so upper and lower textures can be hit by a splat - tm.blockingline = ld; if (!ld->backsector) // one sided line { if (P_PointOnLineSide(mapcampointer->x, mapcampointer->y, ld)) @@ -1841,6 +1840,22 @@ boolean P_IsLineTripWire(const line_t *ld) return ld->tripwire; } +static boolean P_UsingStepUp(mobj_t *thing) +{ + if (thing->flags & MF_NOCLIP) + { + return false; + } + + // orbits have no collision + if (thing->player && thing->player->loop.radius) + { + return false; + } + + return true; +} + // // PIT_CheckLine // Adjusts tm.floorz and tm.ceilingz as lines are contacted @@ -1898,14 +1913,20 @@ static BlockItReturn_t PIT_CheckLine(line_t *ld) // could be crossed in either order. // this line is out of the if so upper and lower textures can be hit by a splat - tm.blockingline = ld; { - UINT8 shouldCollide = LUA_HookMobjLineCollide(tm.thing, tm.blockingline); // checks hook for thing's type + UINT8 shouldCollide = LUA_HookMobjLineCollide(tm.thing, ld); // checks hook for thing's type if (P_MobjWasRemoved(tm.thing)) return BMIT_CONTINUE; // one of them was removed??? if (shouldCollide == 1) - return BMIT_ABORT; // force collide + { + if (tm.sweep) + { + P_TestLine(ld); + } + tm.blocking = true; // force collide + return BMIT_CONTINUE; + } else if (shouldCollide == 2) return BMIT_CONTINUE; // force no collide } @@ -1914,15 +1935,55 @@ static BlockItReturn_t PIT_CheckLine(line_t *ld) { if (P_PointOnLineSide(tm.thing->x, tm.thing->y, ld)) return BMIT_CONTINUE; // don't hit the back side - return BMIT_ABORT; + + if (tm.sweep) + { + P_TestLine(ld); + } + tm.blocking = true; + return BMIT_CONTINUE; } if (P_IsLineBlocking(ld, tm.thing)) - return BMIT_ABORT; + { + if (tm.sweep) + { + P_TestLine(ld); + } + tm.blocking = true; + return BMIT_CONTINUE; + } // set openrange, opentop, openbottom P_LineOpening(ld, tm.thing, &open); + if (tm.sweep && P_UsingStepUp(tm.thing)) + { + // copied from P_TryMove + // TODO: refactor this into one place + if (open.range < tm.thing->height) + { + P_TestLine(ld); + } + else if (tm.maxstep > 0) + { + if (tm.thing->z < open.floor) + { + if (open.floorstep > tm.maxstep) + { + P_TestLine(ld); + } + } + else if (open.ceiling < tm.thing->z + tm.thing->height) + { + if (open.ceilingstep > tm.maxstep) + { + P_TestLine(ld); + } + } + } + } + // adjust floor / ceiling heights if (open.ceiling < tm.ceilingz) { @@ -2042,7 +2103,8 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y, TryMoveResult_t *re tm.bbox[BOXLEFT] = x - tm.thing->radius; newsubsec = R_PointInSubsector(x, y); - tm.ceilingline = tm.blockingline = NULL; + tm.ceilingline = NULL; + tm.blocking = false; // The base floor / ceiling is from the subsector // that contains the point. @@ -2314,23 +2376,33 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y, TryMoveResult_t *re validcount++; + P_ClearTestLines(); + // check lines for (bx = xl; bx <= xh; bx++) { for (by = yl; by <= yh; by++) { - if (!P_BlockLinesIterator(bx, by, PIT_CheckLine)) - { - blockval = false; - } + P_BlockLinesIterator(bx, by, PIT_CheckLine); } } + if (tm.blocking) + { + blockval = false; + } + if (result != NULL) { - result->line = tm.blockingline; + result->line = NULL; result->mo = tm.hitthing; } + else + { + P_ClearTestLines(); + } + + tm.sweep = false; return blockval; } @@ -2379,7 +2451,7 @@ boolean P_CheckCameraPosition(fixed_t x, fixed_t y, camera_t *thiscam) tm.bbox[BOXLEFT] = x - thiscam->radius; newsubsec = R_PointInSubsector(x, y); - tm.ceilingline = tm.blockingline = NULL; + tm.ceilingline = NULL; mapcampointer = thiscam; @@ -2753,22 +2825,6 @@ fixed_t P_GetThingStepUp(mobj_t *thing, fixed_t destX, fixed_t destY) return maxstep; } -static boolean P_UsingStepUp(mobj_t *thing) -{ - if (thing->flags & MF_NOCLIP) - { - return false; - } - - // orbits have no collision - if (thing->player && thing->player->loop.radius) - { - return false; - } - - return true; -} - static boolean increment_move ( mobj_t * thing, @@ -2821,7 +2877,29 @@ increment_move tryy = y; } - if (!P_CheckPosition(thing, tryx, tryy, result)) + if (P_UsingStepUp(thing)) + { + tm.maxstep = P_GetThingStepUp(thing, tryx, tryy); + } + + if (result) + { + tm.sweep = true; + } + + boolean move_ok = P_CheckPosition(thing, tryx, tryy, result); + + if (P_MobjWasRemoved(thing)) + { + return false; + } + + if (result) + { + result->line = P_SweepTestLines(thing->x, thing->y, x, y, thing->radius, &result->normal); + } + + if (!move_ok) { return false; // solid wall or thing } @@ -3466,30 +3544,27 @@ static void P_HitSlideLine(line_t *ld) // // HitBounceLine, for players // -static void P_PlayerHitBounceLine(line_t *ld) +static void P_PlayerHitBounceLine(line_t *ld, vector2_t* normal) { - INT32 side; - angle_t lineangle; fixed_t movelen; fixed_t x, y; - side = P_PointOnLineSide(slidemo->x, slidemo->y, ld); - lineangle = ld->angle - ANGLE_90; - - if (side == 1) - lineangle += ANGLE_180; - - lineangle >>= ANGLETOFINESHIFT; - movelen = P_AproxDistance(tmxmove, tmymove); if (slidemo->player && movelen < (15*mapobjectscale)) movelen = (15*mapobjectscale); - x = FixedMul(movelen, FINECOSINE(lineangle)); - y = FixedMul(movelen, FINESINE(lineangle)); + if (!ld) + { + angle_t th = R_PointToAngle2(0, 0, tmxmove, tmymove); + normal->x = -FCOS(th); + normal->y = -FSIN(th); + } - if (P_IsLineTripWire(ld)) + x = FixedMul(movelen, normal->x); + y = FixedMul(movelen, normal->y); + + if (ld && P_IsLineTripWire(ld)) { tmxmove = x * 4; tmymove = y * 4; @@ -3958,6 +4033,8 @@ papercollision: static void P_BouncePlayerMove(mobj_t *mo, TryMoveResult_t *result) { + extern consvar_t cv_showgremlins; + fixed_t mmomx = 0, mmomy = 0; fixed_t oldmomx = mo->momx, oldmomy = mo->momy; @@ -3982,8 +4059,23 @@ static void P_BouncePlayerMove(mobj_t *mo, TryMoveResult_t *result) slidemo = mo; bestslideline = result->line; - if (bestslideline == NULL) - return; + if (bestslideline == NULL && cv_showgremlins.value) + { + // debug + mobj_t*x = P_SpawnMobj(mo->x, mo->y, mo->z, MT_THOK); + x->frame = FF_FULLBRIGHT | FF_ADD; + x->renderflags = RF_ALWAYSONTOP; + x->color = SKINCOLOR_RED; + + CONS_Printf( + "GREMLIN: leveltime=%u x=%f y=%f z=%f angle=%f\n", + leveltime, + FixedToFloat(mo->x), + FixedToFloat(mo->y), + FixedToFloat(mo->z), + AngleToFloat(R_PointToAngle2(0, 0, oldmomx, oldmomy)) + ); + } if (mo->eflags & MFE_JUSTBOUNCEDWALL) // Stronger push-out { @@ -3996,7 +4088,7 @@ static void P_BouncePlayerMove(mobj_t *mo, TryMoveResult_t *result) tmymove = FixedMul(mmomy, (FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3))); } - if (P_IsLineTripWire(bestslideline)) + if (bestslideline && P_IsLineTripWire(bestslideline)) { // TRIPWIRE CANNOT BE MADE NONBOUNCY K_ApplyTripWire(mo->player, TRIPSTATE_BLOCKED); @@ -4014,7 +4106,7 @@ static void P_BouncePlayerMove(mobj_t *mo, TryMoveResult_t *result) K_SpawnBumpEffect(mo); } - P_PlayerHitBounceLine(bestslideline); + P_PlayerHitBounceLine(bestslideline, &result->normal); mo->eflags |= MFE_JUSTBOUNCEDWALL; mo->momx = tmxmove; @@ -4022,7 +4114,7 @@ static void P_BouncePlayerMove(mobj_t *mo, TryMoveResult_t *result) mo->player->cmomx = tmxmove; mo->player->cmomy = tmymove; - if (!P_IsLineTripWire(bestslideline)) + if (!bestslideline || !P_IsLineTripWire(bestslideline)) { if (!P_TryMove(mo, mo->x + tmxmove, mo->y + tmymove, true, NULL)) { diff --git a/src/p_mobj.c b/src/p_mobj.c index f0baa74e1..447592037 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -1671,7 +1671,7 @@ void P_XYMovement(mobj_t *mo) // blocked move moved = false; - if (LUA_HookMobjMoveBlocked(mo, tm.hitthing, tm.blockingline)) + if (LUA_HookMobjMoveBlocked(mo, tm.hitthing, result.line)) { if (P_MobjWasRemoved(mo)) return; @@ -1679,7 +1679,7 @@ void P_XYMovement(mobj_t *mo) else if (P_MobjWasRemoved(mo)) return; - P_PushSpecialLine(tm.blockingline, mo); + P_PushSpecialLine(result.line, mo); if (mo->flags & MF_MISSILE) { diff --git a/src/p_sweep.cpp b/src/p_sweep.cpp new file mode 100644 index 000000000..d4e9d22ef --- /dev/null +++ b/src/p_sweep.cpp @@ -0,0 +1,271 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include + +#include "p_sweep.hpp" + +using namespace srb2::math; +using namespace srb2::sweep; + +Result SlopeAABBvsLine::vs_slope(const line_segment& l) const +{ + auto [a, b] = l.by_x(); // left, right + LineEquation ql{l}; + unit ls = copysign(kUnit, ql.m()); + + auto hit = [&](const vec2& k, unit xr, unit x, const vec2& n) -> Contact + { + std::optional k2; + + if (l.horizontal()) + { + // Horizontal line: create second contact point on opposite corner. + // TODO: avoid duplicate point + k2 = vec2(std::clamp(x + xr, a.x, b.x), k.y); + } + + return {time(x), n, k, k2}; + }; + + auto slide = [&](const vec2& k, const vec2& s) -> std::optional + { + vec2 kf = k * s; + vec2 r = r_ * s; + vec2 p = k - r; + + // Slide vertically along AABB left/right edge. + unit f = q_.y(p.x) * s.y; + + if (f - r_ > kf.y) + { + // Out of bounds detection. + // This should never slide in front. + // If it does, there was never a hit. + return {}; + } + + if (f + r_ < kf.y) + { + // Slid behind contact point. + // Try sliding horizontally along AABB top/bottom + // edge. + + if (q_.m() == kZero) + { + // Sweep is horizontal. + // It is impossible to slide against a line's + // end by the X axis because the line segment + // lies on that axis. + return {}; + } + + p.x = q_.x(p.y); + f = p.x * s.x; + + if (f - r_ > kf.x) + { + // Slid beyond contact point. + return {}; + } + + if (f + r_ < kf.x) + { + // Out of bounds detection. + // This should never slide behind. + // If it does, there was never a hit. + return {}; + } + + return hit(k, r.x, p.x, {kZero, -s.y}); + } + + return hit(k, r.x, p.x, {-s.x, kZero}); + }; + + // xrs.x = x radius + // xrs.y = x sign + auto bind = [&](const vec2& k, const vec2& xrs, unit ns) -> std::optional + { + if (k.x < a.x) + { + return slide(a, {xrs.y, ls}); + } + + if (k.x > b.x) + { + return slide(b, {xrs.y, -ls}); + } + + return hit(k, xrs.x, k.x + xrs.x, normal(l) * ns); + }; + + if (ql.m() == q_.m()) + { + // Parallel lines can only cross at the ends. + vec2 s{kUnit, ls}; + return order(slide(a, s), slide(b, -s), ds_.x); + } + + vec2 i = ql.intersect(q_); + + // Compare slopes to determine if ray is moving upward or + // downward into line. + // For a positive line, AABB top left corner hits the + // line first if the ray is moving upward. + // Swap diagonal corners to bottom right if moving + // downward. + unit ys = q_.m() * ds_.x < ql.m() * ds_.x ? -kUnit : kUnit; + unit yr = r_ * ys; + + // Swap left/right corners if line is negative. + unit xr = yr * ls; + + // Intersection as if ray were offset -r, +r. + vec2 v = [&] + { + unit y = (q_.m() * xr) + yr; + unit x = y / (ql.m() - q_.m()); + return vec2 {x, (x * q_.m()) + y}; + }(); + + // Find the intersection along diagonally oppposing AABB + // corners. + vec2 xrs{xr, ds_.x}; + return {bind(i + v, xrs, -ys), bind(i - v, -xrs, -ys)}; +} + +// TODO: Comments. Bitch. +Result SlopeAABBvsLine::vs_vertical(const line_segment& l) const +{ + auto [a, b] = l.by_y(); // bottom, top + + auto hit = [&](const vec2& p, std::optional q, unit x, const vec2& n) -> Contact { return {time(x), n, p, q}; }; + + auto bind = [&](const vec2& k, const vec2& a, const vec2& b, const vec2& s, auto limit) -> std::optional + { + vec2 r = r_ * s; + vec2 af = a * s; + unit kyf = k.y * s.y; + + if (kyf + r_ < af.y) + { + if (q_.m() == kZero) + { + return {}; + } + + unit x = q_.x(a.y - r.y); + + if ((x * s.x) - r_ > af.x) + { + return {}; + } + + return hit(a, {}, x, {kZero, -s.y}); + } + + // TODO: avoid duplicate point + vec2 k2{k.x, limit(k.y - r.y, a.y)}; + unit byf = b.y * s.y; + vec2 n{-s.x, kZero}; + + if (kyf + r_ > byf) + { + if (kyf - r_ > byf) + { + return {}; + } + + return hit(b, k2, k.x - r.x, n); + } + + return hit(vec2(k.x, k.y + r.y), k2, k.x - r.x, n); + }; + + vec2 i{a.x, q_.y(a.x)}; + vec2 v{kZero, q_.m() * r_ * ds_.x * ds_.y}; + vec2 s = ds_ * ds_.y; + + // Damn you, template overloads! + auto min = [](unit x, unit y) { return std::min(x, y); }; + auto max = [](unit x, unit y) { return std::max(x, y); }; + + return order(bind(i - v, a, b, s, max), bind(i + v, b, a, -s, min), ds_.y); +} + +Result VerticalAABBvsLine::vs_slope(const line_segment& l) const +{ + auto [a, b] = l.by_x(); // left, right + LineEquation ql{l}; + + auto hit = [&](const vec2& k, unit xr, unit y, const vec2& n) -> Contact + { + std::optional k2; + + if (l.horizontal()) + { + // Horizontal line: create second contact point on opposite corner. + // TODO: avoid duplicate point + k2 = vec2(std::clamp(x_ + xr, a.x, b.x), k.y); + } + + return {time(y), n, k, k2}; + }; + + auto bind = [&](const vec2& a, const vec2& b, const vec2& s) -> std::optional + { + vec2 r = r_ * s; + unit xf = x_ * s.x; + + if (xf - r_ > b.x * s.x) + { + return {}; + } + + unit axf = a.x * s.x; + + if (xf - r_ < axf) + { + if (xf + r_ < axf) + { + return {}; + } + + return hit(a, r.x, a.y - r.y, {kZero, -s.y}); + } + + vec2 i{x_, ql.y(x_)}; + vec2 v{r.x, ql.m() * r.x}; + vec2 k = i - v; + return hit(k, r.x, k.y - r.y, normal(l) * -s.y); + }; + + unit mys = copysign(kUnit, ql.m() * ds_.y); + vec2 s{kUnit, ds_.y * mys}; + return order(bind(a, b, s), bind(b, a, -s), mys); +} + +Result VerticalAABBvsLine::vs_vertical(const line_segment& l) const +{ + // Box does not overlap Y plane. + if (x_ + r_ < l.a.x || x_ - r_ > l.a.x) + { + return {}; + } + + auto [a, b] = l.by_y(); // bottom, top + + auto hit = [&](const vec2& k, unit yr) -> Contact { return {time(k.y + yr), {kZero, -ds_.y}, k}; }; + + // Offset away from line ends. + // Contacts are opposite when swept downward. + return order(hit(a, -r_), hit(b, r_), ds_.y); +} diff --git a/src/p_sweep.hpp b/src/p_sweep.hpp new file mode 100644 index 000000000..68c6ac359 --- /dev/null +++ b/src/p_sweep.hpp @@ -0,0 +1,131 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef p_sweep_hpp +#define p_sweep_hpp + +#include +#include + +#include "math/fixed.hpp" +#include "math/line_equation.hpp" +#include "math/line_segment.hpp" +#include "math/vec.hpp" + +namespace srb2::sweep +{ + +using unit = math::Fixed; +using vec2 = math::Vec2; +using line_segment = math::LineSegment; + +struct Contact +{ + unit z; // time + vec2 n; // normal TODO REMOVE duplicate for each contact + vec2 p; // contact point 1 + std::optional q; // AABBvsLine: contact point 2 +}; + +struct Result +{ + std::optional hit, exit; // TODO result itself should be optional, not each contact +}; + +namespace detail +{ + +template +struct BaseAABBvsLine : protected srb2::math::Traits +{ +public: + Result operator()(const line_segment& l) const + { + auto derived = static_cast(this); + return l.vertical() ? derived->vs_vertical(l) : derived->vs_slope(l); + } + +protected: + unit r_; // AABB radius + vec2 ds_; // sweep direction signs + + BaseAABBvsLine(unit r, const vec2& d, unit pz, unit dz) : + r_(r), ds_(copysign(kUnit, d.x), copysign(kUnit, d.y)), t_(pz, dz) {} + + unit time(unit x) const { return (x - t_.x) / t_.y; } + + static Result order(std::optional&& t1, std::optional&& t2, unit s) + { + return s > kZero ? Result {t1, t2} : Result {t2, t1}; + } + + static vec2 normal(const vec2& v) + { + // Normalize vector so that x is positive -- normal always points up. + return v.normal() * (copysign(kUnit, v.x) / v.magnitude()); + } + + static vec2 normal(const line_segment& l) { return normal(l.b - l.a); } + +private: + vec2 t_; // origin and length for calculating time +}; + +}; // namespace detail + +// Sweep can be represented as y = mx + b +struct SlopeAABBvsLine : detail::BaseAABBvsLine +{ + SlopeAABBvsLine(unit r, const line_segment& l) : SlopeAABBvsLine(r, l.a, l.b - l.a) {} + + Result vs_slope(const line_segment& l) const; + Result vs_vertical(const line_segment& l) const; + +private: + math::LineEquationX q_; + + SlopeAABBvsLine(unit r, const vec2& p, const vec2& d) : BaseAABBvsLine(r, d, p.x, d.x), q_(p, d) {} +}; + +// Sweep is vertical +struct VerticalAABBvsLine : detail::BaseAABBvsLine +{ + VerticalAABBvsLine(unit r, const line_segment& l) : VerticalAABBvsLine(r, l.a, l.b - l.a) {} + + Result vs_slope(const line_segment& l) const; + Result vs_vertical(const line_segment& l) const; + +private: + unit x_; + + VerticalAABBvsLine(unit r, const vec2& p, const vec2& d) : BaseAABBvsLine(r, d, p.y, d.y), x_(p.x) {} +}; + +struct AABBvsLine +{ + AABBvsLine(unit r, const line_segment& l) : + var_(l.vertical() ? var_t {VerticalAABBvsLine(r, l)} : var_t {SlopeAABBvsLine(r, l)}) + { + } + + Result operator()(const line_segment& l) const + { + Result rs; + std::visit([&](auto& sweeper) { rs = sweeper(l); }, var_); + return rs; + } + +private: + using var_t = std::variant; + var_t var_; +}; + +}; // namespace srb2::sweep + +#endif/*p_sweep_hpp*/ diff --git a/src/p_test.cpp b/src/p_test.cpp new file mode 100644 index 000000000..e362f33a5 --- /dev/null +++ b/src/p_test.cpp @@ -0,0 +1,78 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include + +#include "math/fixed.hpp" +#include "p_sweep.hpp" + +#include "p_local.h" + +namespace +{ + +std::vector g_lines; + +}; + +void P_TestLine(line_t* ld) +{ + g_lines.emplace_back(ld); +} + +line_t* P_SweepTestLines(fixed_t ax, fixed_t ay, fixed_t bx, fixed_t by, fixed_t r, vector2_t* return_normal) +{ + using namespace srb2::math; + using namespace srb2::sweep; + + struct Collision + { + unit z; + vec2 normal; + line_t* ld; + + bool operator<(const Collision& b) const { return z < b.z; } + }; + + std::optional collision; + + LineSegment l{{ax, ay}, {bx, by}}; + AABBvsLine sweep{r, l}; + + for (line_t* ld : g_lines) + { + LineSegment ls{{ld->v1->x, ld->v1->y}, {ld->v2->x, ld->v2->y}}; + Result rs = sweep(ls); + if (rs.hit) + { + if (!collision || rs.hit->z < collision->z) + { + collision = {rs.hit->z, rs.hit->n, ld}; + } + } + } + + g_lines.clear(); + + if (!collision) + { + return nullptr; + } + + return_normal->x = Fixed {collision->normal.x}; + return_normal->y = Fixed {collision->normal.y}; + + return collision->ld; +} + +void P_ClearTestLines(void) +{ + g_lines.clear(); +}