From e04868f9c0a251d6aae9a4c85aae971951272dfe Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Sat, 27 Jun 2026 15:22:23 +0200 Subject: [PATCH] Fix first-person camera target and paint HUD rebind after networked spawn Point the first-person camera at the dedicated FirstPersonTarget anchor (eye height) instead of the shared CameraTarget, in both the runtime SetPlayer rebind and the editor ResolveReferences fallback. Rebind the paint HUD to the locally spawned avatar's MeshPainter via a new GameModeManager.PainterChanged event. Since the spawn-on-connect refactor, no player exists at PaintHud.Start(), so the HUD bound to a null painter (no brush masks shown, color/settings were no-ops). Co-Authored-By: Claude Opus 4.8 --- Assets/Prefabs/Hider.prefab | 4 -- Assets/Scripts/FirstPersonController.cs | 12 +++--- Assets/Scripts/GameModeManager.cs | 7 +++ Assets/Scripts/PaintHud.cs | 57 +++++++++++++++++++------ 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/Assets/Prefabs/Hider.prefab b/Assets/Prefabs/Hider.prefab index 4563a8d..c340875 100644 --- a/Assets/Prefabs/Hider.prefab +++ b/Assets/Prefabs/Hider.prefab @@ -16,10 +16,6 @@ PrefabInstance: propertyPath: GlobalObjectIdHash value: 708302045 objectReference: {fileID: 0} - - target: {fileID: 4587432968399885263, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3} - propertyPath: m_Enabled - value: 1 - objectReference: {fileID: 0} - target: {fileID: 5384024074013944205, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3} propertyPath: m_Name value: Hider diff --git a/Assets/Scripts/FirstPersonController.cs b/Assets/Scripts/FirstPersonController.cs index 1cd5fb8..a0fc930 100644 --- a/Assets/Scripts/FirstPersonController.cs +++ b/Assets/Scripts/FirstPersonController.cs @@ -6,7 +6,7 @@ using UnityEngine.InputSystem; /// /// First-person camera. Drives its OWN dedicated : each frame it snaps -/// to the player's CameraTarget (the same point the ThirdPersonCamera tracks) for position and +/// to the player's dedicated FirstPersonTarget (the eye anchor) for position and /// applies free mouse-look (yaw/pitch) for rotation. While active the player's mesh is hidden so the /// body isn't seen from the inside. Movement stays with the player controller, which reads the /// brain-driven Main Camera forward, so WASD is automatically relative to this view. @@ -21,7 +21,7 @@ using UnityEngine.InputSystem; public class FirstPersonController : MonoBehaviour { [Header("Target")] - [Tooltip("Eye position the camera sits at (the player's CameraTarget). Empty = auto-resolved from the player's \"CameraTarget\" child.")] + [Tooltip("Eye position the camera sits at (the player's FirstPersonTarget). Empty = auto-resolved from the player's \"FirstPersonTarget\" child.")] public Transform cameraTarget; [Tooltip("Renderers hidden while first-person is active (so you don't see your own body). Empty = the player's SkinnedMeshRenderers, auto-resolved.")] @@ -136,9 +136,9 @@ public class FirstPersonController : MonoBehaviour Quaternion rot = Quaternion.Euler(_displayPitch, _displayYaw, 0f); if (cameraTarget) { - // Sit at the shared camera target, then raise/lower the eye by the active pose's first-person + // Sit at the first-person eye anchor, then raise/lower the eye by the active pose's first-person // offset. TransformVector keeps it in the target's local up and scales with the player (so it - // tracks PlayerScaler), without disturbing the target the ThirdPersonCamera shares. + // tracks PlayerScaler). Vector3 pos = cameraTarget.position; float offset = _poseController ? _poseController.FirstPersonCameraYOffset : 0f; if (offset != 0f) @@ -185,7 +185,7 @@ public class FirstPersonController : MonoBehaviour if (!p) return; _player = p; _poseController = p.GetComponent(); - Transform t = p.transform.Find("CameraTarget"); + Transform t = p.transform.Find("FirstPersonTarget"); if (t) cameraTarget = t; hiddenRenderers = p.GetComponentsInChildren(true); _resolved = true; @@ -205,7 +205,7 @@ public class FirstPersonController : MonoBehaviour if (!cameraTarget) { - Transform t = player.transform.Find("CameraTarget"); + Transform t = player.transform.Find("FirstPersonTarget"); if (t) cameraTarget = t; } diff --git a/Assets/Scripts/GameModeManager.cs b/Assets/Scripts/GameModeManager.cs index 3b6a91b..0f1a3b2 100644 --- a/Assets/Scripts/GameModeManager.cs +++ b/Assets/Scripts/GameModeManager.cs @@ -54,6 +54,12 @@ public class GameModeManager : MonoBehaviour /// Argument = the now-active mode. Used by UI (e.g. the paint HUD) to follow the mode. public event System.Action ModeChanged; + /// Raised when the painter the HUD should edit changes - i.e. when the local + /// networked avatar spawns and binds its per-avatar painter. + /// With spawn-on-connect there is no painter at scene start, so the paint HUD follows this + /// to (re)bind its widgets to the live painter once it exists. + public event System.Action PainterChanged; + private readonly List _modes = new List(); private int _activeIndex = -1; @@ -122,6 +128,7 @@ public class GameModeManager : MonoBehaviour // The painter lives on the player (per-avatar), so bind the local one. if (!painter) painter = localPlayer.GetComponentInChildren(true); + PainterChanged?.Invoke(painter); Transform camTarget = localPlayer.transform.Find("CameraTarget"); diff --git a/Assets/Scripts/PaintHud.cs b/Assets/Scripts/PaintHud.cs index 1ba8889..421242f 100644 --- a/Assets/Scripts/PaintHud.cs +++ b/Assets/Scripts/PaintHud.cs @@ -87,19 +87,24 @@ public partial class PaintHud : MonoBehaviour private void OnEnable() { - if (modeManager) modeManager.ModeChanged += OnModeChanged; + if (modeManager) + { + modeManager.ModeChanged += OnModeChanged; + modeManager.PainterChanged += OnPainterChanged; + } } private void OnDisable() { - if (modeManager) modeManager.ModeChanged -= OnModeChanged; + if (modeManager) + { + modeManager.ModeChanged -= OnModeChanged; + modeManager.PainterChanged -= OnPainterChanged; + } } private void Start() { - if (!painter) painter = modeManager ? modeManager.Painter : null; - if (!painter) painter = FindFirstObjectByType(); - _font = LoadFont(); EnsureEventSystem(); @@ -112,18 +117,46 @@ public partial class PaintHud : MonoBehaviour _panel = go; } - WirePanel(_panel.transform); - - // Re-sync every widget when the pipette adopts a sampled material (painter -> HUD). - if (painter) painter.Picked += SyncFromPainter; - - // Restore the saved panel positions + widget values from the previous session. - RestoreHudState(); + // Bind to a painter as soon as one exists. With networked spawn-on-connect there is none + // at scene start; BindLocalPlayer then raises PainterChanged when the local avatar spawns + // and OnPainterChanged wires the widgets. In a non-networked scene the painter is already + // present and binds here immediately. + var p = painter; + if (!p) p = modeManager ? modeManager.Painter : null; + if (!p) p = FindFirstObjectByType(); + if (p) BindPainter(p); // Set initial visibility (the manager's first SwitchTo may have fired before this Start). OnModeChanged(modeManager ? modeManager.ActiveMode : null); } + private void OnPainterChanged(MeshPainter newPainter) + { + if (newPainter) BindPainter(newPainter); + } + + /// + /// (Re)binds the HUD's widgets to a live painter. Called from Start when one already exists + /// (non-networked scene) or from the manager's PainterChanged event when the local networked + /// avatar spawns. Binds exactly once per painter: re-binding the same painter is a no-op, so + /// the deferred network path and the immediate scene path never double-add widget listeners. + /// + private void BindPainter(MeshPainter newPainter) + { + if (!newPainter || newPainter == painter || !_panel) return; + + if (painter) painter.Picked -= SyncFromPainter; + painter = newPainter; + + WirePanel(_panel.transform); + + // Re-sync every widget when the pipette adopts a sampled material (painter -> HUD). + painter.Picked += SyncFromPainter; + + // Restore the saved panel positions + widget values now that the painter exists. + RestoreHudState(); + } + private void OnDestroy() { if (painter) painter.Picked -= SyncFromPainter;