Working player + obstacle mvp

This commit is contained in:
Michal Pikulski
2025-11-20 12:24:56 +01:00
parent 28e6205bdd
commit af8cee1226
75 changed files with 7515 additions and 200 deletions

View File

@@ -17,6 +17,7 @@ namespace AppleHillsCamera
public enum AnchorEdge
{
Top,
Middle,
Bottom,
Left,
Right
@@ -49,6 +50,10 @@ namespace AppleHillsCamera
[Tooltip("Whether to account for this object's size in positioning")]
public bool accountForObjectSize = true;
[Header("Custom Anchor Point")]
[Tooltip("Optional: Use this child Transform's world position as the anchor point instead of calculated bounds")]
public Transform customAnchorPoint;
[Header("Visualization")]
[Tooltip("Whether to show the anchor visualization in the editor")]
public bool showVisualization = true;
@@ -302,10 +307,22 @@ namespace AppleHillsCamera
}
/// <summary>
/// Get the combined bounds of all renderers on this object and its children
/// Get the combined bounds of all renderers on this object and its children.
/// If customAnchorPoint is set, returns a zero-size bounds at the anchor point's world position.
/// </summary>
private Bounds GetObjectBounds()
{
// If custom anchor point is specified, use its world position as the bounds center
if (customAnchorPoint != null)
{
// Return zero-size bounds centered at the custom anchor point
// This makes the anchor point the exact position that will snap to the edge
Bounds customBounds = new Bounds(customAnchorPoint.position, Vector3.zero);
_objectBounds = customBounds;
return customBounds;
}
// Default behavior: calculate bounds from renderers
Bounds bounds = new Bounds(transform.position, Vector3.zero);
// Get all renderers in this object and its children
@@ -404,6 +421,8 @@ namespace AppleHillsCamera
{
case AnchorEdge.Top:
return referenceMarker.topMargin;
case AnchorEdge.Middle:
return 0f; // Middle has no margin
case AnchorEdge.Bottom:
return referenceMarker.bottomMargin;
case AnchorEdge.Left:
@@ -445,6 +464,10 @@ namespace AppleHillsCamera
// For top edge, offset is negative (moving down) by the top extent
offsetY = -extents.y - centerOffset.y;
break;
case AnchorEdge.Middle:
// For middle, no offset needed - object centers on middle
offsetY = -centerOffset.y;
break;
case AnchorEdge.Bottom:
// For bottom edge, offset is positive (moving up) by the bottom extent
offsetY = extents.y - centerOffset.y;
@@ -468,6 +491,11 @@ namespace AppleHillsCamera
newPosition.y = cameraPosition.y + cameraOrthoSize - margin + offsetY;
break;
case AnchorEdge.Middle:
// Position at the vertical center of the screen
newPosition.y = cameraPosition.y + offsetY;
break;
case AnchorEdge.Bottom:
// Position from the bottom of the screen
// When margin is 0, object's bottom edge is exactly at the bottom screen edge
@@ -518,6 +546,14 @@ namespace AppleHillsCamera
objectPosition.z
);
case AnchorEdge.Middle:
// Point at vertical center with same X coordinate as the object
return new Vector3(
objectPosition.x,
cameraPosition.y,
objectPosition.z
);
case AnchorEdge.Bottom:
// Point on bottom edge with same X coordinate as the object
return new Vector3(

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 12ea065dca5840acae696a40c0cd96dc
timeCreated: 1763632462

View File

@@ -2,16 +2,13 @@
namespace Minigames.BirdPooper
{
/// <summary>
/// Listens to BirdPlayerController flap events and triggers the flap animation.
/// Uses Animator with trigger parameter to transition from idle to flap animation.
/// Listens to BirdPlayerController flap events and triggers wing flap animation.
/// Uses Animator with a trigger parameter to play flap animation from idle state.
/// </summary>
public class BirdFlapAnimator : MonoBehaviour
{
[Header("Animation Settings")]
[SerializeField] private Animator animator;
[Tooltip("Name of the trigger parameter in the Animator")]
[SerializeField] private string flapTriggerName = "Flap";
@@ -19,7 +16,7 @@ namespace Minigames.BirdPooper
void Awake()
{
// Auto-assign animator if not set
// Auto-assign animator component if not set
if (animator == null)
{
animator = GetComponent<Animator>();
@@ -57,15 +54,15 @@ namespace Minigames.BirdPooper
}
/// <summary>
/// Called when the bird flaps. Triggers the flap animation via Animator parameter.
/// Called when the bird flaps. Triggers the flap animation.
/// </summary>
private void OnBirdFlapped()
{
if (animator == null) return;
// Trigger the flap animation
animator.SetTrigger(flapTriggerName);
Debug.Log($"[BirdFlapAnimator] Flap trigger '{flapTriggerName}' activated");
Debug.Log($"[BirdFlapAnimator] Triggered animation: {flapTriggerName}");
}
}
}

View File

@@ -13,6 +13,7 @@ namespace Minigames.BirdPooper
{
[Header("Events")]
public UnityEngine.Events.UnityEvent OnFlap;
public UnityEngine.Events.UnityEvent OnPlayerDamaged;
private Rigidbody2D rb;
private IBirdPooperSettings settings;
@@ -24,9 +25,11 @@ namespace Minigames.BirdPooper
{
base.OnManagedAwake();
// Initialize event
// Initialize events
if (OnFlap == null)
OnFlap = new UnityEngine.Events.UnityEvent();
if (OnPlayerDamaged == null)
OnPlayerDamaged = new UnityEngine.Events.UnityEvent();
// Load settings
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
@@ -41,7 +44,7 @@ namespace Minigames.BirdPooper
if (rb != null)
{
rb.gravityScale = 0f; // Disable Unity physics gravity
rb.bodyType = RigidbodyType2D.Dynamic;
rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces
// Store the initial X position from the scene
fixedXPosition = rb.position.x;
@@ -92,16 +95,6 @@ namespace Minigames.BirdPooper
}
}
private void Destroy()
{
// Unregister when destroyed
if (Input.InputManager.Instance != null)
{
Input.InputManager.Instance.UnregisterOverrideConsumer(this);
Debug.Log("[BirdPlayerController] Unregistered override input consumer");
}
}
#region ITouchInputConsumer Implementation
public void OnTap(Vector2 tapPosition)
@@ -164,11 +157,16 @@ namespace Minigames.BirdPooper
#endregion
#region Collision Detection
#region Trigger-Based Collision Detection
private void OnCollisionEnter2D(Collision2D collision)
/// <summary>
/// Called when a trigger collider enters this object's trigger.
/// Used for detecting obstacles without physics interactions.
/// </summary>
private void OnTriggerEnter2D(Collider2D other)
{
if (collision.gameObject.CompareTag("Obstacle"))
// Check if the colliding object is tagged as an obstacle
if (other.CompareTag("Obstacle"))
{
HandleDeath();
}
@@ -176,9 +174,15 @@ namespace Minigames.BirdPooper
private void HandleDeath()
{
// Only process death once
if (isDead) return;
isDead = true;
verticalVelocity = 0f;
Debug.Log("[BirdPlayerController] Bird died!");
// Emit damage event - let the game manager handle UI
OnPlayerDamaged?.Invoke();
}
#endregion

View File

@@ -0,0 +1,118 @@
using UnityEngine;
using Core.Lifecycle;
namespace Minigames.BirdPooper
{
/// <summary>
/// Central game manager for Bird Pooper minigame.
/// Manages game flow, UI, obstacle spawning, and reacts to player events.
/// Singleton pattern for easy access.
/// </summary>
public class BirdPooperGameManager : ManagedBehaviour
{
private static BirdPooperGameManager _instance;
public static BirdPooperGameManager Instance => _instance;
[Header("References")]
[SerializeField] private BirdPlayerController player;
[SerializeField] private ObstacleSpawner obstacleSpawner;
[SerializeField] private GameOverScreen gameOverScreen;
internal override void OnManagedAwake()
{
base.OnManagedAwake();
// Set singleton instance
if (_instance != null && _instance != this)
{
Debug.LogWarning("[BirdPooperGameManager] Multiple instances detected! Destroying duplicate.");
Destroy(gameObject);
return;
}
_instance = this;
// Validate references
if (player == null)
{
Debug.LogError("[BirdPooperGameManager] Player reference not assigned!");
}
if (obstacleSpawner == null)
{
Debug.LogError("[BirdPooperGameManager] ObstacleSpawner reference not assigned!");
}
if (gameOverScreen == null)
{
Debug.LogError("[BirdPooperGameManager] GameOverScreen reference not assigned!");
}
else
{
// Hide game over screen on start
gameOverScreen.gameObject.SetActive(false);
}
}
internal override void OnManagedStart()
{
base.OnManagedStart();
// Subscribe to player events
if (player != null)
{
player.OnPlayerDamaged.AddListener(HandlePlayerDamaged);
Debug.Log("[BirdPooperGameManager] Subscribed to player damaged event");
}
// Start obstacle spawning
if (obstacleSpawner != null)
{
obstacleSpawner.StartSpawning();
Debug.Log("[BirdPooperGameManager] Started obstacle spawning");
}
}
internal override void OnManagedDestroy()
{
// Unsubscribe from player events
if (player != null)
{
player.OnPlayerDamaged.RemoveListener(HandlePlayerDamaged);
}
// Clear singleton
if (_instance == this)
{
_instance = null;
}
base.OnManagedDestroy();
}
/// <summary>
/// Called when player takes damage/dies.
/// Shows game over screen.
/// </summary>
private void HandlePlayerDamaged()
{
Debug.Log("[BirdPooperGameManager] Player damaged - showing game over screen");
// Stop spawning obstacles
if (obstacleSpawner != null)
{
obstacleSpawner.StopSpawning();
}
// Show game over screen
if (gameOverScreen != null)
{
gameOverScreen.Show();
}
else
{
Debug.LogError("[BirdPooperGameManager] GameOverScreen reference missing!");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8cb19f77b49b4e299fac404c56e0455a
timeCreated: 1763636566

View File

@@ -0,0 +1,101 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using Core;
using UI;
namespace Minigames.BirdPooper
{
/// <summary>
/// Game over screen for Bird Pooper minigame.
/// Displays when the player dies and allows restarting the level.
/// Uses unscaled time for UI updates (works when Time.timeScale = 0).
/// </summary>
public class GameOverScreen : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private Button dismissButton;
[SerializeField] private CanvasGroup canvasGroup;
private void Awake()
{
// Subscribe to button click
if (dismissButton != null)
{
dismissButton.onClick.AddListener(OnDismissClicked);
}
else
{
Debug.LogError("[GameOverScreen] Dismiss button not assigned!");
}
// Get or add CanvasGroup for fade effects
if (canvasGroup == null)
{
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
// Hide by default
gameObject.SetActive(false);
}
private void OnDestroy()
{
// Unsubscribe from button
if (dismissButton != null)
{
dismissButton.onClick.RemoveListener(OnDismissClicked);
}
}
/// <summary>
/// Show the game over screen and pause the game.
/// </summary>
public void Show()
{
gameObject.SetActive(true);
// Set canvas group for interaction
if (canvasGroup != null)
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
// Pause the game (set timescale to 0)
// PauseMenu uses unscaled time for tweens, so it will still work
Time.timeScale = 0f;
Debug.Log("[GameOverScreen] Game Over - Time.timeScale set to 0");
}
/// <summary>
/// Called when dismiss button is clicked. Reloads the level.
/// </summary>
private async void OnDismissClicked()
{
Debug.Log("[GameOverScreen] Dismiss button clicked - Reloading level");
// Hide this screen first
if (canvasGroup != null)
{
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
gameObject.SetActive(false);
// Reset time scale BEFORE reloading
Time.timeScale = 1f;
// Now reload the current scene with fresh state - skipSave=true prevents re-saving cleared data
var progress = new Progress<float>(p => Logging.Debug($"Loading progress: {p * 100:F0}%"));
await SceneManagerService.Instance.ReloadCurrentScene(progress, autoHideLoadingScreen: true, skipSave: true);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5896ecc85e77484599b2f2c7ac240991
timeCreated: 1763636057

View File

@@ -0,0 +1,300 @@
using UnityEngine;
using Core;
using Core.Settings;
using AppleHillsCamera;
namespace Minigames.BirdPooper
{
/// <summary>
/// Individual obstacle behavior for Bird Pooper minigame.
/// Scrolls left at constant speed and self-destructs when reaching despawn position.
/// Uses trigger colliders for collision detection (no Rigidbody2D needed).
/// Uses EdgeAnchor for vertical positioning (Top/Middle/Bottom).
/// </summary>
[RequireComponent(typeof(Collider2D))]
[RequireComponent(typeof(EdgeAnchor))]
public class Obstacle : MonoBehaviour
{
[Header("Positioning")]
[Tooltip("Which vertical edge to anchor to (Top/Middle/Bottom)")]
[SerializeField] private EdgeAnchor.AnchorEdge verticalAnchor = EdgeAnchor.AnchorEdge.Middle;
private IBirdPooperSettings settings;
private float despawnXPosition;
private bool isInitialized;
private EdgeAnchor edgeAnchor;
/// <summary>
/// Initialize the obstacle with despawn position and EdgeAnchor references.
/// Called by ObstacleSpawner immediately after instantiation.
/// </summary>
/// <param name="despawnX">X position where obstacle should be destroyed</param>
/// <param name="referenceMarker">ScreenReferenceMarker for EdgeAnchor</param>
/// <param name="cameraAdapter">CameraScreenAdapter for EdgeAnchor</param>
public void Initialize(float despawnX, ScreenReferenceMarker referenceMarker, CameraScreenAdapter cameraAdapter)
{
despawnXPosition = despawnX;
isInitialized = true;
// Load settings
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
if (settings == null)
{
Debug.LogError("[Obstacle] BirdPooperSettings not found!");
}
// Tag all child GameObjects with colliders as "Obstacle" for trigger detection
TagChildCollidersRecursive(transform);
// Configure and update EdgeAnchor
edgeAnchor = GetComponent<EdgeAnchor>();
if (edgeAnchor != null)
{
// Assign references from spawner
edgeAnchor.referenceMarker = referenceMarker;
edgeAnchor.cameraAdapter = cameraAdapter;
// Only allow Top, Middle, or Bottom anchoring
if (verticalAnchor == EdgeAnchor.AnchorEdge.Left || verticalAnchor == EdgeAnchor.AnchorEdge.Right)
{
Debug.LogWarning("[Obstacle] Invalid anchor edge (Left/Right not supported). Defaulting to Middle.");
verticalAnchor = EdgeAnchor.AnchorEdge.Middle;
}
edgeAnchor.anchorEdge = verticalAnchor;
edgeAnchor.useReferenceMargin = false; // No custom offset
edgeAnchor.customMargin = 0f;
edgeAnchor.preserveOtherAxes = true; // Keep X position (for scrolling)
edgeAnchor.accountForObjectSize = true;
// Trigger position update
edgeAnchor.UpdatePosition();
Debug.Log($"[Obstacle] EdgeAnchor configured to {verticalAnchor} at position {transform.position}");
}
else
{
Debug.LogError("[Obstacle] EdgeAnchor component not found! Make sure the prefab has an EdgeAnchor component.");
}
Debug.Log($"[Obstacle] Initialized at position {transform.position} with despawn X: {despawnX}");
}
/// <summary>
/// Recursively tag all GameObjects with Collider2D as "Obstacle" for player collision detection.
/// </summary>
private void TagChildCollidersRecursive(Transform current)
{
// Tag this GameObject if it has a collider
Collider2D col = current.GetComponent<Collider2D>();
if (col != null && !current.CompareTag("Obstacle"))
{
current.tag = "Obstacle";
Debug.Log($"[Obstacle] Tagged '{current.name}' as Obstacle");
}
// Recurse to children
foreach (Transform child in current)
{
TagChildCollidersRecursive(child);
}
}
#if UNITY_EDITOR
/// <summary>
/// Called when values are changed in the Inspector (Editor only).
/// Updates EdgeAnchor configuration to match Obstacle settings.
/// Also finds and assigns ScreenReferenceMarker and CameraScreenAdapter for visual updates.
/// </summary>
private void OnValidate()
{
// Only run in editor, not during play mode
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
return;
EdgeAnchor anchor = GetComponent<EdgeAnchor>();
if (anchor != null)
{
// Auto-find and assign references if not set (for editor-time visual updates)
if (anchor.referenceMarker == null)
{
anchor.referenceMarker = FindAnyObjectByType<ScreenReferenceMarker>();
if (anchor.referenceMarker == null)
{
Debug.LogWarning("[Obstacle] No ScreenReferenceMarker found in scene. EdgeAnchor positioning won't work in editor.");
}
}
if (anchor.cameraAdapter == null)
{
anchor.cameraAdapter = FindAnyObjectByType<CameraScreenAdapter>();
// CameraScreenAdapter is optional - EdgeAnchor can auto-find camera
}
// Validate and set anchor edge
if (verticalAnchor == EdgeAnchor.AnchorEdge.Left || verticalAnchor == EdgeAnchor.AnchorEdge.Right)
{
Debug.LogWarning("[Obstacle] Invalid anchor edge (Left/Right not supported). Defaulting to Middle.");
verticalAnchor = EdgeAnchor.AnchorEdge.Middle;
}
// Configure EdgeAnchor to match Obstacle settings
anchor.anchorEdge = verticalAnchor;
anchor.useReferenceMargin = false;
anchor.customMargin = 0f;
anchor.preserveOtherAxes = true;
anchor.accountForObjectSize = true;
// Mark as dirty so Unity saves the changes
UnityEditor.EditorUtility.SetDirty(anchor);
}
// Tag all child GameObjects with colliders as "Obstacle" for collision detection
TagChildCollidersRecursiveEditor(transform);
}
/// <summary>
/// Editor version of recursive tagging for child colliders.
/// </summary>
private void TagChildCollidersRecursiveEditor(Transform current)
{
// Tag this GameObject if it has a collider
Collider2D col = current.GetComponent<Collider2D>();
if (col != null && !current.CompareTag("Obstacle"))
{
current.tag = "Obstacle";
UnityEditor.EditorUtility.SetDirty(current.gameObject);
}
// Recurse to children
foreach (Transform child in current)
{
TagChildCollidersRecursiveEditor(child);
}
}
#endif
private void Update()
{
if (!isInitialized || settings == null) return;
MoveLeft();
CheckBounds();
}
/// <summary>
/// Move obstacle left at constant speed (manual movement, no physics).
/// </summary>
private void MoveLeft()
{
transform.position += Vector3.left * (settings.ObstacleMoveSpeed * Time.deltaTime);
}
/// <summary>
/// Check if obstacle has passed despawn position and destroy if so.
/// </summary>
private void CheckBounds()
{
if (transform.position.x < despawnXPosition)
{
Debug.Log($"[Obstacle] Reached despawn position, destroying at X: {transform.position.x}");
Destroy(gameObject);
}
}
#if UNITY_EDITOR
/// <summary>
/// Draw debug visualization of the obstacle's anchor point.
/// Red horizontal line through custom anchor point OR bounds edge (top/bottom).
/// </summary>
private void OnDrawGizmos()
{
EdgeAnchor anchor = GetComponent<EdgeAnchor>();
if (anchor == null) return;
// Determine what Y position to visualize
float visualY;
// If using custom anchor point, draw line through it
if (anchor.customAnchorPoint != null)
{
visualY = anchor.customAnchorPoint.position.y;
}
else
{
// Get bounds and determine which edge to visualize
Bounds bounds = GetVisualBounds();
// Check which vertical anchor is configured
EdgeAnchor.AnchorEdge edge = anchor.anchorEdge;
if (edge == EdgeAnchor.AnchorEdge.Top)
{
// Show top edge of bounds
visualY = bounds.max.y;
}
else if (edge == EdgeAnchor.AnchorEdge.Bottom)
{
// Show bottom edge of bounds
visualY = bounds.min.y;
}
else // Middle
{
// Show center of bounds
visualY = bounds.center.y;
}
}
// Draw thick red horizontal line through the anchor point
Color oldColor = Gizmos.color;
Gizmos.color = Color.red;
// Draw multiple lines to make it thicker
float lineLength = 2f; // Extend 2 units on each side
Vector3 leftPoint = new Vector3(transform.position.x - lineLength, visualY, transform.position.z);
Vector3 rightPoint = new Vector3(transform.position.x + lineLength, visualY, transform.position.z);
// Draw 5 lines stacked vertically to create thickness
for (int i = -2; i <= 2; i++)
{
float offset = i * 0.02f; // Small vertical offset for thickness
Vector3 offsetLeft = leftPoint + Vector3.up * offset;
Vector3 offsetRight = rightPoint + Vector3.up * offset;
Gizmos.DrawLine(offsetLeft, offsetRight);
}
Gizmos.color = oldColor;
}
/// <summary>
/// Get bounds for visualization purposes (works in editor without initialized settings).
/// </summary>
private Bounds GetVisualBounds()
{
// Get all renderers in this object and its children
Renderer[] renderers = GetComponentsInChildren<Renderer>();
if (renderers.Length > 0)
{
Bounds bounds = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++)
{
bounds.Encapsulate(renderers[i].bounds);
}
return bounds;
}
// Fallback to collider bounds
Collider2D col = GetComponent<Collider2D>();
if (col != null)
{
return col.bounds;
}
// Default small bounds
return new Bounds(transform.position, new Vector3(0.5f, 0.5f, 0.1f));
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 759c93e488a34f8d8f7abf6d8af5c73d
timeCreated: 1763629702

View File

@@ -0,0 +1,184 @@
using UnityEngine;
using Core;
using Core.Settings;
using Core.Lifecycle;
using AppleHillsCamera;
namespace Minigames.BirdPooper
{
/// <summary>
/// Spawns obstacles at regular intervals for Bird Pooper minigame.
/// Uses Transform references for spawn and despawn positions instead of hardcoded values.
/// All obstacles are spawned at Y = 0 (prefabs should be authored accordingly).
/// </summary>
public class ObstacleSpawner : ManagedBehaviour
{
[Header("Spawn Configuration")]
[Tooltip("Transform marking where obstacles spawn (off-screen right)")]
[SerializeField] private Transform spawnPoint;
[Tooltip("Transform marking where obstacles despawn (off-screen left)")]
[SerializeField] private Transform despawnPoint;
[Header("EdgeAnchor References")]
[Tooltip("ScreenReferenceMarker to pass to spawned obstacles")]
[SerializeField] private ScreenReferenceMarker referenceMarker;
[Tooltip("CameraScreenAdapter to pass to spawned obstacles")]
[SerializeField] private CameraScreenAdapter cameraAdapter;
[Header("Obstacle Prefabs")]
[Tooltip("Array of obstacle prefabs to spawn randomly")]
[SerializeField] private GameObject[] obstaclePrefabs;
private IBirdPooperSettings settings;
private float spawnTimer;
private bool isSpawning;
internal override void OnManagedAwake()
{
base.OnManagedAwake();
// Load settings
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
if (settings == null)
{
Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found!");
return;
}
// Validate references
if (spawnPoint == null)
{
Debug.LogError("[ObstacleSpawner] Spawn Point not assigned! Please assign a Transform in the Inspector.");
}
if (despawnPoint == null)
{
Debug.LogError("[ObstacleSpawner] Despawn Point not assigned! Please assign a Transform in the Inspector.");
}
if (obstaclePrefabs == null || obstaclePrefabs.Length == 0)
{
Debug.LogError("[ObstacleSpawner] No obstacle prefabs assigned! Please assign at least one prefab in the Inspector.");
}
if (referenceMarker == null)
{
Debug.LogError("[ObstacleSpawner] ScreenReferenceMarker not assigned! Obstacles need this for EdgeAnchor positioning.");
}
if (cameraAdapter == null)
{
Debug.LogWarning("[ObstacleSpawner] CameraScreenAdapter not assigned. EdgeAnchor will attempt to auto-find camera.");
}
Debug.Log("[ObstacleSpawner] Initialized successfully");
}
private void Update()
{
if (!isSpawning || settings == null || spawnPoint == null) return;
spawnTimer += Time.deltaTime;
if (spawnTimer >= settings.ObstacleSpawnInterval)
{
SpawnObstacle();
spawnTimer = 0f;
}
}
/// <summary>
/// Spawn a random obstacle at the spawn point position (Y = 0).
/// </summary>
private void SpawnObstacle()
{
if (obstaclePrefabs == null || obstaclePrefabs.Length == 0)
{
Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs to spawn!");
return;
}
if (despawnPoint == null)
{
Debug.LogWarning("[ObstacleSpawner] Cannot spawn obstacle without despawn point reference!");
return;
}
// Select random prefab
GameObject selectedPrefab = obstaclePrefabs[Random.Range(0, obstaclePrefabs.Length)];
// Spawn at spawn point position with Y = 0
Vector3 spawnPosition = new Vector3(spawnPoint.position.x, 0f, 0f);
GameObject obstacleObj = Instantiate(selectedPrefab, spawnPosition, Quaternion.identity);
// Initialize obstacle with despawn X position and EdgeAnchor references
Obstacle obstacle = obstacleObj.GetComponent<Obstacle>();
if (obstacle != null)
{
obstacle.Initialize(despawnPoint.position.x, referenceMarker, cameraAdapter);
}
else
{
Debug.LogError($"[ObstacleSpawner] Spawned prefab '{selectedPrefab.name}' does not have Obstacle component!");
Destroy(obstacleObj);
}
Debug.Log($"[ObstacleSpawner] Spawned obstacle '{selectedPrefab.name}' at position {spawnPosition}");
}
/// <summary>
/// Start spawning obstacles.
/// Spawns the first obstacle immediately, then continues with interval-based spawning.
/// </summary>
public void StartSpawning()
{
isSpawning = true;
spawnTimer = 0f;
// Spawn the first obstacle immediately
SpawnObstacle();
Debug.Log("[ObstacleSpawner] Started spawning (first obstacle spawned immediately)");
}
/// <summary>
/// Stop spawning obstacles.
/// </summary>
public void StopSpawning()
{
isSpawning = false;
Debug.Log("[ObstacleSpawner] Stopped spawning");
}
/// <summary>
/// Check if spawner is currently active.
/// </summary>
public bool IsSpawning => isSpawning;
#if UNITY_EDITOR
/// <summary>
/// Draw gizmos in editor to visualize spawn/despawn points.
/// </summary>
private void OnDrawGizmos()
{
if (spawnPoint != null)
{
Gizmos.color = Color.green;
Gizmos.DrawLine(spawnPoint.position + Vector3.up * 10f, spawnPoint.position + Vector3.down * 10f);
Gizmos.DrawWireSphere(spawnPoint.position, 0.5f);
}
if (despawnPoint != null)
{
Gizmos.color = Color.red;
Gizmos.DrawLine(despawnPoint.position + Vector3.up * 10f, despawnPoint.position + Vector3.down * 10f);
Gizmos.DrawWireSphere(despawnPoint.position, 0.5f);
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2cfe555fe3074f75ab3c7b33bf3446e0
timeCreated: 1763629720