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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -