Files
chameleon/Assets/Scripts/GameModeManager.cs
T
adminandClaude Opus 4.8 e04868f9c0 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 <noreply@anthropic.com>
2026-06-27 15:22:23 +02:00

186 lines
7.1 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using Unity.Cinemachine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
/// <summary>
/// Switches the game between high-level modes (Player Control, Paint, Free Cam). The switch
/// key (Tab) cycles to the next registered mode. Register additional modes in
/// <see cref="BuildModes"/>. Attach to a single, persistent scene GameObject.
/// </summary>
[DisallowMultipleComponent]
public class GameModeManager : MonoBehaviour
{
[Header("Controlled Systems")]
[Tooltip("Player movement controller. Empty = found automatically.")]
public PlayerCharacterController player;
[Tooltip("Third-person orbit camera input. Empty = found automatically.")]
public ThirdPersonOrbitInput cameraInput;
[Tooltip("Mesh painter. Empty = found automatically.")]
public MeshPainter painter;
[Tooltip("Free-fly spectator camera. Empty = found automatically. Absent = Free Cam is left out of the cycle.")]
public FreeCamController freeCam;
[Tooltip("First-person camera. Empty = found automatically. Absent = First Person is left out of the cycle.")]
public FirstPersonController firstPerson;
//[Tooltip("Insect / surface-walk controller. Empty = found automatically.")]
//public InsectController insect;
[Header("Switching")]
[Tooltip("Key that cycles to the next mode.")]
#if ENABLE_INPUT_SYSTEM
public Key switchKey = Key.Tab;
#else
public KeyCode switchKey = KeyCode.Tab;
#endif
[Tooltip("Mode active at startup (0 = Player Control).")]
public int startModeIndex = 0;
public PlayerCharacterController Player => player;
public ThirdPersonOrbitInput CameraInput => cameraInput;
public MeshPainter Painter => painter;
public FreeCamController FreeCam => freeCam;
public FirstPersonController FirstPerson => firstPerson;
//public InsectController Insect => insect;
/// <summary>Raised after a mode switch finishes (the new mode's Enter has already run).
/// Argument = the now-active mode. Used by UI (e.g. the paint HUD) to follow the mode.</summary>
public event System.Action<GameMode> ModeChanged;
/// <summary>Raised when the painter the HUD should edit changes - i.e. when the local
/// networked avatar spawns and <see cref="BindLocalPlayer"/> 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.</summary>
public event System.Action<MeshPainter> PainterChanged;
private readonly List<GameMode> _modes = new List<GameMode>();
private int _activeIndex = -1;
/// <summary>The currently active mode (null before startup).</summary>
public GameMode ActiveMode =>
(_activeIndex >= 0 && _activeIndex < _modes.Count) ? _modes[_activeIndex] : null;
private void Awake()
{
if (!player) player = FindFirstObjectByType<PlayerCharacterController>();
if (!cameraInput) cameraInput = FindFirstObjectByType<ThirdPersonOrbitInput>();
if (!painter) painter = FindFirstObjectByType<MeshPainter>();
// Include inactive so a freecam rig that starts disabled is still found and wired.
if (!freeCam) freeCam = FindFirstObjectByType<FreeCamController>(FindObjectsInactive.Include);
if (!firstPerson) firstPerson = FindFirstObjectByType<FirstPersonController>(FindObjectsInactive.Include);
//if (!insect) insect = FindFirstObjectByType<InsectController>();
BuildModes();
}
private void Start()
{
if (_modes.Count > 0)
SwitchTo(Mathf.Clamp(startModeIndex, 0, _modes.Count - 1));
}
/// <summary>Registers all available modes. Add new modes here.</summary>
private void BuildModes()
{
_modes.Clear();
// Player Control only joins the Tab cycle when a gravity player is present, so a scene
// running only the insect walker doesn't trap input in a dead, player-freezing mode.
if (player)
_modes.Add(new PlayerControlMode());
_modes.Add(new PaintMode());
// Free Cam only joins the cycle when a freecam rig is present, so scenes without one are unchanged.
if (freeCam)
_modes.Add(new FreeCamMode());
// First Person only joins the cycle when a first-person rig is present, so scenes without one are unchanged.
if (firstPerson)
_modes.Add(new FirstPersonMode());
// Insect mode only joins the Tab cycle when a surface walker is present, so a scene
// without one behaves exactly as before (no dead, player-freezing mode in the cycle).
//if (insect)
// _modes.Add(new InsectMode());
foreach (var m in _modes)
m.Bind(this);
}
/// <summary>
/// Binds the scene singletons (this manager, the orbit/first-person cameras, the painter) to the
/// locally-owned networked player after it spawns, then enters Player Control. Called from
/// <see cref="NetworkPlayerSetup"/> on the owning client. Before this runs the scene has no player,
/// so the cycle sits in a player-less mode (Paint); this promotes it once the avatar exists.
/// </summary>
public void BindLocalPlayer(PlayerCharacterController localPlayer)
{
if (!localPlayer) return;
player = localPlayer;
// The painter lives on the player (per-avatar), so bind the local one.
if (!painter) painter = localPlayer.GetComponentInChildren<MeshPainter>(true);
PainterChanged?.Invoke(painter);
Transform camTarget = localPlayer.transform.Find("CameraTarget");
// Re-point the third-person orbit camera (Cinemachine Follow/LookAt) at the local avatar.
if (cameraInput)
{
var vcam = cameraInput.GetComponent<CinemachineCamera>();
if (vcam) { vcam.Follow = camTarget; vcam.LookAt = camTarget; }
}
if (firstPerson)
firstPerson.SetPlayer(localPlayer);
// Tear down the pre-spawn (player-less) mode and rebuild with Player Control available.
ActiveMode?.Exit();
_activeIndex = -1;
BuildModes();
int idx = _modes.FindIndex(m => m is PlayerControlMode);
SwitchTo(idx >= 0 ? idx : Mathf.Clamp(startModeIndex, 0, _modes.Count - 1));
}
private void Update()
{
if (_modes.Count == 0) return;
if (WasSwitchPressed())
SwitchTo((_activeIndex + 1) % _modes.Count);
ActiveMode?.Tick();
}
/// <summary>Activates the mode at <paramref name="index"/> (Exit old, Enter new).</summary>
public void SwitchTo(int index)
{
if (index < 0 || index >= _modes.Count || index == _activeIndex) return;
ActiveMode?.Exit();
_activeIndex = index;
ActiveMode.Enter();
Debug.Log($"[GameModeManager] Mode -> {ActiveMode.DisplayName}", this);
ModeChanged?.Invoke(ActiveMode);
}
private bool WasSwitchPressed()
{
#if ENABLE_INPUT_SYSTEM
return Keyboard.current != null && Keyboard.current[switchKey].wasPressedThisFrame;
#else
return Input.GetKeyDown(switchKey);
#endif
}
}