using AppleHills.Core; using UnityEngine; using System; using Core; namespace AppleHillsCamera { /// /// Anchors a game object at a fixed distance from a screen edge. /// [ExecuteInEditMode] // Make it run in the editor public class EdgeAnchor : MonoBehaviour { // Event that fires when the anchor's position is updated public event Action OnPositionUpdated; public enum AnchorEdge { Top, Bottom, Left, Right } [Tooltip("Reference marker that defines the screen edges and margins")] public ScreenReferenceMarker referenceMarker; [Tooltip("Camera adapter to subscribe to for runtime updates")] public CameraScreenAdapter cameraAdapter; [Tooltip("Which screen edge to anchor to")] public AnchorEdge anchorEdge = AnchorEdge.Top; [Tooltip("Whether to use the predefined margin from the reference marker")] public bool useReferenceMargin = true; [Tooltip("Custom margin to use if not using the reference margin")] public float customMargin = 1f; [Tooltip("Whether to adjust the position automatically on Start")] public bool adjustOnStart = true; [Tooltip("Whether to adjust the position when the screen size changes")] public bool adjustOnScreenResize = true; [Tooltip("Whether to preserve the object's position on other axes")] public bool preserveOtherAxes = true; [Tooltip("Whether to account for this object's size in positioning")] public bool accountForObjectSize = true; [Header("Visualization")] [Tooltip("Whether to show the anchor visualization in the editor")] public bool showVisualization = true; [Tooltip("Color for the anchor visualization line")] public Color visualizationColor = new Color(1f, 0f, 0f, 0.8f); [Tooltip("Show object bounds in visualization")] public bool showObjectBounds = true; [Tooltip("Debug mode - print position changes to console")] public bool debugMode = false; private Camera _camera; private int _lastScreenWidth; private int _lastScreenHeight; private float _lastOrthoSize = 0f; private Vector3 _originalPosition; private bool _initialized = false; private Bounds _objectBounds; #if UNITY_EDITOR private void OnDrawGizmos() { if (!showVisualization || referenceMarker == null) return; // Find camera if needed if (_camera == null) FindCamera(); if (_camera == null) return; // Calculate the anchor point (the exact point on the screen edge) Vector3 anchorPoint = CalculateScreenEdgePoint(); // Save original color Color originalColor = Gizmos.color; // Draw line to anchor point Gizmos.color = visualizationColor; Gizmos.DrawLine(transform.position, anchorPoint); // Draw a small sphere at the anchor point Gizmos.DrawSphere(anchorPoint, 0.1f); // Draw object bounds if enabled if (showObjectBounds && accountForObjectSize) { Bounds bounds = GetObjectBounds(); if (bounds.size != Vector3.zero) { Color boundsColor = visualizationColor; boundsColor.a *= 0.3f; Gizmos.color = boundsColor; Gizmos.DrawWireCube(bounds.center, bounds.size); } } // Restore original color Gizmos.color = originalColor; } #endif private void Awake() { _originalPosition = transform.position; FindCamera(); if (_camera != null) { _lastOrthoSize = _camera.orthographicSize; } _lastScreenWidth = Screen.width; _lastScreenHeight = Screen.height; _initialized = true; } private void OnEnable() { if (!_initialized) { _originalPosition = transform.position; FindCamera(); _lastScreenWidth = Screen.width; _lastScreenHeight = Screen.height; _initialized = true; } // Subscribe to camera adapter events if (Application.isPlaying && cameraAdapter != null) { cameraAdapter.OnCameraAdjusted += HandleCameraAdjusted; } // Adjust position immediately when enabled in editor #if UNITY_EDITOR if (!Application.isPlaying) { UpdatePosition(); } #endif } private void OnDisable() { // Unsubscribe from camera adapter events if (cameraAdapter != null) { cameraAdapter.OnCameraAdjusted -= HandleCameraAdjusted; } } private void HandleCameraAdjusted() { // Update position when camera is adjusted if (Application.isPlaying) { if (debugMode) { Logging.Debug($"Camera adjusted event received by {gameObject.name}, updating position"); } // Ensure we have the latest camera reference FindCamera(); UpdatePosition(); } } private void Start() { // If no camera adapter was manually set, try to find one in the scene if (cameraAdapter == null && Application.isPlaying) { FindCameraAdapter(); } // Ensure we're subscribed to camera adapter events if (cameraAdapter != null && Application.isPlaying) { cameraAdapter.OnCameraAdjusted -= HandleCameraAdjusted; // Remove any duplicate subscriptions cameraAdapter.OnCameraAdjusted += HandleCameraAdjusted; // Subscribe } if (adjustOnStart && Application.isPlaying) { UpdatePosition(); } } private void FindCameraAdapter() { // Try to find the camera adapter in the scene var adapters = FindObjectsByType(FindObjectsSortMode.None); if (adapters != null && adapters.Length > 0) { // Prioritize any adapter that's on the same camera we're using foreach (var adapter in adapters) { if (_camera != null && adapter.GetControlledCamera() == _camera) { cameraAdapter = adapter; Logging.Debug($"EdgeAnchor on {gameObject.name} auto-connected to CameraScreenAdapter on {cameraAdapter.gameObject.name}"); return; } } // If no matching camera found, use the first one cameraAdapter = adapters[0]; Logging.Debug($"EdgeAnchor on {gameObject.name} auto-connected to CameraScreenAdapter on {cameraAdapter.gameObject.name}"); } } private void Update() { bool shouldUpdate = false; // Check if we have a valid camera if (_camera == null) { FindCamera(); if (_camera != null) shouldUpdate = true; } // Check if camera's ortho size has changed if (_camera != null && _camera.orthographicSize != _lastOrthoSize) { if (debugMode) { Logging.Debug($"{gameObject.name}: Camera ortho size changed from {_lastOrthoSize} to {_camera.orthographicSize}"); } _lastOrthoSize = _camera.orthographicSize; shouldUpdate = true; } // Update if screen size has changed if (adjustOnScreenResize && (Screen.width != _lastScreenWidth || Screen.height != _lastScreenHeight)) { shouldUpdate = true; _lastScreenWidth = Screen.width; _lastScreenHeight = Screen.height; } // In editor, check for reference marker changes or inspector changes #if UNITY_EDITOR if (!Application.isPlaying) { shouldUpdate = true; } #endif // Update position if needed if (shouldUpdate) { UpdatePosition(); } } private void FindCamera() { Camera prevCamera = _camera; // First check if we have a camera adapter reference if (cameraAdapter != null) { _camera = cameraAdapter.GetControlledCamera(); if (_camera != null) { return; } } // Look for the main camera _camera = Camera.main; // If no main camera found, try to find any camera if (_camera == null) { _camera = FindAnyObjectByType(); } if (_camera == null) { Debug.LogError("EdgeAnchor: No camera found in the scene."); } else if (_camera != prevCamera && debugMode) { Logging.Debug($"{gameObject.name}: Camera reference updated to {_camera.name}"); } } /// /// Get the combined bounds of all renderers on this object and its children /// private Bounds GetObjectBounds() { Bounds bounds = new Bounds(transform.position, Vector3.zero); // Get all renderers in this object and its children Renderer[] renderers = GetComponentsInChildren(); if (renderers.Length > 0) { // Start with the first renderer's bounds bounds = renderers[0].bounds; // Expand to include all other renderers for (int i = 1; i < renderers.Length; i++) { bounds.Encapsulate(renderers[i].bounds); } } else { // No renderers found, create a small placeholder bounds bounds = new Bounds(transform.position, new Vector3(0.1f, 0.1f, 0.1f)); } // Cache the bounds _objectBounds = bounds; return bounds; } /// /// Manually trigger position adjustment based on the anchor settings. /// public void UpdatePosition() { if (referenceMarker == null) { Logging.Warning("EdgeAnchor: Missing reference marker."); return; } if (_camera == null) { FindCamera(); if (_camera == null) return; } // Get the margin value to use float margin = GetMarginValue(); // Calculate the new position based on anchor edge and object size Vector3 newPosition = CalculateAnchoredPosition(margin); // If preserving other axes, keep their original values if (preserveOtherAxes) { switch (anchorEdge) { case AnchorEdge.Top: case AnchorEdge.Bottom: newPosition.x = transform.position.x; newPosition.z = transform.position.z; break; case AnchorEdge.Left: case AnchorEdge.Right: newPosition.y = transform.position.y; newPosition.z = transform.position.z; break; } } // Apply the new position if (debugMode && Vector3.Distance(transform.position, newPosition) > 0.01f) { Logging.Debug($"{gameObject.name} position updated: {transform.position} -> {newPosition}, Camera OrthoSize: {_camera.orthographicSize}"); } transform.position = newPosition; // Notify listeners that the position has been updated OnPositionUpdated?.Invoke(); // Store the current ortho size for change detection if (_camera != null) { _lastOrthoSize = _camera.orthographicSize; } } private float GetMarginValue() { if (!useReferenceMargin) { return customMargin; } switch (anchorEdge) { case AnchorEdge.Top: return referenceMarker.topMargin; case AnchorEdge.Bottom: return referenceMarker.bottomMargin; case AnchorEdge.Left: return referenceMarker.leftMargin; case AnchorEdge.Right: return referenceMarker.rightMargin; default: return customMargin; } } private Vector3 CalculateAnchoredPosition(float margin) { if (_camera == null) return transform.position; // Always get the CURRENT camera properties to ensure we have latest values float cameraOrthoSize = _camera.orthographicSize; float screenAspect = (float)Screen.width / Screen.height; float screenHeight = cameraOrthoSize * 2f; float screenWidth = screenHeight * screenAspect; Vector3 cameraPosition = _camera.transform.position; Vector3 newPosition = transform.position; // Calculate object size offset if needed float offsetX = 0f; float offsetY = 0f; if (accountForObjectSize) { Bounds bounds = GetObjectBounds(); Vector3 extents = bounds.extents; // Half the size Vector3 centerOffset = bounds.center - transform.position; // Offset from pivot to center switch (anchorEdge) { case AnchorEdge.Top: // For top edge, offset is negative (moving down) by the top extent offsetY = -extents.y - centerOffset.y; break; case AnchorEdge.Bottom: // For bottom edge, offset is positive (moving up) by the bottom extent offsetY = extents.y - centerOffset.y; break; case AnchorEdge.Left: // For left edge, offset is positive (moving right) by the left extent offsetX = extents.x - centerOffset.x; break; case AnchorEdge.Right: // For right edge, offset is negative (moving left) by the right extent offsetX = -extents.x - centerOffset.x; break; } } switch (anchorEdge) { case AnchorEdge.Top: // Position from the top of the screen // When margin is 0, object's top edge is exactly at the top screen edge newPosition.y = cameraPosition.y + cameraOrthoSize - margin + 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 newPosition.y = cameraPosition.y - cameraOrthoSize + margin + offsetY; break; case AnchorEdge.Left: // Position from the left of the screen // When margin is 0, object's left edge is exactly at the left screen edge newPosition.x = cameraPosition.x - (screenWidth / 2f) + margin + offsetX; break; case AnchorEdge.Right: // Position from the right of the screen // When margin is 0, object's right edge is exactly at the right screen edge newPosition.x = cameraPosition.x + (screenWidth / 2f) - margin + offsetX; break; } return newPosition; } /// /// Calculates the exact point on the screen edge for visualization purposes /// private Vector3 CalculateScreenEdgePoint() { if (_camera == null) return transform.position; // Get the screen edges in world coordinates float cameraOrthoSize = _camera.orthographicSize; float screenAspect = (float)Screen.width / Screen.height; float screenHeight = cameraOrthoSize * 2f; float screenWidth = screenHeight * screenAspect; Vector3 cameraPosition = _camera.transform.position; Vector3 objectPosition = transform.position; // Calculate the point exactly on the screen edge that corresponds to the object's anchor switch (anchorEdge) { case AnchorEdge.Top: // Point on top edge with same X coordinate as the object return new Vector3( objectPosition.x, cameraPosition.y + cameraOrthoSize, objectPosition.z ); case AnchorEdge.Bottom: // Point on bottom edge with same X coordinate as the object return new Vector3( objectPosition.x, cameraPosition.y - cameraOrthoSize, objectPosition.z ); case AnchorEdge.Left: // Point on left edge with same Y coordinate as the object return new Vector3( cameraPosition.x - (screenWidth / 2f), objectPosition.y, objectPosition.z ); case AnchorEdge.Right: // Point on right edge with same Y coordinate as the object return new Vector3( cameraPosition.x + (screenWidth / 2f), objectPosition.y, objectPosition.z ); default: return objectPosition; } } } }