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;