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:
122
Assets/_Game/Scripts/Skills/Effects/VfxEffect.cs
Normal file
122
Assets/_Game/Scripts/Skills/Effects/VfxEffect.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Skills.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// VFX 생성 효과.
|
||||
/// 지정된 위치에 파티클 프리팹을 생성하고, 생명주기 및 재생 옵션을 제어합니다.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "VfxEffect", menuName = "Colosseum/Skills/Effects/VFX")]
|
||||
public class VfxEffect : SkillEffect
|
||||
{
|
||||
[Header("VFX 설정")]
|
||||
[Tooltip("생성할 VFX 프리팹")]
|
||||
[SerializeField] private GameObject vfxPrefab;
|
||||
|
||||
[Header("위치 설정")]
|
||||
[Tooltip("VFX 생성 위치: Caster=시전자, CasterForward=시전자 정면, Target=대상 위치, GroundPoint=지면 타겟 위치")]
|
||||
[SerializeField] private VfxSpawnLocation spawnLocation = VfxSpawnLocation.Caster;
|
||||
[Tooltip("위치 오프셋")]
|
||||
[SerializeField] private Vector3 offset = Vector3.zero;
|
||||
|
||||
[Header("생명주기")]
|
||||
[Tooltip("VFX 자동 파괴 시간 (0이면 파괴하지 않음)")]
|
||||
[Min(0f)] [SerializeField] private float lifetime = 2f;
|
||||
[Tooltip("시전자에게 부모 설정 여부")]
|
||||
[SerializeField] private bool parentToCaster = false;
|
||||
[Tooltip("VFX 크기 배율")]
|
||||
[Min(0.01f)] [SerializeField] private float scaleMultiplier = 1f;
|
||||
|
||||
[Header("파티클 설정")]
|
||||
[Tooltip("ParticleSystem 자동 재생 여부 (false면 스폰만 하고 외부에서 제어)")]
|
||||
[SerializeField] private bool autoPlay = true;
|
||||
[Tooltip("ParticleSystem 루핑 여부")]
|
||||
[SerializeField] private bool loop = false;
|
||||
|
||||
/// <summary>
|
||||
/// VFX 효과는 순수 시각 효과이므로 모든 클라이언트에서 로컬 실행됩니다.
|
||||
/// </summary>
|
||||
public override bool IsVisualOnly => true;
|
||||
|
||||
private Vector3? groundPosition;
|
||||
|
||||
/// <summary>
|
||||
/// 지면 타겟 위치를 캡처하여 ApplyEffect에서 사용할 수 있도록 합니다.
|
||||
/// </summary>
|
||||
public override void ExecuteOnCast(GameObject caster, GameObject targetOverride = null, Vector3? groundPosition = null)
|
||||
{
|
||||
this.groundPosition = groundPosition;
|
||||
base.ExecuteOnCast(caster, targetOverride, groundPosition);
|
||||
this.groundPosition = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VFX 프리팹을 생성하고 파티클을 재생합니다.
|
||||
/// </summary>
|
||||
protected override void ApplyEffect(GameObject caster, GameObject target)
|
||||
{
|
||||
if (vfxPrefab == null || caster == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
Vector3 spawnPos = GetSpawnPosition(caster, target);
|
||||
Quaternion spawnRot = caster.transform.rotation;
|
||||
Transform parent = parentToCaster ? caster.transform : null;
|
||||
|
||||
// 비제네릭 Instantiate로 캐스팅 예외 방지
|
||||
GameObject instance = (GameObject)Object.Instantiate(vfxPrefab, spawnPos + offset, spawnRot, parent);
|
||||
|
||||
instance.transform.localScale *= scaleMultiplier;
|
||||
|
||||
if (parentToCaster)
|
||||
instance.transform.localPosition = offset;
|
||||
|
||||
if (autoPlay)
|
||||
{
|
||||
ParticleSystem[] particleSystems = instance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
foreach (var ps in particleSystems)
|
||||
{
|
||||
var main = ps.main;
|
||||
main.loop = loop;
|
||||
|
||||
if (loop)
|
||||
main.stopAction = ParticleSystemStopAction.None;
|
||||
|
||||
ps.Clear(true);
|
||||
ps.Play(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (lifetime > 0f)
|
||||
Object.Destroy(instance, lifetime);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"[VfxEffect] VFX 생성 실패: {e.Message}\n{e.StackTrace}", this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// spawnLocation에 따른 VFX 생성 위치를 계산합니다.
|
||||
/// </summary>
|
||||
private Vector3 GetSpawnPosition(GameObject caster, GameObject target)
|
||||
{
|
||||
return spawnLocation switch
|
||||
{
|
||||
VfxSpawnLocation.Caster => caster.transform.position,
|
||||
VfxSpawnLocation.CasterForward => caster.transform.position + caster.transform.forward * 2f,
|
||||
VfxSpawnLocation.Target => target != null ? target.transform.position : caster.transform.position,
|
||||
VfxSpawnLocation.GroundPoint => groundPosition ?? caster.transform.position,
|
||||
_ => caster.transform.position
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum VfxSpawnLocation
|
||||
{
|
||||
Caster,
|
||||
CasterForward,
|
||||
Target,
|
||||
GroundPoint
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Skills/Effects/VfxEffect.cs.meta
Normal file
2
Assets/_Game/Scripts/Skills/Effects/VfxEffect.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69581e050479a094782d2ca9eb142fe4
|
||||
@@ -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)
|
||||
|
||||
@@ -45,14 +45,21 @@ namespace Colosseum.Skills
|
||||
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 void ExecuteOnCast(GameObject caster, GameObject targetOverride = null)
|
||||
public virtual void ExecuteOnCast(GameObject caster, GameObject targetOverride = null, Vector3? groundPosition = null)
|
||||
{
|
||||
List<GameObject> targets = new List<GameObject>();
|
||||
CollectTargets(caster, targets, targetOverride);
|
||||
CollectTargets(caster, targets, targetOverride, groundPosition);
|
||||
|
||||
for (int i = 0; i < targets.Count; i++)
|
||||
{
|
||||
@@ -64,7 +71,7 @@ namespace Colosseum.Skills
|
||||
/// 현재 효과가 영향을 줄 대상 목록을 수집합니다.
|
||||
/// 젬의 적중 이상상태 적용 등에서 동일한 타겟 해석을 재사용하기 위한 경로입니다.
|
||||
/// </summary>
|
||||
public void CollectTargets(GameObject caster, List<GameObject> destination, GameObject targetOverride = null)
|
||||
public void CollectTargets(GameObject caster, List<GameObject> destination, GameObject targetOverride = null, Vector3? groundPosition = null)
|
||||
{
|
||||
if (caster == null || destination == null)
|
||||
return;
|
||||
@@ -76,7 +83,7 @@ namespace Colosseum.Skills
|
||||
break;
|
||||
|
||||
case TargetType.Area:
|
||||
CollectAreaTargets(caster, destination);
|
||||
CollectAreaTargets(caster, destination, groundPosition);
|
||||
break;
|
||||
|
||||
case TargetType.SingleAlly:
|
||||
@@ -135,9 +142,9 @@ namespace Colosseum.Skills
|
||||
};
|
||||
}
|
||||
|
||||
private void CollectAreaTargets(GameObject caster, List<GameObject> destination)
|
||||
private void CollectAreaTargets(GameObject caster, List<GameObject> destination, Vector3? groundPosition = null)
|
||||
{
|
||||
Vector3 center = GetAreaCenter(caster);
|
||||
Vector3 center = GetAreaCenter(caster, groundPosition);
|
||||
Collider[] hits = Physics.OverlapSphere(center, Mathf.Max(areaRadius, fanRadius), targetLayers);
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
@@ -189,7 +196,7 @@ namespace Colosseum.Skills
|
||||
return angle <= fanHalfAngle;
|
||||
}
|
||||
|
||||
private Vector3 GetAreaCenter(GameObject caster)
|
||||
private Vector3 GetAreaCenter(GameObject caster, Vector3? groundPosition = null)
|
||||
{
|
||||
if (caster == null) return Vector3.zero;
|
||||
|
||||
@@ -197,6 +204,7 @@ namespace Colosseum.Skills
|
||||
{
|
||||
AreaCenterType.Caster => caster.transform.position,
|
||||
AreaCenterType.CasterForward => caster.transform.position + caster.transform.forward * areaRadius,
|
||||
AreaCenterType.GroundPoint => groundPosition ?? caster.transform.position,
|
||||
_ => caster.transform.position
|
||||
};
|
||||
}
|
||||
@@ -205,7 +213,7 @@ namespace Colosseum.Skills
|
||||
/// <summary>
|
||||
/// 공격 범위 시각화 (런타임 디버그용)
|
||||
/// </summary>
|
||||
public void DrawDebugRange(GameObject caster, float duration = 1f)
|
||||
public void DrawDebugRange(GameObject caster, float duration = 1f, Vector3? groundPosition = null)
|
||||
{
|
||||
if (targetType != TargetType.Area) return;
|
||||
|
||||
@@ -214,12 +222,15 @@ namespace Colosseum.Skills
|
||||
|
||||
if (areaShape == AreaShapeType.Sphere)
|
||||
{
|
||||
Vector3 center = GetAreaCenter(caster);
|
||||
Vector3 center = GetAreaCenter(caster, groundPosition);
|
||||
DebugDrawSphere(center, areaRadius, Color.red, duration);
|
||||
}
|
||||
else if (areaShape == AreaShapeType.Fan)
|
||||
{
|
||||
DebugDrawFan(casterPos, forward, fanOriginDistance, fanRadius, fanHalfAngle, Color.red, duration);
|
||||
Vector3 center = GetAreaCenter(caster, groundPosition);
|
||||
DebugDrawFan(center, groundPosition.HasValue && areaCenter == AreaCenterType.GroundPoint
|
||||
? (center - casterPos).normalized
|
||||
: forward, fanOriginDistance, fanRadius, fanHalfAngle, Color.red, duration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +307,8 @@ namespace Colosseum.Skills
|
||||
public enum AreaCenterType
|
||||
{
|
||||
Caster,
|
||||
CasterForward
|
||||
CasterForward,
|
||||
GroundPoint // 지면 지점 타겟팅 (Ground Target)
|
||||
}
|
||||
|
||||
public enum AreaShapeType
|
||||
|
||||
@@ -22,11 +22,18 @@ namespace Colosseum.Skills
|
||||
[SerializeField] private GameObject hitEffect;
|
||||
[SerializeField] private float hitEffectDuration = 2f;
|
||||
|
||||
[Header("트레일 이펙트")]
|
||||
[Tooltip("투사체 트레일 프리팹 (TrailRenderer가 포함된 프리팹). 미설정 시 트레일 없음.")]
|
||||
[SerializeField] private GameObject trailPrefab;
|
||||
[Tooltip("투사체 파괴 후 트레일이 남는 시간 (초)")]
|
||||
[Min(0.1f)] [SerializeField] private float trailDuration = 0.3f;
|
||||
|
||||
private GameObject caster;
|
||||
private SkillEffect sourceEffect;
|
||||
private int penetrationCount;
|
||||
private Rigidbody rb;
|
||||
private bool initialized;
|
||||
private GameObject trailInstance;
|
||||
|
||||
/// <summary>
|
||||
/// 투사체 초기화
|
||||
@@ -51,6 +58,17 @@ namespace Colosseum.Skills
|
||||
Physics.IgnoreCollision(mc, cc);
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// 트레일 생성 (서버/클라이언트 양쪽에서 실행)
|
||||
if (trailPrefab != null)
|
||||
{
|
||||
trailInstance = Instantiate(trailPrefab, transform);
|
||||
trailInstance.transform.localPosition = Vector3.zero;
|
||||
trailInstance.transform.localRotation = Quaternion.identity;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 서버에서만 수명 관리
|
||||
@@ -75,11 +93,12 @@ namespace Colosseum.Skills
|
||||
if (!sourceEffect.IsValidTarget(caster, other.gameObject))
|
||||
return;
|
||||
|
||||
// 충돌 이펙트 (서버에서 스폰 → 클라이언트에도 표시되려면 NetworkObject여야 함)
|
||||
// 충돌 이펙트 (서버 + 클라이언트 모두 표시)
|
||||
if (hitEffect != null)
|
||||
{
|
||||
var effect = Instantiate(hitEffect, transform.position, transform.rotation);
|
||||
Destroy(effect, hitEffectDuration);
|
||||
HitEffectClientRpc(transform.position, transform.rotation.eulerAngles);
|
||||
}
|
||||
|
||||
// 효과 적용
|
||||
@@ -96,6 +115,15 @@ namespace Colosseum.Skills
|
||||
private void ServerDespawn()
|
||||
{
|
||||
if (!IsServer || !IsSpawned) return;
|
||||
|
||||
// 트레일을 월드에 남겨서 자연스럽게 사라지게 함
|
||||
if (trailInstance != null)
|
||||
{
|
||||
trailInstance.transform.SetParent(null);
|
||||
Destroy(trailInstance, trailDuration);
|
||||
trailInstance = null;
|
||||
}
|
||||
|
||||
NetworkObject.Despawn(true);
|
||||
}
|
||||
|
||||
@@ -103,5 +131,18 @@ namespace Colosseum.Skills
|
||||
{
|
||||
transform.rotation = Quaternion.LookRotation(direction.normalized);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 클라이언트에 충돌 이펙트 표시
|
||||
/// </summary>
|
||||
[Rpc(SendTo.NotServer)]
|
||||
private void HitEffectClientRpc(Vector3 position, Vector3 eulerAngles)
|
||||
{
|
||||
if (hitEffect != null)
|
||||
{
|
||||
var effect = Instantiate(hitEffect, position, Quaternion.Euler(eulerAngles));
|
||||
Destroy(effect, hitEffectDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user