feat: VFX 인프라 구축 및 Ground Target 시스템 구현
- VfxEffect 스킬 이펙트 클래스 추가 (일회성 VFX 스폰, 위치/스케일/파티클 제어) - SkillEffect.IsVisualOnly 프로퍼티 추가로 서버 가드 없이 모든 클라이언트에서 VFX 로컬 실행 - SkillProjectile 트레일 VFX 지원 (OnNetworkSpawn에서 양쪽 생성, despawn 시 월드 분리) - SkillProjectile HitEffectClientRpc 추가로 충돌 이펙트 클라이언트 동기화 - Ground Target 시스템: 타겟팅 모드 상태머신, 인디케이터, 지면 위치 RPC 전달 - 마법 오름 Ground Target 스킬 에셋 및 VfxEffect 에셋 추가 - 마법 오름 애니메이션 클립 추가 - Ground layer (Layer 7) 추가 - ProjectileBasic에 trailPrefab/hitEffect 필드 추가 - Prefabs/VFX/ 폴더 생성
This commit is contained in:
87
Assets/_Game/Scripts/Player/GroundTargetIndicator.cs
Normal file
87
Assets/_Game/Scripts/Player/GroundTargetIndicator.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 지면 타겟팅 인디케이터.
|
||||
/// 마우스 커서 위치의 지면에 표시되는 반투명 범위 원입니다.
|
||||
/// </summary>
|
||||
public class GroundTargetIndicator : MonoBehaviour
|
||||
{
|
||||
[Header("설정")]
|
||||
[Min(0.1f)] [SerializeField] private float radius = 5f;
|
||||
[SerializeField] private Color indicatorColor = new Color(1f, 0.5f, 0f, 0.35f);
|
||||
|
||||
private GameObject indicatorObject;
|
||||
private Material indicatorMaterial;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
CreateIndicator();
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void CreateIndicator()
|
||||
{
|
||||
// 얇은 실린더로 범위 원 생성
|
||||
indicatorObject = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
||||
indicatorObject.name = "GroundTargetIndicator";
|
||||
indicatorObject.transform.SetParent(transform);
|
||||
indicatorObject.transform.localScale = new Vector3(radius * 2f, 0.02f, radius * 2f);
|
||||
|
||||
// Default 레이어(0) — 지면 레이캐스트에 걸리지 않도록 Ground 레이어와 분리
|
||||
indicatorObject.layer = 0;
|
||||
|
||||
// 콜라이더 제거 — 물리 간섭 방지
|
||||
Destroy(indicatorObject.GetComponent<Collider>());
|
||||
|
||||
// 반투명 Unlit 머티리얼
|
||||
indicatorMaterial = new Material(Shader.Find("Unlit/Transparent"));
|
||||
indicatorMaterial.color = indicatorColor;
|
||||
indicatorObject.GetComponent<MeshRenderer>().material = indicatorMaterial;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인디케이터를 표시합니다.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
if (indicatorObject != null)
|
||||
indicatorObject.SetActive(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인디케이터를 숨깁니다.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
if (indicatorObject != null)
|
||||
indicatorObject.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인디케이터 위치를 갱신합니다.
|
||||
/// </summary>
|
||||
public void UpdatePosition(Vector3 groundPosition)
|
||||
{
|
||||
if (indicatorObject != null)
|
||||
indicatorObject.transform.position = groundPosition + Vector3.up * 0.05f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인디케이터 반경을 변경합니다.
|
||||
/// </summary>
|
||||
public void SetRadius(float newRadius)
|
||||
{
|
||||
radius = newRadius;
|
||||
if (indicatorObject != null)
|
||||
indicatorObject.transform.localScale = new Vector3(radius * 2f, 0.02f, radius * 2f);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (indicatorMaterial != null)
|
||||
Destroy(indicatorMaterial);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18816e6a80579d84baf9ab4ef148e9f3
|
||||
@@ -97,6 +97,8 @@ namespace Colosseum.Player
|
||||
[SerializeField] private LayerMask groundTargetLayers;
|
||||
[Tooltip("지면 타겟팅 최대 사거리")]
|
||||
[Min(1f)] [SerializeField] private float groundTargetMaxRange = 20f;
|
||||
[Tooltip("지면 타겟 인디케이터 (없으면 자동 생성)")]
|
||||
[SerializeField] private GroundTargetIndicator groundTargetIndicator;
|
||||
|
||||
private InputSystem_Actions inputActions;
|
||||
private bool gameplayInputEnabled = true;
|
||||
@@ -458,7 +460,7 @@ namespace Colosseum.Player
|
||||
/// 서버에 스킬 실행 요청
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server)]
|
||||
private void RequestSkillExecutionRpc(int slotIndex, ulong targetNetworkObjectId = 0, Vector3 groundTargetPosition = default)
|
||||
private void RequestSkillExecutionRpc(int slotIndex, ulong targetNetworkObjectId = 0, Vector3 groundTargetPosition = default, bool hasGroundTarget = false)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return;
|
||||
@@ -491,7 +493,7 @@ namespace Colosseum.Player
|
||||
}
|
||||
|
||||
// 지면 타겟 사거리 검증
|
||||
if (groundTargetPosition != default)
|
||||
if (hasGroundTarget)
|
||||
{
|
||||
float distance = Vector3.Distance(transform.position, groundTargetPosition);
|
||||
if (distance > groundTargetMaxRange)
|
||||
@@ -508,14 +510,14 @@ namespace Colosseum.Player
|
||||
}
|
||||
|
||||
// 모든 클라이언트에 스킬 실행 전파
|
||||
BroadcastSkillExecutionRpc(slotIndex, targetNetworkObjectId, groundTargetPosition);
|
||||
BroadcastSkillExecutionRpc(slotIndex, targetNetworkObjectId, groundTargetPosition, hasGroundTarget);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 클라이언트에 스킬 실행 전파
|
||||
/// </summary>
|
||||
[Rpc(SendTo.ClientsAndHost)]
|
||||
private void BroadcastSkillExecutionRpc(int slotIndex, ulong targetNetworkObjectId = 0, Vector3 groundTargetPosition = default)
|
||||
private void BroadcastSkillExecutionRpc(int slotIndex, ulong targetNetworkObjectId = 0, Vector3 groundTargetPosition = default, bool hasGroundTarget = false)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return;
|
||||
@@ -528,12 +530,13 @@ namespace Colosseum.Player
|
||||
GameObject targetOverride = ResolveTargetFromNetworkId(targetNetworkObjectId);
|
||||
|
||||
// 모든 클라이언트에서 스킬 실행 (애니메이션 포함)
|
||||
if (groundTargetPosition != default)
|
||||
if (hasGroundTarget)
|
||||
{
|
||||
skillController.ExecuteSkill(loadoutEntry, targetOverride, groundTargetPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[GroundTarget] Broadcast: hasGroundTarget=false (일반 시전)");
|
||||
skillController.ExecuteSkill(loadoutEntry, targetOverride);
|
||||
}
|
||||
}
|
||||
@@ -869,6 +872,14 @@ namespace Colosseum.Player
|
||||
{
|
||||
actionState = GetOrCreateActionState();
|
||||
}
|
||||
|
||||
// 인디케이터가 없으면 자동 생성
|
||||
if (groundTargetIndicator == null)
|
||||
{
|
||||
groundTargetIndicator = GetComponent<GroundTargetIndicator>();
|
||||
if (groundTargetIndicator == null)
|
||||
groundTargetIndicator = gameObject.AddComponent<GroundTargetIndicator>();
|
||||
}
|
||||
}
|
||||
|
||||
#region 지면 타게팅 (Ground Target)
|
||||
@@ -912,6 +923,10 @@ namespace Colosseum.Player
|
||||
{
|
||||
currentTargetingMode = TargetingMode.GroundTarget;
|
||||
pendingGroundTargetSlotIndex = slotIndex;
|
||||
|
||||
if (groundTargetIndicator != null)
|
||||
groundTargetIndicator.Show();
|
||||
|
||||
Debug.Log($"[GroundTarget] 타겟팅 모드 진입: 슬롯 {slotIndex}");
|
||||
}
|
||||
|
||||
@@ -925,6 +940,9 @@ namespace Colosseum.Player
|
||||
|
||||
currentTargetingMode = TargetingMode.None;
|
||||
pendingGroundTargetSlotIndex = -1;
|
||||
|
||||
if (groundTargetIndicator != null)
|
||||
groundTargetIndicator.Hide();
|
||||
Debug.Log("[GroundTarget] 타겟팅 모드 취소");
|
||||
}
|
||||
|
||||
@@ -948,7 +966,7 @@ namespace Colosseum.Player
|
||||
return false;
|
||||
}
|
||||
|
||||
Ray ray = mainCamera.ScreenPointToRay(new Vector3(Screen.width * 0.5f, Screen.height * 0.5f, 0f));
|
||||
Ray ray = mainCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
|
||||
|
||||
if (!Physics.Raycast(ray, out RaycastHit hit, groundTargetMaxRange, groundTargetLayers))
|
||||
return false;
|
||||
@@ -975,6 +993,9 @@ namespace Colosseum.Player
|
||||
currentTargetingMode = TargetingMode.None;
|
||||
pendingGroundTargetSlotIndex = -1;
|
||||
|
||||
if (groundTargetIndicator != null)
|
||||
groundTargetIndicator.Hide();
|
||||
|
||||
// 캐릭터를 타겟 방향으로 회전
|
||||
Vector3 flatTargetPos = new Vector3(groundPosition.x, transform.position.y, groundPosition.z);
|
||||
if ((flatTargetPos - transform.position).sqrMagnitude > 0.01f)
|
||||
@@ -983,7 +1004,7 @@ namespace Colosseum.Player
|
||||
}
|
||||
|
||||
// 서버에 스킬 실행 요청
|
||||
RequestSkillExecutionRpc(slotIndex, 0, groundPosition);
|
||||
RequestSkillExecutionRpc(slotIndex, 0, groundPosition, true);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -991,6 +1012,12 @@ namespace Colosseum.Player
|
||||
if (currentTargetingMode != TargetingMode.GroundTarget)
|
||||
return;
|
||||
|
||||
// 인디케이터 위치 실시간 갱신
|
||||
if (groundTargetIndicator != null && RaycastForGroundPosition(out Vector3 currentPos))
|
||||
{
|
||||
groundTargetIndicator.UpdatePosition(currentPos);
|
||||
}
|
||||
|
||||
// 좌클릭: 지면 타겟 확정
|
||||
if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
@@ -1029,7 +1056,7 @@ namespace Colosseum.Player
|
||||
return null;
|
||||
}
|
||||
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
Ray ray = mainCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
|
||||
float maxDistance = allyTargetMaxRange > 0f ? allyTargetMaxRange : Mathf.Infinity;
|
||||
|
||||
if (!Physics.Raycast(ray, out RaycastHit hit, maxDistance, allyDetectionLayers))
|
||||
|
||||
Reference in New Issue
Block a user