Files
AppleHillsProduction/Assets/Scripts/Minigames/BirdPooper/Obstacle.cs
tschesky 058af331e0 pooper_minigame (#62)
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #62
2025-11-20 15:16:57 +00:00

301 lines
12 KiB
C#

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
}
}