feat: 아군 타게팅 시스템 구현 — SingleAlly 투사체형 치유/보호막
- 치유/보호막 스킬을 즉발 자가시전에서 투사체형 아군 1인 타겟팅으로 전환 - TargetType.SingleAlly 추가, targetOverride 매개변수로 외부 타겟 주입 지원 - PlayerSkillInput: 카메라 레이캐스트 기반 아군 탐지, 서버 검증, RPC 타겟 ID 전달 - AllyTargetIndicator: 호버 아군 위에 디스크 인디케이터 표시, 사거리/초과 색상 변경 - SpawnEffect: 타겟 방향 회전 보정 - 투사체 스폰 이펙트 에셋 생성 (치유/보호막 각각) - 인디케이터 프리팹 + URP/Unlit 머티리얼 생성 - Player 프리팹에 AllyTargetIndicator 컴포넌트 추가 및 설정 - Input.mousePosition → Mouse.current.position.ReadValue() 수정 (Input System 호환)
This commit is contained in:
200
Assets/_Game/Scripts/UI/AllyTargetIndicator.cs
Normal file
200
Assets/_Game/Scripts/UI/AllyTargetIndicator.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/AllyTargetIndicator.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/AllyTargetIndicator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 630a8eac9f5f6b04b92cf6949ed9185c
|
||||
Reference in New Issue
Block a user