shin0624

Unity Engine / Client Programming / IndieGame Develop Team / Computer Engineering

Game Programmer With Unity Engine GitHub

UnityPortfolio/LoeGriA

[Unity] 에너미 넉백(KnockBack) 효과 보완 및 파티클 이벤트 적용

shin0624 2024. 12. 28. 01:39
 

[Unity] Animator를 사용한 플레이어 조작 및 Raycast를 사용한 Knockback 효과 구현

[Unity] Animator를 사용한 플레이어 조작 및 Raycast를 사용한 Knockback 효과 구현 이전에 멈춰두었던 프로젝트를 다시 잡았다. 인트로 씬부터 다시 플레이 해 보았는데, 뜯어 고칠게 한 두개가

shin0624.tistory.com

이전에 구현한 에너미 넉백(Knockback) 효과를 보완하고, 공격 시 플레이어의 무기에서 파티클 효과가 발생하도록 구현해보았다.

 

1. 넉백 효과

우선 공격 시 에너미가 넉백되는 효과를 Goblin 이외에 다른 에너미들을 구현할 때 재사용하기 쉽도록 interface를 생성하고, 이 인터페이스를 상속받은 BaseHitHandler 추상 클래스를 생성하여 OnHit()메서드를 구현했다.

 

<IDamageable 인터페이스>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IDamageable
{
    void OnHit(float damage, Vector3 hitPoint, Vector3 hitNormal, float knockBackForce);
}

<BaseHitHandler 클래스>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public abstract class BaseHitHandler : MonoBehaviour, IDamageable
{
    //재사용 가능한 추상클래스 베이스 히트 핸들러 생성
    protected Animator animator;
    protected NavMeshAgent agent;
    protected Rigidbody rb;

    protected virtual void Awake()
    {
    animator = GetComponent<Animator>();
        if (animator == null)
            Debug.LogError($"{gameObject.name} is missing an Animator component.");

    agent = GetComponent<NavMeshAgent>();
        if (agent == null)
            Debug.LogError($"{gameObject.name} is missing a NavMeshAgent component.");

    rb = GetComponent<Rigidbody>();
        if (rb == null)
            Debug.LogError($"{gameObject.name} is missing a Rigidbody component.");
    }

    public abstract void OnHit(float damage, Vector3 hitPoint, Vector3 hitNormal, float knockBackForce);
    /* 
    IDamageable 인터페이스의 OnHit 메서드를 구현. 파생클래스에서 오버라이드하여 사용
    damage : 플레이어가 적에게 가하는 데미지 값. 체력 감소량을 결정
    hitpoint : 공격이 적중한 정확한 위치. 파티클 효과나 피격 효과를 표시할 위치 지정에 사용. RaycastHit.point 값을 전달받음
    hitNormal : 충돌이 발생한 표면의 법선 벡터. 튕겨나가는 방향이나 이펙트의 방향을 결정할 때 사용. RaycastHit.normal 값을 전달받음
    knocBackForce : 피격 시 밀려나가는 힘의 크기. 거리에 따라 감소하는 넉백 효과 적용 등에 사용.
    
    */
}

 

플레이어의 공격 애니메이션과 에너미의 넉백 타이밍을 맞추기 위해, PlayerController의 Attack메서드를 코루틴으로 바꾸었다.

public class PlayerController : MonoBehaviour
{
   private IEnumerator AttackRountine()// Attack메서드를 코루틴으로 변경. 애니메이션 클립과 실제 공격 판정 간 간극 해소
    {
        SetState(Define.PlayerState.ATTACK, "ATTACK");
        Anim.SetBool("IsAttack", true);

        yield return new WaitForSeconds(attackDelay);//공격 딜레이 변수값 만큼 대기 후 재생.

        //실제 공격 판정
        Vector3 rayOrigin = transform.position + Vector3.up * 1.5f;
        Vector3 rayDirection = transform.forward;
        float rayRadius = 1.0f;//구체 레이캐스트의 반지름

        //구체 레이캐스트를 사용하여 넓은 범위 감지. 단일 레이캐스트보다 더 자연스러운 무기 판정 가능
        RaycastHit[] hits = Physics.SphereCastAll(rayOrigin, rayRadius, rayDirection, attackRange, enemyLayer);// 에너미 레이어를 새로 설정해서 에너미에게만 효과가 가해지도록 함.
        
        //디버그 시각화
        Debug.DrawRay(rayOrigin, rayDirection * attackRange, Color.red, 3.0f);
        Debug.DrawLine(
            rayOrigin + rayDirection * attackRange + Vector3.up * rayRadius,
            rayOrigin + rayDirection * attackRange - Vector3.up * rayRadius,
            Color.blue,
            3.0f
        );
        foreach(RaycastHit hit in hits)
        {
            if(hit.collider.CompareTag("Enemy"))
            {
               Debug.Log($"Hit object: {hit.collider.gameObject.name} | Layer: {LayerMask.LayerToName(hit.collider.gameObject.layer)}");
                //Rigidbody enemyRb = hit.collider.GetComponent<Rigidbody>();
                IDamageable damageable = hit.collider.GetComponent<IDamageable>();
                if(damageable != null)
                {
                    //거리에 따른 넉백 힘 감소
                      float distanceMultiplier = 1 - (hit.distance / attackRange);
                      float finalKnockbackForce = knockBackForce * distanceMultiplier; // 타격 지점에서 멀 수록 넉백 효과가 약해짐

                    damageable.OnHit(10.0f, hit.point, hit.normal, finalKnockbackForce);//10의 데미지, 공격이 적중한 위치, 충돌 표면의 법선벡터, 밀려나가는 힘의 크기를 매개변수로 전달.
                    // ray가 닿은 콜라이더를 가진 에너미의 idamageable 인터페이스를 찾아서 OnHit 메서드를 호출
                }
            }
        }
        yield return new WaitForSeconds(Anim.GetCurrentAnimatorStateInfo(0).length);//애니메이션 클립 길이만큼 대기
        ResetAttack();//공격 애니메이션 리셋
    }
    
    private void ResetAttack()
    {
        Anim.SetBool("IsAttack", false); // 공격 애니메이션 리셋
    }
    }

 

Goblin의 피격 판정을 위해 Animator에 Hit 클립을 추가하고, HitCoroutine을 선언했다. 어떤 상태에 있던 피격을 당하면 한번 넉백 되고, 플레이어와의 거리에 따라서 적절한 상태로 전환되도록 했다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class EnemyController : MonoBehaviour, IDamageable
{  
    //----------------- 고블린 에너미 기본 변수 -----------------
    [SerializeField] private GameObject Player;
    [SerializeField] private NavMeshAgent Agent; // Bake된 NavMesh에서 활동할 에너미
    [SerializeField] private Animator Anim;
    [SerializeField] private Rigidbody rb;
    private Define.EnemyState state;//에너미 상태 변수
    private float currentHitTime = 0.0f; // 현재 피격 시간
    
 //----------------- 범위, 거리 변수 -----------------
    [SerializeField, Range(0f, 20.0f)] private float ChaseRange = 12.0f;//플레이어 추격 가능 범위
    [SerializeField, Range(0f, 20.0f)] private float DetectionRange = 8.0f;// 플레이어 탐지 거리
    [SerializeField, Range(0f, 20.0f)] private float AttackRange = 2.0f;// 공격 가능 범위
    [SerializeField] private float hitRecoveryTime = 0.5f; // 피격 후 회복 시간
    private List<Vector3> Path = new List<Vector3>();// A*알고리즘으로 계산된 경로를저장할 리스트
    private int CurrentPathIndex = 0;// 에너미가 현재 이동중인 경로 지점의 인덱스. 처음에는 Path[0]으로 이동.
    private float DistanceToPlayer;//플레이어와의 거리를 저장할 변수

 //----------------- 소리, 기타  -----------------
    [SerializeField] private AudioClip roarSound;
    [SerializeField] private AudioClip hitSound;
    private AudioManager audioManager;
    private Coroutine hitCoroutine; // hit 상태 처리를 코루틴으로 수행

    private void Start()
    {
        Player = GameObject.FindGameObjectWithTag("Player");
        state = Define.EnemyState.IDLE;//초기상태 : IDLE
        Agent = GetComponent<NavMeshAgent>();
        Agent.isStopped = true;
        rb = GetComponent<Rigidbody>();
        audioManager = FindAnyObjectByType<AudioManager>();
        BeginPatrol();//처음에 탐지 시작
    }

    private void Update()//Hit상태는 코루틴에서 처리하니까 switch문에서 제외
    {
        DistanceToPlayer = Vector3.Distance(transform.position, Player.transform.position);//플레이어와 에너미 사이의 거리를 계산
        switch (state)
        {
            case Define.EnemyState.IDLE:
            case Define.EnemyState.WALKING:
                Patrol();//경로에 따라 탐색을 계속 진행
                break;
            case Define.EnemyState.RUNNING:
                UpdateChase();// 플레이어를 추격
                break;
            case Define.EnemyState.ATTACK:
                UpdateAttack();//플레이어를 공격
                break;
        }
    }

    private void Patrol()// 탐색상태
    {
        if (Agent.isOnNavMesh && Path.Count > 0)
        {
            Agent.isStopped = false;
            Agent.speed = 3.0f;

            if (DistanceToPlayer <= DetectionRange && state != Define.EnemyState.ATTACK)//탐지 범위 내에 플레이어가 존재하면 && 공격 상태가 아닐 때 추격을 시작한다.
            {
                audioManager.PlaySound(roarSound);
                SetState(Define.EnemyState.RUNNING, "RUNNING");
                return;
            }

            if (!Agent.hasPath || Agent.remainingDistance < 1.0f)//현재 경로가 없거나, 목표 지점에 도달하면
            {
                if (CurrentPathIndex < Path.Count)//경로 상의 다음 지점으로 이동
                {
                    Agent.SetDestination(Path[CurrentPathIndex]);
                    CurrentPathIndex++;//현재 이동중인 경로의 다음 경로로 이동할 것.
                }
                else// 경로 끝에 도달하면 새 경로 계산
                {
                    CalculateNewPath();
                }
            }
        }
    }

    private void BeginPatrol()
    {
        SetState(Define.EnemyState.WALKING, "WALKING"); // 걸어다니며 탐색 시작
        Agent.isStopped = false;
        CalculateNewPath();// 새로운 경로를 계산
    }

    private void UpdateAttack()// 공격 후 -> 플레이어와의 거리가 공격 가능 범위를 넘어간 상태 && 플레이어와의 거리가 아직 탐지 범위에 포함될 때 다시 쫒아가 플레이어를 공격해야 함.
    {
        if (Agent.isOnNavMesh)
        {
            Agent.isStopped = true;//공격 시 그 자리에서 멈춤
            SetState(Define.EnemyState.ATTACK, "ATTACK");
            if (DistanceToPlayer > AttackRange)// 공격범위를 벗어났다면
            {
                UpdateChase();
                return;
            }
        }
    }

    private void UpdateChase()
    {
        if (Agent.isOnNavMesh)
        {
            
            SetState(Define.EnemyState.RUNNING, "RUNNING");
            Agent.isStopped = false;
            Agent.speed = 4.0f;
            Agent.destination = Player.transform.position;// 목적지를 플레이어 포지션으로 설정하여 추격
            if (DistanceToPlayer > ChaseRange)//플레이어와의 거리가 추격 가능 범위를 벗어났다면
            {
                BeginPatrol();//탐지 상태로 전환 
            }
            else if (DistanceToPlayer < AttackRange)//공격 가능 범위까지 다가갔다면
            {
                UpdateAttack();
                return;
            }
        }
    }

    private void CalculateNewPath()// 새로운 경로를 계산하는 메서드
    {
        Vector3 RandomDirection = Random.insideUnitSphere * 10.0f;// 반경 1을 갖는 구 안의 임의 지점 * 10으로 경로 설정
        RandomDirection += transform.position;// 에너미 포지션 값에 랜덤 값을 더한다

        NavMeshHit hit;
        //SamplePosition((Vector3 sourcePosition, out NavmeshHit hit, float maxDistance, int areaMask)
        // 샘플포지션 메서드 : areaMask에 해당하는 NavMesh 중에서, maxDistance 반경 내에서 sourcePosition에 최근접한 위치를 찾아 hit에 담는다.
        if (NavMesh.SamplePosition(RandomDirection, out hit, 10.0f, NavMesh.AllAreas))
        {
            Path.Clear();
            Path.Add(hit.position);
            CurrentPathIndex = 0;// 현재위치를 담는 변수 초기화
            Agent.SetDestination(Path[CurrentPathIndex]);
        }
        
    }

    public void OnHit(float damage, Vector3 hitPoint, Vector3 hitNormal, float knockbackForece)//피격 처리 메서드
    {
        if (state == Define.EnemyState.HIT) return;//피격 상태일 때는 추가 피격을 받지 않는다. 피격 회복시간을 매우 짧게 설정
        if(hitCoroutine!=null)
        {
            StopCoroutine(hitCoroutine);//이전 Hit코루틴이 실행 중이라면 중지.
        }
        hitCoroutine = StartCoroutine(HitRoutine(hitPoint, knockbackForece));//새로운 피격 코루틴 시작
    }

    private IEnumerator HitRoutine(Vector3 hitPoint, float knockbackForece)// Hit상태를 코루틴으로 관리. 이를 통해 피격 판정과 애니메이션 속도 간 동기화 및 피격 후 상태 고정을 예방한다.
    {
        //상태 변화 및 애니메이션 설정
        SetState(Define.EnemyState.HIT, "HIT");
        Agent.isStopped = true;
        currentHitTime = 0.0f;

        if(rb!=null)//넉백 효과 및 사운드 재생
        {
            Vector3 knockbackDirection = (transform.position - hitPoint).normalized;
            knockbackDirection.y = 0;
            
            rb.AddForce(knockbackDirection * knockbackForece, ForceMode.Impulse);
        }
        if(audioManager!=null && hitSound!=null)
        {
            audioManager.PlaySound(hitSound);
        }

        //Hit 애니메이션 길이만큼 대기
        float hitAnimLength = Anim.GetCurrentAnimatorStateInfo(0).length;// 현재 재생중인 애니메이션의 길이를 반환
        yield return new WaitForSeconds(hitAnimLength);

        //Hit 상태에서 복귀
        currentHitTime = 0.0f;
        Agent.isStopped = false;

         // Hit 후 플레이어와의 거리에 따라 적절한 상태로 전환
        if (DistanceToPlayer <= AttackRange)// 공격 당한 후 플레이어와의 거리가 공격 가능 범위 내에 있다면
        {
            SetState(Define.EnemyState.ATTACK, "ATTACK");
        }
        else if (DistanceToPlayer <= DetectionRange)// 공격 당한 후 플레이어를 쫒아갈 수 있다면
        {
            SetState(Define.EnemyState.RUNNING, "RUNNING");
        }
        else//그 외의 경우
        {
            SetState(Define.EnemyState.WALKING, "WALKING");
            BeginPatrol();
        }
        
        hitCoroutine = null;
    }

    private void SetState(Define.EnemyState NewState, string AnimationTrigger)// 상태변경 메서드
    {
        if (state != NewState) 
        {
            state = NewState;
            Anim.ResetTrigger("IDLE");
            Anim.ResetTrigger("WALKING");
            Anim.ResetTrigger("RUNNING");
            Anim.ResetTrigger("HIT");
            Anim.ResetTrigger("ATTACK");
            Anim.SetTrigger(AnimationTrigger);
            Debug.Log($"Enemy state changed to: {NewState} with animation: {AnimationTrigger}");
        }
    }
    
}

 

2. 파티클 시스템 적용

플레이어가 공격할 때, 에너미가 공격 당할 때 각각 파티클 시스템을 적용하려 한다.

에셋 스토어에서  파티클 에셋을 찾아서 임포트해준다.

플레이어가 검을 휘둘렀을 때 사용할 파티클
에너미가 피격당했을 때 사용할 파티클

파티클 시스템을 적용하려면 파티클 객체가 Hierarchy에 존재해야 하는데, 공격 할 때 마다 Instantiate하는 것 보다 오브젝트 풀링으로 관리하면 더 나을 듯 하다.

Hierarchy의 Managers 객체에 공격 파티클 풀과 히트 파티컬 풀을 각각 생성해준다.

 

크기 10의 Queue를 사용하여 오브젝트 풀링을 위한 ParticlePool 스크립트를 작성한다. 이 스크립트를 Manager에 생성했던 공격 파티클 풀, 히트 파티컬 풀 객체에 각각 추가하고, 파티클 시스템 객체를 할당한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ParticlePool : MonoBehaviour
{
    //파티클 시스템을 오브젝트 풀링으로 관리하여 성능 최적화를 시도하기 위한 클래스. 파티클 시스템이 작동하려면 Hierarchy에 객체가 존재하거나 Instantiate로 생성해주어야 하는데, 이를 최소화하기 위함
    [SerializeField] private GameObject particlePrefab;//파티클 프리팹
    [SerializeField] private int poolSize = 10;//풀 크기
    private Queue<GameObject> pool = new Queue<GameObject>();//파티클 오브젝트 풀

    private void Awake() {//초기화 : 풀 크기만큼 오브젝트 생성
        for(int i=0; i<poolSize; i++)
        {
            GameObject particle = Instantiate(particlePrefab);//파티클 생성
            particle.SetActive(false);//비활성화
            pool.Enqueue(particle);//풀에 추가
        }
    }
    
    public GameObject GetParticle()//파티클 오브젝트 풀에서 파티클을 가져오는 메서드
    {
        if(pool.Count>0)//풀에 파티클이 존재하면
        {
            GameObject particle = pool.Dequeue();//풀에서 파티클을 꺼내옴
            particle.SetActive(true);//활성화
            return particle;
        }
        
        else{
            GameObject particle = Instantiate(particlePrefab, transform);//풀에 파티클이 없으면 새로 생성
            particle.SetActive(true);//활성화
            return particle;
        }
    }

    public void ReturnParticle(GameObject particle)//파티클을 풀로 반환하는 메서드
    {
        particle.SetActive(false);//비활성화
        pool.Enqueue(particle);//풀로 반환
    }
}

 

 

이 다음에, 각각 별도의 풀을 사용하여 파티클을 관리할 PlayerAttackParticleManager 클래스를 생성하고, Managers 객체에 추가한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerAttackParticleManager : MonoBehaviour
{
    [SerializeField] private ParticlePool attackParticlePool;//공격 파티클 풀
    [SerializeField] private ParticlePool hitParticlePool;//히트 파티클 풀

    public void PlayAttackParticle(Vector3 position, Quaternion rotation)//공격 시 파티클 재생
    {
       if (attackParticlePool != null)//공격 파티클 풀이 존재하면
        {
            GameObject particle = attackParticlePool.GetParticle();//파티클 오브젝트 풀에서 파티클 가져오기
            particle.transform.position = position;//위치 설정
            particle.transform.rotation = rotation;//회전 설정

            ParticleSystem ps = particle.GetComponent<ParticleSystem>();//파티클 시스템 컴포넌트 가져오기
            if (ps != null)//파티클 시스템이 존재하면
            {
                ps.Play();//파티클 재생
                StartCoroutine(ReturnToPoolAfterDuration(particle, ps.main.duration + ps.main.startLifetime.constant));//파티클 재생 시간 후 풀로 반환
            }
        }
    }

    public void PlayHitParticle(Vector3 hitPoint, Vector3 hitNormal)// 피격 시 파티클 재생 
    {
        if (hitParticlePool != null)//히트 파티클 풀이 존재하면
        {
            GameObject particle = hitParticlePool.GetParticle();//파티클 오브젝트 풀에서 파티클 가져오기
            particle.transform.position = hitPoint;//위치 설정
            particle.transform.rotation = Quaternion.LookRotation(hitNormal);//회전 설정

            ParticleSystem ps = particle.GetComponent<ParticleSystem>();//파티클 시스템 컴포넌트 가져오기
            if (ps != null)
            {
                ps.Play();
                StartCoroutine(ReturnToPoolAfterDuration(particle, ps.main.duration + ps.main.startLifetime.constant));
            }
        }
    }

    private IEnumerator ReturnToPoolAfterDuration(GameObject particle, float delay)// 지정된 시간이 지난 후 파티클 풀로 반환
    {
        yield return new WaitForSeconds(delay);//지정된 시간만큼 대기
        if (attackParticlePool!=null && particle.name.Contains("Attack"))// 공격 파티클이 존재하면
        {
            attackParticlePool.ReturnParticle(particle);//공격 파티클 풀로 반환
        }
        else if(hitParticlePool!=null) //히트 파티클이 존재하면
        {
            hitParticlePool.ReturnParticle(particle);//히트 파티클 풀로 반환
        }
    }
}

 

Managers객체에 파티클 매니저를 추가하고 각 풀을 할당

 

이제 PlayerController와 EnemyController에 각각 파티클 재생을 호출할 코드 몇 줄만 추가하면 완성이다.

PlayerController : PlayerAttackParticleManager 인스턴스 선언, AttackCoroutine에 파티클 호출 추가

EnemyController : PlayerAttackParticleManager 인스턴스 선언, OnHit에 파티클 호출 추가

public class EnemyController : MonoBehaviour, IDamageable
{
......

[SerializeField] private PlayerAttackParticleManager particleManager; // 에너미 공격 이펙트 매니저
   
   ......
   
  public void OnHit(float damage, Vector3 hitPoint, Vector3 hitNormal, float knockbackForece)//피격 처리 메서드
    {
        if (state == Define.EnemyState.HIT) return;//피격 상태일 때는 추가 피격을 받지 않는다. 피격 회복시간을 매우 짧게 설정
        if(hitCoroutine!=null)
        {
            StopCoroutine(hitCoroutine);//이전 Hit코루틴이 실행 중이라면 중지.
        }
        hitCoroutine = StartCoroutine(HitRoutine(hitPoint, knockbackForece));//새로운 피격 코루틴 시작
        particleManager.PlayHitParticle(hitPoint,hitNormal);//피격 파티클 재생
    }
    
    ......
    }

 

public class PlayerController : MonoBehaviour
{
......

[SerializeField] private PlayerAttackParticleManager particleManager;//플레이어 공격 이펙트 매니저

......

 private IEnumerator AttackRountine()// Attack메서드를 코루틴으로 변경. 애니메이션 클립과 실제 공격 판정 간 간극 해소
    {
        SetState(Define.PlayerState.ATTACK, "ATTACK");
        Anim.SetBool("IsAttack", true);

        //공격 파티클 호출
        Vector3 particlePosition = transform.position + transform.forward * 1.5f; //파티클 위치를 검 위치로
        Quaternion particleRotation = transform.rotation;
        particleManager.PlayAttackParticle(particlePosition, particleRotation);//공격 파티클 재생


        yield return new WaitForSeconds(attackDelay);//공격 딜레이 변수값 만큼 대기 후 재생.

        //실제 공격 판정
        Vector3 rayOrigin = transform.position + Vector3.up * 1.5f;
        Vector3 rayDirection = transform.forward;
        float rayRadius = 1.0f;//구체 레이캐스트의 반지름

        //구체 레이캐스트를 사용하여 넓은 범위 감지. 단일 레이캐스트보다 더 자연스러운 무기 판정 가능
        RaycastHit[] hits = Physics.SphereCastAll(rayOrigin, rayRadius, rayDirection, attackRange, enemyLayer);// 에너미 레이어를 새로 설정해서 에너미에게만 효과가 가해지도록 함.
        
        //디버그 시각화
        Debug.DrawRay(rayOrigin, rayDirection * attackRange, Color.red, 3.0f);
        Debug.DrawLine(
            rayOrigin + rayDirection * attackRange + Vector3.up * rayRadius,
            rayOrigin + rayDirection * attackRange - Vector3.up * rayRadius,
            Color.blue,
            3.0f
        );
        foreach(RaycastHit hit in hits)
        {
            if(hit.collider.CompareTag("Enemy"))
            {
               Debug.Log($"Hit object: {hit.collider.gameObject.name} | Layer: {LayerMask.LayerToName(hit.collider.gameObject.layer)}");
                //Rigidbody enemyRb = hit.collider.GetComponent<Rigidbody>();
                IDamageable damageable = hit.collider.GetComponent<IDamageable>();
                if(damageable != null)
                {
                    //거리에 따른 넉백 힘 감소
                      float distanceMultiplier = 1 - (hit.distance / attackRange);
                      float finalKnockbackForce = knockBackForce * distanceMultiplier; // 타격 지점에서 멀 수록 넉백 효과가 약해짐

                    damageable.OnHit(10.0f, hit.point, hit.normal, finalKnockbackForce);//10의 데미지, 공격이 적중한 위치, 충돌 표면의 법선벡터, 밀려나가는 힘의 크기를 매개변수로 전달.
                    // ray가 닿은 콜라이더를 가진 에너미의 idamageable 인터페이스를 찾아서 OnHit 메서드를 호출
                }
            }
        }
        yield return new WaitForSeconds(Anim.GetCurrentAnimatorStateInfo(0).length);//애니메이션 클립 길이만큼 대기
        ResetAttack();//공격 애니메이션 리셋
    }
    
    ......
    }

 

이제 플레이 해보면, 의도한대로 플레이어가 공격했을 때 에너미가 넉백되고, 파티클이 호출되는 것을 볼 수 있다.

 

피격 파티클이 에너미 위치에서 터지게 하고, 애니메이션 재생 속도와 타이밍 동기화가 더 필요할 것 같다.

 

파티클 + 넉백이 모두 추가된 코드를 첨부하면 글이 너무 길어져서, 깃허브 링크로 대체한다.


PlayerController  

 

Unity_LoeGriA_Remake/Assets/Scripts/24.07new/Controller/CharacterController/PlayerController.cs at main · shin0624/Unity_LoeGri

프로젝트 구조 변경으로 LoeGriA 리포지토리 재생성. Contribute to shin0624/Unity_LoeGriA_Remake development by creating an account on GitHub.

github.com

 

EnemyController

 

Unity_LoeGriA_Remake/Assets/Scripts/24.07new/Controller/CharacterController/EnemyContoller.cs at main · shin0624/Unity_LoeGriA_

프로젝트 구조 변경으로 LoeGriA 리포지토리 재생성. Contribute to shin0624/Unity_LoeGriA_Remake development by creating an account on GitHub.

github.com