diff --git a/frontend/public/unity-build/Build/pretty_build.data b/frontend/public/unity-build/Build/build_ball.data similarity index 99% rename from frontend/public/unity-build/Build/pretty_build.data rename to frontend/public/unity-build/Build/build_ball.data index 51223b3..e4dad67 100644 Binary files a/frontend/public/unity-build/Build/pretty_build.data and b/frontend/public/unity-build/Build/build_ball.data differ diff --git a/frontend/public/unity-build/Build/pretty_build.framework.js b/frontend/public/unity-build/Build/build_ball.framework.js similarity index 100% rename from frontend/public/unity-build/Build/pretty_build.framework.js rename to frontend/public/unity-build/Build/build_ball.framework.js diff --git a/frontend/public/unity-build/Build/pretty_build.loader.js b/frontend/public/unity-build/Build/build_ball.loader.js similarity index 100% rename from frontend/public/unity-build/Build/pretty_build.loader.js rename to frontend/public/unity-build/Build/build_ball.loader.js diff --git a/frontend/public/unity-build/Build/pretty_build.wasm b/frontend/public/unity-build/Build/build_ball.wasm similarity index 100% rename from frontend/public/unity-build/Build/pretty_build.wasm rename to frontend/public/unity-build/Build/build_ball.wasm diff --git a/frontend/public/unity-build/index.html b/frontend/public/unity-build/index.html index 07d29ae..754f227 100644 --- a/frontend/public/unity-build/index.html +++ b/frontend/public/unity-build/index.html @@ -52,12 +52,12 @@ } var buildUrl = "Build"; - var loaderUrl = buildUrl + "/pretty_build.loader.js"; + var loaderUrl = buildUrl + "/build_ball.loader.js"; var config = { arguments: [], - dataUrl: buildUrl + "/pretty_build.data", - frameworkUrl: buildUrl + "/pretty_build.framework.js", - codeUrl: buildUrl + "/pretty_build.wasm", + dataUrl: buildUrl + "/build_ball.data", + frameworkUrl: buildUrl + "/build_ball.framework.js", + codeUrl: buildUrl + "/build_ball.wasm", streamingAssetsUrl: "StreamingAssets", companyName: "DefaultCompany", productName: "BallProject", diff --git a/frontend/src/components/GameCanvas.jsx b/frontend/src/components/GameCanvas.jsx index d5d71e3..6ce3101 100644 --- a/frontend/src/components/GameCanvas.jsx +++ b/frontend/src/components/GameCanvas.jsx @@ -3,8 +3,8 @@ import { useState, useEffect, useCallback } from 'react' // Check if Unity build files exist const UNITY_BUILD_PATH = '/unity-build/Build' // Cache-busting version — update this after each Unity build -const UNITY_BUILD_VERSION = '20260520c' -const BUILD_PREFIX = 'pretty_build' +const UNITY_BUILD_VERSION = '20260520d' +const BUILD_PREFIX = 'build_ball' const LOADER_URL = `${UNITY_BUILD_PATH}/${BUILD_PREFIX}.loader.js?v=${UNITY_BUILD_VERSION}` diff --git a/game/Assets/Scripts/GameManager.cs b/game/Assets/Scripts/GameManager.cs index b32d042..65b8e70 100644 --- a/game/Assets/Scripts/GameManager.cs +++ b/game/Assets/Scripts/GameManager.cs @@ -178,8 +178,8 @@ public class GameManager : MonoBehaviour { if (playerRoot == null) return; playerRoot.SetActive(active); - var pc = playerRoot.GetComponentInChildren(true); - if (pc != null) pc.enabled = active; + var vehicle = playerRoot.GetComponentInChildren(true); + if (vehicle != null) vehicle.enabled = active; if (active) { diff --git a/game/Assets/Scripts/Network/DebugNetworkUI.cs b/game/Assets/Scripts/Network/DebugNetworkUI.cs index 9871b55..f63498c 100644 --- a/game/Assets/Scripts/Network/DebugNetworkUI.cs +++ b/game/Assets/Scripts/Network/DebugNetworkUI.cs @@ -116,12 +116,12 @@ public class DebugNetworkUI : MonoBehaviour if (state != null) ImGuiSkin.DrawField("Server Pos", $"({state.x:F1}, {state.y:F1}, {state.z:F1})"); - var pc = FindFirstObjectByType(); - if (pc != null && pc.isActiveAndEnabled) + var vehicle = FindFirstObjectByType(); + if (vehicle != null && vehicle.isActiveAndEnabled) { - var pos = pc.transform.position; + var pos = vehicle.transform.position; ImGuiSkin.DrawField("Live Pos", $"({pos.x:F1}, {pos.y:F1}, {pos.z:F1})"); - var rb = pc.GetComponent(); + var rb = vehicle.vehicleRigidbody; if (rb != null) { var v = rb.linearVelocity; diff --git a/game/Assets/Scripts/Network/LobbyUI.cs b/game/Assets/Scripts/Network/LobbyUI.cs index 198e9d2..24e1aee 100644 --- a/game/Assets/Scripts/Network/LobbyUI.cs +++ b/game/Assets/Scripts/Network/LobbyUI.cs @@ -118,25 +118,22 @@ public class LobbyUI : MonoBehaviour var nm = NetworkManager.Instance; if (nm != null && playerRoot != null) { - var pc = playerRoot.GetComponentInChildren(true); - if (pc != null) + var setup = playerRoot.GetComponentInChildren(true); + if (setup != null) { + var vehicle = setup.GetComponent(); + var rb = vehicle != null ? vehicle.vehicleRigidbody : setup.GetComponent(); var localState = nm.GetLocalPlayerState(); - if (localState != null) + if (localState != null && rb != null) { Vector3 spawnPos = new Vector3(localState.x, localState.y, localState.z); - var rb = pc.GetComponent(); - if (rb != null) - { - rb.linearVelocity = Vector3.zero; - rb.angularVelocity = Vector3.zero; - rb.position = spawnPos; - } - pc.transform.position = spawnPos; - pc.SetSpawnPosition(spawnPos); + rb.linearVelocity = Vector3.zero; + rb.angularVelocity = Vector3.zero; + rb.position = spawnPos; + setup.transform.position = spawnPos; } - pc.enabled = true; - pc.SetupLocalPlayer(nm.LocalPlayerName, nm.LocalPlayerColor); + if (vehicle != null) vehicle.enabled = true; + setup.SetupLocal(nm.LocalPlayerName, nm.LocalPlayerColor); } } @@ -160,8 +157,8 @@ public class LobbyUI : MonoBehaviour if (playerRoot != null) { - var pc = playerRoot.GetComponentInChildren(true); - if (pc != null) pc.enabled = false; + var vehicle = playerRoot.GetComponentInChildren(true); + if (vehicle != null) vehicle.enabled = false; playerRoot.SetActive(false); } diff --git a/game/Assets/Scripts/Network/NetworkManager.cs b/game/Assets/Scripts/Network/NetworkManager.cs index b97a475..8c17d13 100644 --- a/game/Assets/Scripts/Network/NetworkManager.cs +++ b/game/Assets/Scripts/Network/NetworkManager.cs @@ -30,7 +30,7 @@ public class NetworkManager : MonoBehaviour public string LastError { get; private set; } = ""; // Expose remote players for debug UI - public Dictionary RemotePlayers => _remotePlayers; + public Dictionary RemotePlayers => _remotePlayers; // Local player info (set during join) public string LocalPlayerName { get; private set; } = ""; @@ -73,7 +73,7 @@ public class NetworkManager : MonoBehaviour private Client _client; private Room _room; private StateCallbackStrategy _callbacks; - private readonly Dictionary _remotePlayers = new(); + private readonly Dictionary _remotePlayers = new(); private float _broadcastTimer; private const float BROADCAST_INTERVAL = 0.01667f; // ~60/sec private bool _isJoining; @@ -270,14 +270,17 @@ public class NetworkManager : MonoBehaviour { Vector3 spawnPos = new Vector3(player.x, player.y, player.z); - GameObject remoteBall = remotePlayerPrefab != null - ? Instantiate(remotePlayerPrefab, spawnPos, Quaternion.identity) - : GameObject.CreatePrimitive(PrimitiveType.Sphere); - remoteBall.transform.position = spawnPos; - remoteBall.name = $"RemotePlayer_{player.name}_{sessionId[..6]}"; + if (remotePlayerPrefab == null) + { + Debug.LogError("[Network] remotePlayerPrefab not assigned — cannot spawn remote vehicle."); + return; + } + GameObject remote = Instantiate(remotePlayerPrefab, spawnPos, Quaternion.identity); + remote.transform.position = spawnPos; + remote.name = $"RemoteVehicle_{player.name}_{sessionId[..6]}"; - var controller = remoteBall.GetComponent() - ?? remoteBall.AddComponent(); + var controller = remote.GetComponent() + ?? remote.AddComponent(); controller.Initialize(sessionId, player.name, new Color(player.colorR, player.colorG, player.colorB)); @@ -324,20 +327,21 @@ public class NetworkManager : MonoBehaviour // ─── Position Broadcasting ──────────────────────────────────────────── + /// + /// Called by after the local vehicle is ready. + /// Tells us which Rigidbody to broadcast each tick. + /// + public void RegisterLocalVehicle(Transform t, Rigidbody rb) + { + _localPlayer = t; + _localPlayerRb = rb; + Debug.Log($"[Network] Local vehicle registered: {t?.name}"); + } + private void BroadcastPosition() { if (_room == null || !IsConnected) return; - - if (_localPlayer == null) - { - var pc = FindFirstObjectByType(); - if (pc != null) - { - _localPlayer = pc.transform; - _localPlayerRb = pc.GetComponent(); - } - else return; - } + if (_localPlayer == null || _localPlayerRb == null) return; Vector3 pos = _localPlayer.position; Vector3 vel = _localPlayerRb != null ? _localPlayerRb.linearVelocity : Vector3.zero; diff --git a/game/Assets/Scripts/Network/RemoteVehicleSync.cs b/game/Assets/Scripts/Network/RemoteVehicleSync.cs new file mode 100644 index 0000000..fb699f4 --- /dev/null +++ b/game/Assets/Scripts/Network/RemoteVehicleSync.cs @@ -0,0 +1,283 @@ +using UnityEngine; +using NWH.VehiclePhysics2; + +/// +/// Remote vehicle controller. Attached to remote players spawned from the network. +/// - Disables NWH local simulation (VehicleController + WheelControllers + AudioSources). +/// - Sets the Rigidbody kinematic so we can drive it with snapshot interpolation +/// via / . +/// - Local dynamic vehicles bounce naturally off the kinematic remote's colliders. +/// - Adds a floating name label (distance-scaled) and a colored capsule marker. +/// +public class RemoteVehicleSync : MonoBehaviour +{ + [Header("Interpolation")] + public float interpolationDelay = 0.083f; + public float maxExtrapolation = 0.08f; + public float snapDistance = 12f; + public float smoothingSpeed = 24f; + public float rotationSpeed = 24f; + + [Header("Spawn")] + [Tooltip("Seconds after spawn during which colliders are disabled to avoid ejecting overlapping locals at connect.")] + public float spawnGrace = 1.5f; + + public string SessionId { get; private set; } + public string PlayerName { get; private set; } + public Color PlayerColor { get; private set; } + public float SpawnTime { get; private set; } + + private struct Snapshot + { + public double serverTime; + public float localTime; + public Vector3 position; + public Vector3 velocity; + public Quaternion rotation; + public Vector3 angularVelocity; + } + + private const int BUFFER_SIZE = 16; + private readonly Snapshot[] _buffer = new Snapshot[BUFFER_SIZE]; + private int _bufferCount; + private int _newestIndex; + private bool _initialized; + + private Rigidbody _rb; + private VehicleController _vehicle; + private Collider[] _allColliders; + private bool _collidersReenabled; + private Quaternion _currentRotation = Quaternion.identity; + + private GameObject _nameLabelObj; + private TextMesh _nameLabel; + private GameObject _markerObj; + + public void Initialize(string sessionId, string playerName, Color color) + { + SessionId = sessionId; + PlayerName = playerName; + PlayerColor = color; + SpawnTime = Time.time; + _bufferCount = 0; + _currentRotation = transform.rotation; + + // Disable NWH driving simulation: this remote is purely networked, no local AI/input. + _vehicle = GetComponent(); + if (_vehicle != null) _vehicle.enabled = false; + + // Disable all wheel controllers so they don't apply suspension forces. + foreach (var mb in GetComponentsInChildren(true)) + { + var t = mb.GetType(); + if (t.FullName == "NWH.WheelController3D.WheelController" || t.FullName.Contains(".WheelController")) + mb.enabled = false; + } + + // Mute audio sources (engine, skid, etc.) on remotes. + foreach (var src in GetComponentsInChildren(true)) + src.enabled = false; + + // Make the rigidbody kinematic so we drive it from network snapshots. + _rb = _vehicle != null ? _vehicle.vehicleRigidbody : GetComponent(); + if (_rb != null) + { + _rb.isKinematic = true; + _rb.useGravity = false; + _rb.interpolation = RigidbodyInterpolation.Interpolate; + _rb.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative; + } + + // Cache colliders and disable them during the grace window to avoid spawn-time ejection. + _allColliders = GetComponentsInChildren(true); + foreach (var c in _allColliders) + if (c != null && !c.isTrigger) c.enabled = false; + + BuildNameLabel(playerName, color); + BuildColorMarker(color); + + _initialized = true; + Debug.Log($"[RemoteVehicle] Initialized: {playerName} ({sessionId[..6]}) color={color}"); + } + + public void SetTargetState(Vector3 position, Vector3 velocity, Quaternion rotation, double serverTime, Vector3 angularVelocity = default) + { + _newestIndex = (_newestIndex + 1) % BUFFER_SIZE; + _buffer[_newestIndex] = new Snapshot + { + serverTime = serverTime, + localTime = Time.time, + position = position, + velocity = velocity, + rotation = rotation, + angularVelocity = angularVelocity, + }; + if (_bufferCount < BUFFER_SIZE) _bufferCount++; + } + + public void SetVisible(bool visible) + { + foreach (var r in GetComponentsInChildren(true)) + if (r != null) r.enabled = visible; + if (_nameLabelObj != null) _nameLabelObj.SetActive(visible); + if (_markerObj != null) _markerObj.SetActive(visible); + } + + void Update() + { + if (!_initialized) return; + + // Re-enable colliders once grace window has elapsed. + if (!_collidersReenabled && Time.time - SpawnTime > spawnGrace) + { + if (_allColliders != null) + foreach (var c in _allColliders) + if (c != null && !c.isTrigger) c.enabled = true; + _collidersReenabled = true; + } + + if (_bufferCount == 0) return; + + float renderTime = Time.time - interpolationDelay; + int oldestIdx = (_newestIndex - _bufferCount + 1 + BUFFER_SIZE) % BUFFER_SIZE; + + Snapshot older = default, newer = default; + bool found = false; + for (int i = 0; i < _bufferCount - 1; i++) + { + int a = (oldestIdx + i) % BUFFER_SIZE; + int b = (oldestIdx + i + 1) % BUFFER_SIZE; + if (_buffer[a].localTime <= renderTime && _buffer[b].localTime >= renderTime) + { + older = _buffer[a]; + newer = _buffer[b]; + found = true; + break; + } + } + + Vector3 targetPos; + Quaternion targetRot; + if (found) + { + float span = newer.localTime - older.localTime; + float t = span > 0.001f ? (renderTime - older.localTime) / span : 1f; + t = Mathf.Clamp01(t); + targetPos = Vector3.Lerp(older.position, newer.position, t); + targetRot = Quaternion.Slerp(older.rotation, newer.rotation, t); + } + else + { + var newest = _buffer[_newestIndex]; + float elapsed = renderTime - newest.localTime; + if (elapsed < 0) + { + targetPos = _buffer[oldestIdx].position; + targetRot = _buffer[oldestIdx].rotation; + } + else + { + float extTime = Mathf.Min(elapsed, maxExtrapolation); + float damp = 1f - Mathf.Clamp01(elapsed / (maxExtrapolation * 2f)); + targetPos = newest.position + newest.velocity * extTime * damp; + targetRot = newest.rotation; + } + } + + float dist = Vector3.Distance(transform.position, targetPos); + Vector3 newPos; + if (dist > snapDistance) + { + newPos = targetPos; + _currentRotation = targetRot; + } + else + { + float lerpT = 1f - Mathf.Exp(-smoothingSpeed * Time.deltaTime); + newPos = Vector3.Lerp(transform.position, targetPos, lerpT); + } + float rotLerpT = 1f - Mathf.Exp(-rotationSpeed * Time.deltaTime); + _currentRotation = Quaternion.Slerp(_currentRotation, targetRot, rotLerpT); + + if (_rb != null) + { + _rb.MovePosition(newPos); + _rb.MoveRotation(_currentRotation); + } + else + { + transform.position = newPos; + transform.rotation = _currentRotation; + } + + if (_nameLabelObj != null) + { + _nameLabelObj.transform.position = transform.position + Vector3.up * 2.8f; + var cam = Camera.main; + if (cam != null) + { + Vector3 lookDir = cam.transform.position - _nameLabelObj.transform.position; + float camDist = lookDir.magnitude; + lookDir.y = 0f; + if (lookDir.sqrMagnitude > 0.001f) + _nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir); + float scale = Mathf.Clamp(camDist / 8f, 1f, 8f); + _nameLabelObj.transform.localScale = Vector3.one * (0.1f * scale); + } + } + } + + private void BuildNameLabel(string playerName, Color color) + { + _nameLabelObj = new GameObject("NameLabel"); + _nameLabelObj.transform.position = transform.position + Vector3.up * 2.8f; + _nameLabelObj.transform.localScale = Vector3.one * 0.1f; + + _nameLabel = _nameLabelObj.AddComponent(); + _nameLabel.text = playerName; + _nameLabel.fontSize = 144; + _nameLabel.characterSize = 0.15f; + _nameLabel.anchor = TextAnchor.MiddleCenter; + _nameLabel.alignment = TextAlignment.Center; + _nameLabel.color = color; + if (PlayerController.LabelFont != null) _nameLabel.font = PlayerController.LabelFont; + + var mr = _nameLabel.GetComponent(); + if (PlayerController.LabelFont != null && PlayerController.LabelFont.material != null) + mr.material = PlayerController.LabelFont.material; + else + { + var s = Shader.Find("GUI/Text Shader") ?? Shader.Find("Unlit/Texture"); + if (s != null) mr.material = new Material(s); + } + } + + private void BuildColorMarker(Color color) + { + _markerObj = GameObject.CreatePrimitive(PrimitiveType.Capsule); + _markerObj.name = "RemoteMarker"; + DestroyImmediate(_markerObj.GetComponent()); + _markerObj.transform.SetParent(transform, false); + _markerObj.transform.localPosition = new Vector3(0f, 2.4f, 0f); + _markerObj.transform.localScale = new Vector3(0.25f, 0.4f, 0.25f); + + var r = _markerObj.GetComponent(); + if (r != null) + { + var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"); + var mat = new Material(shader); + if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color); + else mat.color = color; + if (mat.HasProperty("_EmissionColor")) mat.SetColor("_EmissionColor", color * 0.6f); + mat.EnableKeyword("_EMISSION"); + r.material = mat; + } + } + + void OnDestroy() + { + if (_nameLabelObj != null) Destroy(_nameLabelObj); + if (_markerObj != null) Destroy(_markerObj); + Debug.Log($"[RemoteVehicle] Destroyed: {PlayerName}"); + } +} diff --git a/game/Assets/Scripts/Network/VehicleLocalSetup.cs b/game/Assets/Scripts/Network/VehicleLocalSetup.cs new file mode 100644 index 0000000..94b61f2 --- /dev/null +++ b/game/Assets/Scripts/Network/VehicleLocalSetup.cs @@ -0,0 +1,109 @@ +using UnityEngine; +using NWH.VehiclePhysics2; + +/// +/// Equivalent of PlayerController.SetupLocalPlayer for the NWH vehicle local player. +/// Attaches a floating name label, a colored marker above the car so other players can +/// spot us at distance, and registers the vehicle's Rigidbody with the NetworkManager +/// so it is broadcast over the wire. +/// +[RequireComponent(typeof(VehicleController))] +public class VehicleLocalSetup : MonoBehaviour +{ + private GameObject _nameLabelObj; + private TextMesh _nameLabel; + private GameObject _markerObj; + private VehicleController _vehicle; + + void Awake() + { + _vehicle = GetComponent(); + } + + public void SetupLocal(string playerName, Color playerColor) + { + if (_vehicle == null) _vehicle = GetComponent(); + var rb = _vehicle.vehicleRigidbody != null ? _vehicle.vehicleRigidbody : GetComponent(); + + // Register with NetworkManager so it broadcasts position from THIS Rigidbody. + if (NetworkManager.Instance != null) + NetworkManager.Instance.RegisterLocalVehicle(transform, rb); + + BuildNameLabel(playerName, playerColor); + BuildColorMarker(playerColor); + + Debug.Log($"[VehicleLocal] Setup complete: {playerName} color={playerColor}"); + } + + private void BuildNameLabel(string playerName, Color color) + { + if (_nameLabelObj != null) Destroy(_nameLabelObj); + _nameLabelObj = new GameObject("LocalNameLabel"); + _nameLabelObj.transform.SetParent(transform.parent, false); + _nameLabelObj.transform.localScale = Vector3.one * 0.1f; + + _nameLabel = _nameLabelObj.AddComponent(); + _nameLabel.text = playerName; + _nameLabel.fontSize = 144; + _nameLabel.characterSize = 0.15f; + _nameLabel.anchor = TextAnchor.MiddleCenter; + _nameLabel.alignment = TextAlignment.Center; + _nameLabel.color = color; + if (PlayerController.LabelFont != null) _nameLabel.font = PlayerController.LabelFont; + + var renderer = _nameLabel.GetComponent(); + if (PlayerController.LabelFont != null && PlayerController.LabelFont.material != null) + renderer.material = PlayerController.LabelFont.material; + else + { + var textShader = Shader.Find("GUI/Text Shader") ?? Shader.Find("Unlit/Texture"); + if (textShader != null) renderer.material = new Material(textShader); + } + } + + private void BuildColorMarker(Color color) + { + // Cone-ish marker above the car so other players can identify us at distance. + if (_markerObj != null) Destroy(_markerObj); + _markerObj = GameObject.CreatePrimitive(PrimitiveType.Capsule); + _markerObj.name = "LocalMarker"; + DestroyImmediate(_markerObj.GetComponent()); + _markerObj.transform.SetParent(transform, false); + _markerObj.transform.localPosition = new Vector3(0f, 2.4f, 0f); + _markerObj.transform.localScale = new Vector3(0.25f, 0.4f, 0.25f); + + var r = _markerObj.GetComponent(); + if (r != null) + { + var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"); + var mat = new Material(shader); + if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color); + else mat.color = color; + if (mat.HasProperty("_EmissionColor")) mat.SetColor("_EmissionColor", color * 0.6f); + mat.EnableKeyword("_EMISSION"); + r.material = mat; + } + } + + void LateUpdate() + { + if (_nameLabelObj != null) + { + _nameLabelObj.transform.position = transform.position + Vector3.up * 2.8f; + var cam = Camera.main; + if (cam != null) + { + Vector3 lookDir = cam.transform.position - _nameLabelObj.transform.position; + lookDir.y = 0f; + if (lookDir.sqrMagnitude > 0.001f) + _nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir); + } + } + } + + void OnDestroy() + { + if (_nameLabelObj != null) Destroy(_nameLabelObj); + if (_markerObj != null) Destroy(_markerObj); + } +} diff --git a/game/Assets/Scripts/Stats/StatsTracker.cs b/game/Assets/Scripts/Stats/StatsTracker.cs index 41fe238..82d7819 100644 --- a/game/Assets/Scripts/Stats/StatsTracker.cs +++ b/game/Assets/Scripts/Stats/StatsTracker.cs @@ -31,8 +31,7 @@ public class StatsTracker : MonoBehaviour private string _cachedName = ""; private float _lastSentTime = -999f; - private PlayerController _pc; - private Rigidbody _rb; + private Rigidbody _rb; void Awake() { @@ -42,7 +41,6 @@ public class StatsTracker : MonoBehaviour void Start() { - _pc = GetComponent(); _rb = GetComponent(); var nm = NetworkManager.Instance; diff --git a/game/Assets/Scripts/UI/ChatUI.cs b/game/Assets/Scripts/UI/ChatUI.cs index d7ea10e..a7514c8 100644 --- a/game/Assets/Scripts/UI/ChatUI.cs +++ b/game/Assets/Scripts/UI/ChatUI.cs @@ -76,8 +76,8 @@ public class ChatUI : MonoBehaviour _pollTimer = POLL_INTERVAL; // poll immediately Cursor.lockState = CursorLockMode.None; Cursor.visible = true; - // Release held movement keys so the ball doesn't keep moving while typing - FindFirstObjectByType()?.ResetInputs(); + // Movement keys are handled by NWH InputSystemVehicleInputProvider via Input System; + // no manual reset needed when chat opens (NWH polls Input System each frame). } else {