@@ -14,6 +14,107 @@ namespace {
1414constexpr float kGravity = -9 .81f ;
1515constexpr float kJumpSpeed = 5 .0f ;
1616constexpr float kPitchMax = 1 .48f ; // ~85 degrees
17+ constexpr float kCollisionEpsilon = 1 .0e-4f ;
18+
19+ bool verticallyOverlaps (const WalkCollider& c, float feetY, float height) {
20+ return feetY < c.maxY - kCollisionEpsilon &&
21+ feetY + height > c.minY + kCollisionEpsilon ;
22+ }
23+
24+ bool horizontallyOverlaps (const WalkCollider& c, float x, float z, float radius) {
25+ return x >= c.minX - radius && x <= c.maxX + radius &&
26+ z >= c.minZ - radius && z <= c.maxZ + radius;
27+ }
28+
29+ // Raycast the player's horizontal centre through an AABB expanded by the
30+ // player radius. This is a swept test, so a long frame cannot tunnel through
31+ // a thin wall. normalX/normalZ identify the face that blocks the motion.
32+ bool sweepExpandedAabb (const WalkCollider& c, float x, float z, float dx, float dz,
33+ float radius, float & hitT, float & normalX, float & normalZ) {
34+ const float minX = c.minX - radius, maxX = c.maxX + radius;
35+ const float minZ = c.minZ - radius, maxZ = c.maxZ + radius;
36+ float entryX, exitX, entryZ, exitZ;
37+ if (std::abs (dx) < kCollisionEpsilon ) {
38+ if (x < minX || x > maxX) return false ;
39+ entryX = -INFINITY ; exitX = INFINITY ;
40+ } else if (dx > 0 .0f ) {
41+ entryX = (minX - x) / dx; exitX = (maxX - x) / dx;
42+ } else {
43+ entryX = (maxX - x) / dx; exitX = (minX - x) / dx;
44+ }
45+ if (std::abs (dz) < kCollisionEpsilon ) {
46+ if (z < minZ || z > maxZ) return false ;
47+ entryZ = -INFINITY ; exitZ = INFINITY ;
48+ } else if (dz > 0 .0f ) {
49+ entryZ = (minZ - z) / dz; exitZ = (maxZ - z) / dz;
50+ } else {
51+ entryZ = (maxZ - z) / dz; exitZ = (minZ - z) / dz;
52+ }
53+
54+ const float entry = std::max (entryX, entryZ);
55+ const float exit = std::min (exitX, exitZ);
56+ if (entry > exit || exit < 0 .0f || entry > 1 .0f ) return false ;
57+
58+ hitT = std::max (0 .0f , entry);
59+ normalX = normalZ = 0 .0f ;
60+ if (entryX > entryZ) normalX = dx > 0 .0f ? -1 .0f : 1 .0f ;
61+ else normalZ = dz > 0 .0f ? -1 .0f : 1 .0f ;
62+ return true ;
63+ }
64+
65+ void pushOutOfOverlaps (float & x, float & z, float feetY, float height, float radius,
66+ std::span<const WalkCollider> colliders) {
67+ // A walk can start inside a newly-enabled collider. Resolve that state
68+ // deterministically before the sweep, choosing the nearest expanded face.
69+ for (int pass = 0 ; pass < 4 ; ++pass) {
70+ bool moved = false ;
71+ for (const auto & c : colliders) {
72+ if (!verticallyOverlaps (c, feetY, height) || !horizontallyOverlaps (c, x, z, radius)) continue ;
73+ const float left = x - (c.minX - radius);
74+ const float right = (c.maxX + radius) - x;
75+ const float front = z - (c.minZ - radius);
76+ const float back = (c.maxZ + radius) - z;
77+ const float nearest = std::min ({left, right, front, back});
78+ if (nearest == left) x = c.minX - radius - kCollisionEpsilon ;
79+ else if (nearest == right) x = c.maxX + radius + kCollisionEpsilon ;
80+ else if (nearest == front) z = c.minZ - radius - kCollisionEpsilon ;
81+ else z = c.maxZ + radius + kCollisionEpsilon ;
82+ moved = true ;
83+ }
84+ if (!moved) return ;
85+ }
86+ }
87+
88+ void moveHorizontally (float & x, float & z, float feetY, float height, float radius,
89+ float dx, float dz, std::span<const WalkCollider> colliders) {
90+ pushOutOfOverlaps (x, z, feetY, height, radius, colliders);
91+ for (int pass = 0 ; pass < 4 && (std::abs (dx) > kCollisionEpsilon || std::abs (dz) > kCollisionEpsilon ); ++pass) {
92+ float bestT = 1 .0f , normalX = 0 .0f , normalZ = 0 .0f ;
93+ bool blocked = false ;
94+ for (const auto & c : colliders) {
95+ if (!verticallyOverlaps (c, feetY, height)) continue ;
96+ float hitT, hitNormalX, hitNormalZ;
97+ if (sweepExpandedAabb (c, x, z, dx, dz, radius, hitT, hitNormalX, hitNormalZ) && hitT < bestT) {
98+ bestT = hitT;
99+ normalX = hitNormalX;
100+ normalZ = hitNormalZ;
101+ blocked = true ;
102+ }
103+ }
104+ if (!blocked) { x += dx; z += dz; return ; }
105+
106+ const float safeT = std::max (0 .0f , bestT - kCollisionEpsilon );
107+ x += dx * safeT;
108+ z += dz * safeT;
109+ const float remaining = 1 .0f - bestT;
110+ dx *= remaining;
111+ dz *= remaining;
112+ // Remove the component into the blocking face, preserving the
113+ // tangential component so diagonal movement slides along walls.
114+ if (normalX != 0 .0f ) dx = 0 .0f ;
115+ if (normalZ != 0 .0f ) dz = 0 .0f ;
116+ }
117+ }
17118} // namespace
18119
19120void WalkController::enter (Vector3 cameraPos, float cameraYaw) {
@@ -38,7 +139,8 @@ WalkController::ExitCameraState WalkController::exit() {
38139}
39140
40141std::optional<WalkController::ExitCameraState> WalkController::update (
41- float dt, const KeyboardState& ks, int mouseDx, int mouseDy)
142+ float dt, const KeyboardState& ks, int mouseDx, int mouseDy,
143+ std::span<const WalkCollider> colliders)
42144{
43145 if (ks.IsKeyDown (Keys::Escape))
44146 return exit ();
@@ -67,14 +169,10 @@ std::optional<WalkController::ExitCameraState> WalkController::update(
67169 float sinY = std::sin (yaw_);
68170 float cosY = std::cos (yaw_);
69171
70- if (fwd) {
71- posX_ += sinY * speed * dt;
72- posZ_ -= cosY * speed * dt;
73- }
74- if (back) {
75- posX_ -= sinY * speed * dt;
76- posZ_ += cosY * speed * dt;
77- }
172+ float moveX = 0 .0f , moveZ = 0 .0f ;
173+ if (fwd) { moveX += sinY * speed * dt; moveZ -= cosY * speed * dt; }
174+ if (back) { moveX -= sinY * speed * dt; moveZ += cosY * speed * dt; }
175+ moveHorizontally (posX_, posZ_, posY_, height, collisionRadius, moveX, moveZ, colliders);
78176
79177 // Jump: Ctrl (only when on ground)
80178 bool ctrl = ks.IsKeyDown (Keys::LeftControl) || ks.IsKeyDown (Keys::RightControl);
@@ -83,14 +181,38 @@ std::optional<WalkController::ExitCameraState> WalkController::update(
83181 onGround_ = false ;
84182 }
85183
86- // Gravity + ground collision
184+ // Gravity + y=0 ground collision + opt-in box-collider floors/ceilings.
87185 velY_ += kGravity * dt;
88- posY_ += velY_ * dt;
89-
90- if (posY_ <= 0 .0f ) {
91- posY_ = 0 .0f ;
92- velY_ = 0 .0f ;
93- onGround_ = true ;
186+ const float oldY = posY_;
187+ float nextY = posY_ + velY_ * dt;
188+ onGround_ = false ;
189+ if (velY_ <= 0 .0f ) {
190+ float landingY = 0 .0f ;
191+ for (const auto & c : colliders) {
192+ if (!horizontallyOverlaps (c, posX_, posZ_, collisionRadius)) continue ;
193+ if (oldY >= c.maxY - kCollisionEpsilon && nextY <= c.maxY + kCollisionEpsilon )
194+ landingY = std::max (landingY, c.maxY );
195+ }
196+ if (nextY <= landingY) {
197+ posY_ = landingY;
198+ velY_ = 0 .0f ;
199+ onGround_ = true ;
200+ } else {
201+ posY_ = nextY;
202+ }
203+ } else {
204+ float ceilingY = INFINITY ;
205+ for (const auto & c : colliders) {
206+ if (!horizontallyOverlaps (c, posX_, posZ_, collisionRadius)) continue ;
207+ if (oldY + height <= c.minY + kCollisionEpsilon && nextY + height >= c.minY - kCollisionEpsilon )
208+ ceilingY = std::min (ceilingY, c.minY );
209+ }
210+ if (ceilingY < INFINITY ) {
211+ posY_ = ceilingY - height;
212+ velY_ = 0 .0f ;
213+ } else {
214+ posY_ = nextY;
215+ }
94216 }
95217
96218 return std::nullopt ;
0 commit comments