Poop obstacle pipeline working

This commit is contained in:
Michal Pikulski
2025-11-21 11:33:49 +01:00
parent b4b17c18ed
commit e9320c6d03
20 changed files with 1341 additions and 371 deletions

View File

@@ -56,12 +56,12 @@ namespace Minigames.BirdPooper
return;
}
// Register as override consumer to capture ALL input (except UI button)
// Register as override consumer to capture ALL input (except UI button)
// Register as default consumer (gets input if nothing else consumes it)
// This allows UI buttons to work while still flapping when tapping empty space
if (Input.InputManager.Instance != null)
{
Input.InputManager.Instance.RegisterOverrideConsumer(this);
Debug.Log("[BirdPlayerController] Registered as override input consumer");
Input.InputManager.Instance.SetDefaultConsumer(this);
Debug.Log("[BirdPlayerController] Registered as default input consumer");
}
else
{
@@ -161,12 +161,12 @@ namespace Minigames.BirdPooper
/// <summary>
/// Called when a trigger collider enters this object's trigger.
/// Used for detecting obstacles without physics interactions.
/// Used for detecting obstacles and targets without physics interactions.
/// </summary>
private void OnTriggerEnter2D(Collider2D other)
{
// Check if the colliding object is tagged as an obstacle
if (other.CompareTag("Obstacle"))
// Check if the colliding object is tagged as an obstacle or target
if (other.CompareTag("Obstacle") || other.CompareTag("Target"))
{
HandleDeath();
}

View File

@@ -16,6 +16,7 @@ namespace Minigames.BirdPooper
[Header("References")]
[SerializeField] private BirdPlayerController player;
[SerializeField] private ObstacleSpawner obstacleSpawner;
[SerializeField] private TargetSpawner targetSpawner;
[SerializeField] private GameOverScreen gameOverScreen;
[SerializeField] private GameObject poopPrefab;
@@ -47,6 +48,11 @@ namespace Minigames.BirdPooper
Debug.LogError("[BirdPooperGameManager] ObstacleSpawner reference not assigned!");
}
if (targetSpawner == null)
{
Debug.LogWarning("[BirdPooperGameManager] TargetSpawner reference not assigned! Targets will not spawn.");
}
if (gameOverScreen == null)
{
Debug.LogError("[BirdPooperGameManager] GameOverScreen reference not assigned!");
@@ -84,6 +90,13 @@ namespace Minigames.BirdPooper
obstacleSpawner.StartSpawning();
Debug.Log("[BirdPooperGameManager] Started obstacle spawning");
}
// Start target spawning
if (targetSpawner != null)
{
targetSpawner.StartSpawning();
Debug.Log("[BirdPooperGameManager] Started target spawning");
}
}
internal override void OnManagedDestroy()
@@ -120,6 +133,12 @@ namespace Minigames.BirdPooper
obstacleSpawner.StopSpawning();
}
// Stop spawning targets
if (targetSpawner != null)
{
targetSpawner.StopSpawning();
}
// Show game over screen
if (gameOverScreen != null)
{

View File

@@ -1,300 +1,28 @@
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).
/// Obstacle entity for Bird Pooper minigame.
/// Inherits scrolling, anchoring, and despawn behavior from ScrollingEntity.
/// Player dies on collision with obstacles.
/// </summary>
[RequireComponent(typeof(Collider2D))]
[RequireComponent(typeof(EdgeAnchor))]
public class Obstacle : MonoBehaviour
public class Obstacle : ScrollingEntity
{
[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.
/// Returns obstacle move speed from settings.
/// </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)
protected override float GetMoveSpeed()
{
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}");
return settings != null ? settings.ObstacleMoveSpeed : 5f;
}
/// <summary>
/// Recursively tag all GameObjects with Collider2D as "Obstacle" for player collision detection.
/// Returns "Obstacle" tag for collision detection.
/// </summary>
private void TagChildCollidersRecursive(Transform current)
protected override string GetColliderTag()
{
// 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);
}
return "Obstacle";
}
#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,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,7 @@
fileFormatVersion: 2
guid: bc9c7bac4482311439b4c2e7879f3d73
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -21,6 +21,12 @@ namespace Minigames.BirdPooper
{
base.OnManagedAwake();
// Tag as Projectile for target detection
if (!gameObject.CompareTag("Projectile"))
{
gameObject.tag = "Projectile";
}
// Load settings
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
if (settings == null)
@@ -37,16 +43,19 @@ namespace Minigames.BirdPooper
Rigidbody2D rb = GetComponent<Rigidbody2D>();
if (rb != null)
{
rb.bodyType = RigidbodyType2D.Dynamic;
rb.gravityScale = 0f; // Manual gravity
rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual control, no physics
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
}
// Verify collider is trigger (for target detection in Phase 5)
Collider2D col = GetComponent<Collider2D>();
if (col != null && !col.isTrigger)
// Find and set all colliders to trigger (we use OnTriggerEnter2D)
Collider2D[] colliders = GetComponentsInChildren<Collider2D>(true);
foreach (Collider2D col in colliders)
{
Debug.LogWarning("[PoopProjectile] Collider should be set as Trigger for target detection!");
if (!col.isTrigger)
{
col.isTrigger = true;
Debug.Log($"[PoopProjectile] Set collider '{col.name}' to trigger");
}
}
}
@@ -91,12 +100,10 @@ namespace Minigames.BirdPooper
/// <summary>
/// Trigger collision detection for targets (Phase 5).
/// TODO: Uncomment when Target.cs is implemented in Phase 5
/// Uses OnTriggerEnter2D with trigger collider.
/// </summary>
private void OnTriggerEnter2D(Collider2D other)
{
// Phase 5 integration - currently commented out
/*
if (other.CompareTag("Target"))
{
// Notify target it was hit
@@ -108,7 +115,6 @@ namespace Minigames.BirdPooper
Destroy(gameObject);
}
*/
}
}
}

View File

@@ -0,0 +1,230 @@
using UnityEngine;
using Core;
using Core.Settings;
using AppleHillsCamera;
namespace Minigames.BirdPooper
{
/// <summary>
/// Abstract base class for all scrolling entities in Bird Pooper minigame.
/// Provides common functionality: manual left-scrolling, EdgeAnchor integration, despawn detection.
/// Subclasses: Obstacle, Target
/// </summary>
[RequireComponent(typeof(Collider2D))]
[RequireComponent(typeof(EdgeAnchor))]
public abstract class ScrollingEntity : MonoBehaviour
{
[Header("Positioning")]
[Tooltip("Which vertical edge to anchor to (Top/Middle/Bottom)")]
[SerializeField] protected EdgeAnchor.AnchorEdge verticalAnchor = EdgeAnchor.AnchorEdge.Middle;
protected IBirdPooperSettings settings;
protected float despawnXPosition;
protected bool isInitialized;
protected EdgeAnchor edgeAnchor;
/// <summary>
/// Initialize the entity with despawn position and EdgeAnchor references.
/// Called by spawner immediately after instantiation.
/// </summary>
public virtual void Initialize(float despawnX, ScreenReferenceMarker referenceMarker, CameraScreenAdapter cameraAdapter)
{
despawnXPosition = despawnX;
isInitialized = true;
// Load settings
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
if (settings == null)
{
Debug.LogError($"[{GetType().Name}] BirdPooperSettings not found!");
}
// Tag all child GameObjects with colliders
TagChildCollidersRecursive(transform);
// Find and set all colliders to trigger (we use OnTriggerEnter2D)
Collider2D[] colliders = GetComponentsInChildren<Collider2D>(true);
foreach (Collider2D col in colliders)
{
if (!col.isTrigger)
{
col.isTrigger = true;
Debug.Log($"[{GetType().Name}] Set collider '{col.name}' to trigger");
}
}
// 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($"[{GetType().Name}] 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($"[{GetType().Name}] EdgeAnchor configured to {verticalAnchor} at position {transform.position}");
}
else
{
Debug.LogError($"[{GetType().Name}] EdgeAnchor component not found! Make sure the prefab has an EdgeAnchor component.");
}
Debug.Log($"[{GetType().Name}] Initialized at position {transform.position} with despawn X: {despawnX}");
}
/// <summary>
/// Recursively tag all GameObjects with Collider2D for collision detection.
/// Subclasses override GetColliderTag() to specify their tag.
/// </summary>
protected virtual void TagChildCollidersRecursive(Transform current)
{
string tagToApply = GetColliderTag();
// Tag this GameObject if it has a collider
Collider2D col = current.GetComponent<Collider2D>();
if (col != null && !current.CompareTag(tagToApply))
{
current.tag = tagToApply;
Debug.Log($"[{GetType().Name}] Tagged '{current.name}' as {tagToApply}");
}
// 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 entity settings.
/// </summary>
protected virtual 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($"[{GetType().Name}] 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($"[{GetType().Name}] Invalid anchor edge (Left/Right not supported). Defaulting to Middle.");
verticalAnchor = EdgeAnchor.AnchorEdge.Middle;
}
// Configure EdgeAnchor to match entity 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
TagChildCollidersRecursiveEditor(transform);
}
/// <summary>
/// Editor version of recursive tagging for child colliders.
/// </summary>
protected virtual void TagChildCollidersRecursiveEditor(Transform current)
{
string tagToApply = GetColliderTag();
// Tag this GameObject if it has a collider
Collider2D col = current.GetComponent<Collider2D>();
if (col != null && !current.CompareTag(tagToApply))
{
current.tag = tagToApply;
UnityEditor.EditorUtility.SetDirty(current.gameObject);
}
// Recurse to children
foreach (Transform child in current)
{
TagChildCollidersRecursiveEditor(child);
}
}
#endif
protected virtual void Update()
{
if (!isInitialized || settings == null) return;
MoveLeft();
CheckBounds();
}
/// <summary>
/// Move entity left at constant speed (manual movement, no physics).
/// Override GetMoveSpeed() to customize speed per entity type.
/// </summary>
protected virtual void MoveLeft()
{
transform.position += Vector3.left * (GetMoveSpeed() * Time.deltaTime);
}
/// <summary>
/// Check if entity has passed despawn position and destroy if so.
/// </summary>
protected virtual void CheckBounds()
{
if (transform.position.x < despawnXPosition)
{
Debug.Log($"[{GetType().Name}] Reached despawn position, destroying at X: {transform.position.x}");
Destroy(gameObject);
}
}
/// <summary>
/// Get the move speed for this entity type.
/// Subclasses override to return appropriate speed from settings.
/// </summary>
protected abstract float GetMoveSpeed();
/// <summary>
/// Get the tag to apply to colliders for this entity type.
/// Subclasses override to return "Obstacle", "Target", etc.
/// </summary>
protected abstract string GetColliderTag();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 69a3d32fc5cf4789a5e6e8dbc5f64996
timeCreated: 1763713709

View File

@@ -0,0 +1,117 @@
using UnityEngine;
using UnityEngine.Events;
using AppleHillsCamera;
namespace Minigames.BirdPooper
{
/// <summary>
/// Target entity for Bird Pooper minigame.
/// Inherits scrolling, anchoring, and despawn behavior from ScrollingEntity.
/// Can be hit by poop projectiles for scoring. Player still dies on collision.
/// </summary>
public class Target : ScrollingEntity
{
[Header("Visual Feedback")]
[SerializeField] private SpriteRenderer spriteRenderer;
[SerializeField] private Color hitColor = Color.green;
[Header("Events")]
public UnityEvent onTargetHit;
private bool isHit;
/// <summary>
/// Initialize target and set up event.
/// </summary>
public override void Initialize(float despawnX, ScreenReferenceMarker referenceMarker, CameraScreenAdapter cameraAdapter)
{
base.Initialize(despawnX, referenceMarker, cameraAdapter);
isHit = false;
// Initialize event
if (onTargetHit == null)
{
onTargetHit = new UnityEvent();
}
// Find and set all colliders to trigger (we use OnTriggerEnter2D)
Collider2D[] colliders = GetComponentsInChildren<Collider2D>(true);
foreach (Collider2D col in colliders)
{
if (!col.isTrigger)
{
col.isTrigger = true;
Debug.Log($"[Target] Set collider '{col.name}' to trigger");
}
}
}
/// <summary>
/// Override Update to stop movement when hit.
/// </summary>
protected override void Update()
{
if (isHit) return; // Don't move or check bounds if hit
base.Update();
}
/// <summary>
/// Returns target move speed from settings.
/// </summary>
protected override float GetMoveSpeed()
{
return settings != null ? settings.TargetMoveSpeed : 4f;
}
/// <summary>
/// Returns "Target" tag for collision detection.
/// </summary>
protected override string GetColliderTag()
{
return "Target";
}
/// <summary>
/// Trigger collision detection for both player and projectiles.
/// Single trigger collider handles both cases.
/// </summary>
private void OnTriggerEnter2D(Collider2D other)
{
// Check for projectile collision
if (other.CompareTag("Projectile") && !isHit)
{
OnHitByProjectile();
}
// Player collision is handled by BirdPlayerController's OnTriggerEnter2D
// (both have trigger colliders, so trigger occurs naturally)
}
/// <summary>
/// Called when target is hit by a poop projectile.
/// Shows visual feedback and notifies manager.
/// </summary>
public void OnHitByProjectile()
{
if (isHit) return;
isHit = true;
// Visual feedback
if (spriteRenderer != null)
{
spriteRenderer.color = hitColor;
}
// Notify manager
onTargetHit?.Invoke();
Debug.Log($"[Target] Hit by projectile at position {transform.position}");
// Destroy after brief delay for visual feedback
Destroy(gameObject, 0.2f);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5331a770bc634a738b82f9450441de12
timeCreated: 1763713776

View File

@@ -0,0 +1,198 @@
using UnityEngine;
using Core;
using Core.Settings;
using Core.Lifecycle;
using AppleHillsCamera;
namespace Minigames.BirdPooper
{
/// <summary>
/// Spawns targets at regular intervals for Bird Pooper minigame.
/// Uses Transform references for spawn and despawn positions.
/// All targets are spawned at Y = 0 (EdgeAnchor positions them vertically).
/// </summary>
public class TargetSpawner : ManagedBehaviour
{
[Header("Spawn Configuration")]
[Tooltip("Transform marking where targets spawn (off-screen right)")]
[SerializeField] private Transform spawnPoint;
[Tooltip("Transform marking where targets despawn (off-screen left)")]
[SerializeField] private Transform despawnPoint;
[Header("EdgeAnchor References")]
[Tooltip("ScreenReferenceMarker to pass to spawned targets")]
[SerializeField] private ScreenReferenceMarker referenceMarker;
[Tooltip("CameraScreenAdapter to pass to spawned targets")]
[SerializeField] private CameraScreenAdapter cameraAdapter;
[Header("Target Prefabs")]
[Tooltip("Array of target prefabs to spawn randomly")]
[SerializeField] private GameObject[] targetPrefabs;
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("[TargetSpawner] BirdPooperSettings not found!");
return;
}
// Validate references
if (spawnPoint == null)
{
Debug.LogError("[TargetSpawner] Spawn Point not assigned! Please assign a Transform in the Inspector.");
}
if (despawnPoint == null)
{
Debug.LogError("[TargetSpawner] Despawn Point not assigned! Please assign a Transform in the Inspector.");
}
if (targetPrefabs == null || targetPrefabs.Length == 0)
{
Debug.LogError("[TargetSpawner] No target prefabs assigned! Please assign at least one prefab in the Inspector.");
}
if (referenceMarker == null)
{
Debug.LogError("[TargetSpawner] ScreenReferenceMarker not assigned! Targets need this for EdgeAnchor positioning.");
}
if (cameraAdapter == null)
{
Debug.LogWarning("[TargetSpawner] CameraScreenAdapter not assigned. EdgeAnchor will attempt to auto-find camera.");
}
Debug.Log("[TargetSpawner] Initialized successfully");
}
private void Update()
{
if (!isSpawning || settings == null)
return;
spawnTimer += Time.deltaTime;
if (spawnTimer >= settings.TargetSpawnInterval)
{
SpawnTarget();
spawnTimer = 0f;
}
}
/// <summary>
/// Spawns a random target prefab at the spawn point.
/// Target is spawned at Y = 0, EdgeAnchor will position it vertically.
/// </summary>
private void SpawnTarget()
{
if (targetPrefabs == null || targetPrefabs.Length == 0 || spawnPoint == null)
{
Debug.LogError("[TargetSpawner] Cannot spawn target - missing prefabs or spawn point!");
return;
}
// Randomly select target prefab
GameObject prefab = targetPrefabs[Random.Range(0, targetPrefabs.Length)];
// Spawn at spawn point X, but Y = 0 (EdgeAnchor will position vertically)
Vector3 spawnPosition = new Vector3(spawnPoint.position.x, 0f, 0f);
GameObject targetObj = Instantiate(prefab, spawnPosition, Quaternion.identity);
// Initialize target
Target target = targetObj.GetComponent<Target>();
if (target != null)
{
float despawnX = despawnPoint != null ? despawnPoint.position.x : -12f;
target.Initialize(despawnX, referenceMarker, cameraAdapter);
// Subscribe to target hit event to notify manager
target.onTargetHit.AddListener(OnTargetHit);
Debug.Log($"[TargetSpawner] Spawned target at {spawnPosition}");
}
else
{
Debug.LogError($"[TargetSpawner] Spawned prefab '{prefab.name}' does not have Target component!");
Destroy(targetObj);
}
}
/// <summary>
/// Called when a target is hit by a projectile.
/// Notifies the game manager for score tracking.
/// </summary>
private void OnTargetHit()
{
// Find and notify manager
BirdPooperGameManager manager = BirdPooperGameManager.Instance;
if (manager != null)
{
manager.OnTargetHit();
}
else
{
Debug.LogWarning("[TargetSpawner] BirdPooperGameManager not found!");
}
}
/// <summary>
/// Start spawning targets at regular intervals.
/// </summary>
public void StartSpawning()
{
isSpawning = true;
spawnTimer = 0f;
Debug.Log("[TargetSpawner] Started spawning targets");
}
/// <summary>
/// Stop spawning new targets (existing targets continue).
/// </summary>
public void StopSpawning()
{
isSpawning = false;
Debug.Log("[TargetSpawner] Stopped spawning targets");
}
/// <summary>
/// Check if spawner is currently spawning.
/// </summary>
public bool IsSpawning => isSpawning;
/// <summary>
/// Draw gizmos to visualize spawn and despawn points in the editor.
/// </summary>
private void OnDrawGizmos()
{
if (spawnPoint != null)
{
Gizmos.color = Color.cyan;
Gizmos.DrawLine(
new Vector3(spawnPoint.position.x, -10f, 0f),
new Vector3(spawnPoint.position.x, 10f, 0f)
);
}
if (despawnPoint != null)
{
Gizmos.color = Color.magenta;
Gizmos.DrawLine(
new Vector3(despawnPoint.position.x, -10f, 0f),
new Vector3(despawnPoint.position.x, 10f, 0f)
);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 16beae843b5f431f9256a56aab02b53d
timeCreated: 1763713803