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

+41 -1
View File
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
using Unity.Cinemachine;
using Chameleon.Components;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
@@ -329,14 +330,24 @@ public class GameModeManager : MonoBehaviour
// without a camera jump (exactly like SetPaused's resume - the mode was never Exited).
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;
// 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;
return;
}
if (_loadFrozen)
{
_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();
}
@@ -382,6 +393,35 @@ public class GameModeManager : MonoBehaviour
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
/// cycle); -1 if the set has no non-paint mode (a degenerate paint-only configuration).</summary>
private int FirstNonPaintFrom(int start)