using UnityEngine; using Core; using Core.Settings; using AppleHillsCamera; namespace Minigames.BirdPooper { /// /// 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). /// [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; /// /// Initialize the obstacle with despawn position and EdgeAnchor references. /// Called by ObstacleSpawner immediately after instantiation. /// /// X position where obstacle should be destroyed /// ScreenReferenceMarker for EdgeAnchor /// CameraScreenAdapter for EdgeAnchor public void Initialize(float despawnX, ScreenReferenceMarker referenceMarker, CameraScreenAdapter cameraAdapter) { despawnXPosition = despawnX; isInitialized = true; // Load settings settings = GameManager.GetSettingsObject(); 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(); 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}"); } /// /// Recursively tag all GameObjects with Collider2D as "Obstacle" for player collision detection. /// private void TagChildCollidersRecursive(Transform current) { // Tag this GameObject if it has a collider Collider2D col = current.GetComponent(); 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 /// /// 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. /// private void OnValidate() { // Only run in editor, not during play mode if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) return; EdgeAnchor anchor = GetComponent(); if (anchor != null) { // Auto-find and assign references if not set (for editor-time visual updates) if (anchor.referenceMarker == null) { anchor.referenceMarker = FindAnyObjectByType(); 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 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); } /// /// Editor version of recursive tagging for child colliders. /// private void TagChildCollidersRecursiveEditor(Transform current) { // Tag this GameObject if it has a collider Collider2D col = current.GetComponent(); 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(); } /// /// Move obstacle left at constant speed (manual movement, no physics). /// private void MoveLeft() { transform.position += Vector3.left * (settings.ObstacleMoveSpeed * Time.deltaTime); } /// /// Check if obstacle has passed despawn position and destroy if so. /// 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 /// /// Draw debug visualization of the obstacle's anchor point. /// Red horizontal line through custom anchor point OR bounds edge (top/bottom). /// private void OnDrawGizmos() { EdgeAnchor anchor = GetComponent(); 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; } /// /// Get bounds for visualization purposes (works in editor without initialized settings). /// private Bounds GetVisualBounds() { // Get all renderers in this object and its children Renderer[] renderers = GetComponentsInChildren(); 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(); if (col != null) { return col.bounds; } // Default small bounds return new Bounds(transform.position, new Vector3(0.5f, 0.5f, 0.1f)); } #endif } }