Freeze players during map load: kill gravity and hide own mesh
- PlayerCharacterController: GravityEnabled toggle. When off, hold vertical velocity at zero and skip steep-slope sliding so a frozen avatar neither falls nor drifts; true is byte-for-byte the old path. - GameModeManager: during Loading, additionally disable gravity and hide the local body (SkinnedMesh + fur shell), re-asserted each frame to catch the mid-load lobby->match respawn. Restored (body shown, gravity on) before ActiveMode.Enter() on release to Hide. - MatchManager: enter Loading *before* SwapScenes so still-bound lobby avatars are frozen before their floor unloads. Fixes the player falling into the void during the lobby->map scene swap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
fba4780c0e
commit
a2c8a2c75d
3 files changed
+74
-9
No files matched your search
@@ -151,6 +151,14 @@ public class PlayerCharacterController : MonoBehaviour
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ControlEnabled { get; set; } = true;
|
public bool ControlEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When false, gravity, ground-stick and slope-sliding are suspended and the vertical velocity is
|
||||||
|
/// held at rest, so the avatar hovers at its spawn position instead of settling. Set by the
|
||||||
|
/// GameModeManager during the pre-Hide loading barrier (alongside <see cref="ControlEnabled"/>) and
|
||||||
|
/// restored when the Hide phase begins.
|
||||||
|
/// </summary>
|
||||||
|
public bool GravityEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When set, the body faces this world-space yaw (degrees) every frame instead of turning toward its
|
/// When set, the body faces this world-space yaw (degrees) every frame instead of turning toward its
|
||||||
/// movement direction. First-person sets it so the character always faces where the camera looks;
|
/// movement direction. First-person sets it so the character always faces where the camera looks;
|
||||||
@@ -418,20 +426,30 @@ public class PlayerCharacterController : MonoBehaviour
|
|||||||
// (also covers balancing on a sharp box edge).
|
// (also covers balancing on a sharp box edge).
|
||||||
float slopeAngle = Vector3.Angle(_groundNormal, Vector3.up);
|
float slopeAngle = Vector3.Angle(_groundNormal, Vector3.up);
|
||||||
bool onSteepSlope = slideDownSteepSlopes
|
bool onSteepSlope = slideDownSteepSlopes
|
||||||
|
&& GravityEnabled
|
||||||
&& _controller.isGrounded
|
&& _controller.isGrounded
|
||||||
&& slopeAngle > _controller.slopeLimit + 0.01f;
|
&& slopeAngle > _controller.slopeLimit + 0.01f;
|
||||||
|
|
||||||
if (_controller.isGrounded)
|
if (!GravityEnabled)
|
||||||
{
|
{
|
||||||
if (_verticalVelocity < 0f)
|
// Loading barrier freeze: no gravity or ground-stick, hold vertical velocity at rest so the
|
||||||
_verticalVelocity = groundedStickVelocity;
|
// avatar keeps its spawn position (movement input is already cut via ControlEnabled).
|
||||||
|
_verticalVelocity = 0f;
|
||||||
// No jumping off a slope too steep to stand on.
|
|
||||||
if (jumpPressed && !onSteepSlope)
|
|
||||||
_verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_controller.isGrounded)
|
||||||
|
{
|
||||||
|
if (_verticalVelocity < 0f)
|
||||||
|
_verticalVelocity = groundedStickVelocity;
|
||||||
|
|
||||||
_verticalVelocity += gravity * Time.deltaTime;
|
// No jumping off a slope too steep to stand on.
|
||||||
|
if (jumpPressed && !onSteepSlope)
|
||||||
|
_verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
||||||
|
}
|
||||||
|
|
||||||
|
_verticalVelocity += gravity * Time.deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
Vector3 slideVelocity = Vector3.zero;
|
Vector3 slideVelocity = Vector3.zero;
|
||||||
if (onSteepSlope)
|
if (onSteepSlope)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Unity.Cinemachine;
|
using Unity.Cinemachine;
|
||||||
|
using Chameleon.Components;
|
||||||
#if ENABLE_INPUT_SYSTEM
|
#if ENABLE_INPUT_SYSTEM
|
||||||
using UnityEngine.InputSystem;
|
using UnityEngine.InputSystem;
|
||||||
#endif
|
#endif
|
||||||
@@ -329,14 +330,24 @@ public class GameModeManager : MonoBehaviour
|
|||||||
// without a camera jump (exactly like SetPaused's resume - the mode was never Exited).
|
// without a camera jump (exactly like SetPaused's resume - the mode was never Exited).
|
||||||
if (MovementGated())
|
if (MovementGated())
|
||||||
{
|
{
|
||||||
if (player) player.ControlEnabled = false;
|
if (player)
|
||||||
|
{
|
||||||
|
player.ControlEnabled = false;
|
||||||
|
player.GravityEnabled = false; // hover in place; don't settle to the floor while loading
|
||||||
|
}
|
||||||
if (painter) painter.PaintingEnabled = false;
|
if (painter) painter.PaintingEnabled = false;
|
||||||
|
// Hide the own body so the frozen avatar doesn't hang in the half-loaded world. Re-asserted every
|
||||||
|
// frame (not once) because a lobby->match respawn rebinds `player` mid-load, so the fresh avatar
|
||||||
|
// must be hidden too.
|
||||||
|
SetOwnerBodyHidden(true);
|
||||||
_loadFrozen = true;
|
_loadFrozen = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_loadFrozen)
|
if (_loadFrozen)
|
||||||
{
|
{
|
||||||
_loadFrozen = false;
|
_loadFrozen = false;
|
||||||
|
if (player) player.GravityEnabled = true; // Enter() doesn't touch gravity, so restore it here
|
||||||
|
SetOwnerBodyHidden(false); // show the body, then let Enter() re-hide it if first-person
|
||||||
ActiveMode?.Enter();
|
ActiveMode?.Enter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,6 +393,35 @@ public class GameModeManager : MonoBehaviour
|
|||||||
match.LocalSubPhase == MatchManager.MatchSubPhase.Loading;
|
match.LocalSubPhase == MatchManager.MatchSubPhase.Loading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cached body/fur renderers of the currently bound local avatar, re-resolved only when `player` changes
|
||||||
|
// (a lobby->match respawn rebinds mid-load), so the per-frame Loading hide doesn't walk the hierarchy each tick.
|
||||||
|
private PlayerCharacterController _bodyHideOwner;
|
||||||
|
private SkinnedMeshRenderer[] _bodyHideRenderers;
|
||||||
|
private FurShellRenderer[] _bodyHideFur;
|
||||||
|
|
||||||
|
/// <summary>Hides/shows the LOCAL player's own body (skinned mesh + fur) during the pre-match loading freeze,
|
||||||
|
/// so the owner doesn't watch its frozen avatar hang in the half-loaded world. Local-only (renderer.enabled
|
||||||
|
/// is not replicated, so remotes are unaffected - their view is driven by <see cref="NetworkPlayerSetup"/>).
|
||||||
|
/// On show the freeze release re-enters the active mode, so first-person re-applies its own-body self-hide.</summary>
|
||||||
|
private void SetOwnerBodyHidden(bool hidden)
|
||||||
|
{
|
||||||
|
if (player != _bodyHideOwner)
|
||||||
|
{
|
||||||
|
_bodyHideOwner = player;
|
||||||
|
_bodyHideRenderers = player ? player.GetComponentsInChildren<SkinnedMeshRenderer>(true) : null;
|
||||||
|
_bodyHideFur = player ? player.GetComponentsInChildren<FurShellRenderer>(true) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_bodyHideRenderers != null)
|
||||||
|
foreach (var r in _bodyHideRenderers)
|
||||||
|
if (r) r.enabled = !hidden;
|
||||||
|
// Fur draws via a GPU-instanced camera hook, not a Renderer, so the skinned-mesh toggle misses it;
|
||||||
|
// disabling the component unsubscribes that hook (mirrors FirstPersonController.SetMeshHidden).
|
||||||
|
if (_bodyHideFur != null)
|
||||||
|
foreach (var f in _bodyHideFur)
|
||||||
|
if (f) f.enabled = !hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>First non-Paint mode index at or after <paramref name="start"/> (wrapping once through the
|
/// <summary>First non-Paint mode index at or after <paramref name="start"/> (wrapping once through the
|
||||||
/// cycle); -1 if the set has no non-paint mode (a degenerate paint-only configuration).</summary>
|
/// cycle); -1 if the set has no non-paint mode (a degenerate paint-only configuration).</summary>
|
||||||
private int FirstNonPaintFrom(int start)
|
private int FirstNonPaintFrom(int start)
|
||||||
|
|||||||
@@ -605,6 +605,13 @@ public class MatchManager : MonoBehaviour
|
|||||||
|
|
||||||
private IEnumerator MatchSequence(HashSet<ulong> hunterIds)
|
private IEnumerator MatchSequence(HashSet<ulong> hunterIds)
|
||||||
{
|
{
|
||||||
|
// Freeze everyone in Loading BEFORE unloading the lobby: the lobby floor vanishes a moment before the
|
||||||
|
// map's is loaded, so the still-bound lobby avatars would otherwise drop into the void during the swap.
|
||||||
|
// Loading gates movement, gravity AND the local own-body mesh on every client (see GameModeManager); the
|
||||||
|
// readiness barrier below (BeginLoadingBarrierServer re-asserts Loading) holds that hold until every
|
||||||
|
// client has the map + all match avatars ready, then Hide releases it.
|
||||||
|
SetPhaseServer(MatchSubPhase.Loading, 0f);
|
||||||
|
|
||||||
yield return SwapScenes(lobbySceneName, matchSceneName);
|
yield return SwapScenes(lobbySceneName, matchSceneName);
|
||||||
|
|
||||||
RespawnAll(id => hunterIds.Contains(id) ? hunterPrefab : hiderPrefab, matchSpawnCenter);
|
RespawnAll(id => hunterIds.Contains(id) ? hunterPrefab : hiderPrefab, matchSpawnCenter);
|
||||||
|
|||||||
Reference in new issue
Block a user