using System; using UnityEngine; using UnityEngine.InputSystem; using Colosseum.Combat; namespace Colosseum.UI { /// /// 아군 타게팅 UI 피드백 컴포넌트. /// 커서가 아군 위에 있을 때 시각적 인디케이터를 표시하고, /// 현재 호버 중인 아군 정보를 외부에 제공합니다. /// Player 프리팹에 부착하여 사용합니다. /// public class AllyTargetIndicator : MonoBehaviour { [Header("레이캐스트 설정")] [Tooltip("아군 탐지용 레이캐스트 레이어")] [SerializeField] private LayerMask allyDetectionLayers; [Tooltip("아군 타게팅 최대 사거리 (0이면 무제한)")] [Min(0f)] [SerializeField] private float maxRange = 30f; [Tooltip("시야 차단 확인 여부")] [SerializeField] private bool requireLineOfSight = true; [Tooltip("시야 차단 확인용 레이어")] [SerializeField] private LayerMask lineOfSightBlockLayers; [Header("인디케이터 설정")] [Tooltip("타겟 위에 표시할 인디케이터 프리팹 (World Space UI)")] [SerializeField] private GameObject indicatorPrefab; [Tooltip("인디케이터 높이 오프셋 (타겟 위치 + 이 값)")] [Min(0f)] [SerializeField] private float indicatorHeightOffset = 2.2f; [Tooltip("사거리 내 아군 색상")] [SerializeField] private Color inRangeColor = new Color(0.2f, 1f, 0.2f, 0.8f); [Tooltip("사거리 외 아군 색상")] [SerializeField] private Color outOfRangeColor = new Color(1f, 0.3f, 0.3f, 0.8f); [Header("범위 기준")] [Tooltip("SingleAlly 스킬의 사거리를 기준으로 색상을 변경합니다")] [SerializeField] private bool useSkillRange = true; private Camera mainCamera; private GameObject currentIndicator; private GameObject lastHoveredTarget; /// /// 현재 호버 중인 아군 GameObject /// public GameObject CurrentHoverTarget => lastHoveredTarget; /// /// 호버 대상이 변경되었을 때 발생합니다. /// null이면 호버 대상이 없음을 의미합니다. /// public event Action OnHoverTargetChanged; private void Start() { mainCamera = Camera.main; } private void Update() { if (mainCamera == null) { mainCamera = Camera.main; if (mainCamera == null) return; } if (!enabled) return; GameObject newTarget = PerformRaycast(); if (newTarget != lastHoveredTarget) { lastHoveredTarget = newTarget; UpdateIndicator(); OnHoverTargetChanged?.Invoke(newTarget); } } /// /// 카메라에서 커서 방향으로 레이캐스트하여 아군을 탐색합니다. /// private GameObject PerformRaycast() { if (allyDetectionLayers.value == 0) return null; Ray ray = mainCamera.ScreenPointToRay(Mouse.current.position.ReadValue()); float maxDistance = maxRange > 0f ? maxRange : Mathf.Infinity; if (!Physics.Raycast(ray, out RaycastHit hit, maxDistance, allyDetectionLayers)) return null; GameObject hitObject = hit.collider.gameObject; // 팀 체크 if (!Team.IsSameTeam(gameObject, hitObject)) return null; // 생존 체크 var damageable = hitObject.GetComponent(); if (damageable != null && damageable.IsDead) return null; // 시야 차단 체크 if (requireLineOfSight && lineOfSightBlockLayers.value != 0) { Vector3 casterPos = transform.position + Vector3.up * 1.5f; Vector3 targetPos = hitObject.transform.position + Vector3.up * 1.5f; Vector3 direction = targetPos - casterPos; float distance = direction.magnitude; if (Physics.Raycast(casterPos, direction.normalized, out RaycastHit losHit, distance, lineOfSightBlockLayers)) { if (losHit.collider.gameObject != hitObject) return null; } } return hitObject; } /// /// 인디케이터 프리팹을 생성/이동/제거하여 시각 피드백을 업데이트합니다. /// private void UpdateIndicator() { if (lastHoveredTarget == null) { DestroyIndicator(); return; } if (indicatorPrefab == null) return; if (currentIndicator == null) { currentIndicator = Instantiate(indicatorPrefab); } // 타겟 위치에 인디케이터 배치 Vector3 targetPosition = lastHoveredTarget.transform.position; targetPosition.y += indicatorHeightOffset; currentIndicator.transform.position = targetPosition; // 사거리에 따라 색상 변경 bool isInRange = true; if (useSkillRange && maxRange > 0f) { float distance = Vector3.Distance(transform.position, lastHoveredTarget.transform.position); isInRange = distance <= maxRange; } SetIndicatorColor(isInRange ? inRangeColor : outOfRangeColor); } private void DestroyIndicator() { if (currentIndicator != null) { Destroy(currentIndicator); currentIndicator = null; } } private void SetIndicatorColor(Color color) { if (currentIndicator == null) return; var renderer = currentIndicator.GetComponent(); if (renderer != null) { renderer.material.color = color; return; } #if UNITY_UI // UI Toolkit / UGUI 색상 변경 (필요 시 확장) #endif } private void OnDestroy() { DestroyIndicator(); } private void OnDisable() { DestroyIndicator(); lastHoveredTarget = null; OnHoverTargetChanged?.Invoke(null); } } }