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:
adminandClaude Opus 4.8 committed 2026-07-06 07:55:40 +02:00
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)
+41 -1
View File
@@ -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)
+7
View File
@@ -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);