Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 103f8859d4 |
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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}`
|
||||
|
||||
|
||||
|
||||
@@ -178,8 +178,8 @@ public class GameManager : MonoBehaviour
|
||||
{
|
||||
if (playerRoot == null) return;
|
||||
playerRoot.SetActive(active);
|
||||
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
||||
if (pc != null) pc.enabled = active;
|
||||
var vehicle = playerRoot.GetComponentInChildren<NWH.VehiclePhysics2.VehicleController>(true);
|
||||
if (vehicle != null) vehicle.enabled = active;
|
||||
|
||||
if (active)
|
||||
{
|
||||
|
||||
@@ -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<PlayerController>();
|
||||
if (pc != null && pc.isActiveAndEnabled)
|
||||
var vehicle = FindFirstObjectByType<NWH.VehiclePhysics2.VehicleController>();
|
||||
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<Rigidbody>();
|
||||
var rb = vehicle.vehicleRigidbody;
|
||||
if (rb != null)
|
||||
{
|
||||
var v = rb.linearVelocity;
|
||||
|
||||
@@ -118,25 +118,22 @@ public class LobbyUI : MonoBehaviour
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null && playerRoot != null)
|
||||
{
|
||||
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
||||
if (pc != null)
|
||||
var setup = playerRoot.GetComponentInChildren<VehicleLocalSetup>(true);
|
||||
if (setup != null)
|
||||
{
|
||||
var vehicle = setup.GetComponent<NWH.VehiclePhysics2.VehicleController>();
|
||||
var rb = vehicle != null ? vehicle.vehicleRigidbody : setup.GetComponent<Rigidbody>();
|
||||
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<Rigidbody>();
|
||||
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<PlayerController>(true);
|
||||
if (pc != null) pc.enabled = false;
|
||||
var vehicle = playerRoot.GetComponentInChildren<NWH.VehiclePhysics2.VehicleController>(true);
|
||||
if (vehicle != null) vehicle.enabled = false;
|
||||
playerRoot.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class NetworkManager : MonoBehaviour
|
||||
public string LastError { get; private set; } = "";
|
||||
|
||||
// Expose remote players for debug UI
|
||||
public Dictionary<string, RemotePlayerController> RemotePlayers => _remotePlayers;
|
||||
public Dictionary<string, RemoteVehicleSync> 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<GameState> _room;
|
||||
private StateCallbackStrategy<GameState> _callbacks;
|
||||
private readonly Dictionary<string, RemotePlayerController> _remotePlayers = new();
|
||||
private readonly Dictionary<string, RemoteVehicleSync> _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<RemotePlayerController>()
|
||||
?? remoteBall.AddComponent<RemotePlayerController>();
|
||||
var controller = remote.GetComponent<RemoteVehicleSync>()
|
||||
?? remote.AddComponent<RemoteVehicleSync>();
|
||||
|
||||
controller.Initialize(sessionId, player.name,
|
||||
new Color(player.colorR, player.colorG, player.colorB));
|
||||
@@ -324,20 +327,21 @@ public class NetworkManager : MonoBehaviour
|
||||
|
||||
// ─── 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()
|
||||
{
|
||||
if (_room == null || !IsConnected) return;
|
||||
|
||||
if (_localPlayer == null)
|
||||
{
|
||||
var pc = FindFirstObjectByType<PlayerController>();
|
||||
if (pc != null)
|
||||
{
|
||||
_localPlayer = pc.transform;
|
||||
_localPlayerRb = pc.GetComponent<Rigidbody>();
|
||||
}
|
||||
else return;
|
||||
}
|
||||
if (_localPlayer == null || _localPlayerRb == null) return;
|
||||
|
||||
Vector3 pos = _localPlayer.position;
|
||||
Vector3 vel = _localPlayerRb != null ? _localPlayerRb.linearVelocity : Vector3.zero;
|
||||
|
||||
283
game/Assets/Scripts/Network/RemoteVehicleSync.cs
Normal file
283
game/Assets/Scripts/Network/RemoteVehicleSync.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
109
game/Assets/Scripts/Network/VehicleLocalSetup.cs
Normal file
109
game/Assets/Scripts/Network/VehicleLocalSetup.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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<PlayerController>();
|
||||
_rb = GetComponent<Rigidbody>();
|
||||
|
||||
var nm = NetworkManager.Instance;
|
||||
|
||||
@@ -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<PlayerController>()?.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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user