Compare commits

...

2 Commits

Author SHA1 Message Date
Michal Pikulski
0b966e9976 Working screen anchoring 2025-10-13 16:31:52 +02:00
Michal Pikulski
a55ba5e29c First steps with camera framing 2025-10-13 15:34:07 +02:00
12 changed files with 1261 additions and 134 deletions

View File

@@ -342,17 +342,17 @@ LineRenderer:
m_SortingLayer: 0
m_SortingOrder: 0
m_Positions:
- {x: -0.15602553, y: 3.8573277, z: 0}
- {x: -0.1566351, y: 3.753693, z: 0}
- {x: -0.1572447, y: 3.6518767, z: 0}
- {x: -0.15785426, y: 3.5518785, z: 0}
- {x: -0.15846384, y: 3.453699, z: 0}
- {x: -0.15907341, y: 3.3573375, z: 0}
- {x: -0.15968299, y: 3.2627945, z: 0}
- {x: -0.16029257, y: 3.1700697, z: 0}
- {x: -0.16090216, y: 3.0791638, z: 0}
- {x: -0.16151173, y: 2.9900756, z: 0}
- {x: -0.16212131, y: 2.9028063, z: 0}
- {x: -0.15602553, y: 4.0749445, z: 0}
- {x: -0.1566351, y: 3.9736378, z: 0}
- {x: -0.1572447, y: 3.8729858, z: 0}
- {x: -0.15785426, y: 3.7729874, z: 0}
- {x: -0.15846384, y: 3.6736438, z: 0}
- {x: -0.15907341, y: 3.5749543, z: 0}
- {x: -0.15968299, y: 3.4769194, z: 0}
- {x: -0.16029257, y: 3.3795385, z: 0}
- {x: -0.16090216, y: 3.2828126, z: 0}
- {x: -0.16151173, y: 3.1867406, z: 0}
- {x: -0.16212131, y: 3.0913231, z: 0}
m_Parameters:
serializedVersion: 3
widthMultiplier: 1
@@ -454,6 +454,7 @@ GameObject:
- component: {fileID: 224729333}
- component: {fileID: 224729332}
- component: {fileID: 224729334}
- component: {fileID: 224729335}
m_Layer: 0
m_Name: CinemachineCamera
m_TagString: Untagged
@@ -541,6 +542,21 @@ Animator:
m_AllowConstantClipSamplingOptimization: 1
m_KeepAnimatorStateOnDisable: 0
m_WriteDefaultValuesOnDisable: 0
--- !u!114 &224729335
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 224729330}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8a71a21143bd4f4992d08829084d1e3b, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::AppleHillsCamera.CameraScreenAdapter
referenceMarker: {fileID: 1651034645}
adjustOnStart: 1
adjustOnScreenResize: 1
--- !u!1 &323864663
GameObject:
m_ObjectHideFlags: 0
@@ -882,6 +898,7 @@ GameObject:
- component: {fileID: 747976404}
- component: {fileID: 747976405}
- component: {fileID: 747976406}
- component: {fileID: 747976407}
m_Layer: 0
m_Name: BottleMarine
m_TagString: Player
@@ -898,7 +915,7 @@ Transform:
m_GameObject: {fileID: 747976396}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 2.9799, z: 0}
m_LocalPosition: {x: 0, y: 3.1975174, z: 0}
m_LocalScale: {x: 0.57574, y: 0.57574, z: 0.57574}
m_ConstrainProportionsScale: 0
m_Children:
@@ -918,6 +935,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d39dbaae819c4a128a11ca60fbbc98c9, type: 3}
m_Name:
m_EditorClassIdentifier:
edgeAnchor: {fileID: 747976407}
--- !u!114 &747976399
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -1089,6 +1107,31 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::Minigames.DivingForPictures.Utilities.BottlePauser
wobbleReference: {fileID: 747976399}
--- !u!114 &747976407
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 747976396}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ed380d10e1e04ae7990e5c726c929063, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::AppleHillsCamera.EdgeAnchor
referenceMarker: {fileID: 1651034645}
cameraAdapter: {fileID: 224729335}
anchorEdge: 0
useReferenceMargin: 0
customMargin: 2
adjustOnStart: 1
adjustOnScreenResize: 0
preserveOtherAxes: 1
accountForObjectSize: 1
showVisualization: 1
visualizationColor: {r: 1, g: 0, b: 1, a: 1}
showObjectBounds: 1
debugMode: 0
--- !u!1 &824396214
GameObject:
m_ObjectHideFlags: 0
@@ -1324,17 +1367,17 @@ LineRenderer:
m_SortingLayer: 0
m_SortingOrder: 0
m_Positions:
- {x: -0.15602553, y: 3.8573277, z: 0}
- {x: -0.11662118, y: 3.66006, z: 0}
- {x: -0.07721684, y: 3.4853156, z: 0}
- {x: -0.03781248, y: 3.3330944, z: 0}
- {x: 0.0015918687, y: 3.2033968, z: 0}
- {x: 0.040996216, y: 3.0962222, z: 0}
- {x: 0.08040057, y: 3.011571, z: 0}
- {x: 0.11980491, y: 2.9494433, z: 0}
- {x: 0.15920927, y: 2.9098392, z: 0}
- {x: 0.1986136, y: 2.892758, z: 0}
- {x: 0.23801796, y: 2.8982003, z: 0}
- {x: -0.15602553, y: 4.074945, z: 0}
- {x: -0.11662118, y: 3.8796225, z: 0}
- {x: -0.07721684, y: 3.7057445, z: 0}
- {x: -0.03781248, y: 3.5533106, z: 0}
- {x: 0.0015918687, y: 3.4223216, z: 0}
- {x: 0.040996216, y: 3.3127766, z: 0}
- {x: 0.08040057, y: 3.2246757, z: 0}
- {x: 0.11980491, y: 3.1580195, z: 0}
- {x: 0.15920927, y: 3.1128078, z: 0}
- {x: 0.1986136, y: 3.0890403, z: 0}
- {x: 0.23801796, y: 3.0867171, z: 0}
m_Parameters:
serializedVersion: 3
widthMultiplier: 1
@@ -1867,17 +1910,17 @@ LineRenderer:
m_SortingLayer: 0
m_SortingOrder: 0
m_Positions:
- {x: -0.15602553, y: 3.8573277, z: 0}
- {x: -0.18956745, y: 3.6568341, z: 0}
- {x: -0.22310936, y: 3.479415, z: 0}
- {x: -0.25665125, y: 3.3250687, z: 0}
- {x: -0.29019317, y: 3.193797, z: 0}
- {x: -0.32373506, y: 3.0855987, z: 0}
- {x: -0.35727698, y: 3.0004745, z: 0}
- {x: -0.39081886, y: 2.938424, z: 0}
- {x: -0.4243608, y: 2.8994474, z: 0}
- {x: -0.45790267, y: 2.8835444, z: 0}
- {x: -0.4914446, y: 2.8907156, z: 0}
- {x: -0.15602553, y: 4.074945, z: 0}
- {x: -0.18956745, y: 3.8764977, z: 0}
- {x: -0.22310936, y: 3.7000232, z: 0}
- {x: -0.25665125, y: 3.5455205, z: 0}
- {x: -0.29019317, y: 3.4129908, z: 0}
- {x: -0.32373506, y: 3.3024333, z: 0}
- {x: -0.35727698, y: 3.213848, z: 0}
- {x: -0.39081886, y: 3.1472356, z: 0}
- {x: -0.4243608, y: 3.1025953, z: 0}
- {x: -0.45790267, y: 3.0799277, z: 0}
- {x: -0.4914446, y: 3.0792325, z: 0}
m_Parameters:
serializedVersion: 3
widthMultiplier: 1
@@ -1968,6 +2011,59 @@ MonoBehaviour:
ropeDamping: 0.3
initialSeparationDistance: 0.1
initialFallImpulse: 2
--- !u!1 &1651034644
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1651034646}
- component: {fileID: 1651034645}
m_Layer: 0
m_Name: ScreenReferenceMarker
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1651034645
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1651034644}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3058fe4801134fea916ad685f924668f, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::AppleHillsCamera.ScreenReferenceMarker
targetWidth: 6.2
topMargin: 0.2
bottomMargin: 0.2
leftMargin: 0.2
rightMargin: 0.2
gizmoColor: {r: 0, g: 1, b: 0, a: 0.5}
screenEdgeColor: {r: 1, g: 1, b: 0, a: 0.3}
showVerticalMargins: 1
showHorizontalMargins: 1
--- !u!4 &1651034646
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1651034644}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1679185997
GameObject:
m_ObjectHideFlags: 0
@@ -2031,6 +2127,7 @@ MonoBehaviour:
- {fileID: 2956826569642009690, guid: b3501bc08c6afbf4f868bc7b6c2a2d92, type: 3}
- {fileID: 2956826569642009690, guid: 4f9c8bd3fb844484492a54a2998b9b3b, type: 3}
- {fileID: 2956826569642009690, guid: 16ad8a46d2c7c534982f4ee943e06f74, type: 3}
initialTilePrefab: {fileID: 0}
onTileSpawned:
m_PersistentCalls:
m_Calls: []
@@ -2040,6 +2137,22 @@ MonoBehaviour:
onLastTileLeft:
m_PersistentCalls:
m_Calls: []
depthDifficultyRanges:
- minDepth: 0
maxDepth: 10
difficulty: 1
- minDepth: 11
maxDepth: 20
difficulty: 2
- minDepth: 21
maxDepth: 30
difficulty: 3
- minDepth: 31
maxDepth: 40
difficulty: 4
- minDepth: 41
maxDepth: 2147483647
difficulty: 5
--- !u!1 &1787733334
GameObject:
m_ObjectHideFlags: 0
@@ -2337,7 +2450,7 @@ Transform:
m_GameObject: {fileID: 2106431001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -0.165, y: 2.509, z: 0}
m_LocalPosition: {x: -0.165, y: 2.697517, z: 0}
m_LocalScale: {x: 0.57574, y: 0.57574, z: 0.57574}
m_ConstrainProportionsScale: 0
m_Children:
@@ -2366,7 +2479,7 @@ MonoBehaviour:
bottleWobble: {fileID: 747976399}
followStiffness: 4
useWobbleOffset: 1
baseY: 2.5
verticalDistance: 0.5
--- !u!114 &2106431004
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2410,3 +2523,4 @@ SceneRoots:
- {fileID: 1679185998}
- {fileID: 323864665}
- {fileID: 424805726}
- {fileID: 1651034646}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 95f834c9cbc34cff9490c5582d66e463
timeCreated: 1760359480

View File

@@ -0,0 +1,127 @@
using UnityEngine;
using Unity.Cinemachine;
using System;
namespace AppleHillsCamera
{
/// <summary>
/// Adjusts the camera's orthographic size to match the target width from a ScreenReferenceMarker.
/// Works with both regular cameras and Cinemachine virtual cameras.
/// </summary>
public class CameraScreenAdapter : MonoBehaviour
{
[Tooltip("Reference that defines the target width to match")]
public ScreenReferenceMarker referenceMarker;
[Tooltip("Whether to adjust the camera automatically on Start")]
public bool adjustOnStart = true;
[Tooltip("Whether to adjust the camera automatically when the screen size changes")]
public bool adjustOnScreenResize = true;
// Event that other components can subscribe to when camera is adjusted
public event Action OnCameraAdjusted;
private Camera _regularCamera;
private CinemachineCamera _virtualCamera;
private int _lastScreenWidth;
private int _lastScreenHeight;
private bool _usingCinemachine;
private void Awake()
{
// Try to get regular camera first
_regularCamera = GetComponent<Camera>();
// Try to get Cinemachine camera if no regular camera or if both exist
_virtualCamera = GetComponent<CinemachineCamera>();
// Determine which camera type we're using
_usingCinemachine = _virtualCamera != null;
_lastScreenWidth = Screen.width;
_lastScreenHeight = Screen.height;
if (!_usingCinemachine && _regularCamera == null)
{
Debug.LogError("CameraScreenAdapter: No camera component found. Add this script to a GameObject with either Camera or CinemachineCamera component.");
enabled = false;
return;
}
}
private void Start()
{
if (adjustOnStart)
{
AdjustCamera();
}
}
private void Update()
{
if (adjustOnScreenResize &&
(Screen.width != _lastScreenWidth || Screen.height != _lastScreenHeight))
{
AdjustCamera();
_lastScreenWidth = Screen.width;
_lastScreenHeight = Screen.height;
}
}
/// <summary>
/// Manually trigger camera adjustment to match the reference width.
/// </summary>
public void AdjustCamera()
{
if (referenceMarker == null)
{
Debug.LogWarning("CameraScreenAdapter: Missing reference marker.");
return;
}
// Calculate the orthographic size based on the target width and screen aspect ratio
float targetWidth = referenceMarker.targetWidth;
float screenAspect = (float)Screen.height / Screen.width;
// Orthographic size is half the height, so we calculate:
// orthoSize = (targetWidth / 2) * (screenHeight / screenWidth)
float orthoSize = (targetWidth / 2f) * screenAspect;
// Apply the calculated size to the camera
if (_usingCinemachine)
{
// Apply to Cinemachine virtual camera
var lens = _virtualCamera.Lens;
lens.OrthographicSize = orthoSize;
_virtualCamera.Lens = lens;
Debug.Log($"Cinemachine Camera adapted: Width={targetWidth}, Aspect={screenAspect:F2}, OrthoSize={orthoSize:F2}");
}
else
{
// Apply to regular camera
if (_regularCamera.orthographic)
{
_regularCamera.orthographicSize = orthoSize;
Debug.Log($"Camera adapted: Width={targetWidth}, Aspect={screenAspect:F2}, OrthoSize={orthoSize:F2}");
}
else
{
Debug.LogWarning("CameraScreenAdapter: Regular camera is not in orthographic mode.");
return;
}
}
// Notify subscribers that the camera has been adjusted
OnCameraAdjusted?.Invoke();
}
/// <summary>
/// Gets the camera component being controlled by this adapter
/// </summary>
public Camera GetControlledCamera()
{
return _usingCinemachine ? _virtualCamera.GetComponent<Camera>() : _regularCamera;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8a71a21143bd4f4992d08829084d1e3b
timeCreated: 1760359498

View File

@@ -0,0 +1,549 @@
using AppleHills.Core;
using UnityEngine;
using System;
namespace AppleHillsCamera
{
/// <summary>
/// Anchors a game object at a fixed distance from a screen edge.
/// </summary>
[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)
{
Debug.Log($"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 = FindObjectsOfType<CameraScreenAdapter>();
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;
Debug.Log($"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];
Debug.Log($"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)
{
Debug.Log($"{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<Camera>();
}
if (_camera == null)
{
Debug.LogError("EdgeAnchor: No camera found in the scene.");
}
else if (_camera != prevCamera && debugMode)
{
Debug.Log($"{gameObject.name}: Camera reference updated to {_camera.name}");
}
}
/// <summary>
/// Get the combined bounds of all renderers on this object and its children
/// </summary>
private Bounds GetObjectBounds()
{
Bounds bounds = new Bounds(transform.position, Vector3.zero);
// Get all renderers in this object and its children
Renderer[] renderers = GetComponentsInChildren<Renderer>();
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;
}
/// <summary>
/// Manually trigger position adjustment based on the anchor settings.
/// </summary>
public void UpdatePosition()
{
if (referenceMarker == null)
{
Debug.LogWarning("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)
{
Debug.Log($"{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;
}
/// <summary>
/// Calculates the exact point on the screen edge for visualization purposes
/// </summary>
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;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ed380d10e1e04ae7990e5c726c929063
timeCreated: 1760359522

View File

@@ -0,0 +1,142 @@
using UnityEngine;
namespace AppleHillsCamera
{
/// <summary>
/// Defines reference sizes and distances for screen adaptation.
/// Used by CameraScreenAdapter and EdgeAnchor components.
/// </summary>
public class ScreenReferenceMarker : MonoBehaviour
{
[Header("Horizontal Reference")]
[Tooltip("The target width that should match the screen width")]
public float targetWidth = 10f;
[Header("Vertical References")]
[Tooltip("Distance from top of screen to use for anchoring")]
public float topMargin = 1f;
[Tooltip("Distance from bottom of screen to use for anchoring")]
public float bottomMargin = 1f;
[Tooltip("Distance from left of screen to use for anchoring")]
public float leftMargin = 1f;
[Tooltip("Distance from right of screen to use for anchoring")]
public float rightMargin = 1f;
[Header("Visualization")]
[Tooltip("Color to use for gizmo visualization")]
public Color gizmoColor = new Color(0f, 1f, 0f, 0.5f);
[Tooltip("Color to use for screen edge visualization")]
public Color screenEdgeColor = new Color(1f, 1f, 0f, 0.3f);
[Tooltip("Show the vertical margins in scene view")]
public bool showVerticalMargins = true;
[Tooltip("Show the horizontal margins in scene view")]
public bool showHorizontalMargins = true;
#if UNITY_EDITOR
private void OnDrawGizmos()
{
// Save original color
Color originalColor = Gizmos.color;
// Set the color for our gizmos
Gizmos.color = gizmoColor;
Vector3 position = transform.position;
// Draw the width reference
Vector3 left = position + Vector3.left * (targetWidth / 2f);
Vector3 right = position + Vector3.right * (targetWidth / 2f);
Gizmos.DrawLine(left, right);
// Draw vertical endpoints
float endCapSize = 0.5f;
Gizmos.DrawLine(left, left + Vector3.up * endCapSize);
Gizmos.DrawLine(left, left + Vector3.down * endCapSize);
Gizmos.DrawLine(right, right + Vector3.up * endCapSize);
Gizmos.DrawLine(right, right + Vector3.down * endCapSize);
// Calculate visual screen edges based on actual camera viewport
float halfWidth = targetWidth / 2f;
float halfHeight;
// Try to get camera references in the preferred order
// 1. Try to find the main camera in the scene (highest priority)
Camera mainCamera = Camera.main;
if (mainCamera != null && mainCamera.orthographic)
{
// Use the main camera's actual orthographic size for the height
halfHeight = mainCamera.orthographicSize;
}
else
{
// 2. Use Game/Simulator window resolution
float gameViewAspect = (float)Screen.height / Screen.width;
halfHeight = halfWidth * gameViewAspect;
// 3. Fallback to the scene view camera if needed
UnityEditor.SceneView sceneView = UnityEditor.SceneView.lastActiveSceneView;
if (sceneView != null && sceneView.camera != null && sceneView.camera.orthographic)
{
// Use the scene view camera's aspect ratio instead
float sceneAspect = sceneView.camera.pixelHeight / (float)sceneView.camera.pixelWidth;
halfHeight = halfWidth * sceneAspect;
}
}
// Screen edge positions
Vector3 topEdge = position + Vector3.up * halfHeight;
Vector3 bottomEdge = position + Vector3.down * halfHeight;
Vector3 leftEdge = position + Vector3.left * halfWidth;
Vector3 rightEdge = position + Vector3.right * halfWidth;
// Draw screen edges with yellow color
Gizmos.color = screenEdgeColor;
// Draw full screen rectangle
Gizmos.DrawLine(leftEdge + Vector3.up * halfHeight, rightEdge + Vector3.up * halfHeight); // Top edge
Gizmos.DrawLine(leftEdge + Vector3.down * halfHeight, rightEdge + Vector3.down * halfHeight); // Bottom edge
Gizmos.DrawLine(leftEdge + Vector3.up * halfHeight, leftEdge + Vector3.down * halfHeight); // Left edge
Gizmos.DrawLine(rightEdge + Vector3.up * halfHeight, rightEdge + Vector3.down * halfHeight); // Right edge
// Draw margin references if enabled
Gizmos.color = gizmoColor;
if (showVerticalMargins)
{
// Top margin (distance from top edge)
Gizmos.DrawLine(
topEdge + Vector3.down * topMargin + Vector3.left * (targetWidth / 4f),
topEdge + Vector3.down * topMargin + Vector3.right * (targetWidth / 4f));
// Bottom margin (distance from bottom edge)
Gizmos.DrawLine(
bottomEdge + Vector3.up * bottomMargin + Vector3.left * (targetWidth / 4f),
bottomEdge + Vector3.up * bottomMargin + Vector3.right * (targetWidth / 4f));
}
if (showHorizontalMargins)
{
// Left margin (distance from left edge)
Gizmos.DrawLine(
leftEdge + Vector3.right * leftMargin + Vector3.up * (halfHeight / 2f),
leftEdge + Vector3.right * leftMargin + Vector3.down * (halfHeight / 2f));
// Right margin (distance from right edge)
Gizmos.DrawLine(
rightEdge + Vector3.left * rightMargin + Vector3.up * (halfHeight / 2f),
rightEdge + Vector3.left * rightMargin + Vector3.down * (halfHeight / 2f));
}
// Restore original color
Gizmos.color = originalColor;
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3058fe4801134fea916ad685f924668f
timeCreated: 1760359480

View File

@@ -1,123 +1,128 @@
using System;
using AppleHills.Core.Settings;
using Core;
using Input;
using Interactions;
using UnityEngine;
using AppleHills.Core.Settings;
using Core; // Added for IInteractionSettings
/// <summary>
/// Handles level switching when interacted with. Applies switch data and triggers scene transitions.
/// </summary>
public class LevelSwitch : MonoBehaviour
// Added for IInteractionSettings
namespace LevelS
{
/// <summary>
/// Data for this level switch (target scene, icon, etc).
/// Handles level switching when interacted with. Applies switch data and triggers scene transitions.
/// </summary>
public LevelSwitchData switchData;
private SpriteRenderer _iconRenderer;
private Interactable _interactable;
// Settings reference
private IInteractionSettings _interactionSettings;
private bool _isActive = true;
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
void Awake()
public class LevelSwitch : MonoBehaviour
{
_isActive = true;
if (_iconRenderer == null)
_iconRenderer = GetComponent<SpriteRenderer>();
_interactable = GetComponent<Interactable>();
if (_interactable != null)
/// <summary>
/// Data for this level switch (target scene, icon, etc).
/// </summary>
public LevelSwitchData switchData;
private SpriteRenderer _iconRenderer;
private Interactable _interactable;
// Settings reference
private IInteractionSettings _interactionSettings;
private bool _isActive = true;
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
void Awake()
{
_interactable.characterArrived.AddListener(OnCharacterArrived);
_isActive = true;
if (_iconRenderer == null)
_iconRenderer = GetComponent<SpriteRenderer>();
_interactable = GetComponent<Interactable>();
if (_interactable != null)
{
_interactable.characterArrived.AddListener(OnCharacterArrived);
}
// Initialize settings reference
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
ApplySwitchData();
}
// Initialize settings reference
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
ApplySwitchData();
}
/// <summary>
/// Unity OnDestroy callback. Cleans up event handlers.
/// </summary>
void OnDestroy()
{
if (_interactable != null)
/// <summary>
/// Unity OnDestroy callback. Cleans up event handlers.
/// </summary>
void OnDestroy()
{
_interactable.characterArrived.RemoveListener(OnCharacterArrived);
if (_interactable != null)
{
_interactable.characterArrived.RemoveListener(OnCharacterArrived);
}
}
}
#if UNITY_EDITOR
/// <summary>
/// Unity OnValidate callback. Ensures icon and data are up to date in editor.
/// </summary>
void OnValidate()
{
if (_iconRenderer == null)
_iconRenderer = GetComponent<SpriteRenderer>();
ApplySwitchData();
}
/// <summary>
/// Unity OnValidate callback. Ensures icon and data are up to date in editor.
/// </summary>
void OnValidate()
{
if (_iconRenderer == null)
_iconRenderer = GetComponent<SpriteRenderer>();
ApplySwitchData();
}
#endif
/// <summary>
/// Applies the switch data to the level switch (icon, name, etc).
/// </summary>
public void ApplySwitchData()
{
if (switchData != null)
/// <summary>
/// Applies the switch data to the level switch (icon, name, etc).
/// </summary>
public void ApplySwitchData()
{
if (_iconRenderer != null)
_iconRenderer.sprite = switchData.mapSprite;
gameObject.name = switchData.targetLevelSceneName;
// Optionally update other fields, e.g. description
if (switchData != null)
{
if (_iconRenderer != null)
_iconRenderer.sprite = switchData.mapSprite;
gameObject.name = switchData.targetLevelSceneName;
// Optionally update other fields, e.g. description
}
}
}
/// <summary>
/// Handles the start of an interaction (shows confirmation menu and switches the level if confirmed).
/// </summary>
private void OnCharacterArrived()
{
if (switchData == null || string.IsNullOrEmpty(switchData.targetLevelSceneName) || !_isActive)
return;
var menuPrefab = _interactionSettings?.LevelSwitchMenuPrefab;
if (menuPrefab == null)
/// <summary>
/// Handles the start of an interaction (shows confirmation menu and switches the level if confirmed).
/// </summary>
private void OnCharacterArrived()
{
Debug.LogError("LevelSwitchMenu prefab not assigned in InteractionSettings!");
return;
if (switchData == null || string.IsNullOrEmpty(switchData.targetLevelSceneName) || !_isActive)
return;
var menuPrefab = _interactionSettings?.LevelSwitchMenuPrefab;
if (menuPrefab == null)
{
Debug.LogError("LevelSwitchMenu prefab not assigned in InteractionSettings!");
return;
}
// Spawn the menu overlay (assume Canvas parent is handled in prefab setup)
var menuGo = Instantiate(menuPrefab);
var menu = menuGo.GetComponent<LevelSwitchMenu>();
if (menu == null)
{
Debug.LogError("LevelSwitchMenu component missing on prefab!");
Destroy(menuGo);
return;
}
// Setup menu with data and callbacks
menu.Setup(switchData, OnMenuConfirm, OnMenuCancel);
_isActive = false; // Prevent re-triggering until menu is closed
// Switch input mode to UI only
InputManager.Instance.SetInputMode(InputMode.UI);
}
// Spawn the menu overlay (assume Canvas parent is handled in prefab setup)
var menuGo = Instantiate(menuPrefab);
var menu = menuGo.GetComponent<LevelSwitchMenu>();
if (menu == null)
private async void OnMenuConfirm()
{
Debug.LogError("LevelSwitchMenu component missing on prefab!");
Destroy(menuGo);
return;
var progress = new Progress<float>(p => Debug.Log($"Loading progress: {p * 100:F0}%"));
await SceneManagerService.Instance.SwitchSceneAsync(switchData.targetLevelSceneName, progress);
}
// Setup menu with data and callbacks
menu.Setup(switchData, OnMenuConfirm, OnMenuCancel);
_isActive = false; // Prevent re-triggering until menu is closed
// Switch input mode to UI only
InputManager.Instance.SetInputMode(InputMode.UI);
}
private async void OnMenuConfirm()
{
var progress = new Progress<float>(p => Debug.Log($"Loading progress: {p * 100:F0}%"));
await SceneManagerService.Instance.SwitchSceneAsync(switchData.targetLevelSceneName, progress);
}
private void OnMenuCancel()
{
_isActive = true; // Allow interaction again if cancelled
private void OnMenuCancel()
{
_isActive = true; // Allow interaction again if cancelled
}
}
}

View File

@@ -1,6 +1,7 @@
using UnityEngine;
using AppleHills.Core.Settings;
using Input;
using AppleHillsCamera;
namespace Minigames.DivingForPictures
{
@@ -10,6 +11,10 @@ namespace Minigames.DivingForPictures
/// </summary>
public class PlayerController : MonoBehaviour, ITouchInputConsumer
{
[Tooltip("Reference to the edge anchor that this player should follow for Y position")]
[SerializeField] private EdgeAnchor edgeAnchor;
// Settings reference
private IDivingMinigameSettings _settings;
@@ -42,6 +47,38 @@ namespace Minigames.DivingForPictures
_targetFingerX = transform.position.x;
_isTouchActive = false;
// Try to find edge anchor if not assigned
if (edgeAnchor == null)
{
// First try to find edge anchor on the same object or parent
edgeAnchor = GetComponentInParent<EdgeAnchor>();
// If not found, find any edge anchor in the scene
if (edgeAnchor == null)
{
edgeAnchor = FindObjectOfType<EdgeAnchor>();
if (edgeAnchor == null)
{
Debug.LogWarning("[PlayerController] No EdgeAnchor found in scene. Origin Y position won't update with camera changes.");
}
else
{
Debug.Log($"[PlayerController] Auto-connected to EdgeAnchor on {edgeAnchor.gameObject.name}");
}
}
}
// Subscribe to edge anchor events if it exists
if (edgeAnchor != null)
{
// Unsubscribe first to prevent duplicate subscriptions
edgeAnchor.OnPositionUpdated -= UpdateOriginYFromAnchor;
edgeAnchor.OnPositionUpdated += UpdateOriginYFromAnchor;
// Update origin Y based on current anchor position
UpdateOriginYFromAnchor();
}
DivingGameManager.Instance.OnGameInitialized += Initialize;
// If game is already initialized, initialize immediately
@@ -70,6 +107,12 @@ namespace Minigames.DivingForPictures
private void OnDestroy()
{
DivingGameManager.Instance.OnGameInitialized -= Initialize;
// Unsubscribe from edge anchor events
if (edgeAnchor != null)
{
edgeAnchor.OnPositionUpdated -= UpdateOriginYFromAnchor;
}
}
/// <summary>
@@ -185,5 +228,31 @@ namespace Minigames.DivingForPictures
}
transform.position = new Vector3(newX, newY, transform.position.z);
}
/// <summary>
/// Updates the origin Y position based on camera adjustments
/// </summary>
public void UpdateOriginY(float newOriginY)
{
_originY = newOriginY;
}
/// <summary>
/// Updates the origin Y position based on the current position of the player
/// This method is intended to be called by the camera adapter when the camera is adjusted.
/// </summary>
private void UpdateOriginYFromCurrentPosition()
{
_originY = transform.position.y;
}
/// <summary>
/// Updates the origin Y position based on the current position of the edge anchor
/// This method is intended to be called by the edge anchor when its position is updated.
/// </summary>
private void UpdateOriginYFromAnchor()
{
_originY = edgeAnchor.transform.position.y;
}
}
}

View File

@@ -16,12 +16,43 @@ public class RockFollower : MonoBehaviour
/// </summary>
public bool useWobbleOffset = true;
/// <summary>
/// The base Y position for the rock.
/// The vertical distance between the rock and the bottle.
/// </summary>
public float baseY = -6f;
[SerializeField] private float verticalDistance = 6f;
private float velocityX; // For SmoothDamp
#if UNITY_EDITOR
/// <summary>
/// Called in editor when properties are changed.
/// Updates the object's position when verticalDistance is modified.
/// </summary>
private void OnValidate()
{
// Only update position if playing or in prefab mode
if (Application.isPlaying || UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this))
return;
if (bottleTransform != null)
{
// Calculate the new Y position based on bottle's position and the updated verticalDistance
float newY = bottleTransform.position.y - verticalDistance;
// Apply the wobble offset if enabled
if (useWobbleOffset && bottleWobble != null)
{
newY += bottleWobble.VerticalOffset;
}
// Update only the Y position, keeping X and Z unchanged
transform.position = new Vector3(transform.position.x, newY, transform.position.z);
// Mark the scene as dirty to ensure changes are saved
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(UnityEngine.SceneManagement.SceneManager.GetActiveScene());
}
}
#endif
void Update()
{
if (bottleTransform == null) return;
@@ -33,8 +64,8 @@ public class RockFollower : MonoBehaviour
// Smoothly follow bottle's X with stiffer motion
float newX = Mathf.SmoothDamp(currentX, targetX, ref velocityX, 1f / followStiffness);
// Calculate Y position
float newY = baseY;
// Calculate Y position based on bottle's position and vertical distance
float newY = bottleTransform.position.y - verticalDistance;
if (useWobbleOffset && bottleWobble != null)
{
newY += bottleWobble.VerticalOffset;

View File

@@ -0,0 +1,78 @@
# Camera Screen Adaptation System
This system helps adapt your 2D game's orthographic camera to different screen sizes, ensuring content is displayed consistently across various devices. It also provides tools for anchoring game objects to screen edges.
## Components
### 1. ScreenReferenceMarker
This component defines the target width and edge margins for your level content.
- **Target Width**: The desired width to be displayed on screen
- **Edge Margins**: Distances from screen edges for anchoring objects
- **Visualization**: Editor-only visualization of the reference sizes
### 2. CameraScreenAdapter
Automatically adjusts the camera's orthographic size to ensure the target width (defined by the ScreenReferenceMarker) matches the screen width.
- **Reference Marker**: Link to your ScreenReferenceMarker
- **Adapt on Resolution Change**: Automatically update when screen resolution changes
### 3. EdgeAnchor
Anchors game objects to screen edges, maintaining consistent distance when the camera's orthographic size changes.
- **Anchor Edge**: Which edge to anchor to (Top, Bottom, Left, Right)
- **Reference Marker**: Uses the same marker as the CameraScreenAdapter
- **Additional Offset**: Extra distance from the edge margin
- **Continuous Update**: Whether to update position every frame
### 4. CameraScreenVisualizer
Helper component for visualizing camera boundaries in the editor.
## Setup Instructions
### Basic Setup
1. **Create the Reference Marker:**
- Add a new empty GameObject to your scene
- Add the `ScreenReferenceMarker` component to it
- Position it at the center of your target view
- Set the `targetWidth` to match your level's ideal width
- Adjust margin values as needed
2. **Configure the Camera:**
- Add the `CameraScreenAdapter` component to your main camera
- Reference the ScreenReferenceMarker you created
- Optionally, add `CameraScreenVisualizer` for better editor visualization
3. **Anchor Game Objects:**
- For any objects that should maintain position relative to screen edges:
- Add the `EdgeAnchor` component
- Set the desired anchor edge (e.g., Top)
- Reference the same ScreenReferenceMarker
- Adjust additional offset if needed
### Example: Top-Edge Anchoring
For objects that need to stay a fixed distance from the top of the screen:
1. Set up your ScreenReferenceMarker with an appropriate topMargin value
2. Add EdgeAnchor to the object you want to anchor
3. Set AnchorEdge to Top
4. Adjust additionalOffset for fine-tuning
## Tips for Level Design
- Place the ScreenReferenceMarker at the center of your scene's critical content
- Use the visual gizmos to see how your reference width relates to camera boundaries
- Test your scene in different aspect ratios to ensure proper adaptation
- For complex levels, you might want multiple reference markers for different sections
## Runtime Considerations
- The system automatically adapts when the screen size changes
- The CameraScreenAdapter can be disabled if you need to temporarily override the automatic sizing
- EdgeAnchor components can be disabled when their objects don't need to maintain edge alignment