Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 103f8859d4 |
Binary file not shown.
@@ -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",
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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 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;
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user