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>
|
||||
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>
|
||||
/// 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;
|
||||
@@ -418,20 +426,30 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
// (also covers balancing on a sharp box edge).
|
||||
float slopeAngle = Vector3.Angle(_groundNormal, Vector3.up);
|
||||
bool onSteepSlope = slideDownSteepSlopes
|
||||
&& GravityEnabled
|
||||
&& _controller.isGrounded
|
||||
&& slopeAngle > _controller.slopeLimit + 0.01f;
|
||||
|
||||
if (_controller.isGrounded)
|
||||
if (!GravityEnabled)
|
||||
{
|
||||
if (_verticalVelocity < 0f)
|
||||
_verticalVelocity = groundedStickVelocity;
|
||||
|
||||
// No jumping off a slope too steep to stand on.
|
||||
if (jumpPressed && !onSteepSlope)
|
||||
_verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
||||
// Loading barrier freeze: no gravity or ground-stick, hold vertical velocity at rest so the
|
||||
// avatar keeps its spawn position (movement input is already cut via ControlEnabled).
|
||||
_verticalVelocity = 0f;
|
||||
}
|
||||
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;
|
||||
if (onSteepSlope)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -605,6 +605,13 @@ public class MatchManager : MonoBehaviour
|
||||
|
||||
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);
|
||||
|
||||
RespawnAll(id => hunterIds.Contains(id) ? hunterPrefab : hiderPrefab, matchSpawnCenter);
|
||||
|
||||
Reference in new issue
Block a user