Working item switcher

This commit is contained in:
Michal Pikulski
2025-12-15 00:05:35 +01:00
parent a78085adee
commit 8995dd1949
24 changed files with 2046 additions and 57 deletions

View File

@@ -67,13 +67,6 @@ namespace Input
{
base.OnManagedStart();
// Register with InputManager
if (InputManager.Instance != null)
{
InputManager.Instance.SetDefaultConsumer(this);
Logging.Debug($"[{GetType().Name}] Registered as default input consumer");
}
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
}

View File

@@ -33,6 +33,9 @@ namespace Input
// Track which consumer is handling the current hold operation
private ITouchInputConsumer _activeHoldConsumer;
// Controller registration system
private readonly Dictionary<string, ITouchInputConsumer> _registeredControllers = new Dictionary<string, ITouchInputConsumer>();
/// <summary>
/// Singleton instance of the InputManager. No longer creates an instance if one doesn't exist.
@@ -408,5 +411,116 @@ namespace Input
consumer.OnTap(worldPos);
return true;
}
#region Controller Registration System
/// <summary>
/// Registers a controller with a unique name for later switching.
/// </summary>
/// <param name="controllerName">Unique name for the controller</param>
/// <param name="controller">The controller instance to register</param>
/// <param name="setAsDefaultConsumer">If true, sets this controller as the default input consumer</param>
public void RegisterController(string controllerName, ITouchInputConsumer controller, bool setAsDefaultConsumer = false)
{
if (string.IsNullOrEmpty(controllerName))
{
Debug.LogError("[InputManager] Cannot register controller with null or empty name.");
return;
}
if (controller == null)
{
Debug.LogError($"[InputManager] Cannot register null controller for name: {controllerName}");
return;
}
if (_registeredControllers.ContainsKey(controllerName))
{
Debug.LogWarning($"[InputManager] Controller with name '{controllerName}' is already registered. Overwriting.");
}
_registeredControllers[controllerName] = controller;
Logging.Debug($"Controller registered: {controllerName}");
if (setAsDefaultConsumer)
{
SetDefaultConsumer(controller);
Logging.Debug($"Controller '{controllerName}' set as default consumer.");
}
}
/// <summary>
/// Unregisters a controller by name.
/// </summary>
/// <param name="controllerName">Name of the controller to unregister</param>
public void UnregisterController(string controllerName)
{
if (string.IsNullOrEmpty(controllerName))
{
Debug.LogError("[InputManager] Cannot unregister controller with null or empty name.");
return;
}
if (_registeredControllers.Remove(controllerName))
{
Logging.Debug($"Controller unregistered: {controllerName}");
}
else
{
Debug.LogWarning($"[InputManager] Attempted to unregister non-existent controller: {controllerName}");
}
}
/// <summary>
/// Gets a registered controller by name.
/// </summary>
/// <param name="controllerName">Name of the controller to retrieve</param>
/// <returns>The controller if found, null otherwise</returns>
public ITouchInputConsumer GetController(string controllerName)
{
if (string.IsNullOrEmpty(controllerName))
{
Debug.LogError("[InputManager] Cannot get controller with null or empty name.");
return null;
}
if (_registeredControllers.TryGetValue(controllerName, out ITouchInputConsumer controller))
{
return controller;
}
Debug.LogWarning($"[InputManager] Controller not found: {controllerName}");
return null;
}
/// <summary>
/// Switches to a registered controller by name, setting it as the default consumer.
/// </summary>
/// <param name="controllerName">Name of the controller to switch to</param>
/// <returns>True if the switch was successful, false otherwise</returns>
public bool SwitchToController(string controllerName)
{
ITouchInputConsumer controller = GetController(controllerName);
if (controller != null)
{
SetDefaultConsumer(controller);
Logging.Debug($"Switched to controller: {controllerName}");
return true;
}
return false;
}
/// <summary>
/// Checks if a controller with the given name is registered.
/// </summary>
/// <param name="controllerName">Name to check</param>
/// <returns>True if registered, false otherwise</returns>
public bool IsControllerRegistered(string controllerName)
{
return !string.IsNullOrEmpty(controllerName) && _registeredControllers.ContainsKey(controllerName);
}
#endregion
}
}

View File

@@ -39,6 +39,18 @@ namespace Input
_movementSettings = configs.DefaultPlayerMovement;
}
internal override void OnManagedStart()
{
base.OnManagedStart();
// Register with InputManager as default consumer
if (InputManager.Instance != null)
{
InputManager.Instance.RegisterController("trafalgar", this, setAsDefaultConsumer: true);
Logging.Debug($"[PlayerTouchController] Registered controller '{gameObject.name}' as default consumer");
}
}
#region ITouchInputConsumer Overrides (Add InterruptMoveTo)
public override void OnTap(Vector2 worldPosition)

View File

@@ -0,0 +1,318 @@
using System;
using System.Collections;
using Core;
using Input;
using Interactions;
using Minigames.TrashMaze.Core;
using Minigames.TrashMaze.Data;
using Unity.Cinemachine;
using UnityEngine;
namespace Items
{
/// <summary>
/// Saveable data for ControllerSwitchItem state
/// </summary>
[Serializable]
public class ControllerSwitchItemSaveData
{
public bool hasBeenUsed;
}
/// <summary>
/// Camera switching mode for controller switch items
/// </summary>
public enum CameraSwitchMode
{
/// <summary>
/// No camera switching - controller switch only
/// </summary>
None,
/// <summary>
/// Use a direct reference to a Cinemachine camera
/// </summary>
DirectReference,
/// <summary>
/// Use TrashMazeCameraController state manager API
/// </summary>
TrashMazeCameraState
}
/// <summary>
/// An interactable item that switches control from one character controller to another.
/// When clicked:
/// 1. The selected character moves to this item's position
/// 2. Upon arrival, the current controller is disabled
/// 3. Camera blends to the target camera (based on camera mode)
/// 4. Once the blend completes, control switches to the target controller
/// </summary>
public class ControllerSwitchItem : SaveableInteractable
{
[Header("Controller Switch Settings")]
[Tooltip("Name of the controller to switch to (must match GameObject name of the controller)")]
[SerializeField] private string targetControllerName;
[Header("Camera Settings")]
[Tooltip("How to switch the camera when changing controllers")]
[SerializeField] private CameraSwitchMode cameraSwitchMode = CameraSwitchMode.None;
[Tooltip("Direct camera reference (only used if Camera Switch Mode is DirectReference)")]
[SerializeField] private CinemachineCamera targetVirtualCamera;
[Tooltip("Target camera state (only used if Camera Switch Mode is TrashMazeCameraState)")]
[SerializeField] private TrashMazeCameraState targetCameraState;
[Header("Visual Feedback")]
[Tooltip("Visual representation to hide after use (optional)")]
[SerializeField] private GameObject visualRepresentation;
// State
private bool _hasBeenUsed;
private PlayerTouchController _currentPlayerController;
private bool _isSwitching;
public override string SaveId => $"{gameObject.scene.name}/ControllerSwitchItem/{gameObject.name}";
internal override void OnManagedAwake()
{
base.OnManagedAwake();
if (string.IsNullOrEmpty(targetControllerName))
{
Debug.LogError($"[ControllerSwitchItem] {gameObject.name} has no target controller name specified!");
}
}
internal override void OnManagedStart()
{
base.OnManagedStart();
// Apply state after restoration
if (_hasBeenUsed && isOneTime)
{
DisableVisual();
}
}
protected override bool CanBeClicked()
{
// Cannot be clicked if already used (one-time) or if currently switching
if (_isSwitching)
return false;
if (isOneTime && _hasBeenUsed)
return false;
// Check if target controller is registered
if (!InputManager.Instance.IsControllerRegistered(targetControllerName))
{
Debug.LogWarning($"[ControllerSwitchItem] Target controller '{targetControllerName}' is not registered with InputManager.");
return false;
}
return base.CanBeClicked();
}
protected override bool DoInteraction()
{
if (_isSwitching)
return false;
// By the time this is called, the interacting character has already arrived at this item
// We just need to perform the controller/camera switch
Logging.Debug("[ControllerSwitchItem] Starting controller switch sequence");
// Start the async switch sequence (camera blend + controller switch)
StartCoroutine(SwitchControllerSequence());
// Return true immediately - interaction is considered successful
// The coroutine will handle the actual switching asynchronously
return true;
}
private IEnumerator SwitchControllerSequence()
{
_isSwitching = true;
// Step 1: Get current player controller (the one we're switching FROM)
_currentPlayerController = FindFirstObjectByType<PlayerTouchController>();
if (_currentPlayerController == null)
{
Debug.LogError("[ControllerSwitchItem] Could not find PlayerTouchController in scene!");
_isSwitching = false;
yield break;
}
Logging.Debug("[ControllerSwitchItem] Character has arrived, beginning switch");
// Step 2: Disable current player controller
_currentPlayerController.enabled = false;
Logging.Debug("[ControllerSwitchItem] Disabled current player controller");
// Step 3: Blend to target camera based on mode
yield return SwitchCamera();
// Step 4: Switch to target controller
ITouchInputConsumer targetController = InputManager.Instance.GetController(targetControllerName);
if (targetController != null)
{
// Enable the target controller if it's a MonoBehaviour
if (targetController is MonoBehaviour targetMono)
{
targetMono.enabled = true;
Logging.Debug($"[ControllerSwitchItem] Enabled target controller: {targetControllerName}");
}
// Switch input control to the target controller
bool switchSuccess = InputManager.Instance.SwitchToController(targetControllerName);
if (switchSuccess)
{
Logging.Debug($"[ControllerSwitchItem] Successfully switched input to controller: {targetControllerName}");
}
else
{
Debug.LogError($"[ControllerSwitchItem] Failed to switch to controller: {targetControllerName}");
}
}
else
{
Debug.LogError($"[ControllerSwitchItem] Target controller '{targetControllerName}' not found!");
}
// Step 5: Mark as used if one-time use
if (isOneTime)
{
_hasBeenUsed = true;
DisableVisual();
}
_isSwitching = false;
}
private IEnumerator SwitchCamera()
{
switch (cameraSwitchMode)
{
case CameraSwitchMode.None:
// No camera switching
Logging.Debug("[ControllerSwitchItem] No camera switching configured");
break;
case CameraSwitchMode.DirectReference:
if (targetVirtualCamera != null)
{
Logging.Debug($"[ControllerSwitchItem] Blending to camera: {targetVirtualCamera.name}");
// Set the target camera as highest priority
targetVirtualCamera.Priority = 100;
// Wait for camera blend to complete
yield return WaitForCameraBlend();
}
else
{
Debug.LogWarning("[ControllerSwitchItem] DirectReference mode selected but no camera assigned!");
}
break;
case CameraSwitchMode.TrashMazeCameraState:
if (TrashMazeCameraController.Instance != null)
{
Logging.Debug($"[ControllerSwitchItem] Switching to camera state: {targetCameraState}");
// Use the state manager API
if (targetCameraState == TrashMazeCameraState.Gameplay)
{
TrashMazeCameraController.Instance.SwitchToGameplay();
}
else if (targetCameraState == TrashMazeCameraState.Maze)
{
TrashMazeCameraController.Instance.SwitchToMaze();
}
// Wait for camera blend to complete
yield return WaitForCameraBlend();
}
else
{
Debug.LogError("[ControllerSwitchItem] TrashMazeCameraController instance not found in scene!");
}
break;
}
}
private IEnumerator WaitForCameraBlend()
{
CinemachineBrain brain = Camera.main?.GetComponent<CinemachineBrain>();
if (brain != null)
{
// Wait until blend is not active
while (brain.IsBlending)
{
yield return null;
}
Logging.Debug("[ControllerSwitchItem] Camera blend completed");
}
else
{
// If no brain, just wait a brief moment
yield return new WaitForSeconds(0.5f);
}
}
private void DisableVisual()
{
if (visualRepresentation != null)
{
visualRepresentation.SetActive(false);
}
}
#region Save/Load
protected override object GetSerializableState()
{
return new ControllerSwitchItemSaveData
{
hasBeenUsed = _hasBeenUsed
};
}
protected override void ApplySerializableState(string serializedData)
{
try
{
var data = JsonUtility.FromJson<ControllerSwitchItemSaveData>(serializedData);
_hasBeenUsed = data.hasBeenUsed;
Logging.Debug($"[ControllerSwitchItem] Restored state: hasBeenUsed={_hasBeenUsed}");
}
catch (Exception e)
{
Debug.LogError($"[ControllerSwitchItem] Failed to deserialize save data: {e.Message}");
}
}
#endregion
#if UNITY_EDITOR
private void OnValidate()
{
// Visual feedback in editor
if (string.IsNullOrEmpty(targetControllerName))
{
name = "ControllerSwitchItem (UNCONFIGURED)";
}
else
{
name = $"ControllerSwitchItem_To_{targetControllerName}";
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 915abd653d714ea3ae11bbf14feafb1e
timeCreated: 1765747971

View File

@@ -56,7 +56,10 @@ public class GlowOutline : ManagedBehaviour
foreach (SpriteRenderer childSprite in childrenSprites)
{
if (itemSprite.sprite != null)
if (!itemSprite)
continue;
if (itemSprite?.sprite != null)
{
childSprite.sprite = itemSprite.sprite;
childSprite.material = outlineMaterial;

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 44bf6c911c674dc98cc5a06ad14c7d56
timeCreated: 1765747971

View File

@@ -51,6 +51,18 @@ namespace Minigames.TrashMaze.Core
_visionRadius = configs.FollowerMovement.TrashMazeVisionRadius;
Logging.Debug($"[PulverController] Loaded vision radius from settings: {_visionRadius}");
}
internal override void OnManagedStart()
{
base.OnManagedStart();
// Register with InputManager (not as default consumer)
if (InputManager.Instance != null)
{
InputManager.Instance.RegisterController("pulver", this, setAsDefaultConsumer: false);
Logging.Debug($"[PulverController] Registered controller '{gameObject.name}'");
}
}
protected override void Update()
{

View File

@@ -0,0 +1,106 @@
using Common.Camera;
using Core;
using Minigames.TrashMaze.Data;
using Unity.Cinemachine;
namespace Minigames.TrashMaze.Core
{
/// <summary>
/// Manages camera states for the Trash Maze minigame.
/// Handles transitions between Gameplay (level exploration) and Maze (inside maze exploration) cameras.
/// Provides singleton access for easy camera switching from items and other systems.
/// </summary>
public class TrashMazeCameraController : CameraStateManager<TrashMazeCameraState>
{
#region Singleton
private static TrashMazeCameraController _instance;
/// <summary>
/// Singleton instance of the camera controller
/// </summary>
public static TrashMazeCameraController Instance => _instance;
#endregion
#region Lifecycle
internal override void OnManagedAwake()
{
// Base class handles InitializeCameraMap() and ValidateCameras()
base.OnManagedAwake();
// Set singleton
if (_instance != null && _instance != this)
{
Logging.Warning("[TrashMazeCameraController] Multiple instances detected! Destroying duplicate.");
Destroy(gameObject);
return;
}
_instance = this;
}
internal override void OnManagedStart()
{
base.OnManagedStart();
// Start in gameplay camera by default
SwitchToGameplay();
}
internal override void OnManagedDestroy()
{
base.OnManagedDestroy();
if (_instance == this)
{
_instance = null;
}
}
#endregion
#region Public API
/// <summary>
/// Switch to the main gameplay camera (level exploration)
/// </summary>
public void SwitchToGameplay()
{
SwitchToState(TrashMazeCameraState.Gameplay);
if (showDebugLogs)
Logging.Debug("[TrashMazeCameraController] Switched to Gameplay camera");
}
/// <summary>
/// Switch to the maze camera (inside maze exploration)
/// </summary>
public void SwitchToMaze()
{
SwitchToState(TrashMazeCameraState.Maze);
if (showDebugLogs)
Logging.Debug("[TrashMazeCameraController] Switched to Maze camera");
}
/// <summary>
/// Get the gameplay camera
/// </summary>
public CinemachineCamera GetGameplayCamera()
{
return GetCamera(TrashMazeCameraState.Gameplay);
}
/// <summary>
/// Get the maze camera
/// </summary>
public CinemachineCamera GetMazeCamera()
{
return GetCamera(TrashMazeCameraState.Maze);
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d058b159d0aa43699eaba263b7b8c5a7
timeCreated: 1765749988

View File

@@ -13,8 +13,7 @@ namespace Minigames.TrashMaze.Core
public static TrashMazeController Instance { get; private set; }
[Header("Player")]
[SerializeField] private PulverController pulverPrefab;
[SerializeField] private Transform startPosition;
[SerializeField] private PulverController pulverController;
[Header("Background")]
[Tooltip("Background sprite renderer - world size and center are inferred from its bounds")]
@@ -27,7 +26,6 @@ namespace Minigames.TrashMaze.Core
private static readonly int WorldSizeID = Shader.PropertyToID("_WorldSize");
private static readonly int WorldCenterID = Shader.PropertyToID("_WorldCenter");
private PulverController _pulverInstance;
private bool _mazeCompleted;
internal override void OnManagedAwake()
@@ -59,8 +57,8 @@ namespace Minigames.TrashMaze.Core
// Infer world bounds from background renderer and set shader globals
ApplyBackgroundBoundsToShader();
// Spawn player
SpawnPulver();
// Validate player reference
InitializePulver();
Logging.Debug("[TrashMazeController] Trash Maze initialized");
}
@@ -108,18 +106,15 @@ namespace Minigames.TrashMaze.Core
$"Size=({worldSize.x:F2}, {worldSize.y:F2}), Center=({worldCenter.x:F2}, {worldCenter.y:F2})");
}
private void SpawnPulver()
private void InitializePulver()
{
if (pulverPrefab == null)
if (pulverController == null)
{
Logging.Error("[TrashMazeController] Pulver prefab not assigned!");
Logging.Error("[TrashMazeController] PulverController reference not assigned! Please assign it in the Inspector.");
return;
}
Vector3 spawnPosition = startPosition != null ? startPosition.position : Vector3.zero;
_pulverInstance = Instantiate(pulverPrefab, spawnPosition, Quaternion.identity);
Logging.Debug($"[TrashMazeController] Pulver spawned at {spawnPosition}");
Logging.Debug($"[TrashMazeController] Pulver controller initialized at {pulverController.transform.position}");
}
/// <summary>

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c1c86c04b4dc4dd5add77ba3bb17f95e
timeCreated: 1765749918

View File

@@ -0,0 +1,19 @@
namespace Minigames.TrashMaze.Data
{
/// <summary>
/// Camera states for Trash Maze minigame
/// </summary>
public enum TrashMazeCameraState
{
/// <summary>
/// Main gameplay camera following Trafalgar around the level
/// </summary>
Gameplay,
/// <summary>
/// Maze camera following Pulver when exploring the maze alone
/// </summary>
Maze
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4e26aed52b5e4597b3cbba9191fe463b
timeCreated: 1765749918