Stash paralax work
This commit is contained in:
@@ -2457,6 +2457,37 @@ MonoBehaviour:
|
|||||||
trajectoryPreview: {fileID: 1309397783}
|
trajectoryPreview: {fileID: 1309397783}
|
||||||
showDebugLogs: 0
|
showDebugLogs: 0
|
||||||
airplanePrefab: {fileID: 2043346932243838886, guid: 582ed0c37f4ec6c4e930ddabea174eca, type: 3}
|
airplanePrefab: {fileID: 2043346932243838886, guid: 582ed0c37f4ec6c4e930ddabea174eca, type: 3}
|
||||||
|
--- !u!1 &1405572708
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1405572709}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: SpawnContainer
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &1405572709
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1405572708}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 1784207402}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1001 &1414189130
|
--- !u!1001 &1414189130
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -3702,6 +3733,7 @@ GameObject:
|
|||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 1784207402}
|
- component: {fileID: 1784207402}
|
||||||
- component: {fileID: 1784207403}
|
- component: {fileID: 1784207403}
|
||||||
|
- component: {fileID: 1784207404}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: SpawnManager
|
m_Name: SpawnManager
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -3724,6 +3756,7 @@ Transform:
|
|||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 1219431443}
|
- {fileID: 1219431443}
|
||||||
- {fileID: 1287102785}
|
- {fileID: 1287102785}
|
||||||
|
- {fileID: 1405572709}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!114 &1784207403
|
--- !u!114 &1784207403
|
||||||
@@ -3738,6 +3771,9 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 70f14ee4b04b46b793ec2652fd2ca7b9, type: 3}
|
m_Script: {fileID: 11500000, guid: 70f14ee4b04b46b793ec2652fd2ca7b9, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.AirplaneSpawnManager
|
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.AirplaneSpawnManager
|
||||||
|
obstacleSpawner: {fileID: 0}
|
||||||
|
groundSpawner: {fileID: 0}
|
||||||
|
parallaxSpawner: {fileID: 0}
|
||||||
targetPrefabs:
|
targetPrefabs:
|
||||||
- targetKey: bob_target
|
- targetKey: bob_target
|
||||||
prefab: {fileID: 3207629437433571205, guid: 9f4bb48933059e543b60ac782d2140d8, type: 3}
|
prefab: {fileID: 3207629437433571205, guid: 9f4bb48933059e543b60ac782d2140d8, type: 3}
|
||||||
@@ -3751,42 +3787,37 @@ MonoBehaviour:
|
|||||||
specifiedY: 0
|
specifiedY: 0
|
||||||
randomYMin: -5
|
randomYMin: -5
|
||||||
randomYMax: 5
|
randomYMax: 5
|
||||||
positiveObjectPrefabs:
|
|
||||||
- prefab: {fileID: 497267990420767357, guid: dc32284941c693c4f9e422f4197ca61e, type: 3}
|
|
||||||
spawnPositionMode: 2
|
|
||||||
specifiedY: 0
|
|
||||||
randomYMin: 3
|
|
||||||
randomYMax: 40
|
|
||||||
- prefab: {fileID: 1917678391913987792, guid: 989121c9099e41e469824ddeaf0e34a5, type: 3}
|
|
||||||
spawnPositionMode: 0
|
|
||||||
specifiedY: 0
|
|
||||||
randomYMin: 5
|
|
||||||
randomYMax: 20
|
|
||||||
- prefab: {fileID: 7032677151789119314, guid: 7dc33e43acead834ba6a231b67cfd2d9, type: 3}
|
|
||||||
spawnPositionMode: 2
|
|
||||||
specifiedY: 0
|
|
||||||
randomYMin: 3
|
|
||||||
randomYMax: 40
|
|
||||||
negativeObjectPrefabs:
|
|
||||||
- prefab: {fileID: 1186710456879913970, guid: 006b956651124704dbae5bd4faab3152, type: 3}
|
|
||||||
spawnPositionMode: 2
|
|
||||||
specifiedY: 0
|
|
||||||
randomYMin: 3
|
|
||||||
randomYMax: 40
|
|
||||||
- prefab: {fileID: 2434350760695575337, guid: f3188909ff4e845499a5cbfd0ae93101, type: 3}
|
|
||||||
spawnPositionMode: 2
|
|
||||||
specifiedY: 0
|
|
||||||
randomYMin: 3
|
|
||||||
randomYMax: 40
|
|
||||||
groundTilePrefabs:
|
|
||||||
- {fileID: 5175967588203935335, guid: a9b4569fcc08080479d99b9c3bcee089, type: 3}
|
|
||||||
targetDisplayUI: {fileID: 1520040329}
|
targetDisplayUI: {fileID: 1520040329}
|
||||||
launchController: {fileID: 1309397785}
|
launchController: {fileID: 1309397785}
|
||||||
dynamicSpawnThresholdMarker: {fileID: 1653173574}
|
dynamicSpawnThresholdMarker: {fileID: 1653173574}
|
||||||
spawnedObjectsParent: {fileID: 1219431443}
|
targetParent: {fileID: 1405572709}
|
||||||
groundTilesParent: {fileID: 1287102785}
|
|
||||||
groundSpawnY: -5
|
|
||||||
showDebugLogs: 0
|
showDebugLogs: 0
|
||||||
|
--- !u!114 &1784207404
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1784207401}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 0a550c256a3d4d91a17adf6d0338f022, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.Spawning.ObstacleDistanceSpawner
|
||||||
|
poolMode: 0
|
||||||
|
pools: []
|
||||||
|
globalMinDistance: 5
|
||||||
|
globalMaxDistance: 15
|
||||||
|
spawnPoint: {fileID: 0}
|
||||||
|
spawnContainer: {fileID: 0}
|
||||||
|
recencyPenaltyDuration: 10
|
||||||
|
showDebugLogs: 0
|
||||||
|
positiveNegativeRatio: 0.5
|
||||||
|
positivePoolIndices:
|
||||||
|
negativePoolIndices:
|
||||||
|
groundLayer: 0
|
||||||
|
maxGroundRaycastDistance: 50
|
||||||
|
defaultObjectYOffset: 0
|
||||||
--- !u!1 &1810521056
|
--- !u!1 &1810521056
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@@ -71,6 +71,35 @@ namespace Minigames.Airplane.Core
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Camera Tracking API
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the transform of the currently active camera.
|
||||||
|
/// Used by parallax elements to track camera movement.
|
||||||
|
/// </summary>
|
||||||
|
public Transform GetActiveCameraTransform()
|
||||||
|
{
|
||||||
|
var activeCamera = GetCamera(CurrentState);
|
||||||
|
return activeCamera != null ? activeCamera.transform : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override to fire additional state changed events.
|
||||||
|
/// </summary>
|
||||||
|
public override void SwitchToState(AirplaneCameraState newState)
|
||||||
|
{
|
||||||
|
AirplaneCameraState oldState = CurrentState;
|
||||||
|
base.SwitchToState(newState);
|
||||||
|
|
||||||
|
// Base class already fires OnStateChanged event, we just add logging
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[AirplaneCameraManager] Camera state changed: {oldState} -> {newState}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Flight Camera Follow
|
#region Flight Camera Follow
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -459,6 +459,8 @@ namespace Minigames.Airplane.Core
|
|||||||
|
|
||||||
if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Introducing {_currentPerson.PersonName}...");
|
if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Introducing {_currentPerson.PersonName}...");
|
||||||
|
|
||||||
|
if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Checking camera state... Current: {(cameraManager != null ? cameraManager.CurrentState.ToString() : "NULL")}");
|
||||||
|
|
||||||
// Blend to next person camera if not already there
|
// Blend to next person camera if not already there
|
||||||
// (Person shuffle flow already blends to NextPerson, so might already be there)
|
// (Person shuffle flow already blends to NextPerson, so might already be there)
|
||||||
if (cameraManager != null && cameraManager.CurrentState != AirplaneCameraState.NextPerson)
|
if (cameraManager != null && cameraManager.CurrentState != AirplaneCameraState.NextPerson)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
3
Assets/Scripts/Minigames/Airplane/Core/Spawning.meta
Normal file
3
Assets/Scripts/Minigames/Airplane/Core/Spawning.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5dc5cd57fce342659690bcf1c3411048
|
||||||
|
timeCreated: 1765965907
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Core;
|
||||||
|
using Core.Lifecycle;
|
||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Minigames.Airplane.Core.Spawning
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for distance-based spawners in the airplane minigame.
|
||||||
|
/// Handles pool management, time-based unlocking, distance tracking, and recency diversity.
|
||||||
|
/// Derived classes implement specific spawn logic via SpawnFromPool.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseDistanceSpawner : ManagedBehaviour
|
||||||
|
{
|
||||||
|
#region Inspector Fields
|
||||||
|
|
||||||
|
[Header("Pool Configuration")]
|
||||||
|
[Tooltip("How pools are combined: Together = shared spawn stream, Exclusive = independent spawning")]
|
||||||
|
[SerializeField] protected SpawnPoolMode poolMode = SpawnPoolMode.Together;
|
||||||
|
|
||||||
|
[Tooltip("Spawn pools ordered by difficulty/progression")]
|
||||||
|
[SerializeField] protected SpawnPoolConfig[] pools;
|
||||||
|
|
||||||
|
[Header("Global Spawn Parameters")]
|
||||||
|
[Tooltip("Minimum distance between spawns (used when poolMode = Together or as fallback)")]
|
||||||
|
[SerializeField] protected float globalMinDistance = 5f;
|
||||||
|
|
||||||
|
[Tooltip("Maximum distance between spawns (used when poolMode = Together or as fallback)")]
|
||||||
|
[SerializeField] protected float globalMaxDistance = 15f;
|
||||||
|
|
||||||
|
[Header("Spawn References")]
|
||||||
|
[Tooltip("Transform marking spawn position (typically off-screen right)")]
|
||||||
|
[SerializeField] protected Transform spawnPoint;
|
||||||
|
|
||||||
|
[Tooltip("Optional parent for spawned objects (organization)")]
|
||||||
|
[SerializeField] protected Transform spawnContainer;
|
||||||
|
|
||||||
|
[Header("Recency Tracking")]
|
||||||
|
[Tooltip("Time penalty (seconds) applied to recently-used prefabs for diversity")]
|
||||||
|
[SerializeField] protected float recencyPenaltyDuration = 10f;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] protected bool showDebugLogs;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region State
|
||||||
|
|
||||||
|
// Tracking
|
||||||
|
protected Transform PlaneTransform;
|
||||||
|
protected float GameTime;
|
||||||
|
protected bool IsSpawning;
|
||||||
|
protected float LastSpawnedX;
|
||||||
|
|
||||||
|
// Pool state
|
||||||
|
protected List<int> UnlockedPoolIndices = new List<int>();
|
||||||
|
protected Dictionary<GameObject, float> LastUsedTimes = new Dictionary<GameObject, float>();
|
||||||
|
|
||||||
|
// Together mode state
|
||||||
|
protected float NextSpawnX;
|
||||||
|
|
||||||
|
// Exclusive mode state
|
||||||
|
protected Dictionary<int, float> PoolNextSpawnX = new Dictionary<int, float>();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Initialization
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the spawner. Call this before starting spawning.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Initialize()
|
||||||
|
{
|
||||||
|
ValidateConfiguration();
|
||||||
|
|
||||||
|
// Unlock pool 0 immediately
|
||||||
|
if (pools != null && pools.Length > 0)
|
||||||
|
{
|
||||||
|
UnlockedPoolIndices.Add(0);
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[{GetType().Name}] Initialized with pool 0 unlocked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize exclusive mode spawn positions if needed
|
||||||
|
if (poolMode == SpawnPoolMode.Exclusive)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < pools.Length; i++)
|
||||||
|
{
|
||||||
|
PoolNextSpawnX[i] = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void ValidateConfiguration()
|
||||||
|
{
|
||||||
|
if (pools == null || pools.Length == 0)
|
||||||
|
{
|
||||||
|
Logging.Error($"[{GetType().Name}] No pools configured!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spawnPoint == null)
|
||||||
|
{
|
||||||
|
Logging.Warning($"[{GetType().Name}] Spawn point not assigned!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate pool unlock times are monotonically increasing
|
||||||
|
for (int i = 1; i < pools.Length; i++)
|
||||||
|
{
|
||||||
|
if (pools[i].unlockTime < pools[i - 1].unlockTime)
|
||||||
|
{
|
||||||
|
Logging.Warning($"[{GetType().Name}] Pool {i} unlock time ({pools[i].unlockTime}s) is earlier than pool {i-1} ({pools[i-1].unlockTime}s)!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public API
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start tracking the plane and enable spawning.
|
||||||
|
/// </summary>
|
||||||
|
public void StartTracking(Transform planeTransform, float startX)
|
||||||
|
{
|
||||||
|
PlaneTransform = planeTransform;
|
||||||
|
IsSpawning = true;
|
||||||
|
GameTime = 0f;
|
||||||
|
LastSpawnedX = startX;
|
||||||
|
|
||||||
|
// Initialize next spawn position
|
||||||
|
if (poolMode == SpawnPoolMode.Together)
|
||||||
|
{
|
||||||
|
NextSpawnX = startX + Random.Range(globalMinDistance, globalMaxDistance);
|
||||||
|
}
|
||||||
|
else // Exclusive
|
||||||
|
{
|
||||||
|
foreach (int poolIndex in UnlockedPoolIndices)
|
||||||
|
{
|
||||||
|
float minDist = pools[poolIndex].GetMinDistance(globalMinDistance);
|
||||||
|
float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance);
|
||||||
|
PoolNextSpawnX[poolIndex] = startX + Random.Range(minDist, maxDist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[{GetType().Name}] Started tracking from X={startX}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop spawning and tracking.
|
||||||
|
/// </summary>
|
||||||
|
public void StopTracking()
|
||||||
|
{
|
||||||
|
IsSpawning = false;
|
||||||
|
PlaneTransform = null;
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[{GetType().Name}] Stopped tracking");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update game time and check for pool unlocks.
|
||||||
|
/// Call this every frame from the orchestrator.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateGameTime(float deltaTime)
|
||||||
|
{
|
||||||
|
GameTime += deltaTime;
|
||||||
|
CheckPoolUnlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-spawn objects from start to end X position.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void PreSpawn(float startX, float endX)
|
||||||
|
{
|
||||||
|
if (poolMode == SpawnPoolMode.Together)
|
||||||
|
{
|
||||||
|
PreSpawnTogether(startX, endX);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PreSpawnExclusive(startX, endX);
|
||||||
|
}
|
||||||
|
|
||||||
|
LastSpawnedX = endX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update spawning based on plane position.
|
||||||
|
/// Call this every frame from the orchestrator.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void UpdateSpawning(float spawnAheadDistance)
|
||||||
|
{
|
||||||
|
if (!IsSpawning || PlaneTransform == null) return;
|
||||||
|
|
||||||
|
float planeX = PlaneTransform.position.x;
|
||||||
|
|
||||||
|
if (poolMode == SpawnPoolMode.Together)
|
||||||
|
{
|
||||||
|
UpdateSpawningTogether(planeX, spawnAheadDistance);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateSpawningExclusive(planeX, spawnAheadDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clean up all spawned objects.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Cleanup()
|
||||||
|
{
|
||||||
|
if (spawnContainer != null)
|
||||||
|
{
|
||||||
|
foreach (Transform child in spawnContainer)
|
||||||
|
{
|
||||||
|
Destroy(child.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LastUsedTimes.Clear();
|
||||||
|
UnlockedPoolIndices.Clear();
|
||||||
|
UnlockedPoolIndices.Add(0); // Re-add pool 0
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[{GetType().Name}] Cleanup complete");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Pool Management
|
||||||
|
|
||||||
|
protected void CheckPoolUnlocks()
|
||||||
|
{
|
||||||
|
for (int i = 1; i < pools.Length; i++)
|
||||||
|
{
|
||||||
|
if (UnlockedPoolIndices.Contains(i)) continue;
|
||||||
|
|
||||||
|
if (GameTime >= pools[i].unlockTime)
|
||||||
|
{
|
||||||
|
UnlockedPoolIndices.Add(i);
|
||||||
|
|
||||||
|
// If exclusive mode, initialize spawn position for this pool
|
||||||
|
if (poolMode == SpawnPoolMode.Exclusive && PlaneTransform != null)
|
||||||
|
{
|
||||||
|
float minDist = pools[i].GetMinDistance(globalMinDistance);
|
||||||
|
float maxDist = pools[i].GetMaxDistance(globalMaxDistance);
|
||||||
|
PoolNextSpawnX[i] = PlaneTransform.position.x + Random.Range(minDist, maxDist);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[{GetType().Name}] Unlocked pool {i} '{pools[i].description}' at time {GameTime:F2}s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GameObject SelectPrefabFromPools(out int selectedPoolIndex)
|
||||||
|
{
|
||||||
|
selectedPoolIndex = -1;
|
||||||
|
|
||||||
|
if (UnlockedPoolIndices.Count == 0) return null;
|
||||||
|
|
||||||
|
// Build weighted list of all available prefabs
|
||||||
|
List<GameObject> availablePrefabs = new List<GameObject>();
|
||||||
|
List<int> prefabPoolIndices = new List<int>();
|
||||||
|
List<float> prefabWeights = new List<float>();
|
||||||
|
|
||||||
|
foreach (int poolIndex in UnlockedPoolIndices)
|
||||||
|
{
|
||||||
|
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
||||||
|
|
||||||
|
foreach (GameObject prefab in pools[poolIndex].prefabs)
|
||||||
|
{
|
||||||
|
if (prefab == null) continue;
|
||||||
|
|
||||||
|
availablePrefabs.Add(prefab);
|
||||||
|
prefabPoolIndices.Add(poolIndex);
|
||||||
|
|
||||||
|
// Calculate weight based on recency
|
||||||
|
float weight = 1f;
|
||||||
|
if (LastUsedTimes.TryGetValue(prefab, out float lastUsedTime))
|
||||||
|
{
|
||||||
|
float timeSinceUse = GameTime - lastUsedTime;
|
||||||
|
if (timeSinceUse < recencyPenaltyDuration)
|
||||||
|
{
|
||||||
|
weight = timeSinceUse / recencyPenaltyDuration; // 0 to 1 linear recovery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefabWeights.Add(weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availablePrefabs.Count == 0) return null;
|
||||||
|
|
||||||
|
// Select using weighted random
|
||||||
|
int selectedIndex = WeightedRandom(prefabWeights);
|
||||||
|
GameObject selectedPrefab = availablePrefabs[selectedIndex];
|
||||||
|
selectedPoolIndex = prefabPoolIndices[selectedIndex];
|
||||||
|
|
||||||
|
// Update recency
|
||||||
|
LastUsedTimes[selectedPrefab] = GameTime;
|
||||||
|
|
||||||
|
return selectedPrefab;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int WeightedRandom(List<float> weights)
|
||||||
|
{
|
||||||
|
float totalWeight = 0f;
|
||||||
|
foreach (float w in weights) totalWeight += w;
|
||||||
|
|
||||||
|
float randomValue = Random.Range(0f, totalWeight);
|
||||||
|
float cumulative = 0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < weights.Count; i++)
|
||||||
|
{
|
||||||
|
cumulative += weights[i];
|
||||||
|
if (randomValue <= cumulative) return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return weights.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Together Mode Spawning
|
||||||
|
|
||||||
|
protected virtual void PreSpawnTogether(float startX, float endX)
|
||||||
|
{
|
||||||
|
float currentX = startX + Random.Range(globalMinDistance, globalMaxDistance);
|
||||||
|
|
||||||
|
while (currentX <= endX)
|
||||||
|
{
|
||||||
|
SpawnAtPosition(currentX);
|
||||||
|
currentX += Random.Range(globalMinDistance, globalMaxDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
NextSpawnX = currentX;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void UpdateSpawningTogether(float planeX, float spawnAheadDistance)
|
||||||
|
{
|
||||||
|
float spawnTriggerX = LastSpawnedX + spawnAheadDistance;
|
||||||
|
|
||||||
|
if (planeX >= spawnTriggerX && planeX >= NextSpawnX)
|
||||||
|
{
|
||||||
|
SpawnAtPosition(NextSpawnX);
|
||||||
|
LastSpawnedX = NextSpawnX;
|
||||||
|
NextSpawnX += Random.Range(globalMinDistance, globalMaxDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Exclusive Mode Spawning
|
||||||
|
|
||||||
|
protected virtual void PreSpawnExclusive(float startX, float endX)
|
||||||
|
{
|
||||||
|
foreach (int poolIndex in UnlockedPoolIndices)
|
||||||
|
{
|
||||||
|
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
||||||
|
|
||||||
|
float minDist = pools[poolIndex].GetMinDistance(globalMinDistance);
|
||||||
|
float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance);
|
||||||
|
float currentX = startX + Random.Range(minDist, maxDist);
|
||||||
|
|
||||||
|
while (currentX <= endX)
|
||||||
|
{
|
||||||
|
SpawnFromPool(poolIndex, currentX);
|
||||||
|
currentX += Random.Range(minDist, maxDist);
|
||||||
|
}
|
||||||
|
|
||||||
|
PoolNextSpawnX[poolIndex] = currentX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void UpdateSpawningExclusive(float planeX, float spawnAheadDistance)
|
||||||
|
{
|
||||||
|
foreach (int poolIndex in UnlockedPoolIndices)
|
||||||
|
{
|
||||||
|
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
||||||
|
if (!PoolNextSpawnX.ContainsKey(poolIndex)) continue;
|
||||||
|
|
||||||
|
float nextX = PoolNextSpawnX[poolIndex];
|
||||||
|
float spawnTargetX = planeX + spawnAheadDistance;
|
||||||
|
|
||||||
|
if (nextX <= spawnTargetX)
|
||||||
|
{
|
||||||
|
SpawnFromPool(poolIndex, nextX);
|
||||||
|
|
||||||
|
float minDist = pools[poolIndex].GetMinDistance(globalMinDistance);
|
||||||
|
float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance);
|
||||||
|
PoolNextSpawnX[poolIndex] = nextX + Random.Range(minDist, maxDist);
|
||||||
|
LastSpawnedX = Mathf.Max(LastSpawnedX, nextX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Spawning
|
||||||
|
|
||||||
|
protected void SpawnAtPosition(float xPosition)
|
||||||
|
{
|
||||||
|
int selectedPoolIndex;
|
||||||
|
GameObject prefab = SelectPrefabFromPools(out selectedPoolIndex);
|
||||||
|
if (prefab == null) return;
|
||||||
|
|
||||||
|
SpawnFromPool(selectedPoolIndex, xPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawn a specific prefab from a pool at the given X position.
|
||||||
|
/// Override this in derived classes to implement specific spawn logic.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void SpawnFromPool(int poolIndex, float xPosition)
|
||||||
|
{
|
||||||
|
// Derived classes implement specific spawn logic
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GameObject InstantiatePrefab(GameObject prefab, Vector3 position)
|
||||||
|
{
|
||||||
|
GameObject instance = Instantiate(prefab, position, Quaternion.identity);
|
||||||
|
|
||||||
|
if (spawnContainer != null)
|
||||||
|
{
|
||||||
|
instance.transform.SetParent(spawnContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 387858903c2c44e0ab007cb2ac886343
|
||||||
|
timeCreated: 1765965907
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Core;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Minigames.Airplane.Core.Spawning
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns ground tiles at fixed intervals.
|
||||||
|
/// Inherits distance-based spawning from BaseDistanceSpawner.
|
||||||
|
/// </summary>
|
||||||
|
public class GroundDistanceSpawner : BaseDistanceSpawner
|
||||||
|
{
|
||||||
|
[Header("Ground-Specific")]
|
||||||
|
[Tooltip("Y position at which to spawn ground tiles")]
|
||||||
|
[SerializeField] private float groundSpawnY = -18f;
|
||||||
|
|
||||||
|
protected override void SpawnFromPool(int poolIndex, float xPosition)
|
||||||
|
{
|
||||||
|
if (poolIndex < 0 || poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Select random tile from pool
|
||||||
|
GameObject tilePrefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)];
|
||||||
|
if (tilePrefab == null) return;
|
||||||
|
|
||||||
|
// Calculate spawn position
|
||||||
|
Vector3 spawnPosition = new Vector3(xPosition, groundSpawnY, 0f);
|
||||||
|
|
||||||
|
// Instantiate
|
||||||
|
GameObject instance = InstantiatePrefab(tilePrefab, spawnPosition);
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[GroundDistanceSpawner] Spawned ground tile at X={xPosition:F2}, Y={groundSpawnY:F2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 76db01f1871c4b9ea66fb981b01b3ee1
|
||||||
|
timeCreated: 1765965959
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
using Core;
|
||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using Minigames.Airplane.Interactive;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Minigames.Airplane.Core.Spawning
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns obstacle objects (positive and negative) with weighted ratio management.
|
||||||
|
/// Inherits distance-based spawning from BaseDistanceSpawner.
|
||||||
|
/// </summary>
|
||||||
|
public class ObstacleDistanceSpawner : BaseDistanceSpawner
|
||||||
|
{
|
||||||
|
[Header("Obstacle-Specific")]
|
||||||
|
[Tooltip("Ratio of positive to negative objects (0 = all negative, 1 = all positive)")]
|
||||||
|
[Range(0f, 1f)]
|
||||||
|
[SerializeField] private float positiveNegativeRatio = 0.5f;
|
||||||
|
|
||||||
|
[Tooltip("Array indices that should be treated as positive objects")]
|
||||||
|
[SerializeField] private int[] positivePoolIndices;
|
||||||
|
|
||||||
|
[Tooltip("Array indices that should be treated as negative objects")]
|
||||||
|
[SerializeField] private int[] negativePoolIndices;
|
||||||
|
|
||||||
|
[Header("Positioning")]
|
||||||
|
[SerializeField] private int groundLayer;
|
||||||
|
[SerializeField] private float maxGroundRaycastDistance = 50f;
|
||||||
|
[SerializeField] private float defaultObjectYOffset;
|
||||||
|
|
||||||
|
private int _positiveSpawnCount;
|
||||||
|
private int _negativeSpawnCount;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
_positiveSpawnCount = 0;
|
||||||
|
_negativeSpawnCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SpawnFromPool(int poolIndex, float xPosition)
|
||||||
|
{
|
||||||
|
if (poolIndex < 0 || poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Determine if this should spawn positive or negative
|
||||||
|
bool isPositivePool = System.Array.IndexOf(positivePoolIndices, poolIndex) >= 0;
|
||||||
|
bool isNegativePool = System.Array.IndexOf(negativePoolIndices, poolIndex) >= 0;
|
||||||
|
|
||||||
|
// If pool is not specifically marked, use weighted random
|
||||||
|
bool spawnPositive = isPositivePool || (!isNegativePool && ShouldSpawnPositive());
|
||||||
|
|
||||||
|
// Select random prefab from pool
|
||||||
|
GameObject prefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)];
|
||||||
|
if (prefab == null) return;
|
||||||
|
|
||||||
|
// Get spawn position from spawnPoint or use default
|
||||||
|
float spawnY = spawnPoint != null ? spawnPoint.position.y : 0f;
|
||||||
|
Vector3 tempPosition = new Vector3(xPosition, spawnY, 0f);
|
||||||
|
|
||||||
|
// Instantiate
|
||||||
|
GameObject instance = InstantiatePrefab(prefab, tempPosition);
|
||||||
|
|
||||||
|
// Try to get spawn entry for positioning
|
||||||
|
var spawnEntry = prefab.GetComponent<PrefabSpawnEntryComponent>();
|
||||||
|
if (spawnEntry != null)
|
||||||
|
{
|
||||||
|
PositionObject(instance, xPosition, spawnEntry.spawnPositionMode,
|
||||||
|
spawnEntry.specifiedY, spawnEntry.randomYMin, spawnEntry.randomYMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize if implements interface
|
||||||
|
var initializable = instance.GetComponent<ISpawnInitializable>();
|
||||||
|
if (initializable != null)
|
||||||
|
{
|
||||||
|
initializable.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track counts
|
||||||
|
if (spawnPositive)
|
||||||
|
_positiveSpawnCount++;
|
||||||
|
else
|
||||||
|
_negativeSpawnCount++;
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ObstacleDistanceSpawner] Spawned {(spawnPositive ? "positive" : "negative")} at X={xPosition:F2} from pool {poolIndex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldSpawnPositive()
|
||||||
|
{
|
||||||
|
int totalSpawned = _positiveSpawnCount + _negativeSpawnCount;
|
||||||
|
|
||||||
|
// First few spawns - use pure random
|
||||||
|
if (totalSpawned < 5)
|
||||||
|
{
|
||||||
|
return Random.value <= positiveNegativeRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate current ratio and adjust
|
||||||
|
float currentRatio = totalSpawned > 0 ? (float)_positiveSpawnCount / totalSpawned : 0.5f;
|
||||||
|
float targetRatio = positiveNegativeRatio;
|
||||||
|
|
||||||
|
float adjustedProbability;
|
||||||
|
if (currentRatio < targetRatio)
|
||||||
|
{
|
||||||
|
adjustedProbability = Mathf.Lerp(targetRatio, 1f, (targetRatio - currentRatio) * 2f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
adjustedProbability = Mathf.Lerp(0f, targetRatio, 1f - (currentRatio - targetRatio) * 2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Random.value <= adjustedProbability;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PositionObject(GameObject obj, float xPosition, SpawnPositionMode mode,
|
||||||
|
float specifiedY, float randomYMin, float randomYMax)
|
||||||
|
{
|
||||||
|
if (obj == null) return;
|
||||||
|
|
||||||
|
float targetY;
|
||||||
|
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case SpawnPositionMode.SnapToGround:
|
||||||
|
targetY = SnapToGround(obj, xPosition);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpawnPositionMode.SpecifiedY:
|
||||||
|
targetY = specifiedY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpawnPositionMode.RandomRange:
|
||||||
|
targetY = Random.Range(randomYMin, randomYMax);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
targetY = 0f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 newPosition = obj.transform.position;
|
||||||
|
newPosition.y = targetY;
|
||||||
|
obj.transform.position = newPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float SnapToGround(GameObject obj, float xPosition)
|
||||||
|
{
|
||||||
|
Vector2 rayOrigin = new Vector2(xPosition, 0f);
|
||||||
|
int layerMask = 1 << groundLayer;
|
||||||
|
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, maxGroundRaycastDistance, layerMask);
|
||||||
|
|
||||||
|
if (hit.collider != null)
|
||||||
|
{
|
||||||
|
float groundY = hit.point.y;
|
||||||
|
Bounds bounds = GetObjectBounds(obj);
|
||||||
|
return groundY + bounds.extents.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultObjectYOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bounds GetObjectBounds(GameObject obj)
|
||||||
|
{
|
||||||
|
Renderer objRenderer = obj.GetComponentInChildren<Renderer>();
|
||||||
|
if (objRenderer != null) return objRenderer.bounds;
|
||||||
|
|
||||||
|
Collider2D objCollider2D = obj.GetComponentInChildren<Collider2D>();
|
||||||
|
if (objCollider2D != null) return objCollider2D.bounds;
|
||||||
|
|
||||||
|
Collider objCollider3D = obj.GetComponentInChildren<Collider>();
|
||||||
|
if (objCollider3D != null) return objCollider3D.bounds;
|
||||||
|
|
||||||
|
return new Bounds(obj.transform.position, Vector3.one);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Cleanup()
|
||||||
|
{
|
||||||
|
base.Cleanup();
|
||||||
|
_positiveSpawnCount = 0;
|
||||||
|
_negativeSpawnCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper component to store spawn entry data on prefabs.
|
||||||
|
/// Attach this to prefabs that need specific positioning.
|
||||||
|
/// </summary>
|
||||||
|
public class PrefabSpawnEntryComponent : MonoBehaviour
|
||||||
|
{
|
||||||
|
public SpawnPositionMode spawnPositionMode = SpawnPositionMode.SnapToGround;
|
||||||
|
public float specifiedY;
|
||||||
|
public float randomYMin = -5f;
|
||||||
|
public float randomYMax = 5f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0a550c256a3d4d91a17adf6d0338f022
|
||||||
|
timeCreated: 1765965959
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using Core;
|
||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using Minigames.Airplane.Interactive;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Minigames.Airplane.Core.Spawning
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns parallax background elements across 3 fixed layers (Background/Middle/Foreground).
|
||||||
|
/// Always operates in Exclusive pool mode.
|
||||||
|
/// Integrates with AirplaneCameraManager to provide camera reference to parallax elements.
|
||||||
|
/// </summary>
|
||||||
|
public class ParallaxBackgroundSpawner : BaseDistanceSpawner
|
||||||
|
{
|
||||||
|
[Header("Parallax-Specific")]
|
||||||
|
[Tooltip("Sort layer names for each parallax layer")]
|
||||||
|
[SerializeField] private string backgroundSortLayer = "Background";
|
||||||
|
[SerializeField] private string middleSortLayer = "Midground";
|
||||||
|
[SerializeField] private string foregroundSortLayer = "Foreground";
|
||||||
|
|
||||||
|
[Header("Camera Integration")]
|
||||||
|
[Tooltip("Camera manager for tracking active camera")]
|
||||||
|
[SerializeField] private AirplaneCameraManager cameraManager;
|
||||||
|
|
||||||
|
private Transform _currentCameraTransform;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
// Force exclusive mode
|
||||||
|
poolMode = SpawnPoolMode.Exclusive;
|
||||||
|
|
||||||
|
// Validate exactly 3 pools
|
||||||
|
if (pools == null || pools.Length != 3)
|
||||||
|
{
|
||||||
|
Logging.Error("[ParallaxBackgroundSpawner] Must have exactly 3 pools (Background, Middle, Foreground)!");
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
// Subscribe to camera changes
|
||||||
|
if (cameraManager != null)
|
||||||
|
{
|
||||||
|
cameraManager.OnStateChanged += HandleCameraChanged;
|
||||||
|
_currentCameraTransform = cameraManager.GetActiveCameraTransform();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logging.Warning("[ParallaxBackgroundSpawner] Camera manager not assigned! Parallax elements won't track camera.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (cameraManager != null)
|
||||||
|
{
|
||||||
|
cameraManager.OnStateChanged -= HandleCameraChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCameraChanged(AirplaneCameraState oldState, AirplaneCameraState newState)
|
||||||
|
{
|
||||||
|
if (cameraManager == null) return;
|
||||||
|
|
||||||
|
_currentCameraTransform = cameraManager.GetActiveCameraTransform();
|
||||||
|
|
||||||
|
// Update all existing parallax elements with new camera
|
||||||
|
if (spawnContainer != null)
|
||||||
|
{
|
||||||
|
foreach (Transform child in spawnContainer)
|
||||||
|
{
|
||||||
|
var parallaxElement = child.GetComponent<ParallaxElement>();
|
||||||
|
if (parallaxElement != null)
|
||||||
|
{
|
||||||
|
parallaxElement.SetCameraTransform(_currentCameraTransform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ParallaxBackgroundSpawner] Camera changed to {newState}, updated parallax elements");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SpawnFromPool(int poolIndex, float xPosition)
|
||||||
|
{
|
||||||
|
if (poolIndex < 0 || poolIndex >= 3 || !pools[poolIndex].HasPrefabs)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Select random prefab from pool
|
||||||
|
GameObject prefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)];
|
||||||
|
if (prefab == null) return;
|
||||||
|
|
||||||
|
// Get spawn Y from spawnPoint or default
|
||||||
|
float spawnY = spawnPoint != null ? spawnPoint.position.y : 0f;
|
||||||
|
Vector3 spawnPosition = new Vector3(xPosition, spawnY, 0f);
|
||||||
|
|
||||||
|
// Instantiate
|
||||||
|
GameObject instance = InstantiatePrefab(prefab, spawnPosition);
|
||||||
|
|
||||||
|
// Determine parallax layer based on pool index
|
||||||
|
ParallaxLayer layer = (ParallaxLayer)poolIndex;
|
||||||
|
|
||||||
|
// Get or add ParallaxElement component
|
||||||
|
ParallaxElement parallaxElement = instance.GetComponent<ParallaxElement>();
|
||||||
|
if (parallaxElement == null)
|
||||||
|
{
|
||||||
|
parallaxElement = instance.AddComponent<ParallaxElement>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure parallax element
|
||||||
|
parallaxElement.SetLayer(layer);
|
||||||
|
parallaxElement.SetCameraTransform(_currentCameraTransform);
|
||||||
|
|
||||||
|
// Set sort layer on sprite renderer
|
||||||
|
SetSortLayer(instance, layer);
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ParallaxBackgroundSpawner] Spawned {layer} element at X={xPosition:F2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetSortLayer(GameObject obj, ParallaxLayer layer)
|
||||||
|
{
|
||||||
|
SpriteRenderer spriteRenderer = obj.GetComponentInChildren<SpriteRenderer>();
|
||||||
|
if (spriteRenderer == null) return;
|
||||||
|
|
||||||
|
string sortLayerName = layer switch
|
||||||
|
{
|
||||||
|
ParallaxLayer.Background => backgroundSortLayer,
|
||||||
|
ParallaxLayer.Middle => middleSortLayer,
|
||||||
|
ParallaxLayer.Foreground => foregroundSortLayer,
|
||||||
|
_ => "Default"
|
||||||
|
};
|
||||||
|
|
||||||
|
spriteRenderer.sortingLayerName = sortLayerName;
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ParallaxBackgroundSpawner] Set sort layer '{sortLayerName}' for {layer}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b126f2d189024140ac0bc6fb14155c7f
|
||||||
|
timeCreated: 1765965959
|
||||||
24
Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs
Normal file
24
Assets/Scripts/Minigames/Airplane/Data/ParallaxLayer.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace Minigames.Airplane.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines parallax layer depth for background elements.
|
||||||
|
/// </summary>
|
||||||
|
public enum ParallaxLayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Furthest back layer, moves slowest (lowest parallax factor).
|
||||||
|
/// </summary>
|
||||||
|
Background,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Middle layer, moves at medium speed.
|
||||||
|
/// </summary>
|
||||||
|
Middle,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closest layer, moves fastest (highest parallax factor).
|
||||||
|
/// </summary>
|
||||||
|
Foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 30cd4bc1743c44089146678153542aae
|
||||||
|
timeCreated: 1765965854
|
||||||
45
Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs
Normal file
45
Assets/Scripts/Minigames/Airplane/Data/SpawnPoolConfig.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Minigames.Airplane.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for a single spawn pool.
|
||||||
|
/// Contains prefabs, unlock timing, and optional spawn parameter overrides.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class SpawnPoolConfig
|
||||||
|
{
|
||||||
|
[Tooltip("Prefabs that can spawn from this pool")]
|
||||||
|
public GameObject[] prefabs;
|
||||||
|
|
||||||
|
[Tooltip("Time (seconds from game start) when this pool becomes available. 0 = available immediately.")]
|
||||||
|
public float unlockTime = 0f;
|
||||||
|
|
||||||
|
[Tooltip("Description for this pool (editor reference only)")]
|
||||||
|
public string description = "Pool";
|
||||||
|
|
||||||
|
[Header("Spawn Parameter Overrides (0 = use global)")]
|
||||||
|
[Tooltip("Override minimum spawn distance for this pool (0 = use global)")]
|
||||||
|
public float overrideMinDistance = 0f;
|
||||||
|
|
||||||
|
[Tooltip("Override maximum spawn distance for this pool (0 = use global)")]
|
||||||
|
public float overrideMaxDistance = 0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if this pool has valid prefabs assigned.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPrefabs => prefabs != null && prefabs.Length > 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get effective minimum distance (uses override if non-zero, otherwise uses global).
|
||||||
|
/// </summary>
|
||||||
|
public float GetMinDistance(float globalMin) => overrideMinDistance > 0f ? overrideMinDistance : globalMin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get effective maximum distance (uses override if non-zero, otherwise uses global).
|
||||||
|
/// </summary>
|
||||||
|
public float GetMaxDistance(float globalMax) => overrideMaxDistance > 0f ? overrideMaxDistance : globalMax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 51d29acb7cdd48819588acd1401456a9
|
||||||
|
timeCreated: 1765965854
|
||||||
21
Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs
Normal file
21
Assets/Scripts/Minigames/Airplane/Data/SpawnPoolMode.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Minigames.Airplane.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines how multiple spawn pools are combined.
|
||||||
|
/// </summary>
|
||||||
|
public enum SpawnPoolMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All unlocked pools contribute to a single shared spawn stream.
|
||||||
|
/// Objects spawn at regular intervals considering all available pools.
|
||||||
|
/// </summary>
|
||||||
|
Together,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Each pool spawns independently with its own timing.
|
||||||
|
/// Multiple objects can spawn simultaneously from different pools.
|
||||||
|
/// </summary>
|
||||||
|
Exclusive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5206332ad0e54ed4b20df47663202967
|
||||||
|
timeCreated: 1765965854
|
||||||
150
Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs
Normal file
150
Assets/Scripts/Minigames/Airplane/Interactive/ParallaxElement.cs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
using Core;
|
||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Minigames.Airplane.Interactive
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parallax element that adjusts position based on camera movement.
|
||||||
|
/// Creates depth illusion by moving at different speeds for different layers.
|
||||||
|
/// Continuously tracks active camera for seamless transitions.
|
||||||
|
/// </summary>
|
||||||
|
public class ParallaxElement : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Layer Configuration")]
|
||||||
|
[Tooltip("Which parallax layer this element belongs to")]
|
||||||
|
[SerializeField] private ParallaxLayer layer = ParallaxLayer.Background;
|
||||||
|
|
||||||
|
[Header("Parallax Settings")]
|
||||||
|
[Tooltip("Global parallax strength multiplier (0 = no parallax, 1 = full)")]
|
||||||
|
[SerializeField] private float globalStrength = 1f;
|
||||||
|
|
||||||
|
[Tooltip("Per-layer parallax factors (Background/Middle/Foreground)")]
|
||||||
|
[SerializeField] private float backgroundFactor = 0.3f;
|
||||||
|
[SerializeField] private float middleFactor = 0.6f;
|
||||||
|
[SerializeField] private float foregroundFactor = 0.9f;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] private bool showDebugLogs;
|
||||||
|
|
||||||
|
private Transform _cameraTransform;
|
||||||
|
private Vector3 _startPosition;
|
||||||
|
private float _startCameraX;
|
||||||
|
private bool _isInitialized;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
// Ensure correct sort layer
|
||||||
|
EnsureSortLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
_startPosition = transform.position;
|
||||||
|
|
||||||
|
if (_cameraTransform != null)
|
||||||
|
{
|
||||||
|
_startCameraX = _cameraTransform.position.x;
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!_isInitialized || _cameraTransform == null) return;
|
||||||
|
|
||||||
|
ApplyParallax();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the parallax layer for this element.
|
||||||
|
/// </summary>
|
||||||
|
public void SetLayer(ParallaxLayer newLayer)
|
||||||
|
{
|
||||||
|
layer = newLayer;
|
||||||
|
EnsureSortLayer();
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ParallaxElement] Layer set to {layer}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the camera transform to track.
|
||||||
|
/// Call this when camera changes.
|
||||||
|
/// </summary>
|
||||||
|
public void SetCameraTransform(Transform cameraTransform)
|
||||||
|
{
|
||||||
|
if (cameraTransform == null) return;
|
||||||
|
|
||||||
|
// If camera changed, recalculate base position
|
||||||
|
if (_cameraTransform != null && _cameraTransform != cameraTransform)
|
||||||
|
{
|
||||||
|
// Smooth transition: current world position becomes new start position
|
||||||
|
_startPosition = transform.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cameraTransform = cameraTransform;
|
||||||
|
_startCameraX = _cameraTransform.position.x;
|
||||||
|
_isInitialized = true;
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ParallaxElement] Camera set, starting camera X={_startCameraX:F2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyParallax()
|
||||||
|
{
|
||||||
|
// Calculate camera displacement from start
|
||||||
|
float cameraDisplacement = _cameraTransform.position.x - _startCameraX;
|
||||||
|
|
||||||
|
// Get layer-specific parallax factor
|
||||||
|
float layerFactor = GetLayerFactor();
|
||||||
|
|
||||||
|
// Calculate parallax offset (reduced displacement based on layer depth)
|
||||||
|
float parallaxOffset = cameraDisplacement * layerFactor * globalStrength;
|
||||||
|
|
||||||
|
// Apply offset to start position
|
||||||
|
Vector3 newPosition = _startPosition;
|
||||||
|
newPosition.x += parallaxOffset;
|
||||||
|
transform.position = newPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetLayerFactor()
|
||||||
|
{
|
||||||
|
return layer switch
|
||||||
|
{
|
||||||
|
ParallaxLayer.Background => backgroundFactor,
|
||||||
|
ParallaxLayer.Middle => middleFactor,
|
||||||
|
ParallaxLayer.Foreground => foregroundFactor,
|
||||||
|
_ => 1f
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureSortLayer()
|
||||||
|
{
|
||||||
|
SpriteRenderer spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||||
|
if (spriteRenderer == null) return;
|
||||||
|
|
||||||
|
// Sort layer is set by spawner, this is just a validation/fallback
|
||||||
|
string expectedLayer = layer switch
|
||||||
|
{
|
||||||
|
ParallaxLayer.Background => "Background",
|
||||||
|
ParallaxLayer.Middle => "Midground",
|
||||||
|
ParallaxLayer.Foreground => "Foreground",
|
||||||
|
_ => "Default"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (spriteRenderer.sortingLayerName != expectedLayer)
|
||||||
|
{
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ParallaxElement] Adjusting sort layer to '{expectedLayer}' for {layer}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b32c96c5365743d3a1a5c849ba340b6e
|
||||||
|
timeCreated: 1765965978
|
||||||
Reference in New Issue
Block a user