Working player + obstacle mvp
This commit is contained in:
@@ -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(
|
||||
|
||||
3
Assets/Scripts/Editor.meta
Normal file
3
Assets/Scripts/Editor.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12ea065dca5840acae696a40c0cd96dc
|
||||
timeCreated: 1763632462
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
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