Files
Colosseum/Assets/_Game/Scripts/Skills/SkillEffect.cs
dal4segno 8cd2623163 feat: 아군 타게팅 시스템 구현 — SingleAlly 투사체형 치유/보호막
- 치유/보호막 스킬을 즉발 자가시전에서 투사체형 아군 1인 타겟팅으로 전환

- TargetType.SingleAlly 추가, targetOverride 매개변수로 외부 타겟 주입 지원

- PlayerSkillInput: 카메라 레이캐스트 기반 아군 탐지, 서버 검증, RPC 타겟 ID 전달

- AllyTargetIndicator: 호버 아군 위에 디스크 인디케이터 표시, 사거리/초과 색상 변경

- SpawnEffect: 타겟 방향 회전 보정

- 투사체 스폰 이펙트 에셋 생성 (치유/보호막 각각)

- 인디케이터 프리팹 + URP/Unlit 머티리얼 생성

- Player 프리팹에 AllyTargetIndicator 컴포넌트 추가 및 설정

- Input.mousePosition → Mouse.current.position.ReadValue() 수정 (Input System 호환)
2026-03-31 23:06:13 +09:00

308 lines
11 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using Colosseum;
using Colosseum.Weapons;
namespace Colosseum.Skills
{
/// <summary>
/// 스킬 효과의 기본 클래스.
/// 모든 효과는 이 클래스를 상속받아 구현합니다.
/// 효과 발동 타이밍은 애니메이션 이벤트(OnEffect)로 제어합니다.
/// </summary>
public abstract class SkillEffect : ScriptableObject
{
[Header("대상 설정")]
[Tooltip("Self: 시전자, Area: 범위 내 대상, SingleAlly: 아군 1인 (외부에서 타겟 주입)")]
[SerializeField] protected TargetType targetType = TargetType.Self;
[Header("Area Settings (TargetType이 Area일 때)")]
[Tooltip("범위 내에서 공격할 대상 필터")]
[SerializeField] protected TargetTeam targetTeam = TargetTeam.Enemy;
[SerializeField] protected AreaCenterType areaCenter = AreaCenterType.Caster;
[SerializeField] protected AreaShapeType areaShape = AreaShapeType.Sphere;
[SerializeField] protected LayerMask targetLayers;
[Tooltip("Area 범위 효과일 때 시전자 본인 포함 여부")]
[SerializeField] protected bool includeCasterInArea = false;
[Header("Sphere Settings")]
[Min(0.1f)] [SerializeField] protected float areaRadius = 3f;
[Header("Fan Settings")]
[Tooltip("부채꼴 원점이 캐릭터로부터 떨어진 거리")]
[Min(0f)] [SerializeField] protected float fanOriginDistance = 1f;
[Tooltip("부채꼴 반지름")]
[Min(0.1f)] [SerializeField] protected float fanRadius = 3f;
[Tooltip("부채꼴 좌우 각도 (각 방향으로 이 각도만큼 벌어짐, 총 각도 = 2배)")]
[Range(0f, 180f)] [SerializeField] protected float fanHalfAngle = 45f;
// Properties
public TargetType TargetType => targetType;
public TargetTeam TargetTeam => targetTeam;
public AreaShapeType AreaShape => areaShape;
public float AreaRadius => areaRadius;
public float FanOriginDistance => fanOriginDistance;
public float FanRadius => fanRadius;
public float FanHalfAngle => fanHalfAngle;
public bool IncludeCasterInArea => includeCasterInArea;
/// <summary>
/// 스킬 시전 시 호출
/// </summary>
public void ExecuteOnCast(GameObject caster, GameObject targetOverride = null)
{
List<GameObject> targets = new List<GameObject>();
CollectTargets(caster, targets, targetOverride);
for (int i = 0; i < targets.Count; i++)
{
ApplyEffect(caster, targets[i]);
}
}
/// <summary>
/// 현재 효과가 영향을 줄 대상 목록을 수집합니다.
/// 젬의 적중 이상상태 적용 등에서 동일한 타겟 해석을 재사용하기 위한 경로입니다.
/// </summary>
public void CollectTargets(GameObject caster, List<GameObject> destination, GameObject targetOverride = null)
{
if (caster == null || destination == null)
return;
switch (targetType)
{
case TargetType.Self:
AddUniqueTarget(destination, caster);
break;
case TargetType.Area:
CollectAreaTargets(caster, destination);
break;
case TargetType.SingleAlly:
if (targetOverride != null && IsCorrectTeam(caster, targetOverride))
AddUniqueTarget(destination, targetOverride);
else
AddUniqueTarget(destination, caster);
break;
}
}
/// <summary>
/// 투사체 충돌 시 호출
/// </summary>
public void ExecuteOnHit(GameObject caster, GameObject hitTarget)
{
if (IsValidTarget(caster, hitTarget))
{
ApplyEffect(caster, hitTarget);
}
}
/// <summary>
/// 실제 효과 적용 (상속받은 클래스에서 구현)
/// </summary>
protected abstract void ApplyEffect(GameObject caster, GameObject target);
/// <summary>
/// 충돌한 대상이 유효한 타겟인지 확인
/// </summary>
public bool IsValidTarget(GameObject caster, GameObject target)
{
if (target == null) return false;
// 레이어 체크
if (targetLayers.value != 0 && (targetLayers.value & (1 << target.layer)) == 0)
return false;
// 팀 체크
return IsCorrectTeam(caster, target);
}
private bool IsCorrectTeam(GameObject caster, GameObject target)
{
// Team 컴포넌트가 없는 오브젝트(환경, 바닥, 벽 등)는 타겟 제외
if (target.GetComponent<Team>() == null) return false;
bool isSameTeam = Team.IsSameTeam(caster, target);
return targetTeam switch
{
TargetTeam.Enemy => !isSameTeam,
TargetTeam.Ally => isSameTeam,
TargetTeam.All => true,
_ => true
};
}
private void CollectAreaTargets(GameObject caster, List<GameObject> destination)
{
Vector3 center = GetAreaCenter(caster);
Collider[] hits = Physics.OverlapSphere(center, Mathf.Max(areaRadius, fanRadius), targetLayers);
foreach (var hit in hits)
{
if (!includeCasterInArea && hit.gameObject == caster) continue;
if (!IsCorrectTeam(caster, hit.gameObject)) continue;
// 부채꼴 판정
if (areaShape == AreaShapeType.Fan)
{
if (!IsInFanShape(caster, hit.transform.position))
continue;
}
AddUniqueTarget(destination, hit.gameObject);
}
}
private static void AddUniqueTarget(List<GameObject> destination, GameObject target)
{
if (target == null || destination.Contains(target))
return;
destination.Add(target);
}
/// <summary>
/// 타겟이 부채꼴 범위 내에 있는지 확인
/// </summary>
private bool IsInFanShape(GameObject caster, Vector3 targetPosition)
{
Vector3 casterPos = caster.transform.position;
Vector3 casterForward = caster.transform.forward;
// 부채꼴 원점 계산
Vector3 fanOrigin = casterPos + casterForward * fanOriginDistance;
// 원점에서 타겟까지의 방향과 거리
Vector3 toTarget = targetPosition - fanOrigin;
float distance = toTarget.magnitude;
// 거리 체크
if (distance > fanRadius)
return false;
// 각도 체크 (Y축 무시)
Vector3 toTargetFlat = new Vector3(toTarget.x, 0f, toTarget.z).normalized;
Vector3 casterForwardFlat = new Vector3(casterForward.x, 0f, casterForward.z).normalized;
float angle = Vector3.Angle(casterForwardFlat, toTargetFlat);
return angle <= fanHalfAngle;
}
private Vector3 GetAreaCenter(GameObject caster)
{
if (caster == null) return Vector3.zero;
return areaCenter switch
{
AreaCenterType.Caster => caster.transform.position,
AreaCenterType.CasterForward => caster.transform.position + caster.transform.forward * areaRadius,
_ => caster.transform.position
};
}
#region Debug Visualization
/// <summary>
/// 공격 범위 시각화 (런타임 디버그용)
/// </summary>
public void DrawDebugRange(GameObject caster, float duration = 1f)
{
if (targetType != TargetType.Area) return;
Vector3 casterPos = caster.transform.position;
Vector3 forward = caster.transform.forward;
if (areaShape == AreaShapeType.Sphere)
{
Vector3 center = GetAreaCenter(caster);
DebugDrawSphere(center, areaRadius, Color.red, duration);
}
else if (areaShape == AreaShapeType.Fan)
{
DebugDrawFan(casterPos, forward, fanOriginDistance, fanRadius, fanHalfAngle, Color.red, duration);
}
}
private void DebugDrawSphere(Vector3 center, float radius, Color color, float duration)
{
int segments = 32;
float step = 360f / segments;
// XZ 평면 원
Vector3 prevPoint = center + new Vector3(radius, 0, 0);
for (int i = 1; i <= segments; i++)
{
float angle = i * step * Mathf.Deg2Rad;
Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
Debug.DrawLine(prevPoint, newPoint, color, duration);
prevPoint = newPoint;
}
// 수직선 (높이 표시)
Debug.DrawLine(center + Vector3.up * 0.1f, center + Vector3.up * 2f, color, duration);
}
private void DebugDrawFan(Vector3 casterPos, Vector3 forward, float originDistance, float radius, float halfAngle, Color color, float duration)
{
Vector3 fanOrigin = casterPos + forward * originDistance;
Vector3 forwardFlat = new Vector3(forward.x, 0f, forward.z).normalized;
// 부채꼴의 양끝 방향 계산
Quaternion leftRot = Quaternion.Euler(0, -halfAngle, 0);
Quaternion rightRot = Quaternion.Euler(0, halfAngle, 0);
Vector3 leftDir = leftRot * forwardFlat;
Vector3 rightDir = rightRot * forwardFlat;
// 부채꼴 호 그리기
int arcSegments = Mathf.Max(8, Mathf.CeilToInt(halfAngle * 2 / 5f)); // 5도당 1세그먼트
Vector3 prevPoint = fanOrigin + leftDir * radius;
for (int i = 1; i <= arcSegments; i++)
{
float t = (float)i / arcSegments;
float angle = -halfAngle + t * halfAngle * 2;
Quaternion rot = Quaternion.Euler(0, angle, 0);
Vector3 dir = rot * forwardFlat;
Vector3 newPoint = fanOrigin + dir * radius;
Debug.DrawLine(prevPoint, newPoint, color, duration);
prevPoint = newPoint;
}
// 부채꼴 경계선
Debug.DrawLine(fanOrigin, fanOrigin + leftDir * radius, color, duration);
Debug.DrawLine(fanOrigin, fanOrigin + rightDir * radius, color, duration);
// 원점 표시
Debug.DrawLine(fanOrigin + Vector3.up * 0.1f, fanOrigin + Vector3.up * 1.5f, color, duration);
}
#endregion
}
public enum TargetType
{
Self,
Area,
SingleAlly
}
public enum TargetTeam
{
Enemy,
Ally,
All
}
public enum AreaCenterType
{
Caster,
CasterForward
}
public enum AreaShapeType
{
Sphere, // 원형 범위
Fan // 부채꼴 범위
}
}