Generic MVP working

This commit is contained in:
Michal Pikulski
2025-12-05 12:18:29 +01:00
committed by Michal Pikulski
parent 11833ba503
commit ab579e2d21
11 changed files with 303 additions and 177 deletions

View File

@@ -70,6 +70,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c56b7c4096b59584c93f2cfa79230643
m_Address: Settings/AirplaneSettings
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 11da9bb90d9dd5848b4f7629415a6937, type: 2}
m_SchemaSet:

View File

@@ -3,6 +3,7 @@ using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using Core.Settings;
using Minigames.Airplane.Settings;
namespace AppleHills.Core.Settings.Editor
{
@@ -10,7 +11,8 @@ namespace AppleHills.Core.Settings.Editor
{
private Vector2 scrollPosition;
private List<BaseSettings> allSettings = new List<BaseSettings>();
private string[] tabNames = new string[] { "Player & Follower", "Interaction & Items", "Diving Minigame", "Card System", "Card Sorting", "Bird Pooper", "Statue Dressup", "Fort Fight" };
private string[] tabNames = new string[] { "Player & Follower", "Interaction & Items", "Diving Minigame",
"Card System", "Card Sorting", "Bird Pooper", "Statue Dressup", "Fort Fight", "Airplane" };
private int selectedTab = 0;
private Dictionary<string, SerializedObject> serializedSettingsObjects = new Dictionary<string, SerializedObject>();
private GUIStyle headerStyle;
@@ -53,6 +55,7 @@ namespace AppleHills.Core.Settings.Editor
CreateSettingsIfMissing<BirdPooperSettings>("BirdPooperSettings");
CreateSettingsIfMissing<StatueDressupSettings>("StatueDressupSettings");
CreateSettingsIfMissing<Minigames.FortFight.Core.FortFightSettings>("FortFightSettings");
CreateSettingsIfMissing<AirplaneSettings>("AirplaneSettings");
}
private void CreateSettingsIfMissing<T>(string fileName) where T : BaseSettings
@@ -134,6 +137,9 @@ namespace AppleHills.Core.Settings.Editor
case 7: // Fort Fight
DrawSettingsEditor<Minigames.FortFight.Core.FortFightSettings>();
break;
case 8: // Airplane
DrawSettingsEditor<Minigames.Airplane.Settings.AirplaneSettings>();
break;
}
EditorGUILayout.EndScrollView();

View File

@@ -4875,10 +4875,18 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: aa30fcfc16ed44d59edd73fd0224d03c, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::Minigames.FortFight.Core.CameraController
wideViewCamera: {fileID: 858149304}
playerOneCamera: {fileID: 846792105}
playerTwoCamera: {fileID: 630420675}
projectileCamera: {fileID: 1592155790}
cameraMappings:
- state: 0
camera: {fileID: 858149304}
- state: 1
camera: {fileID: 846792105}
- state: 2
camera: {fileID: 630420675}
- state: 3
camera: {fileID: 1592155790}
inactivePriority: 10
activePriority: 20
showDebugLogs: 0
--- !u!4 &1674657453
Transform:
m_ObjectHideFlags: 0
@@ -5442,7 +5450,7 @@ Transform:
m_GameObject: {fileID: 1810521056}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalPosition: {x: 14.3, y: -2, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Core;
using Core.Lifecycle;
using Unity.Cinemachine;
@@ -7,15 +8,40 @@ using UnityEngine;
namespace Common.Camera
{
/// <summary>
/// Serializable mapping between a camera state and its Cinemachine camera.
/// Used to assign cameras in the Inspector for each enum state.
/// </summary>
[Serializable]
public class CameraStateMapping<TState> where TState : Enum
{
[Tooltip("The state this camera represents")]
public TState state;
[Tooltip("The Cinemachine camera for this state")]
public CinemachineCamera camera;
public CameraStateMapping(TState state)
{
this.state = state;
this.camera = null;
}
}
/// <summary>
/// Generic state-based camera controller using Cinemachine.
/// Manages camera transitions by setting priorities on virtual cameras.
/// Type parameter TState must be an enum representing camera states.
/// </summary>
///
public abstract class CameraStateManager<TState> : ManagedBehaviour where TState : Enum
{
#region Configuration
[Header("Camera Mappings")]
[Tooltip("Assign cameras for each state - list auto-populates from enum")]
[SerializeField] protected List<CameraStateMapping<TState>> cameraMappings = new List<CameraStateMapping<TState>>();
[Header("Camera Priority Settings")]
[Tooltip("Priority for inactive cameras")]
[SerializeField] protected int inactivePriority = 10;
@@ -47,11 +73,66 @@ namespace Common.Camera
#endregion
#region Lifecycle
/// <summary>
/// Initialize camera mappings and validate them.
/// Subclasses should call base.OnManagedAwake() to get automatic initialization.
/// If custom initialization is needed, override without calling base.
/// </summary>
internal override void OnManagedAwake()
{
base.OnManagedAwake();
// Initialize cameras from Inspector mappings
InitializeCameraMap();
// Validate all cameras are assigned
ValidateCameras();
}
#endregion
#region Initialization
/// <summary>
/// Register a camera for a specific state.
/// Call this in subclass OnManagedAwake to set up the camera map.
/// Initialize camera mappings from Inspector-assigned list.
/// Call this in OnManagedAwake - no need to manually register cameras!
/// This is the preferred method for new implementations.
/// </summary>
protected void InitializeCameraMap()
{
_cameraMap.Clear();
// Build dictionary from serialized mappings
foreach (var mapping in cameraMappings)
{
if (mapping.camera == null)
{
Logging.Warning($"[{GetType().Name}] No camera assigned for state {mapping.state}");
continue;
}
_cameraMap[mapping.state] = mapping.camera;
mapping.camera.Priority.Value = inactivePriority;
if (showDebugLogs)
Logging.Debug($"[{GetType().Name}] Registered camera '{mapping.camera.gameObject.name}' for state {mapping.state}");
}
_isInitialized = true;
if (_cameraMap.Count == 0)
{
Logging.Warning($"[{GetType().Name}] No cameras registered!");
}
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Initialized with {_cameraMap.Count} cameras");
}
/// <summary>
/// DEPRECATED: Use InitializeCameraMap() instead for cleaner code.
/// Kept for backward compatibility with existing implementations.
/// </summary>
protected void RegisterCamera(TState state, CinemachineCamera pCamera)
{
@@ -67,16 +148,14 @@ namespace Common.Camera
}
_cameraMap[state] = pCamera;
// Set all cameras to inactive priority initially
pCamera.Priority.Value = inactivePriority;
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Registered camera '{pCamera.gameObject.name}' for state {state}");
}
/// <summary>
/// Finalize initialization after all cameras are registered.
/// Call this at the end of subclass OnManagedAwake.
/// DEPRECATED: Use InitializeCameraMap() instead.
/// Kept for backward compatibility with existing implementations.
/// </summary>
protected void FinalizeInitialization()
{
@@ -155,13 +234,64 @@ namespace Common.Camera
#region Validation
/// <summary>
/// Validate that all required states have cameras registered.
/// Subclasses can override to add custom validation.
/// Validate that all enum states have cameras registered.
/// Override to add custom validation (e.g., check for specific components).
/// </summary>
protected virtual void ValidateCameras()
{
// Subclasses should implement specific validation
// Check that all enum values have cameras assigned
foreach (TState state in Enum.GetValues(typeof(TState)))
{
if (!_cameraMap.ContainsKey(state))
{
Logging.Warning($"[{GetType().Name}] No camera assigned for state {state}");
}
else if (_cameraMap[state] == null)
{
Logging.Error($"[{GetType().Name}] Camera for state {state} is null!");
}
}
}
#endregion
#region Editor Support
#if UNITY_EDITOR
/// <summary>
/// Auto-populate camera mappings list with all enum values.
/// Called automatically in the Editor when the component is added or values change.
/// </summary>
protected virtual void OnValidate()
{
// Get all enum values
TState[] allStates = (TState[])Enum.GetValues(typeof(TState));
// Add missing states to the list
foreach (TState state in allStates)
{
bool exists = cameraMappings.Any(m => EqualityComparer<TState>.Default.Equals(m.state, state));
if (!exists)
{
cameraMappings.Add(new CameraStateMapping<TState>(state));
}
}
// Remove mappings for states that no longer exist in the enum
cameraMappings.RemoveAll(m => !System.Array.Exists(allStates, s => EqualityComparer<TState>.Default.Equals(s, m.state)));
// Sort by enum order for cleaner Inspector display
cameraMappings = cameraMappings.OrderBy(m => (int)(object)m.state).ToList();
}
/// <summary>
/// Initialize list when component is first added
/// </summary>
protected virtual void Reset()
{
OnValidate();
}
#endif
#endregion
}

View File

@@ -68,12 +68,17 @@ namespace Common.Visual
#region Public API - Visibility
/// <summary>
/// Show the trajectory preview line
/// Show the trajectory preview line.
/// Clears any existing trajectory data so nothing displays until UpdateTrajectory is called.
/// </summary>
public void Show()
{
if (_lineRenderer != null)
{
// Clear old trajectory data
_lineRenderer.positionCount = 0;
// Enable the line renderer
_lineRenderer.enabled = true;
}
}
@@ -106,6 +111,21 @@ namespace Common.Visual
}
}
/// <summary>
/// Force hide the trajectory immediately, clearing any lock state.
/// Use this when transitioning turns or resetting the slingshot.
/// </summary>
public void ForceHide()
{
_isLocked = false;
_lockTimer = 0f;
if (_lineRenderer != null)
{
_lineRenderer.enabled = false;
}
}
#endregion
#region Public API - Update Trajectory (Multiple Overloads)

View File

@@ -20,27 +20,12 @@ namespace Minigames.Airplane.Core
#endregion
#region Inspector References
[Header("Cinemachine Cameras")]
[Tooltip("Camera for intro sequence")]
[SerializeField] private CinemachineCamera introCamera;
[Tooltip("Camera for showing the next person")]
[SerializeField] private CinemachineCamera nextPersonCamera;
[Tooltip("Camera for aiming view")]
[SerializeField] private CinemachineCamera aimingCamera;
[Tooltip("Camera that follows the airplane (should have CinemachineFollow)")]
[SerializeField] private CinemachineCamera flightCamera;
#endregion
#region Lifecycle
internal override void OnManagedAwake()
{
// Base class handles InitializeCameraMap() and ValidateCameras()
base.OnManagedAwake();
// Set singleton
@@ -51,18 +36,6 @@ namespace Minigames.Airplane.Core
return;
}
_instance = this;
// Register cameras
RegisterCamera(AirplaneCameraState.Intro, introCamera);
RegisterCamera(AirplaneCameraState.NextPerson, nextPersonCamera);
RegisterCamera(AirplaneCameraState.Aiming, aimingCamera);
RegisterCamera(AirplaneCameraState.Flight, flightCamera);
// Finalize initialization
FinalizeInitialization();
// Validate
ValidateCameras();
}
internal override void OnManagedDestroy()
@@ -81,28 +54,13 @@ namespace Minigames.Airplane.Core
protected override void ValidateCameras()
{
if (introCamera == null)
{
Logging.Error("[AirplaneCameraManager] Intro camera not assigned!");
}
// Base class validates all enum states have cameras assigned
base.ValidateCameras();
if (nextPersonCamera == null)
// Additional validation: Check if flight camera has follow component
var flightCamera = GetCamera(AirplaneCameraState.Flight);
if (flightCamera != null)
{
Logging.Error("[AirplaneCameraManager] Next person camera not assigned!");
}
if (aimingCamera == null)
{
Logging.Error("[AirplaneCameraManager] Aiming camera not assigned!");
}
if (flightCamera == null)
{
Logging.Error("[AirplaneCameraManager] Flight camera not assigned!");
}
else
{
// Verify flight camera has follow component
var followComponent = flightCamera.GetComponent<CinemachineFollow>();
if (followComponent == null)
{
@@ -120,6 +78,7 @@ namespace Minigames.Airplane.Core
/// </summary>
public void StartFollowingAirplane(Transform airplaneTransform)
{
var flightCamera = GetCamera(AirplaneCameraState.Flight);
if (flightCamera == null)
{
Logging.Warning("[AirplaneCameraManager] Cannot follow airplane - flight camera not assigned!");
@@ -146,6 +105,7 @@ namespace Minigames.Airplane.Core
/// </summary>
public void StopFollowingAirplane()
{
var flightCamera = GetCamera(AirplaneCameraState.Flight);
if (flightCamera == null) return;
// Clear the follow target

View File

@@ -1,5 +1,5 @@
using Core;
using Core.Lifecycle;
using Common.Camera;
using Core;
using Minigames.FortFight.Data;
using Unity.Cinemachine;
using UnityEngine;
@@ -8,11 +8,10 @@ namespace Minigames.FortFight.Core
{
/// <summary>
/// Manages camera states and transitions for the Fort Fight minigame.
/// Extends CameraStateManager to use the common state-based camera system.
/// Subscribes to turn events and switches camera views accordingly.
/// Uses Cinemachine for smooth camera blending.
/// Singleton pattern for easy access.
/// </summary>
public class CameraController : ManagedBehaviour
public class CameraController : CameraStateManager<FortFightCameraState>
{
#region Singleton
@@ -31,41 +30,15 @@ namespace Minigames.FortFight.Core
#endregion
#region Inspector References
[Header("Cinemachine Cameras")]
[Tooltip("Virtual camera showing wide battlefield view (both forts)")]
[SerializeField] private CinemachineCamera wideViewCamera;
[Tooltip("Player One's dedicated camera (position this in the scene for Player 1's view)")]
[SerializeField] private CinemachineCamera playerOneCamera;
[Tooltip("Player Two's dedicated camera (position this in the scene for Player 2's view)")]
[SerializeField] private CinemachineCamera playerTwoCamera;
[Tooltip("Camera that follows projectiles in flight (should have CinemachineFollow component)")]
[SerializeField] private CinemachineCamera projectileCamera;
// Note: TurnManager accessed via singleton
#endregion
#region Public Properties
public CinemachineCamera WideViewCamera => wideViewCamera;
public CinemachineCamera PlayerOneCamera => playerOneCamera;
public CinemachineCamera PlayerTwoCamera => playerTwoCamera;
public CinemachineCamera ProjectileCamera => projectileCamera;
#endregion
#region Lifecycle
internal override void OnManagedAwake()
{
// Base class handles InitializeCameraMap() and ValidateCameras()
base.OnManagedAwake();
// Register singleton
// Set singleton
if (_instance != null && _instance != this)
{
Logging.Warning("[CameraController] Multiple instances detected! Destroying duplicate.");
@@ -73,28 +46,6 @@ namespace Minigames.FortFight.Core
return;
}
_instance = this;
// Validate references
if (wideViewCamera == null)
{
Logging.Error("[CameraController] Wide view camera not assigned!");
}
if (playerOneCamera == null)
{
Logging.Error("[CameraController] Player One camera not assigned!");
}
if (playerTwoCamera == null)
{
Logging.Error("[CameraController] Player Two camera not assigned!");
}
if (projectileCamera == null)
{
Logging.Warning("[CameraController] Projectile camera not assigned - projectiles won't be followed!");
}
}
internal override void OnManagedStart()
@@ -136,75 +87,55 @@ namespace Minigames.FortFight.Core
#region Event Handlers
/// <summary>
/// Called when a player's turn starts - activate their dedicated camera
/// Called when a player's turn starts - switch to appropriate camera state
/// </summary>
private void HandleTurnStarted(PlayerData player, TurnState turnState)
{
if (showDebugLogs)
Logging.Debug($"[CameraController] Turn started for {player.PlayerName} (Index: {player.PlayerIndex}, State: {turnState})");
// If transitioning, show wide view
if (turnState == TurnState.TransitioningTurn)
{
ActivateCamera(wideViewCamera);
SwitchToState(FortFightCameraState.WideView);
return;
}
// Activate the appropriate player camera based on player index
// Switch to appropriate player camera based on index
if (player.PlayerIndex == 0)
{
// Player One's turn
ActivateCamera(playerOneCamera);
SwitchToState(FortFightCameraState.PlayerOne);
}
else if (player.PlayerIndex == 1)
{
// Player Two's turn
ActivateCamera(playerTwoCamera);
SwitchToState(FortFightCameraState.PlayerTwo);
}
else
{
Logging.Warning($"[CameraController] Unknown player index: {player.PlayerIndex}, defaulting to wide view");
ActivateCamera(wideViewCamera);
SwitchToState(FortFightCameraState.WideView);
}
}
/// <summary>
/// Called when a player's turn ends - camera switches handled by turn state changes
/// Called when a player's turn ends
/// </summary>
private void HandleTurnEnded(PlayerData player)
{
Logging.Debug($"[CameraController] Turn ended for {player.PlayerName}");
if (showDebugLogs) Logging.Debug($"[CameraController] Turn ended for {player.PlayerName}");
// Camera switching happens via OnTurnStarted when state changes to TransitioningTurn
}
/// <summary>
/// Activate a specific camera by setting its priority highest
/// </summary>
private void ActivateCamera(CinemachineCamera camera)
{
if (camera == null) return;
// Set all cameras to low priority
if (wideViewCamera != null) wideViewCamera.Priority.Value = 10;
if (playerOneCamera != null) playerOneCamera.Priority.Value = 10;
if (playerTwoCamera != null) playerTwoCamera.Priority.Value = 10;
if (projectileCamera != null) projectileCamera.Priority.Value = 10;
// Set target camera to high priority
camera.Priority.Value = 20;
Logging.Debug($"[CameraController] Activated camera: {camera.gameObject.name}");
}
#endregion
#region Projectile Tracking
/// <summary>
/// Start following a projectile with the projectile camera.
/// Called when a projectile is launched.
/// Start following a projectile with the projectile camera
/// </summary>
public void StartFollowingProjectile(Transform projectileTransform)
{
var projectileCamera = GetCamera(FortFightCameraState.Projectile);
if (projectileCamera == null)
{
Logging.Warning("[CameraController] Cannot follow projectile - projectile camera not assigned!");
@@ -217,37 +148,31 @@ namespace Minigames.FortFight.Core
return;
}
// Verify CinemachineFollow component exists (optional check)
var followComponent = projectileCamera.GetComponent<CinemachineFollow>();
if (followComponent == null)
{
Logging.Error("[CameraController] Projectile camera missing CinemachineFollow component!");
return;
}
// Set the follow target on the CinemachineCamera's Target property
// Set the follow target
projectileCamera.Target.TrackingTarget = projectileTransform;
// Activate the projectile camera
ActivateCamera(projectileCamera);
// Switch to projectile camera
SwitchToState(FortFightCameraState.Projectile);
if (showDebugLogs)
Logging.Debug($"[CameraController] Now following projectile: {projectileTransform.gameObject.name}");
}
/// <summary>
/// Stop following the projectile and return to wide view.
/// Called when projectile has settled.
/// Stop following the projectile and return to wide view
/// </summary>
public void StopFollowingProjectile()
{
var projectileCamera = GetCamera(FortFightCameraState.Projectile);
if (projectileCamera == null) return;
// Clear the follow target on the CinemachineCamera's Target property
// Clear the follow target
projectileCamera.Target.TrackingTarget = null;
// Return to wide view
ActivateCamera(wideViewCamera);
SwitchToState(FortFightCameraState.WideView);
if (showDebugLogs)
Logging.Debug("[CameraController] Stopped following projectile, returned to wide view");
}
@@ -256,11 +181,11 @@ namespace Minigames.FortFight.Core
#region Public API
/// <summary>
/// Manually switch to wide view (useful for game start/end)
/// Manually switch to wide view
/// </summary>
public void ShowWideView()
{
ActivateCamera(wideViewCamera);
SwitchToState(FortFightCameraState.WideView);
}
/// <summary>
@@ -270,23 +195,35 @@ namespace Minigames.FortFight.Core
{
if (playerIndex == 0)
{
ActivateCamera(playerOneCamera);
SwitchToState(FortFightCameraState.PlayerOne);
}
else if (playerIndex == 1)
{
ActivateCamera(playerTwoCamera);
SwitchToState(FortFightCameraState.PlayerTwo);
}
}
#endregion
#region Editor Helpers
#region Validation
#if UNITY_EDITOR
private void OnValidate()
protected override void ValidateCameras()
{
// Base class validates all enum states have cameras assigned
base.ValidateCameras();
// Additional validation: Check if projectile camera has follow component
var projectileCamera = GetCamera(FortFightCameraState.Projectile);
if (projectileCamera != null)
{
var followComponent = projectileCamera.GetComponent<CinemachineFollow>();
if (followComponent == null)
{
Logging.Warning("[CameraController] Projectile camera missing CinemachineFollow component!");
}
#endif
}
}
#endregion
}

View File

@@ -95,6 +95,17 @@ namespace Minigames.FortFight.Core
#region Override Methods
public override void Enable()
{
// Clear any locked trajectory from previous turn
if (trajectoryPreview != null)
{
trajectoryPreview.ForceHide();
}
base.Enable();
}
protected override void StartDrag(Vector2 worldPosition)
{
// Check ammo before starting drag

View File

@@ -61,5 +61,15 @@
Medium, // Moderate deviations, moderate thinking
Hard // Minimal deviations, faster thinking
}
}
/// <summary>
/// Camera states for Fort Fight minigame
/// </summary>
public enum FortFightCameraState
{
WideView, // Shows entire battlefield
PlayerOne, // Player 1's view
PlayerTwo, // Player 2's view
Projectile // Follows projectile in flight
}
}

View File

@@ -0,0 +1,31 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1c277e2fec3d42e2b3b0bed1b8a33beb, type: 3}
m_Name: AirplaneSettings
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Settings.AirplaneSettings
slingshotSettings:
maxDragDistance: 5
baseLaunchForce: 20
minForceMultiplier: 0.1
maxForceMultiplier: 1
trajectoryPoints: 20
trajectoryTimeStep: 0.1
trajectoryLockDuration: 0
autoRegisterInput: 1
airplaneMass: 1
maxFlightTime: 10
cameraFollowSmoothing: 5
flightCameraZoom: 5
introDuration: 1
personIntroDuration: 1
evaluationDuration: 1
showDebugLogs: 0

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c56b7c4096b59584c93f2cfa79230643
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant: