Finalize the parallax work
This commit is contained in:
@@ -150,8 +150,7 @@ namespace Minigames.Airplane.Core
|
||||
// Update spawner game times for pool unlocking
|
||||
UpdateSpawnerTimes(deltaTime);
|
||||
|
||||
// Update dynamic spawning
|
||||
UpdateDynamicSpawning();
|
||||
// Dynamic spawning is handled by coroutine (SpawnUpdateCoroutine)
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -297,6 +296,9 @@ namespace Minigames.Airplane.Core
|
||||
_isSpawningActive = true;
|
||||
_gameTime = 0f;
|
||||
|
||||
// Start spawn update coroutine
|
||||
StartSpawnUpdates();
|
||||
|
||||
// Start UI tracking with calculated target position
|
||||
if (targetDisplayUI != null)
|
||||
{
|
||||
@@ -317,6 +319,9 @@ namespace Minigames.Airplane.Core
|
||||
_isSpawningActive = false;
|
||||
_planeTransform = null;
|
||||
|
||||
// Stop spawn update coroutine
|
||||
StopSpawnUpdates();
|
||||
|
||||
// Stop UI tracking
|
||||
if (targetDisplayUI != null)
|
||||
{
|
||||
@@ -500,54 +505,81 @@ namespace Minigames.Airplane.Core
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDynamicSpawning()
|
||||
private Coroutine _spawnUpdateCoroutine;
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine that periodically updates spawn horizons for all spawners.
|
||||
/// Runs every 0.2 seconds while plane is active.
|
||||
/// </summary>
|
||||
private System.Collections.IEnumerator SpawnUpdateCoroutine()
|
||||
{
|
||||
if (_planeTransform == null || !_isSpawningActive) return;
|
||||
WaitForSeconds wait = new WaitForSeconds(0.2f);
|
||||
|
||||
float planeX = _planeTransform.position.x;
|
||||
|
||||
// Track furthest X position reached
|
||||
if (planeX > _furthestReachedX)
|
||||
while (_isSpawningActive && _planeTransform != null)
|
||||
{
|
||||
_furthestReachedX = planeX;
|
||||
float planeX = _planeTransform.position.x;
|
||||
|
||||
// Track furthest X position reached
|
||||
if (planeX > _furthestReachedX)
|
||||
{
|
||||
_furthestReachedX = planeX;
|
||||
}
|
||||
|
||||
// Check if plane should trigger new spawning
|
||||
bool shouldSpawnNewContent = !_isRetryAttempt || planeX > (_furthestReachedX - _settings.SpawnDistanceAhead);
|
||||
|
||||
if (shouldSpawnNewContent)
|
||||
{
|
||||
// Push spawn horizons to spawners - they decide independently if spawning is needed
|
||||
|
||||
// Obstacles - standard spawn distance
|
||||
if (obstacleSpawner != null)
|
||||
{
|
||||
float obstacleHorizon = planeX + _settings.SpawnDistanceAhead;
|
||||
obstacleSpawner.UpdateSpawnHorizon(obstacleHorizon);
|
||||
}
|
||||
|
||||
// Ground - spawn further ahead (2x distance)
|
||||
if (groundSpawner != null)
|
||||
{
|
||||
float groundHorizon = planeX + (_settings.SpawnDistanceAhead * 2f);
|
||||
groundSpawner.UpdateSpawnHorizon(groundHorizon);
|
||||
}
|
||||
|
||||
// Parallax - spawn at 1.5x distance, each pool spawns independently
|
||||
if (parallaxSpawner != null)
|
||||
{
|
||||
float parallaxHorizon = planeX + (_settings.SpawnDistanceAhead * 1.5f);
|
||||
parallaxSpawner.UpdateSpawnHorizon(parallaxHorizon);
|
||||
}
|
||||
}
|
||||
|
||||
yield return wait;
|
||||
}
|
||||
|
||||
// Check if plane should trigger new spawning
|
||||
bool shouldSpawnNewContent = !_isRetryAttempt || planeX > (_furthestReachedX - _settings.SpawnDistanceAhead);
|
||||
|
||||
if (shouldSpawnNewContent)
|
||||
if (showDebugLogs)
|
||||
{
|
||||
// Centralized distance checking - orchestrator decides when to spawn
|
||||
|
||||
// Obstacles - check if next spawn point is within ahead distance
|
||||
if (obstacleSpawner != null)
|
||||
{
|
||||
float nextObstacleX = obstacleSpawner.GetNextSpawnX();
|
||||
if (nextObstacleX <= planeX + _settings.SpawnDistanceAhead)
|
||||
{
|
||||
obstacleSpawner.SpawnNext();
|
||||
}
|
||||
}
|
||||
|
||||
// Ground - spawn further ahead (2x distance)
|
||||
if (groundSpawner != null)
|
||||
{
|
||||
float nextGroundX = groundSpawner.GetNextSpawnX();
|
||||
if (nextGroundX <= planeX + (_settings.SpawnDistanceAhead * 2f))
|
||||
{
|
||||
groundSpawner.SpawnNext();
|
||||
}
|
||||
}
|
||||
|
||||
// Parallax - spawn at 1.5x distance
|
||||
if (parallaxSpawner != null)
|
||||
{
|
||||
float nextParallaxX = parallaxSpawner.GetNextSpawnX();
|
||||
if (nextParallaxX <= planeX + (_settings.SpawnDistanceAhead * 1.5f))
|
||||
{
|
||||
parallaxSpawner.SpawnNext();
|
||||
}
|
||||
}
|
||||
Logging.Debug("[AirplaneSpawnManager] Spawn update coroutine stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private void StartSpawnUpdates()
|
||||
{
|
||||
StopSpawnUpdates();
|
||||
_spawnUpdateCoroutine = StartCoroutine(SpawnUpdateCoroutine());
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug("[AirplaneSpawnManager] Started spawn update coroutine");
|
||||
}
|
||||
}
|
||||
|
||||
private void StopSpawnUpdates()
|
||||
{
|
||||
if (_spawnUpdateCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_spawnUpdateCoroutine);
|
||||
_spawnUpdateCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,75 +831,22 @@ namespace Minigames.Airplane.Core
|
||||
|
||||
/// <summary>
|
||||
/// Position an object based on the specified spawn mode.
|
||||
/// Used for target positioning.
|
||||
/// Delegates to PositioningUtility for all positioning logic.
|
||||
/// </summary>
|
||||
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:
|
||||
Logging.Error($"[AirplaneSpawnManager] Unknown spawn position mode: {mode}");
|
||||
targetY = 0f;
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply position
|
||||
Vector3 newPosition = obj.transform.position;
|
||||
newPosition.y = targetY;
|
||||
obj.transform.position = newPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snap an object to the ground using raycast.
|
||||
/// </summary>
|
||||
private float SnapToGround(GameObject obj, float xPosition)
|
||||
{
|
||||
Vector2 rayOrigin = new Vector2(xPosition, 0.0f);
|
||||
int layerMask = 1 << _settings.GroundLayer;
|
||||
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, _settings.MaxGroundRaycastDistance, layerMask);
|
||||
|
||||
if (hit.collider != null)
|
||||
{
|
||||
float groundY = hit.point.y;
|
||||
Bounds bounds = GetObjectBounds(obj);
|
||||
return groundY + bounds.extents.y;
|
||||
}
|
||||
|
||||
return _settings.FallbackYPosition;
|
||||
Spawning.PositioningUtility.PositionObject(obj, xPosition, mode, specifiedY, randomYMin, randomYMax,
|
||||
_settings, _settings.ShowDebugLogs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the bounds of an object.
|
||||
/// Delegates to PositioningUtility.
|
||||
/// </summary>
|
||||
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);
|
||||
return Spawning.PositioningUtility.GetObjectBounds(obj);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -120,54 +120,61 @@ namespace Minigames.Airplane.Core.Spawning
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn the next object at the predetermined NextSpawnX position.
|
||||
/// Updates LastSpawnedX and calculates new NextSpawnX.
|
||||
/// Called by orchestrator when distance check determines spawning is needed.
|
||||
/// Update the spawn horizon and spawn objects if needed.
|
||||
/// Called by orchestrator to push spawn updates.
|
||||
/// Each spawner independently decides if spawning is needed.
|
||||
/// </summary>
|
||||
public virtual void SpawnNext()
|
||||
public virtual void UpdateSpawnHorizon(float horizonX)
|
||||
{
|
||||
if (poolMode == SpawnPoolMode.Together)
|
||||
{
|
||||
UpdateSpawnHorizonTogether(horizonX);
|
||||
}
|
||||
else // Exclusive mode - each pool checks independently
|
||||
{
|
||||
UpdateSpawnHorizonExclusive(horizonX);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSpawnHorizonTogether(float horizonX)
|
||||
{
|
||||
// Keep spawning while next spawn point is behind the horizon
|
||||
while (NextSpawnX < horizonX)
|
||||
{
|
||||
SpawnAtPosition(NextSpawnX);
|
||||
LastSpawnedX = NextSpawnX;
|
||||
NextSpawnX = LastSpawnedX + Random.Range(settings.ObjectSpawnMinDistance, settings.ObjectSpawnMaxDistance);
|
||||
}
|
||||
else // Exclusive mode - spawn from each pool independently
|
||||
{
|
||||
foreach (int poolIndex in UnlockedPoolIndices)
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
||||
if (!PoolNextSpawnX.ContainsKey(poolIndex)) continue;
|
||||
|
||||
SpawnFromPool(poolIndex, PoolNextSpawnX[poolIndex]);
|
||||
|
||||
float minDist = pools[poolIndex].GetMinDistance(settings.ObjectSpawnMinDistance);
|
||||
float maxDist = pools[poolIndex].GetMaxDistance(settings.ObjectSpawnMaxDistance);
|
||||
PoolNextSpawnX[poolIndex] += Random.Range(minDist, maxDist);
|
||||
LastSpawnedX = Mathf.Max(LastSpawnedX, PoolNextSpawnX[poolIndex]);
|
||||
Logging.Debug($"[{GetType().Name}] Together mode: Spawned at {LastSpawnedX:F2}, next at {NextSpawnX:F2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the X position where the next spawn will occur.
|
||||
/// Used by orchestrator for distance checking.
|
||||
/// </summary>
|
||||
public float GetNextSpawnX()
|
||||
private void UpdateSpawnHorizonExclusive(float horizonX)
|
||||
{
|
||||
if (poolMode == SpawnPoolMode.Together)
|
||||
// Each pool spawns independently based on its own position
|
||||
foreach (int poolIndex in UnlockedPoolIndices)
|
||||
{
|
||||
return NextSpawnX;
|
||||
}
|
||||
else // Exclusive - return the closest spawn point across all pools
|
||||
{
|
||||
float closest = float.MaxValue;
|
||||
foreach (var kvp in PoolNextSpawnX)
|
||||
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
||||
if (!PoolNextSpawnX.ContainsKey(poolIndex)) continue;
|
||||
|
||||
float minDist = pools[poolIndex].GetMinDistance(settings.ObjectSpawnMinDistance);
|
||||
float maxDist = pools[poolIndex].GetMaxDistance(settings.ObjectSpawnMaxDistance);
|
||||
|
||||
// Keep spawning from this pool while its next spawn is behind horizon
|
||||
while (PoolNextSpawnX[poolIndex] < horizonX)
|
||||
{
|
||||
if (kvp.Value < closest)
|
||||
closest = kvp.Value;
|
||||
SpawnFromPool(poolIndex, PoolNextSpawnX[poolIndex]);
|
||||
PoolNextSpawnX[poolIndex] += Random.Range(minDist, maxDist);
|
||||
LastSpawnedX = Mathf.Max(LastSpawnedX, PoolNextSpawnX[poolIndex]);
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] Exclusive mode: Pool {poolIndex} spawned at {PoolNextSpawnX[poolIndex]:F2}");
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,15 +322,28 @@ namespace Minigames.Airplane.Core.Spawning
|
||||
|
||||
protected virtual void PreSpawnTogether(float startX, float endX)
|
||||
{
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] PreSpawnTogether START: Range X={startX:F2} to X={endX:F2}, Min/Max distance=[{settings.ObjectSpawnMinDistance:F1}-{settings.ObjectSpawnMaxDistance:F1}]");
|
||||
}
|
||||
|
||||
float currentX = startX + Random.Range(settings.ObjectSpawnMinDistance, settings.ObjectSpawnMaxDistance);
|
||||
int objectsSpawned = 0;
|
||||
float firstSpawnX = currentX;
|
||||
|
||||
while (currentX <= endX)
|
||||
{
|
||||
SpawnAtPosition(currentX);
|
||||
objectsSpawned++;
|
||||
currentX += Random.Range(settings.ObjectSpawnMinDistance, settings.ObjectSpawnMaxDistance);
|
||||
}
|
||||
|
||||
NextSpawnX = currentX;
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] PreSpawnTogether COMPLETE: Spawned {objectsSpawned} objects from X={firstSpawnX:F2} to X={NextSpawnX:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -332,21 +352,74 @@ namespace Minigames.Airplane.Core.Spawning
|
||||
|
||||
protected virtual void PreSpawnExclusive(float startX, float endX)
|
||||
{
|
||||
foreach (int poolIndex in UnlockedPoolIndices)
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
||||
Logging.Debug($"[{GetType().Name}] PreSpawnExclusive START: Range X={startX:F2} to X={endX:F2}, Total pools={pools.Length}");
|
||||
}
|
||||
|
||||
// Pre-spawn only from pools with unlockTime <= 0 (immediate availability)
|
||||
// Pools with positive unlock times are meant to unlock during gameplay progression
|
||||
int spawnedPoolCount = 0;
|
||||
int skippedPoolCount = 0;
|
||||
|
||||
for (int poolIndex = 0; poolIndex < pools.Length; poolIndex++)
|
||||
{
|
||||
if (!pools[poolIndex].HasPrefabs)
|
||||
{
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Warning($"[{GetType().Name}] Pool {poolIndex} skipped - no prefabs assigned");
|
||||
}
|
||||
skippedPoolCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip pools that unlock later during gameplay
|
||||
if (pools[poolIndex].unlockTime > 0f)
|
||||
{
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] Pool {poolIndex} '{pools[poolIndex].description}' skipped - unlockTime={pools[poolIndex].unlockTime}s (will unlock during gameplay)");
|
||||
}
|
||||
skippedPoolCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
float minDist = pools[poolIndex].GetMinDistance(settings.ObjectSpawnMinDistance);
|
||||
float maxDist = pools[poolIndex].GetMaxDistance(settings.ObjectSpawnMaxDistance);
|
||||
float currentX = startX + Random.Range(minDist, maxDist);
|
||||
|
||||
int objectsSpawned = 0;
|
||||
float firstSpawnX = currentX;
|
||||
|
||||
while (currentX <= endX)
|
||||
{
|
||||
SpawnFromPool(poolIndex, currentX);
|
||||
objectsSpawned++;
|
||||
currentX += Random.Range(minDist, maxDist);
|
||||
}
|
||||
|
||||
PoolNextSpawnX[poolIndex] = currentX;
|
||||
spawnedPoolCount++;
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] Pool {poolIndex} '{pools[poolIndex].description}': unlockTime={pools[poolIndex].unlockTime}s, prefabs={pools[poolIndex].prefabs.Length}, distance=[{minDist:F1}-{maxDist:F1}], spawned {objectsSpawned} objects from X={firstSpawnX:F2} to X={PoolNextSpawnX[poolIndex]:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all pools with unlockTime <= 0 are marked as unlocked for dynamic spawning
|
||||
for (int i = 0; i < pools.Length; i++)
|
||||
{
|
||||
if (pools[i].unlockTime <= 0f && !UnlockedPoolIndices.Contains(i))
|
||||
{
|
||||
UnlockedPoolIndices.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] PreSpawnExclusive COMPLETE: {spawnedPoolCount} pools spawned, {skippedPoolCount} pools skipped, {UnlockedPoolIndices.Count} pools unlocked for dynamic spawning");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,87 +460,13 @@ namespace Minigames.Airplane.Core.Spawning
|
||||
|
||||
/// <summary>
|
||||
/// Position an object based on the specified spawn mode.
|
||||
/// Used by spawners to apply Y positioning after instantiation.
|
||||
/// Delegates to PositioningUtility for all positioning logic.
|
||||
/// </summary>
|
||||
protected 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snap an object to the ground using raycast.
|
||||
/// Positions object so its bottom bounds touch the ground surface.
|
||||
/// </summary>
|
||||
protected float SnapToGround(GameObject obj, float xPosition)
|
||||
{
|
||||
// Raycast from high up to ensure we're above the ground
|
||||
Vector2 rayOrigin = new Vector2(xPosition, settings.MaxGroundRaycastDistance);
|
||||
int layerMask = 1 << settings.GroundLayer;
|
||||
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, settings.MaxGroundRaycastDistance + 100f, layerMask);
|
||||
|
||||
if (hit.collider != null)
|
||||
{
|
||||
float groundY = hit.point.y;
|
||||
Bounds bounds = GetObjectBounds(obj);
|
||||
float finalY = groundY + bounds.extents.y;
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] SnapToGround: X={xPosition:F2}, Ground={groundY:F2}, Final={finalY:F2}");
|
||||
}
|
||||
|
||||
return finalY;
|
||||
}
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Warning($"[{GetType().Name}] SnapToGround FAILED at X={xPosition:F2} - no ground found! Using fallback Y={settings.FallbackYPosition:F2}");
|
||||
}
|
||||
|
||||
return settings.FallbackYPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the bounds of an object from its Renderer or Collider.
|
||||
/// </summary>
|
||||
protected 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);
|
||||
PositioningUtility.PositionObject(obj, xPosition, mode, specifiedY, randomYMin, randomYMax,
|
||||
settings, settings.ShowDebugLogs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -37,26 +37,46 @@ namespace Minigames.Airplane.Core.Spawning
|
||||
// Override to use fixed intervals instead of random distances
|
||||
protected override void PreSpawnTogether(float startX, float endX)
|
||||
{
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[GroundDistanceSpawner] PreSpawnTogether START: Range X={startX:F2} to X={endX:F2}, Fixed interval={_groundSpawnInterval:F1}");
|
||||
}
|
||||
|
||||
float currentX = startX;
|
||||
int tilesSpawned = 0;
|
||||
|
||||
while (currentX <= endX)
|
||||
{
|
||||
SpawnAtPosition(currentX);
|
||||
tilesSpawned++;
|
||||
currentX += _groundSpawnInterval; // FIXED interval, not random
|
||||
}
|
||||
|
||||
NextSpawnX = currentX;
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[GroundDistanceSpawner] PreSpawnTogether COMPLETE: Spawned {tilesSpawned} ground tiles from X={startX:F2} to X={NextSpawnX:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override SpawnNext to use fixed ground intervals instead of random distances.
|
||||
/// Called by orchestrator when distance check determines spawning is needed.
|
||||
/// </summary>
|
||||
public override void SpawnNext()
|
||||
public override void UpdateSpawnHorizon(float horizonX)
|
||||
{
|
||||
SpawnAtPosition(NextSpawnX);
|
||||
LastSpawnedX = NextSpawnX;
|
||||
NextSpawnX += _groundSpawnInterval; // FIXED interval, not random
|
||||
// Keep spawning while next spawn point is behind the horizon
|
||||
while (NextSpawnX < horizonX)
|
||||
{
|
||||
SpawnAtPosition(NextSpawnX);
|
||||
LastSpawnedX = NextSpawnX;
|
||||
NextSpawnX += _groundSpawnInterval; // FIXED interval, not random
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[GroundDistanceSpawner] Spawned at {LastSpawnedX:F2}, next at {NextSpawnX:F2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void SpawnFromPool(int poolIndex, float xPosition)
|
||||
|
||||
@@ -13,10 +13,27 @@ namespace Minigames.Airplane.Core.Spawning
|
||||
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";
|
||||
[Tooltip("Sort layer for all parallax elements")]
|
||||
[SerializeField] private string parallaxSortLayer = "Background";
|
||||
|
||||
[Tooltip("Sort order for background layer (furthest back)")]
|
||||
[SerializeField] private int backgroundSortOrder = -50;
|
||||
|
||||
[Tooltip("Sort order increment between layers")]
|
||||
[SerializeField] private int sortOrderIncrement = 10;
|
||||
|
||||
[Header("Parallax Configuration")]
|
||||
[Tooltip("Global parallax strength multiplier (0 = no effect, 1 = full effect)")]
|
||||
[SerializeField] private float globalStrength = 1f;
|
||||
|
||||
[Tooltip("Background layer movement speed (typically slowest, e.g. 0.3)")]
|
||||
[SerializeField] private float backgroundSpeed = 0.3f;
|
||||
|
||||
[Tooltip("Middle layer movement speed (typically medium, e.g. 0.6)")]
|
||||
[SerializeField] private float middleSpeed = 0.6f;
|
||||
|
||||
[Tooltip("Foreground layer movement speed (typically fastest, e.g. 0.9)")]
|
||||
[SerializeField] private float foregroundSpeed = 0.9f;
|
||||
|
||||
[Header("Camera Integration")]
|
||||
[Tooltip("Camera manager for tracking active camera")]
|
||||
@@ -71,7 +88,7 @@ namespace Minigames.Airplane.Core.Spawning
|
||||
var parallaxElement = child.GetComponent<ParallaxElement>();
|
||||
if (parallaxElement != null)
|
||||
{
|
||||
parallaxElement.SetCameraTransform(_currentCameraTransform);
|
||||
parallaxElement.UpdateCamera(_currentCameraTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,25 +124,65 @@ namespace Minigames.Airplane.Core.Spawning
|
||||
parallaxElement = instance.AddComponent<ParallaxElement>();
|
||||
}
|
||||
|
||||
// Configure parallax element
|
||||
parallaxElement.SetLayer(layer);
|
||||
parallaxElement.SetCameraTransform(_currentCameraTransform);
|
||||
|
||||
// Set sort layer on sprite renderer
|
||||
SetSortLayer(instance, layer);
|
||||
|
||||
// Log Y position BEFORE positioning
|
||||
Vector3 positionBeforePositionObject = instance.transform.position;
|
||||
|
||||
// Try to get spawn entry for Y positioning (same logic as obstacles)
|
||||
var spawnEntry = prefab.GetComponent<PrefabSpawnEntryComponent>();
|
||||
if (spawnEntry != null)
|
||||
{
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[ParallaxBackgroundSpawner] '{prefab.name}' has PrefabSpawnEntryComponent: mode={spawnEntry.spawnPositionMode}, specifiedY={spawnEntry.specifiedY}, randomRange=[{spawnEntry.randomYMin},{spawnEntry.randomYMax}]");
|
||||
}
|
||||
|
||||
// Use per-prefab configuration
|
||||
PositionObject(instance, xPosition, spawnEntry.spawnPositionMode,
|
||||
spawnEntry.specifiedY, spawnEntry.randomYMin, spawnEntry.randomYMax);
|
||||
}
|
||||
// Otherwise stays at Y=0
|
||||
else
|
||||
{
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[ParallaxBackgroundSpawner] '{prefab.name}' NO component, using global: mode={settings.DefaultObstaclePositionMode}");
|
||||
}
|
||||
|
||||
// Fall back to global default configuration from settings
|
||||
PositionObject(instance, xPosition, settings.DefaultObstaclePositionMode,
|
||||
settings.FallbackYPosition, settings.DefaultObstacleRandomYMin, settings.DefaultObstacleRandomYMax);
|
||||
}
|
||||
|
||||
// Log Y position AFTER positioning
|
||||
Vector3 positionAfterPositionObject = instance.transform.position;
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[ParallaxBackgroundSpawner] Spawned {layer} element at X={xPosition:F2}");
|
||||
Logging.Debug($"[ParallaxBackgroundSpawner] Y positioning: BEFORE={positionBeforePositionObject.y:F2}, AFTER={positionAfterPositionObject.y:F2}, CHANGED={positionAfterPositionObject.y != positionBeforePositionObject.y}");
|
||||
}
|
||||
|
||||
// IMPORTANT: Initialize parallax element AFTER positioning so it captures the correct Y coordinate
|
||||
// The parallax element caches _startPosition which includes Y coordinate
|
||||
// If we initialize before positioning, Y=0 gets cached and objects appear at wrong height
|
||||
float layerSpeed = GetLayerSpeed(layer);
|
||||
parallaxElement.Initialize(layer, layerSpeed, globalStrength, _currentCameraTransform);
|
||||
|
||||
// Verify that Initialize didn't move the object
|
||||
Vector3 positionAfterInitialize = instance.transform.position;
|
||||
|
||||
if (Mathf.Abs(positionAfterInitialize.y - positionAfterPositionObject.y) > 0.01f)
|
||||
{
|
||||
Logging.Error($"[ParallaxBackgroundSpawner] BUG: Y position changed during Initialize! Before={positionAfterPositionObject.y:F2}, After={positionAfterInitialize.y:F2}");
|
||||
}
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
string positionModeInfo = spawnEntry != null
|
||||
? $", positionMode={spawnEntry.spawnPositionMode} (per-prefab)"
|
||||
: $", positionMode={settings.DefaultObstaclePositionMode} (global fallback)";
|
||||
Logging.Debug($"[ParallaxBackgroundSpawner] FINAL: '{prefab.name}' at WORLD position X={positionAfterInitialize.x:F2}, Y={positionAfterInitialize.y:F2}, Z={positionAfterInitialize.z:F2}{positionModeInfo}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,21 +191,30 @@ namespace Minigames.Airplane.Core.Spawning
|
||||
SpriteRenderer spriteRenderer = obj.GetComponentInChildren<SpriteRenderer>();
|
||||
if (spriteRenderer == null) return;
|
||||
|
||||
string sortLayerName = layer switch
|
||||
{
|
||||
ParallaxLayer.Background => backgroundSortLayer,
|
||||
ParallaxLayer.Middle => middleSortLayer,
|
||||
ParallaxLayer.Foreground => foregroundSortLayer,
|
||||
_ => "Default"
|
||||
};
|
||||
// All parallax objects use the same sort layer
|
||||
spriteRenderer.sortingLayerName = parallaxSortLayer;
|
||||
|
||||
spriteRenderer.sortingLayerName = sortLayerName;
|
||||
// Calculate sort order based on layer depth
|
||||
// Background (0): -50, Middle (1): -40, Foreground (2): -30
|
||||
int sortOrder = backgroundSortOrder + ((int)layer * sortOrderIncrement);
|
||||
spriteRenderer.sortingOrder = sortOrder;
|
||||
|
||||
if (settings.ShowDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[ParallaxBackgroundSpawner] Set sort layer '{sortLayerName}' for {layer}");
|
||||
Logging.Debug($"[ParallaxBackgroundSpawner] Set '{parallaxSortLayer}' layer with sort order {sortOrder} for {layer}");
|
||||
}
|
||||
}
|
||||
|
||||
private float GetLayerSpeed(ParallaxLayer layer)
|
||||
{
|
||||
return layer switch
|
||||
{
|
||||
ParallaxLayer.Background => backgroundSpeed,
|
||||
ParallaxLayer.Middle => middleSpeed,
|
||||
ParallaxLayer.Foreground => foregroundSpeed,
|
||||
_ => 1f
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
using Core;
|
||||
using Core.Settings;
|
||||
using Minigames.Airplane.Data;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.Airplane.Core.Spawning
|
||||
{
|
||||
/// <summary>
|
||||
/// Static utility for object positioning in the airplane minigame.
|
||||
/// Provides common positioning logic used by spawners and managers.
|
||||
/// Handles snap-to-ground, specified Y, and random Y positioning.
|
||||
/// </summary>
|
||||
public static class PositioningUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// Position an object based on the specified spawn mode.
|
||||
/// Handles all positioning modes: SnapToGround, SpecifiedY, and RandomRange.
|
||||
/// </summary>
|
||||
public static void PositionObject(GameObject obj, float xPosition, SpawnPositionMode mode,
|
||||
float specifiedY, float randomYMin, float randomYMax, IAirplaneSettings settings, bool showDebugLogs = false)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Warning("[PositioningUtility] PositionObject called with null object!");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// First, set an initial Y position based on mode
|
||||
float initialY;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case SpawnPositionMode.SnapToGround:
|
||||
// Start at fallback position, SnapToGround will adjust
|
||||
initialY = settings.FallbackYPosition;
|
||||
break;
|
||||
|
||||
case SpawnPositionMode.SpecifiedY:
|
||||
initialY = specifiedY;
|
||||
break;
|
||||
|
||||
case SpawnPositionMode.RandomRange:
|
||||
initialY = Random.Range(randomYMin, randomYMax);
|
||||
break;
|
||||
|
||||
default:
|
||||
initialY = 0f;
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Warning($"[PositioningUtility] Unknown spawn position mode: {mode}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Set initial position
|
||||
obj.transform.position = new Vector3(xPosition, initialY, 0f);
|
||||
|
||||
// For SnapToGround mode, now adjust the object to align with ground
|
||||
if (mode == SpawnPositionMode.SnapToGround)
|
||||
{
|
||||
SnapToGround(obj, xPosition, settings, showDebugLogs);
|
||||
}
|
||||
|
||||
// Draw debug visualization AFTER positioning
|
||||
if (showDebugLogs)
|
||||
{
|
||||
DrawObjectBoundsDebug(obj, mode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snap an object to the ground using raycast.
|
||||
/// Positions object so its bottom bounds touch the TOP surface of the ground.
|
||||
/// Uses simple measure-and-adjust approach that works with any pivot configuration.
|
||||
/// </summary>
|
||||
public static void SnapToGround(GameObject obj, float xPosition, IAirplaneSettings settings, bool showDebugLogs = false)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Warning("[PositioningUtility] SnapToGround called with null object!");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Raycast from high up to find the TOP of the ground surface
|
||||
Vector2 rayOrigin = new Vector2(xPosition, settings.MaxGroundRaycastDistance);
|
||||
int layerMask = 1 << settings.GroundLayer;
|
||||
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, settings.MaxGroundRaycastDistance + 100f, layerMask);
|
||||
|
||||
if (hit.collider == null)
|
||||
{
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Warning($"[PositioningUtility] SnapToGround FAILED at X={xPosition:F2} - no ground found! Object stays at current Y.");
|
||||
}
|
||||
return; // Can't snap, leave object where it is
|
||||
}
|
||||
|
||||
// Ground surface is at the raycast hit point (TOP of ground collider)
|
||||
float groundY = hit.point.y;
|
||||
|
||||
// Measure where the object's bottom actually is RIGHT NOW
|
||||
Bounds bounds = GetObjectBounds(obj);
|
||||
float currentBottom = bounds.min.y;
|
||||
|
||||
// Calculate how far off we are from desired position
|
||||
float offset = currentBottom - groundY;
|
||||
|
||||
// Adjust the object's position to align bottom with ground
|
||||
Vector3 pos = obj.transform.position;
|
||||
pos.y -= offset; // Move up or down by the difference
|
||||
obj.transform.position = pos;
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
// Draw raycast (cyan)
|
||||
Debug.DrawLine(rayOrigin, hit.point, Color.cyan, 5f);
|
||||
|
||||
// Draw ground hit point (magenta)
|
||||
Debug.DrawLine(new Vector3(xPosition - 1f, groundY, 0f),
|
||||
new Vector3(xPosition + 1f, groundY, 0f), Color.magenta, 5f);
|
||||
|
||||
// Draw object bottom after snapping (red)
|
||||
Bounds finalBounds = GetObjectBounds(obj);
|
||||
Debug.DrawLine(new Vector3(xPosition - 1f, finalBounds.min.y, 0f),
|
||||
new Vector3(xPosition + 1f, finalBounds.min.y, 0f), Color.red, 5f);
|
||||
|
||||
Logging.Debug($"[PositioningUtility] SnapToGround: X={xPosition:F2}, GroundTop={groundY:F2}, " +
|
||||
$"BottomWas={currentBottom:F2}, Offset={offset:F2}, FinalY={obj.transform.position.y:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the bounds of an object from its Renderer or Collider.
|
||||
/// Searches child objects if no component is found on the root.
|
||||
/// </summary>
|
||||
public static Bounds GetObjectBounds(GameObject obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return new Bounds(Vector3.zero, Vector3.one);
|
||||
}
|
||||
|
||||
// Try renderer first (most common for sprites)
|
||||
Renderer objRenderer = obj.GetComponentInChildren<Renderer>();
|
||||
if (objRenderer != null) return objRenderer.bounds;
|
||||
|
||||
// Try 2D collider
|
||||
Collider2D objCollider2D = obj.GetComponentInChildren<Collider2D>();
|
||||
if (objCollider2D != null) return objCollider2D.bounds;
|
||||
|
||||
// Try 3D collider
|
||||
Collider objCollider3D = obj.GetComponentInChildren<Collider>();
|
||||
if (objCollider3D != null) return objCollider3D.bounds;
|
||||
|
||||
// Fallback to transform position with unit size
|
||||
return new Bounds(obj.transform.position, Vector3.one);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw debug visualization for object bounds after positioning.
|
||||
/// Shows the actual bounds in world space at the object's final position.
|
||||
/// </summary>
|
||||
public static void DrawObjectBoundsDebug(GameObject obj, SpawnPositionMode mode)
|
||||
{
|
||||
if (obj == null) return;
|
||||
|
||||
Bounds bounds = default;
|
||||
string sourceType = "none";
|
||||
|
||||
// Get bounds from whatever component is available
|
||||
Renderer objRenderer = obj.GetComponentInChildren<Renderer>();
|
||||
if (objRenderer != null)
|
||||
{
|
||||
bounds = objRenderer.bounds;
|
||||
sourceType = "Renderer";
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider2D objCollider2D = obj.GetComponentInChildren<Collider2D>();
|
||||
if (objCollider2D != null)
|
||||
{
|
||||
bounds = objCollider2D.bounds;
|
||||
sourceType = "Collider2D";
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider objCollider3D = obj.GetComponentInChildren<Collider>();
|
||||
if (objCollider3D != null)
|
||||
{
|
||||
bounds = objCollider3D.bounds;
|
||||
sourceType = "Collider3D";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceType == "none") return;
|
||||
|
||||
Vector3 center = bounds.center;
|
||||
Vector3 extents = bounds.extents;
|
||||
float height = bounds.size.y;
|
||||
|
||||
// Draw bounds box (green)
|
||||
Debug.DrawLine(new Vector3(center.x - extents.x, center.y - extents.y, center.z),
|
||||
new Vector3(center.x + extents.x, center.y - extents.y, center.z), Color.green, 5f);
|
||||
Debug.DrawLine(new Vector3(center.x - extents.x, center.y + extents.y, center.z),
|
||||
new Vector3(center.x + extents.x, center.y + extents.y, center.z), Color.green, 5f);
|
||||
Debug.DrawLine(new Vector3(center.x - extents.x, center.y - extents.y, center.z),
|
||||
new Vector3(center.x - extents.x, center.y + extents.y, center.z), Color.green, 5f);
|
||||
Debug.DrawLine(new Vector3(center.x + extents.x, center.y - extents.y, center.z),
|
||||
new Vector3(center.x + extents.x, center.y + extents.y, center.z), Color.green, 5f);
|
||||
|
||||
// Draw center point (yellow cross)
|
||||
Debug.DrawLine(new Vector3(center.x - 1f, center.y, center.z),
|
||||
new Vector3(center.x + 1f, center.y, center.z), Color.yellow, 5f);
|
||||
Debug.DrawLine(new Vector3(center.x, center.y - 1f, center.z),
|
||||
new Vector3(center.x, center.y + 1f, center.z), Color.yellow, 5f);
|
||||
|
||||
// Draw bottom edge (red thick line) - where we expect the object to touch ground
|
||||
Debug.DrawLine(new Vector3(center.x - extents.x, center.y - extents.y, center.z),
|
||||
new Vector3(center.x + extents.x, center.y - extents.y, center.z), Color.red, 5f);
|
||||
|
||||
// Draw top edge (cyan)
|
||||
Debug.DrawLine(new Vector3(center.x - extents.x, center.y + extents.y, center.z),
|
||||
new Vector3(center.x + extents.x, center.y + extents.y, center.z), Color.cyan, 5f);
|
||||
|
||||
Logging.Debug($"[PositioningUtility] ObjectBounds: {obj.name}, Mode={mode}, Source={sourceType}, " +
|
||||
$"Height={height:F2}, Center.y={center.y:F2}, Bottom.y={center.y - extents.y:F2}, " +
|
||||
$"Top.y={center.y + extents.y:F2}, Scale={obj.transform.localScale.y:F2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 369dac4c5d0943329fd0918ea52bdd4b
|
||||
timeCreated: 1766006091
|
||||
@@ -7,48 +7,27 @@ 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.
|
||||
/// Configuration is provided by ParallaxBackgroundSpawner at initialization.
|
||||
/// IMPORTANT: Always calculates position relative to ORIGINAL spawn position and camera,
|
||||
/// ensuring deterministic behavior during camera blends and switches.
|
||||
/// </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;
|
||||
// Runtime state (set by spawner via Initialize)
|
||||
private ParallaxLayer _layer;
|
||||
private float _layerSpeed;
|
||||
private float _globalStrength;
|
||||
|
||||
// Current camera being tracked
|
||||
private Transform _cameraTransform;
|
||||
private Vector3 _startPosition;
|
||||
private float _startCameraX;
|
||||
|
||||
// ORIGINAL spawn state - NEVER reset after initialization
|
||||
// This ensures objects return to spawn position when camera returns to spawn position
|
||||
private Vector3 _originalSpawnPosition;
|
||||
private float _originalCameraX;
|
||||
|
||||
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;
|
||||
@@ -57,94 +36,69 @@ namespace Minigames.Airplane.Interactive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the parallax layer for this element.
|
||||
/// Initialize the parallax element with spawner-provided settings.
|
||||
/// Called by ParallaxBackgroundSpawner when spawning.
|
||||
/// Sets the ORIGINAL spawn position and camera position - these are never reset.
|
||||
/// IMPORTANT: Uses Camera.main for tracking to ensure smooth updates during Cinemachine blends.
|
||||
/// </summary>
|
||||
public void SetLayer(ParallaxLayer newLayer)
|
||||
public void Initialize(ParallaxLayer layer, float layerSpeed, float globalStrength, Transform cameraTransform)
|
||||
{
|
||||
layer = newLayer;
|
||||
EnsureSortLayer();
|
||||
_layer = layer;
|
||||
_layerSpeed = layerSpeed;
|
||||
_globalStrength = globalStrength;
|
||||
|
||||
if (showDebugLogs)
|
||||
// ALWAYS use Camera.main for tracking - this ensures smooth updates during Cinemachine blends
|
||||
// Virtual cameras don't move during blends, but Camera.main does
|
||||
_cameraTransform = Camera.main != null ? Camera.main.transform : cameraTransform;
|
||||
|
||||
// Store ORIGINAL spawn position - never reset
|
||||
_originalSpawnPosition = transform.position;
|
||||
|
||||
if (_cameraTransform != null)
|
||||
{
|
||||
Logging.Debug($"[ParallaxElement] Layer set to {layer}");
|
||||
// Store ORIGINAL camera X - never reset
|
||||
_originalCameraX = _cameraTransform.position.x;
|
||||
_isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the camera transform to track.
|
||||
/// Call this when camera changes.
|
||||
/// Update the camera transform when camera changes.
|
||||
/// Called by ParallaxBackgroundSpawner when camera switches or blends.
|
||||
/// ONLY updates the camera reference - does NOT reset start position or camera X.
|
||||
/// This ensures parallax remains deterministic across camera switches.
|
||||
/// IMPORTANT: Always uses Camera.main to ensure smooth updates during blends.
|
||||
/// </summary>
|
||||
public void SetCameraTransform(Transform cameraTransform)
|
||||
public void UpdateCamera(Transform newCameraTransform)
|
||||
{
|
||||
if (cameraTransform == null) return;
|
||||
// ALWAYS use Camera.main for tracking - this ensures smooth updates during Cinemachine blends
|
||||
// We ignore the passed transform and always use Camera.main
|
||||
Transform mainCamera = Camera.main != null ? Camera.main.transform : newCameraTransform;
|
||||
|
||||
// If camera changed, recalculate base position
|
||||
if (_cameraTransform != null && _cameraTransform != cameraTransform)
|
||||
{
|
||||
// Smooth transition: current world position becomes new start position
|
||||
_startPosition = transform.position;
|
||||
}
|
||||
if (mainCamera == null) return;
|
||||
|
||||
_cameraTransform = cameraTransform;
|
||||
_startCameraX = _cameraTransform.position.x;
|
||||
// Simply update the camera reference - do NOT reset positions
|
||||
// The parallax will continue to calculate relative to original spawn state
|
||||
_cameraTransform = mainCamera;
|
||||
_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;
|
||||
// Calculate camera displacement from ORIGINAL camera position
|
||||
// This ensures objects return to spawn position when camera returns to spawn position
|
||||
float cameraDisplacement = _cameraTransform.position.x - _originalCameraX;
|
||||
|
||||
// Get layer-specific parallax factor
|
||||
float layerFactor = GetLayerFactor();
|
||||
// Calculate parallax offset - negative to move opposite direction
|
||||
// Lower speed = appears further away (moves less)
|
||||
// Camera moves right (+) → background moves left (-) at reduced speed
|
||||
float parallaxOffset = -cameraDisplacement * _layerSpeed * _globalStrength;
|
||||
|
||||
// Calculate parallax offset (reduced displacement based on layer depth)
|
||||
float parallaxOffset = cameraDisplacement * layerFactor * globalStrength;
|
||||
|
||||
// Apply offset to start position
|
||||
Vector3 newPosition = _startPosition;
|
||||
// Apply offset to ORIGINAL spawn position
|
||||
Vector3 newPosition = _originalSpawnPosition;
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,8 +117,12 @@ namespace Minigames.BirdPooper
|
||||
/// </summary>
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
// Skip validation for prefab assets (only run on scene instances)
|
||||
if (UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this))
|
||||
return;
|
||||
|
||||
// Only run in editor, not during play mode
|
||||
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
if (Application.isPlaying || !Application.isEditor)
|
||||
return;
|
||||
|
||||
EdgeAnchor anchor = GetComponent<EdgeAnchor>();
|
||||
|
||||
Reference in New Issue
Block a user