Add NoClimb climb filter and kill-plane rescue to player controller
Two player-controller additions: NoClimb: colliders tagged "NoClimb" are now invisible to wall-climb and ceiling-grab. The grab detector (OnControllerColliderHit) and all three climb probes (ProbeWall, ProbeCeiling, WallInFront) skip them, so such a surface still blocks the capsule physically but can never be grabbed, re-probed onto, or treated as a ceiling. WalkableGroundBelow is left untouched (it only accepts walkable ground). The tag is read from a serialized field (default "NoClimb"); an empty tag disables the check. Rescue: a kill-plane fail-safe. The owner captures its spawn point on the first Update frame (after OnNetworkSpawn's ring offset has landed) and, if it falls below rescueMinY (-100), teleports back there with all momentum cleared, toggling the CharacterController around the write like PlaceOnSpawnRing. Owner-only; the authoritative NetworkTransform replicates the reset. Runs ahead of the control gate so it rescues in any mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
dadf9d88d4
commit
198db9a3ee
3 files changed
+60
-30
No files matched your search
@@ -126,6 +126,7 @@ MonoBehaviour:
|
|||||||
gravity: -14.715
|
gravity: -14.715
|
||||||
groundedStickVelocity: -2
|
groundedStickVelocity: -2
|
||||||
fallAnimationSpeed: 0.1
|
fallAnimationSpeed: 0.1
|
||||||
|
rescueMinY: -100
|
||||||
slideDownSteepSlopes: 1
|
slideDownSteepSlopes: 1
|
||||||
slideAcceleration: 20
|
slideAcceleration: 20
|
||||||
maxSlideSpeed: 8
|
maxSlideSpeed: 8
|
||||||
@@ -144,6 +145,7 @@ MonoBehaviour:
|
|||||||
wallExitGrace: 0.15
|
wallExitGrace: 0.15
|
||||||
ceilingGrabDistance: 0.02
|
ceilingGrabDistance: 0.02
|
||||||
wallTopOutBoost: 0.3
|
wallTopOutBoost: 0.3
|
||||||
|
noClimbTag: NoClimb
|
||||||
cameraOrbit: {fileID: 0}
|
cameraOrbit: {fileID: 0}
|
||||||
meshOffsetTarget: {fileID: 0}
|
meshOffsetTarget: {fileID: 0}
|
||||||
cameraTarget: {fileID: 4348940595172967010}
|
cameraTarget: {fileID: 4348940595172967010}
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ public class PlayerCharacterController : MonoBehaviour
|
|||||||
"animation. Only while airborne (not grounded, not wall-climbing). Scales with player size.")]
|
"animation. Only while airborne (not grounded, not wall-climbing). Scales with player size.")]
|
||||||
public float fallAnimationSpeed = 4f;
|
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")]
|
[Header("Slope Sliding")]
|
||||||
[Tooltip("Slide down surfaces steeper than the CharacterController's Slope Limit instead of balancing on them.")]
|
[Tooltip("Slide down surfaces steeper than the CharacterController's Slope Limit instead of balancing on them.")]
|
||||||
public bool slideDownSteepSlopes = true;
|
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.")]
|
[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;
|
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.")]
|
[Tooltip("Camera orbit input; paused while dragging the wall-climb offset. Auto-found if empty.")]
|
||||||
[SerializeField] private ThirdPersonOrbitInput cameraOrbit;
|
[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 _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 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 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).
|
// Ground contact normal from the latest Move (for slope sliding).
|
||||||
private Vector3 _groundNormal = Vector3.up;
|
private Vector3 _groundNormal = Vector3.up;
|
||||||
@@ -304,6 +315,21 @@ public class PlayerCharacterController : MonoBehaviour
|
|||||||
|
|
||||||
void Update()
|
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;
|
float h = 0f, v = 0f;
|
||||||
bool jumpPressed = false;
|
bool jumpPressed = false;
|
||||||
bool sprinting = false;
|
bool sprinting = false;
|
||||||
@@ -471,6 +497,24 @@ public class PlayerCharacterController : MonoBehaviour
|
|||||||
_isFalling = !_controller.isGrounded && _verticalVelocity < -fallAnimationSpeed * CapsuleScale;
|
_isFalling = !_controller.isGrounded && _verticalVelocity < -fallAnimationSpeed * CapsuleScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="NetworkPlayerSetup.PlaceOnSpawnRing"/>). Owner-only; the authoritative
|
||||||
|
/// NetworkTransform replicates the new position to remotes.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enters wall-climb mode only when the move input actively pushes into a wall
|
/// Enters wall-climb mode only when the move input actively pushes into a wall
|
||||||
/// (not on mere contact). Leaving while climbing is handled in
|
/// (not on mere contact). Leaving while climbing is handled in
|
||||||
@@ -950,6 +994,13 @@ public class PlayerCharacterController : MonoBehaviour
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool IsPlayerCollider(Collider c) => c.GetComponentInParent<PlayerCharacterController>();
|
private static bool IsPlayerCollider(Collider c) => c.GetComponentInParent<PlayerCharacterController>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the collider is tagged as non-climbable (<see cref="noClimbTag"/>): 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).
|
||||||
|
/// </summary>
|
||||||
|
private bool IsNoClimb(Collider c) => !string.IsNullOrEmpty(noClimbTag) && c && c.CompareTag(noClimbTag);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raycasts from the character center toward the surface (along -_wallNormal) to find the
|
/// 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
|
/// 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;
|
bool found = false;
|
||||||
for (int i = 0; i < count; i++)
|
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;
|
continue;
|
||||||
if (_wallProbe[i].distance < best)
|
if (_wallProbe[i].distance < best)
|
||||||
{
|
{
|
||||||
@@ -1018,7 +1069,7 @@ public class PlayerCharacterController : MonoBehaviour
|
|||||||
bool found = false;
|
bool found = false;
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
if (IsPlayerCollider(_wallProbe[i].collider))
|
if (IsPlayerCollider(_wallProbe[i].collider) || IsNoClimb(_wallProbe[i].collider))
|
||||||
continue;
|
continue;
|
||||||
if (_wallProbe[i].normal.y >= CeilingNormalThreshold) // not a ceiling
|
if (_wallProbe[i].normal.y >= CeilingNormalThreshold) // not a ceiling
|
||||||
continue;
|
continue;
|
||||||
@@ -1090,7 +1141,7 @@ public class PlayerCharacterController : MonoBehaviour
|
|||||||
origin, -_wallNormal, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
|
origin, -_wallNormal, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
if (IsPlayerCollider(_wallProbe[i].collider))
|
if (IsPlayerCollider(_wallProbe[i].collider) || IsNoClimb(_wallProbe[i].collider))
|
||||||
continue;
|
continue;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1198,7 +1249,7 @@ public class PlayerCharacterController : MonoBehaviour
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnControllerColliderHit(ControllerColliderHit hit)
|
private void OnControllerColliderHit(ControllerColliderHit hit)
|
||||||
{
|
{
|
||||||
if (enableWallClimb)
|
if (enableWallClimb && !IsNoClimb(hit.collider))
|
||||||
{
|
{
|
||||||
float angle = Vector3.Angle(hit.normal, Vector3.up);
|
float angle = Vector3.Angle(hit.normal, Vector3.up);
|
||||||
if (angle >= wallMinAngle)
|
if (angle >= wallMinAngle)
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
%TAG !u! tag:unity3d.com,2011:
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
--- !u!78 &1
|
--- !u!78 &1
|
||||||
TagManager:
|
TagManager:
|
||||||
serializedVersion: 2
|
serializedVersion: 3
|
||||||
tags: []
|
tags:
|
||||||
|
- NoClimb
|
||||||
layers:
|
layers:
|
||||||
- Default
|
- Default
|
||||||
- TransparentFX
|
- TransparentFX
|
||||||
@@ -50,27 +51,3 @@ TagManager:
|
|||||||
- Light Layer 5
|
- Light Layer 5
|
||||||
- Light Layer 6
|
- Light Layer 6
|
||||||
- Light Layer 7
|
- Light Layer 7
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
-
|
|
||||||
Reference in new issue
Block a user