1 Commits
master ... Car

Author SHA1 Message Date
103f8859d4 feat(Car): wire NWH Vehicle Physics 2 — scripts only, scene/prefabs still TODO
Code-only first pass. The local Player.prefab (ball) is still in the scene
until the user creates the PlayerCar / RemoteCar prefab variants in the Editor.

New scripts:
- VehicleLocalSetup: attaches name label + colored capsule marker to the local
  vehicle, registers its Rigidbody with NetworkManager for broadcast.
- RemoteVehicleSync: snapshot interpolation for remote vehicles. Disables the
  NWH VehicleController + every WheelController + every AudioSource on init.
  Makes the rigidbody kinematic, MovePosition-driven from network. Disables all
  non-trigger colliders during the 1.5s spawn grace window to avoid ejecting
  overlapping locals at connect.

Adapted scripts:
- NetworkManager: RegisterLocalVehicle(Transform, Rigidbody) replaces the
  FindFirstObjectByType<PlayerController>() lookup. Remote spawn now wires
  RemoteVehicleSync instead of RemotePlayerController.
- LobbyUI: OnConnected drives VehicleLocalSetup.SetupLocal; OnDisconnected
  toggles NWH.VehicleController instead of PlayerController.
- GameManager.SetPlayerActive: toggles VehicleController, not PlayerController.
- DebugNetworkUI: live position read from VehicleController.vehicleRigidbody.
- ChatUI: drops PlayerController.ResetInputs() (NWH polls Input System each
  frame; no manual reset needed when chat opens).
- StatsTracker: drops dead _pc field; Rigidbody still gets resolved via
  GetComponent on the host GameObject (will be the vehicle on Car).

Frontend (deployed earlier on master): build_ball replaces pretty_build assets.
2026-05-20 18:34:40 +02:00
14 changed files with 444 additions and 53 deletions

View File

@@ -52,12 +52,12 @@
} }
var buildUrl = "Build"; var buildUrl = "Build";
var loaderUrl = buildUrl + "/pretty_build.loader.js"; var loaderUrl = buildUrl + "/build_ball.loader.js";
var config = { var config = {
arguments: [], arguments: [],
dataUrl: buildUrl + "/pretty_build.data", dataUrl: buildUrl + "/build_ball.data",
frameworkUrl: buildUrl + "/pretty_build.framework.js", frameworkUrl: buildUrl + "/build_ball.framework.js",
codeUrl: buildUrl + "/pretty_build.wasm", codeUrl: buildUrl + "/build_ball.wasm",
streamingAssetsUrl: "StreamingAssets", streamingAssetsUrl: "StreamingAssets",
companyName: "DefaultCompany", companyName: "DefaultCompany",
productName: "BallProject", productName: "BallProject",

View File

@@ -3,8 +3,8 @@ import { useState, useEffect, useCallback } from 'react'
// Check if Unity build files exist // Check if Unity build files exist
const UNITY_BUILD_PATH = '/unity-build/Build' const UNITY_BUILD_PATH = '/unity-build/Build'
// Cache-busting version — update this after each Unity build // Cache-busting version — update this after each Unity build
const UNITY_BUILD_VERSION = '20260520c' const UNITY_BUILD_VERSION = '20260520d'
const BUILD_PREFIX = 'pretty_build' const BUILD_PREFIX = 'build_ball'
const LOADER_URL = `${UNITY_BUILD_PATH}/${BUILD_PREFIX}.loader.js?v=${UNITY_BUILD_VERSION}` const LOADER_URL = `${UNITY_BUILD_PATH}/${BUILD_PREFIX}.loader.js?v=${UNITY_BUILD_VERSION}`

View File

@@ -178,8 +178,8 @@ public class GameManager : MonoBehaviour
{ {
if (playerRoot == null) return; if (playerRoot == null) return;
playerRoot.SetActive(active); playerRoot.SetActive(active);
var pc = playerRoot.GetComponentInChildren<PlayerController>(true); var vehicle = playerRoot.GetComponentInChildren<NWH.VehiclePhysics2.VehicleController>(true);
if (pc != null) pc.enabled = active; if (vehicle != null) vehicle.enabled = active;
if (active) if (active)
{ {

View File

@@ -116,12 +116,12 @@ public class DebugNetworkUI : MonoBehaviour
if (state != null) if (state != null)
ImGuiSkin.DrawField("Server Pos", $"({state.x:F1}, {state.y:F1}, {state.z:F1})"); ImGuiSkin.DrawField("Server Pos", $"({state.x:F1}, {state.y:F1}, {state.z:F1})");
var pc = FindFirstObjectByType<PlayerController>(); var vehicle = FindFirstObjectByType<NWH.VehiclePhysics2.VehicleController>();
if (pc != null && pc.isActiveAndEnabled) 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})"); ImGuiSkin.DrawField("Live Pos", $"({pos.x:F1}, {pos.y:F1}, {pos.z:F1})");
var rb = pc.GetComponent<Rigidbody>(); var rb = vehicle.vehicleRigidbody;
if (rb != null) if (rb != null)
{ {
var v = rb.linearVelocity; var v = rb.linearVelocity;

View File

@@ -118,25 +118,22 @@ public class LobbyUI : MonoBehaviour
var nm = NetworkManager.Instance; var nm = NetworkManager.Instance;
if (nm != null && playerRoot != null) if (nm != null && playerRoot != null)
{ {
var pc = playerRoot.GetComponentInChildren<PlayerController>(true); var setup = playerRoot.GetComponentInChildren<VehicleLocalSetup>(true);
if (pc != null) if (setup != null)
{ {
var vehicle = setup.GetComponent<NWH.VehiclePhysics2.VehicleController>();
var rb = vehicle != null ? vehicle.vehicleRigidbody : setup.GetComponent<Rigidbody>();
var localState = nm.GetLocalPlayerState(); var localState = nm.GetLocalPlayerState();
if (localState != null) if (localState != null && rb != null)
{ {
Vector3 spawnPos = new Vector3(localState.x, localState.y, localState.z); Vector3 spawnPos = new Vector3(localState.x, localState.y, localState.z);
var rb = pc.GetComponent<Rigidbody>(); rb.linearVelocity = Vector3.zero;
if (rb != null) rb.angularVelocity = Vector3.zero;
{ rb.position = spawnPos;
rb.linearVelocity = Vector3.zero; setup.transform.position = spawnPos;
rb.angularVelocity = Vector3.zero;
rb.position = spawnPos;
}
pc.transform.position = spawnPos;
pc.SetSpawnPosition(spawnPos);
} }
pc.enabled = true; if (vehicle != null) vehicle.enabled = true;
pc.SetupLocalPlayer(nm.LocalPlayerName, nm.LocalPlayerColor); setup.SetupLocal(nm.LocalPlayerName, nm.LocalPlayerColor);
} }
} }
@@ -160,8 +157,8 @@ public class LobbyUI : MonoBehaviour
if (playerRoot != null) if (playerRoot != null)
{ {
var pc = playerRoot.GetComponentInChildren<PlayerController>(true); var vehicle = playerRoot.GetComponentInChildren<NWH.VehiclePhysics2.VehicleController>(true);
if (pc != null) pc.enabled = false; if (vehicle != null) vehicle.enabled = false;
playerRoot.SetActive(false); playerRoot.SetActive(false);
} }

View File

@@ -30,7 +30,7 @@ public class NetworkManager : MonoBehaviour
public string LastError { get; private set; } = ""; public string LastError { get; private set; } = "";
// Expose remote players for debug UI // Expose remote players for debug UI
public Dictionary<string, RemotePlayerController> RemotePlayers => _remotePlayers; public Dictionary<string, RemoteVehicleSync> RemotePlayers => _remotePlayers;
// Local player info (set during join) // Local player info (set during join)
public string LocalPlayerName { get; private set; } = ""; public string LocalPlayerName { get; private set; } = "";
@@ -73,7 +73,7 @@ public class NetworkManager : MonoBehaviour
private Client _client; private Client _client;
private Room<GameState> _room; private Room<GameState> _room;
private StateCallbackStrategy<GameState> _callbacks; private StateCallbackStrategy<GameState> _callbacks;
private readonly Dictionary<string, RemotePlayerController> _remotePlayers = new(); private readonly Dictionary<string, RemoteVehicleSync> _remotePlayers = new();
private float _broadcastTimer; private float _broadcastTimer;
private const float BROADCAST_INTERVAL = 0.01667f; // ~60/sec private const float BROADCAST_INTERVAL = 0.01667f; // ~60/sec
private bool _isJoining; private bool _isJoining;
@@ -270,14 +270,17 @@ public class NetworkManager : MonoBehaviour
{ {
Vector3 spawnPos = new Vector3(player.x, player.y, player.z); Vector3 spawnPos = new Vector3(player.x, player.y, player.z);
GameObject remoteBall = remotePlayerPrefab != null if (remotePlayerPrefab == null)
? Instantiate(remotePlayerPrefab, spawnPos, Quaternion.identity) {
: GameObject.CreatePrimitive(PrimitiveType.Sphere); Debug.LogError("[Network] remotePlayerPrefab not assigned — cannot spawn remote vehicle.");
remoteBall.transform.position = spawnPos; return;
remoteBall.name = $"RemotePlayer_{player.name}_{sessionId[..6]}"; }
GameObject remote = Instantiate(remotePlayerPrefab, spawnPos, Quaternion.identity);
remote.transform.position = spawnPos;
remote.name = $"RemoteVehicle_{player.name}_{sessionId[..6]}";
var controller = remoteBall.GetComponent<RemotePlayerController>() var controller = remote.GetComponent<RemoteVehicleSync>()
?? remoteBall.AddComponent<RemotePlayerController>(); ?? remote.AddComponent<RemoteVehicleSync>();
controller.Initialize(sessionId, player.name, controller.Initialize(sessionId, player.name,
new Color(player.colorR, player.colorG, player.colorB)); new Color(player.colorR, player.colorG, player.colorB));
@@ -324,20 +327,21 @@ public class NetworkManager : MonoBehaviour
// ─── Position Broadcasting ──────────────────────────────────────────── // ─── Position Broadcasting ────────────────────────────────────────────
/// <summary>
/// Called by <see cref="VehicleLocalSetup"/> after the local vehicle is ready.
/// Tells us which Rigidbody to broadcast each tick.
/// </summary>
public void RegisterLocalVehicle(Transform t, Rigidbody rb)
{
_localPlayer = t;
_localPlayerRb = rb;
Debug.Log($"[Network] Local vehicle registered: {t?.name}");
}
private void BroadcastPosition() private void BroadcastPosition()
{ {
if (_room == null || !IsConnected) return; if (_room == null || !IsConnected) return;
if (_localPlayer == null || _localPlayerRb == null) return;
if (_localPlayer == null)
{
var pc = FindFirstObjectByType<PlayerController>();
if (pc != null)
{
_localPlayer = pc.transform;
_localPlayerRb = pc.GetComponent<Rigidbody>();
}
else return;
}
Vector3 pos = _localPlayer.position; Vector3 pos = _localPlayer.position;
Vector3 vel = _localPlayerRb != null ? _localPlayerRb.linearVelocity : Vector3.zero; Vector3 vel = _localPlayerRb != null ? _localPlayerRb.linearVelocity : Vector3.zero;

View File

@@ -0,0 +1,283 @@
using UnityEngine;
using NWH.VehiclePhysics2;
/// <summary>
/// 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 <see cref="Rigidbody.MovePosition"/> / <see cref="Rigidbody.MoveRotation"/>.
/// - Local dynamic vehicles bounce naturally off the kinematic remote's colliders.
/// - Adds a floating name label (distance-scaled) and a colored capsule marker.
/// </summary>
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<VehicleController>();
if (_vehicle != null) _vehicle.enabled = false;
// Disable all wheel controllers so they don't apply suspension forces.
foreach (var mb in GetComponentsInChildren<MonoBehaviour>(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<AudioSource>(true))
src.enabled = false;
// Make the rigidbody kinematic so we drive it from network snapshots.
_rb = _vehicle != null ? _vehicle.vehicleRigidbody : GetComponent<Rigidbody>();
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<Collider>(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<Renderer>(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<TextMesh>();
_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<MeshRenderer>();
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<Collider>());
_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<Renderer>();
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}");
}
}

View File

@@ -0,0 +1,109 @@
using UnityEngine;
using NWH.VehiclePhysics2;
/// <summary>
/// Equivalent of <c>PlayerController.SetupLocalPlayer</c> 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.
/// </summary>
[RequireComponent(typeof(VehicleController))]
public class VehicleLocalSetup : MonoBehaviour
{
private GameObject _nameLabelObj;
private TextMesh _nameLabel;
private GameObject _markerObj;
private VehicleController _vehicle;
void Awake()
{
_vehicle = GetComponent<VehicleController>();
}
public void SetupLocal(string playerName, Color playerColor)
{
if (_vehicle == null) _vehicle = GetComponent<VehicleController>();
var rb = _vehicle.vehicleRigidbody != null ? _vehicle.vehicleRigidbody : GetComponent<Rigidbody>();
// 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<TextMesh>();
_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<MeshRenderer>();
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<Collider>());
_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<Renderer>();
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);
}
}

View File

@@ -31,8 +31,7 @@ public class StatsTracker : MonoBehaviour
private string _cachedName = ""; private string _cachedName = "";
private float _lastSentTime = -999f; private float _lastSentTime = -999f;
private PlayerController _pc; private Rigidbody _rb;
private Rigidbody _rb;
void Awake() void Awake()
{ {
@@ -42,7 +41,6 @@ public class StatsTracker : MonoBehaviour
void Start() void Start()
{ {
_pc = GetComponent<PlayerController>();
_rb = GetComponent<Rigidbody>(); _rb = GetComponent<Rigidbody>();
var nm = NetworkManager.Instance; var nm = NetworkManager.Instance;

View File

@@ -76,8 +76,8 @@ public class ChatUI : MonoBehaviour
_pollTimer = POLL_INTERVAL; // poll immediately _pollTimer = POLL_INTERVAL; // poll immediately
Cursor.lockState = CursorLockMode.None; Cursor.lockState = CursorLockMode.None;
Cursor.visible = true; Cursor.visible = true;
// Release held movement keys so the ball doesn't keep moving while typing // Movement keys are handled by NWH InputSystemVehicleInputProvider via Input System;
FindFirstObjectByType<PlayerController>()?.ResetInputs(); // no manual reset needed when chat opens (NWH polls Input System each frame).
} }
else else
{ {