diff --git a/Assets/Scripts/Components/Controller/PlayerCharacterController.cs b/Assets/Scripts/Components/Controller/PlayerCharacterController.cs index 48fe433..df29f00 100644 --- a/Assets/Scripts/Components/Controller/PlayerCharacterController.cs +++ b/Assets/Scripts/Components/Controller/PlayerCharacterController.cs @@ -151,6 +151,14 @@ public class PlayerCharacterController : MonoBehaviour /// public bool ControlEnabled { get; set; } = true; + /// + /// 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 ) and + /// restored when the Hide phase begins. + /// + public bool GravityEnabled { get; set; } = true; + /// /// 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) diff --git a/Assets/Scripts/GameMode/GameModeManager.cs b/Assets/Scripts/GameMode/GameModeManager.cs index e225ed3..33e76dd 100644 --- a/Assets/Scripts/GameMode/GameModeManager.cs +++ b/Assets/Scripts/GameMode/GameModeManager.cs @@ -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; + + /// 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 ). + /// On show the freeze release re-enters the active mode, so first-person re-applies its own-body self-hide. + private void SetOwnerBodyHidden(bool hidden) + { + if (player != _bodyHideOwner) + { + _bodyHideOwner = player; + _bodyHideRenderers = player ? player.GetComponentsInChildren(true) : null; + _bodyHideFur = player ? player.GetComponentsInChildren(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; + } + /// First non-Paint mode index at or after (wrapping once through the /// cycle); -1 if the set has no non-paint mode (a degenerate paint-only configuration). private int FirstNonPaintFrom(int start) diff --git a/Assets/Scripts/Network/MatchManager.cs b/Assets/Scripts/Network/MatchManager.cs index e7e3eb4..47b8327 100644 --- a/Assets/Scripts/Network/MatchManager.cs +++ b/Assets/Scripts/Network/MatchManager.cs @@ -605,6 +605,13 @@ public class MatchManager : MonoBehaviour private IEnumerator MatchSequence(HashSet 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);