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:
2026-04-02 22:25:19 +09:00
parent 57ab230c61
commit 188b134062
34 changed files with 42905 additions and 102 deletions

View File

@@ -74,6 +74,7 @@ namespace Colosseum.Skills
private int currentRepeatCount = 1;
private int currentIterationIndex = 0;
private GameObject currentTargetOverride;
private Vector3? currentGroundTargetPosition;
// 쿨타임 추적
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
@@ -270,6 +271,17 @@ namespace Colosseum.Skills
return ExecuteSkillInternal(loadoutEntry);
}
/// <summary>
/// 지면 타겟 위치와 함께 스킬 시전.
/// Ground Target 스킬에서 사용합니다.
/// </summary>
public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry, GameObject targetOverride, Vector3 groundTargetPosition)
{
currentTargetOverride = targetOverride;
currentGroundTargetPosition = groundTargetPosition;
return ExecuteSkillInternal(loadoutEntry);
}
/// <summary>
/// 스킬 시전 공통 로직
/// </summary>
@@ -328,17 +340,29 @@ namespace Colosseum.Skills
if (currentSkill == null)
return;
// VFX는 모든 클라이언트에서 로컬 생성 (서버 가드 무시)
for (int i = 0; i < currentCastStartEffects.Count; i++)
{
SkillEffect effect = currentCastStartEffects[i];
if (effect != null && effect.IsVisualOnly)
{
if (debugMode) Debug.Log($"[Skill] Cast start VFX: {effect.name} (index {i})");
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
}
}
// 게임플레이 효과는 서버에서만 실행
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
return;
for (int i = 0; i < currentCastStartEffects.Count; i++)
{
SkillEffect effect = currentCastStartEffects[i];
if (effect == null)
if (effect == null || effect.IsVisualOnly)
continue;
if (debugMode) Debug.Log($"[Skill] Cast start effect: {effect.name} (index {i})");
effect.ExecuteOnCast(gameObject, currentTargetOverride);
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
}
if (currentCastStartAbnormalities.Count <= 0)
@@ -371,23 +395,35 @@ namespace Colosseum.Skills
if (currentSkill == null || currentTriggeredEffects.Count == 0)
return;
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
return;
if (currentSkill.SkillClip != null && currentSkill.SkillClip.events != null && currentSkill.SkillClip.events.Length > 0)
return;
if (!currentTriggeredEffects.TryGetValue(0, out List<SkillEffect> effectsAtZero))
return;
// VFX는 모든 클라이언트에서 로컬 생성 (서버 가드 무시)
for (int i = 0; i < effectsAtZero.Count; i++)
{
SkillEffect effect = effectsAtZero[i];
if (effect == null || effect.TargetType != TargetType.Self)
if (effect != null && effect.IsVisualOnly && effect.TargetType == TargetType.Self)
{
if (debugMode) Debug.Log($"[Skill] Immediate self VFX: {effect.name} (index {i})");
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
}
}
// 게임플레이 효과는 서버에서만 실행
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
return;
for (int i = 0; i < effectsAtZero.Count; i++)
{
SkillEffect effect = effectsAtZero[i];
if (effect == null || effect.TargetType != TargetType.Self || effect.IsVisualOnly)
continue;
if (debugMode) Debug.Log($"[Skill] Immediate self effect: {effect.name} (index {i})");
effect.ExecuteOnCast(gameObject, currentTargetOverride);
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
}
}
@@ -561,8 +597,6 @@ namespace Colosseum.Skills
/// </summary>
public void OnEffect(int index)
{
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return;
if (currentSkill == null)
{
if (debugMode) Debug.LogWarning("[Effect] No skill executing");
@@ -583,10 +617,24 @@ namespace Colosseum.Skills
return;
}
// VFX는 모든 클라이언트에서 로컬 생성 (서버 가드 무시)
for (int i = 0; i < effects.Count; i++)
{
SkillEffect effect = effects[i];
if (effect == null)
if (effect != null && effect.IsVisualOnly)
{
if (debugMode) Debug.Log($"[Effect] VFX: {effect.name} (index {index})");
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
}
}
// 게임플레이 효과는 서버에서만 실행
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return;
for (int i = 0; i < effects.Count; i++)
{
SkillEffect effect = effects[i];
if (effect == null || effect.IsVisualOnly)
continue;
if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})");
@@ -594,13 +642,11 @@ namespace Colosseum.Skills
// 공격 범위 시각화
if (showAreaDebug)
{
effect.DrawDebugRange(gameObject, debugDrawDuration);
effect.DrawDebugRange(gameObject, debugDrawDuration, currentGroundTargetPosition);
}
effect.ExecuteOnCast(gameObject, currentTargetOverride);
effect.ExecuteOnCast(gameObject, currentTargetOverride, currentGroundTargetPosition);
}
ApplyTriggeredAbnormalities(index, effects);
}
/// <summary>
@@ -684,6 +730,7 @@ namespace Colosseum.Skills
currentTriggeredAbnormalities.Clear();
currentTriggeredTargetsBuffer.Clear();
currentTargetOverride = null;
currentGroundTargetPosition = null;
currentClipSequenceIndex = 0;
currentRepeatCount = 1;
currentIterationIndex = 0;
@@ -709,7 +756,7 @@ namespace Colosseum.Skills
if (effect == null || effect.TargetType == TargetType.Self)
continue;
effect.CollectTargets(gameObject, currentTriggeredTargetsBuffer, currentTargetOverride);
effect.CollectTargets(gameObject, currentTriggeredTargetsBuffer, currentTargetOverride, currentGroundTargetPosition);
}
if (currentTriggeredTargetsBuffer.Count == 0)