pooper_minigame (#62)
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #62
This commit is contained in:
3
Assets/Scripts/Minigames/BirdPooper.meta
Normal file
3
Assets/Scripts/Minigames/BirdPooper.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa5389b421944c6e8f43d7fad84ef470
|
||||
timeCreated: 1763596581
|
||||
69
Assets/Scripts/Minigames/BirdPooper/BirdFlapAnimator.cs
Normal file
69
Assets/Scripts/Minigames/BirdPooper/BirdFlapAnimator.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
{
|
||||
[SerializeField] private Animator animator;
|
||||
[Tooltip("Name of the trigger parameter in the Animator")]
|
||||
[SerializeField] private string flapTriggerName = "Flap";
|
||||
|
||||
private BirdPlayerController birdController;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// Auto-assign animator component if not set
|
||||
if (animator == null)
|
||||
{
|
||||
animator = GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
{
|
||||
Debug.LogError("[BirdFlapAnimator] No Animator component found! Please add an Animator component or assign one in the Inspector.");
|
||||
}
|
||||
}
|
||||
|
||||
// Find parent BirdPlayerController
|
||||
birdController = GetComponentInParent<BirdPlayerController>();
|
||||
if (birdController == null)
|
||||
{
|
||||
Debug.LogError("[BirdFlapAnimator] No BirdPlayerController found in parent! This component must be a child of the bird GameObject.");
|
||||
}
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
// Subscribe to flap event
|
||||
if (birdController != null && birdController.OnFlap != null)
|
||||
{
|
||||
birdController.OnFlap.AddListener(OnBirdFlapped);
|
||||
Debug.Log("[BirdFlapAnimator] Subscribed to OnFlap event");
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
// Unsubscribe from flap event
|
||||
if (birdController != null && birdController.OnFlap != null)
|
||||
{
|
||||
birdController.OnFlap.RemoveListener(OnBirdFlapped);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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] Triggered animation: {flapTriggerName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ba8e12a2b8b4d468ccfa995ed56980c
|
||||
timeCreated: 1763600531
|
||||
197
Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs
Normal file
197
Assets/Scripts/Minigames/BirdPooper/BirdPlayerController.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using Core.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// Bird player controller with Flappy Bird-style flight mechanics.
|
||||
/// Responds to tap input to flap, with manual gravity simulation.
|
||||
/// </summary>
|
||||
public class BirdPlayerController : ManagedBehaviour, ITouchInputConsumer
|
||||
{
|
||||
[Header("Events")]
|
||||
public UnityEngine.Events.UnityEvent OnFlap;
|
||||
public UnityEngine.Events.UnityEvent OnPlayerDamaged;
|
||||
|
||||
private Rigidbody2D rb;
|
||||
private IBirdPooperSettings settings;
|
||||
private float verticalVelocity = 0f;
|
||||
private bool isDead = false;
|
||||
private float fixedXPosition; // Store the initial X position from the scene
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Initialize events
|
||||
if (OnFlap == null)
|
||||
OnFlap = new UnityEngine.Events.UnityEvent();
|
||||
if (OnPlayerDamaged == null)
|
||||
OnPlayerDamaged = new UnityEngine.Events.UnityEvent();
|
||||
|
||||
// Load settings
|
||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Rigidbody2D component (Dynamic with gravityScale = 0)
|
||||
rb = GetComponent<Rigidbody2D>();
|
||||
if (rb != null)
|
||||
{
|
||||
rb.gravityScale = 0f; // Disable Unity physics gravity
|
||||
rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces
|
||||
|
||||
// Store the initial X position from the scene
|
||||
fixedXPosition = rb.position.x;
|
||||
Debug.Log($"[BirdPlayerController] Fixed X position set to: {fixedXPosition}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] Rigidbody2D component not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Register as override consumer to capture ALL input (except UI button)
|
||||
// Register as override consumer to capture ALL input (except UI button)
|
||||
if (Input.InputManager.Instance != null)
|
||||
{
|
||||
Input.InputManager.Instance.RegisterOverrideConsumer(this);
|
||||
Debug.Log("[BirdPlayerController] Registered as override input consumer");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] InputManager instance not found!");
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isDead && settings != null && rb != null)
|
||||
{
|
||||
// Apply manual gravity
|
||||
verticalVelocity -= settings.Gravity * Time.deltaTime;
|
||||
|
||||
// Cap fall speed (terminal velocity)
|
||||
if (verticalVelocity < -settings.MaxFallSpeed)
|
||||
verticalVelocity = -settings.MaxFallSpeed;
|
||||
|
||||
// Update position manually
|
||||
Vector2 newPosition = rb.position;
|
||||
newPosition.y += verticalVelocity * Time.deltaTime;
|
||||
newPosition.x = fixedXPosition; // Keep X fixed at scene-configured position
|
||||
|
||||
// Clamp Y position to bounds
|
||||
newPosition.y = Mathf.Clamp(newPosition.y, settings.MinY, settings.MaxY);
|
||||
|
||||
rb.MovePosition(newPosition);
|
||||
|
||||
// Update rotation based on velocity
|
||||
UpdateRotation();
|
||||
}
|
||||
}
|
||||
|
||||
#region ITouchInputConsumer Implementation
|
||||
|
||||
public void OnTap(Vector2 tapPosition)
|
||||
{
|
||||
if (!isDead && settings != null)
|
||||
{
|
||||
verticalVelocity = settings.FlapForce;
|
||||
Debug.Log($"[BirdPlayerController] Flap! velocity = {verticalVelocity}");
|
||||
|
||||
// Emit flap event
|
||||
OnFlap?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnHoldStart(Vector2 position) { }
|
||||
public void OnHoldMove(Vector2 position) { }
|
||||
public void OnHoldEnd(Vector2 position) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rotation
|
||||
|
||||
/// <summary>
|
||||
/// Updates the bird's rotation based on vertical velocity.
|
||||
/// Bird tilts up when flapping, down when falling.
|
||||
/// </summary>
|
||||
private void UpdateRotation()
|
||||
{
|
||||
if (settings == null) return;
|
||||
|
||||
// Map velocity to rotation angle
|
||||
// When falling at max speed (-MaxFallSpeed): -MaxRotationAngle (down)
|
||||
// When at flap velocity (+FlapForce): +MaxRotationAngle (up)
|
||||
float velocityPercent = Mathf.InverseLerp(
|
||||
-settings.MaxFallSpeed,
|
||||
settings.FlapForce,
|
||||
verticalVelocity
|
||||
);
|
||||
float targetAngle = Mathf.Lerp(
|
||||
-settings.MaxRotationAngle,
|
||||
settings.MaxRotationAngle,
|
||||
velocityPercent
|
||||
);
|
||||
|
||||
// Get current angle (handle 0-360 wrapping to -180-180)
|
||||
float currentAngle = transform.rotation.eulerAngles.z;
|
||||
if (currentAngle > 180f)
|
||||
currentAngle -= 360f;
|
||||
|
||||
// Smooth interpolation to target
|
||||
float smoothedAngle = Mathf.Lerp(
|
||||
currentAngle,
|
||||
targetAngle,
|
||||
settings.RotationSpeed * Time.deltaTime
|
||||
);
|
||||
|
||||
// Apply rotation to Z axis only (2D rotation)
|
||||
transform.rotation = Quaternion.Euler(0, 0, smoothedAngle);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Trigger-Based Collision Detection
|
||||
|
||||
/// <summary>
|
||||
/// Called when a trigger collider enters this object's trigger.
|
||||
/// Used for detecting obstacles without physics interactions.
|
||||
/// </summary>
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
// Check if the colliding object is tagged as an obstacle
|
||||
if (other.CompareTag("Obstacle"))
|
||||
{
|
||||
HandleDeath();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
#region Public Properties
|
||||
|
||||
public bool IsDead => isDead;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d56e44aad9044f8808b892c7a5cfc50
|
||||
timeCreated: 1763596581
|
||||
118
Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs
Normal file
118
Assets/Scripts/Minigames/BirdPooper/BirdPooperGameManager.cs
Normal 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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8cb19f77b49b4e299fac404c56e0455a
|
||||
timeCreated: 1763636566
|
||||
101
Assets/Scripts/Minigames/BirdPooper/GameOverScreen.cs
Normal file
101
Assets/Scripts/Minigames/BirdPooper/GameOverScreen.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5896ecc85e77484599b2f2c7ac240991
|
||||
timeCreated: 1763636057
|
||||
300
Assets/Scripts/Minigames/BirdPooper/Obstacle.cs
Normal file
300
Assets/Scripts/Minigames/BirdPooper/Obstacle.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Minigames/BirdPooper/Obstacle.cs.meta
Normal file
3
Assets/Scripts/Minigames/BirdPooper/Obstacle.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 759c93e488a34f8d8f7abf6d8af5c73d
|
||||
timeCreated: 1763629702
|
||||
184
Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs
Normal file
184
Assets/Scripts/Minigames/BirdPooper/ObstacleSpawner.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cfe555fe3074f75ab3c7b33bf3446e0
|
||||
timeCreated: 1763629720
|
||||
Reference in New Issue
Block a user