- 치유/보호막 스킬을 즉발 자가시전에서 투사체형 아군 1인 타겟팅으로 전환 - TargetType.SingleAlly 추가, targetOverride 매개변수로 외부 타겟 주입 지원 - PlayerSkillInput: 카메라 레이캐스트 기반 아군 탐지, 서버 검증, RPC 타겟 ID 전달 - AllyTargetIndicator: 호버 아군 위에 디스크 인디케이터 표시, 사거리/초과 색상 변경 - SpawnEffect: 타겟 방향 회전 보정 - 투사체 스폰 이펙트 에셋 생성 (치유/보호막 각각) - 인디케이터 프리팹 + URP/Unlit 머티리얼 생성 - Player 프리팹에 AllyTargetIndicator 컴포넌트 추가 및 설정 - Input.mousePosition → Mouse.current.position.ReadValue() 수정 (Input System 호환)
201 lines
6.6 KiB
C#
201 lines
6.6 KiB
C#
using System;
|
|
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
|
|
using Colosseum.Combat;
|
|
|
|
namespace Colosseum.UI
|
|
{
|
|
/// <summary>
|
|
/// 아군 타게팅 UI 피드백 컴포넌트.
|
|
/// 커서가 아군 위에 있을 때 시각적 인디케이터를 표시하고,
|
|
/// 현재 호버 중인 아군 정보를 외부에 제공합니다.
|
|
/// Player 프리팹에 부착하여 사용합니다.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 현재 호버 중인 아군 GameObject
|
|
/// </summary>
|
|
public GameObject CurrentHoverTarget => lastHoveredTarget;
|
|
|
|
/// <summary>
|
|
/// 호버 대상이 변경되었을 때 발생합니다.
|
|
/// null이면 호버 대상이 없음을 의미합니다.
|
|
/// </summary>
|
|
public event Action<GameObject> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 카메라에서 커서 방향으로 레이캐스트하여 아군을 탐색합니다.
|
|
/// </summary>
|
|
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<IDamageable>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 인디케이터 프리팹을 생성/이동/제거하여 시각 피드백을 업데이트합니다.
|
|
/// </summary>
|
|
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<Renderer>();
|
|
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);
|
|
}
|
|
}
|
|
}
|