- SkillData: 채널링 필드 추가 (지속시간, 틱 간격, 틱/종료 효과, VFX 프리팹, 마운트 경로, 크기 배율) - SkillController: 채널링 상태 관리 (Start/Update/End), VFX 생성/파괴, 틱 효과 주기 발동, 버튼 해제로 중단 - SkillEffect: Beam(원통) 범위 판정 추가 (OverlapCapsule), 디버그 시각화 - PlayerSkillInput: 스킬 취소(canceled) 이벤트 구독 → 채널링 중단 통지 - SkillLoadoutEntry: 채널링 틱/종료 효과 수집 메서드 추가 - 스킬 데이터/이펙트/애니메이션/VFX 에셋 추가 (채널링 스킬용) - PolygonParticleFX VFX 에셋 패키지 추가 (Materials, Models, Prefabs, Textures, Scenes)
404 lines
16 KiB
C#
404 lines
16 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;
|
|
public AreaCenterType AreaCenter => areaCenter;
|
|
|
|
/// <summary>
|
|
/// 이 효과가 순수 시각 효과인지 확인합니다.
|
|
/// true이면 서버 가드를 무시하고 모든 클라이언트에서 로컬 실행됩니다.
|
|
/// </summary>
|
|
public virtual bool IsVisualOnly => false;
|
|
|
|
/// <summary>
|
|
/// 스킬 시전 시 호출
|
|
/// </summary>
|
|
public virtual void ExecuteOnCast(GameObject caster, GameObject targetOverride = null, Vector3? groundPosition = null)
|
|
{
|
|
List<GameObject> targets = new List<GameObject>();
|
|
CollectTargets(caster, targets, targetOverride, groundPosition);
|
|
|
|
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, Vector3? groundPosition = null)
|
|
{
|
|
if (caster == null || destination == null)
|
|
return;
|
|
|
|
switch (targetType)
|
|
{
|
|
case TargetType.Self:
|
|
AddUniqueTarget(destination, caster);
|
|
break;
|
|
|
|
case TargetType.Area:
|
|
CollectAreaTargets(caster, destination, groundPosition);
|
|
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? groundPosition = null)
|
|
{
|
|
switch (areaShape)
|
|
{
|
|
case AreaShapeType.Beam:
|
|
CollectBeamTargets(caster, destination);
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
Vector3 center = GetAreaCenter(caster, groundPosition);
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 빔(원통) 범위 내 타겟을 수집합니다.
|
|
/// 시전자 정면 fanOriginDistance 위치에서 areaRadius 길이의 원통을 판정합니다.
|
|
/// </summary>
|
|
private void CollectBeamTargets(GameObject caster, List<GameObject> destination)
|
|
{
|
|
Vector3 casterPos = caster.transform.position;
|
|
Vector3 forward = caster.transform.forward;
|
|
float beamLength = Mathf.Max(areaRadius, 0.1f);
|
|
float beamRadius = Mathf.Max(fanRadius, 0.1f);
|
|
float startOffset = Mathf.Max(fanOriginDistance, 0f);
|
|
|
|
Vector3 origin = casterPos + forward * startOffset;
|
|
Vector3 point1 = origin + forward * beamRadius;
|
|
Vector3 point2 = origin + forward * (beamLength - beamRadius);
|
|
|
|
Collider[] hits = Physics.OverlapCapsule(point1, point2, beamRadius, targetLayers);
|
|
foreach (var hit in hits)
|
|
{
|
|
if (!includeCasterInArea && hit.gameObject == caster) continue;
|
|
if (!IsCorrectTeam(caster, hit.gameObject)) 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, Vector3? groundPosition = null)
|
|
{
|
|
if (caster == null) return Vector3.zero;
|
|
|
|
return areaCenter switch
|
|
{
|
|
AreaCenterType.Caster => caster.transform.position,
|
|
AreaCenterType.CasterForward => caster.transform.position + caster.transform.forward * areaRadius,
|
|
AreaCenterType.GroundPoint => groundPosition ?? caster.transform.position,
|
|
_ => caster.transform.position
|
|
};
|
|
}
|
|
|
|
#region Debug Visualization
|
|
/// <summary>
|
|
/// 공격 범위 시각화 (런타임 디버그용)
|
|
/// </summary>
|
|
public void DrawDebugRange(GameObject caster, float duration = 1f, Vector3? groundPosition = null)
|
|
{
|
|
if (targetType != TargetType.Area) return;
|
|
|
|
Vector3 casterPos = caster.transform.position;
|
|
Vector3 forward = caster.transform.forward;
|
|
|
|
if (areaShape == AreaShapeType.Sphere)
|
|
{
|
|
Vector3 center = GetAreaCenter(caster, groundPosition);
|
|
DebugDrawSphere(center, areaRadius, Color.red, duration);
|
|
}
|
|
else if (areaShape == AreaShapeType.Fan)
|
|
{
|
|
Vector3 center = GetAreaCenter(caster, groundPosition);
|
|
DebugDrawFan(center, groundPosition.HasValue && areaCenter == AreaCenterType.GroundPoint
|
|
? (center - casterPos).normalized
|
|
: forward, fanOriginDistance, fanRadius, fanHalfAngle, Color.red, duration);
|
|
}
|
|
else if (areaShape == AreaShapeType.Beam)
|
|
{
|
|
DebugDrawBeam(casterPos, forward, areaRadius, fanRadius, fanOriginDistance, 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 빔(원통) 범위를 디버그 시각화합니다.
|
|
/// 시전자 정면 startOffset 위치에서 사거리만큼의 원통과 단면 원을 그립니다.
|
|
/// </summary>
|
|
private void DebugDrawBeam(Vector3 casterPos, Vector3 forward, float beamLength, float beamRadius, float startOffset, Color color, float duration)
|
|
{
|
|
Vector3 origin = casterPos + forward * startOffset;
|
|
Vector3 end = origin + forward * beamLength;
|
|
int segments = 16;
|
|
float step = 360f / segments;
|
|
|
|
// 시작 단면 원
|
|
Vector3 prevStart = origin + new Vector3(beamRadius, 0, 0);
|
|
for (int i = 1; i <= segments; i++)
|
|
{
|
|
float angle = i * step * Mathf.Deg2Rad;
|
|
Vector3 next = origin + new Vector3(Mathf.Cos(angle) * beamRadius, 0, Mathf.Sin(angle) * beamRadius);
|
|
Debug.DrawLine(prevStart, next, color, duration);
|
|
prevStart = next;
|
|
}
|
|
|
|
// 끝 단면 원
|
|
Vector3 prevEnd = end + new Vector3(beamRadius, 0, 0);
|
|
for (int i = 1; i <= segments; i++)
|
|
{
|
|
float angle = i * step * Mathf.Deg2Rad;
|
|
Vector3 next = end + new Vector3(Mathf.Cos(angle) * beamRadius, 0, Mathf.Sin(angle) * beamRadius);
|
|
Debug.DrawLine(prevEnd, next, color, duration);
|
|
prevEnd = next;
|
|
}
|
|
|
|
// 네 모서리 연결선
|
|
Debug.DrawLine(origin + forward * 0 + new Vector3(beamRadius, 0, 0), end + new Vector3(beamRadius, 0, 0), color, duration);
|
|
Debug.DrawLine(origin + new Vector3(-beamRadius, 0, 0), end + new Vector3(-beamRadius, 0, 0), color, duration);
|
|
Debug.DrawLine(origin + new Vector3(0, 0, beamRadius), end + new Vector3(0, 0, beamRadius), color, duration);
|
|
Debug.DrawLine(origin + new Vector3(0, 0, -beamRadius), end + new Vector3(0, 0, -beamRadius), color, duration);
|
|
|
|
// 중심선
|
|
Debug.DrawLine(origin, end, color, duration);
|
|
|
|
// 높이 표시
|
|
Debug.DrawLine(origin + Vector3.up * 0.1f, origin + Vector3.up * 1.5f, color, duration);
|
|
Debug.DrawLine(end + Vector3.up * 0.1f, end + Vector3.up * 1.5f, color, duration);
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
public enum TargetType
|
|
{
|
|
Self,
|
|
Area,
|
|
SingleAlly
|
|
}
|
|
|
|
public enum TargetTeam
|
|
{
|
|
Enemy,
|
|
Ally,
|
|
All
|
|
}
|
|
|
|
public enum AreaCenterType
|
|
{
|
|
Caster,
|
|
CasterForward,
|
|
GroundPoint // 지면 지점 타겟팅 (Ground Target)
|
|
}
|
|
|
|
public enum AreaShapeType
|
|
{
|
|
Sphere, // 원형 범위
|
|
Fan, // 부채꼴 범위
|
|
Beam // 원통 범위 (areaRadius=사거리, fanRadius=빔 폭)
|
|
}
|
|
}
|