diff --git a/Assets/Prefabs/Player.prefab b/Assets/Prefabs/Player.prefab
index 8e8e2aa..7c08607 100644
--- a/Assets/Prefabs/Player.prefab
+++ b/Assets/Prefabs/Player.prefab
@@ -126,6 +126,7 @@ MonoBehaviour:
gravity: -14.715
groundedStickVelocity: -2
fallAnimationSpeed: 0.1
+ rescueMinY: -100
slideDownSteepSlopes: 1
slideAcceleration: 20
maxSlideSpeed: 8
@@ -144,6 +145,7 @@ MonoBehaviour:
wallExitGrace: 0.15
ceilingGrabDistance: 0.02
wallTopOutBoost: 0.3
+ noClimbTag: NoClimb
cameraOrbit: {fileID: 0}
meshOffsetTarget: {fileID: 0}
cameraTarget: {fileID: 4348940595172967010}
diff --git a/Assets/Scripts/PlayerCharacterController.cs b/Assets/Scripts/PlayerCharacterController.cs
index 5296df9..e846861 100644
--- a/Assets/Scripts/PlayerCharacterController.cs
+++ b/Assets/Scripts/PlayerCharacterController.cs
@@ -75,6 +75,12 @@ public class PlayerCharacterController : MonoBehaviour
"animation. Only while airborne (not grounded, not wall-climbing). Scales with player size.")]
public float fallAnimationSpeed = 4f;
+ [Header("Rescue")]
+ [Tooltip("Kill-plane fail-safe: if the player falls below this world Y (e.g. off the map into the void), " +
+ "they are teleported back to where this avatar spawned (spawn center + per-client ring offset) " +
+ "with all momentum cleared. Owner-only; the authoritative NetworkTransform replicates the reset.")]
+ public float rescueMinY = -100f;
+
[Header("Slope Sliding")]
[Tooltip("Slide down surfaces steeper than the CharacterController's Slope Limit instead of balancing on them.")]
public bool slideDownSteepSlopes = true;
@@ -136,6 +142,9 @@ public class PlayerCharacterController : MonoBehaviour
[Tooltip("Upward hop when topping out over a wall/slope edge, so holding W carries you onto the ledge instead of dropping back onto the face and re-grabbing. In meters of jump height; 0 = no hop.")]
public float wallTopOutBoost = 0.3f;
+ [Tooltip("Colliders with this tag are invisible to wall-climb and ceiling-grab: they still block the capsule physically, but can never be grabbed, re-probed onto, or used as a ceiling. Leave empty to disable.")]
+ public string noClimbTag = "NoClimb";
+
[Tooltip("Camera orbit input; paused while dragging the wall-climb offset. Auto-found if empty.")]
[SerializeField] private ThirdPersonOrbitInput cameraOrbit;
@@ -224,6 +233,8 @@ public class PlayerCharacterController : MonoBehaviour
private bool _movingOnGround; // snapshot: grounded & moving above the run threshold (exposed via IsMovingOnGround)
private bool _isFalling; // snapshot: airborne & descending past the fall threshold (exposed via IsFalling)
private float _groundOffset; // signed vertical mesh raise from the ground mouse pose-adjust (+ up / - down); folded into SetMeshLean
+ private Vector3 _spawnPosition; // world position this avatar spawned at; the rescue (kill-plane) teleport target
+ private bool _spawnCaptured; // false until the first Update captures the spawn position (after OnNetworkSpawn's ring offset has landed)
// Ground contact normal from the latest Move (for slope sliding).
private Vector3 _groundNormal = Vector3.up;
@@ -304,6 +315,21 @@ public class PlayerCharacterController : MonoBehaviour
void Update()
{
+ // Kill-plane fail-safe (owner-only: the controller is disabled on remotes). Capture the spawn
+ // point on the first frame -- by now OnNetworkSpawn's ring offset has landed -- then snap the
+ // player back if they fall through the world. Runs ahead of the control gate so it rescues in
+ // any mode (paint/free-cam included).
+ if (!_spawnCaptured)
+ {
+ _spawnPosition = transform.position;
+ _spawnCaptured = true;
+ }
+ else if (transform.position.y < rescueMinY)
+ {
+ RescueToSpawn();
+ return;
+ }
+
float h = 0f, v = 0f;
bool jumpPressed = false;
bool sprinting = false;
@@ -471,6 +497,24 @@ public class PlayerCharacterController : MonoBehaviour
_isFalling = !_controller.isGrounded && _verticalVelocity < -fallAnimationSpeed * CapsuleScale;
}
+ ///
+ /// Kill-plane rescue: snap the avatar back to its captured spawn point and clear all momentum. The
+ /// CharacterController overrides direct transform writes, so it is toggled around the move (same
+ /// pattern as ). Owner-only; the authoritative
+ /// NetworkTransform replicates the new position to remotes.
+ ///
+ private void RescueToSpawn()
+ {
+ bool reenable = _controller && _controller.enabled;
+ if (_controller) _controller.enabled = false;
+ transform.position = _spawnPosition;
+ if (_controller) _controller.enabled = reenable;
+
+ _verticalVelocity = 0f;
+ _horizontalVelocity = Vector3.zero;
+ _slideSpeed = 0f;
+ }
+
///
/// Enters wall-climb mode only when the move input actively pushes into a wall
/// (not on mere contact). Leaving while climbing is handled in
@@ -950,6 +994,13 @@ public class PlayerCharacterController : MonoBehaviour
///
private static bool IsPlayerCollider(Collider c) => c.GetComponentInParent();
+ ///
+ /// True if the collider is tagged as non-climbable (): every climb probe
+ /// and the grab detector skip it, so such a wall/ceiling blocks the capsule but is never grabbed,
+ /// re-probed onto, or treated as a ceiling. Empty/unset tag disables the check (never throws).
+ ///
+ private bool IsNoClimb(Collider c) => !string.IsNullOrEmpty(noClimbTag) && c && c.CompareTag(noClimbTag);
+
///
/// Raycasts from the character center toward the surface (along -_wallNormal) to find the
/// current normal and the gap (surface clearance) to it. The capsule's support distance
@@ -973,7 +1024,7 @@ public class PlayerCharacterController : MonoBehaviour
bool found = false;
for (int i = 0; i < count; i++)
{
- if (IsPlayerCollider(_wallProbe[i].collider)) // skip self and other player avatars
+ if (IsPlayerCollider(_wallProbe[i].collider) || IsNoClimb(_wallProbe[i].collider)) // skip self, other player avatars, and no-climb surfaces
continue;
if (_wallProbe[i].distance < best)
{
@@ -1018,7 +1069,7 @@ public class PlayerCharacterController : MonoBehaviour
bool found = false;
for (int i = 0; i < count; i++)
{
- if (IsPlayerCollider(_wallProbe[i].collider))
+ if (IsPlayerCollider(_wallProbe[i].collider) || IsNoClimb(_wallProbe[i].collider))
continue;
if (_wallProbe[i].normal.y >= CeilingNormalThreshold) // not a ceiling
continue;
@@ -1090,7 +1141,7 @@ public class PlayerCharacterController : MonoBehaviour
origin, -_wallNormal, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
for (int i = 0; i < count; i++)
{
- if (IsPlayerCollider(_wallProbe[i].collider))
+ if (IsPlayerCollider(_wallProbe[i].collider) || IsNoClimb(_wallProbe[i].collider))
continue;
return true;
}
@@ -1198,7 +1249,7 @@ public class PlayerCharacterController : MonoBehaviour
///
private void OnControllerColliderHit(ControllerColliderHit hit)
{
- if (enableWallClimb)
+ if (enableWallClimb && !IsNoClimb(hit.collider))
{
float angle = Vector3.Angle(hit.normal, Vector3.up);
if (angle >= wallMinAngle)
diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset
index 6413d11..4a02290 100644
--- a/ProjectSettings/TagManager.asset
+++ b/ProjectSettings/TagManager.asset
@@ -2,8 +2,9 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!78 &1
TagManager:
- serializedVersion: 2
- tags: []
+ serializedVersion: 3
+ tags:
+ - NoClimb
layers:
- Default
- TransparentFX
@@ -50,27 +51,3 @@ TagManager:
- Light Layer 5
- Light Layer 6
- Light Layer 7
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -