(주말 공부)XR플밍 - (18) UnityEngine3D 응용프로그래밍 - 미니 프로젝트 - TPS 기초/플레이어 공격 오류 버그 수정 및 플레이어 죽음, 몬스터 공격 구현 (5/18)

이형원·2025년 5월 18일
0

XR플밍

목록 보기
77/215

1. 공격 오류 버그 수정 내용

어제자 작업의 일환으로 문제가 되었던 공격 인식 버그에 관해 해결이 완료되었다.

문제점이 있었던 건 크게 두 가지로 나뉘었다.

  1. 총의 Target Layer가 Everything으로 설정되어 있었던 것.
    이로 인해 Trigger 영역이나 다른 영역까지 인식이 되어 몬스터보다 앞에 있는 영역이 인식되었던 것
  2. 몬스터가 Rigidbody가 있었던 것

우선 해당 문제를 해결하기 위해 Monster Layer를 만들고, Monster한테 해당 Layer를 반영한 다음 Gun에 Target Layer를 설정하였다.

이렇게 해도 총이 정말 일부 공격 범위로만 맞거나(ex. 머리 끝자락 왼쪽, 머리 끝자락 오른쪽) 안 맞는 경우가 생겨서 여러가지로 테스트해 봤다.
Raycast를 표시하게 해서 총구가 제대로 향하는지도 확인해봤는데, 분명 총구는 제대로 향하고 있었음을 확인할 수 있었다. 그러면 대체 왜 인식을 하지 못하는 걸까. 결국 검색까지 하면서 알아낸 것은 다음과 같다.

해당 내용을 해석한 바를 나름대로 정리해보자면, 요점은 아래와 같다.

한 오브젝트에 Collider와 Rigidbody가 같이 있을 경우, Raycast를 쐈을 때 Collider가 아닌 Rigidbody를 찾는 경우가 생긴다. Rigidbody로 Ray가 발사되었을 경우 RaycastHit이 반환되지 않을 수 있으므로, Rigidbody를 제거하거나 RaycastHit.collider.transform으로 찾는 방식을 취해야 한다.

일단 지금 상황과 같은 경우는 Rigidbody가 필요한 상황이 아니었기 때문에, Rigidbody를 제거하는 방식으로 진행해 보았다. 그 결과, 이제까지 문제가 생겼던 총의 몬스터 타격 관련 문제가 해결되었다.

2. Player의 죽음 구현

플레이어의 죽음을 구현하는 방법은 생각보다 간단했다. 하지만 애니메이션 쪽에서 생각보다 애를 먹었기 때문에 해당 내용만 간단하게 서술하고자 한다.

...

public void TakeDamage(int value)
{
    _status.CurrentHp.Value -= value;

    if (_status.CurrentHp.Value <= 0) Dead();
}

public void Dead()
{
    _animator.SetTrigger("IsAlive");        
    isControlActive = false;
}

...
  • 유의할 점

지금 보면 정말 별 거 아닌 실수이긴 했지만, Player가 죽었을 때 모든 Layer에 Player-Die 애니메이션을 반영해야 한다.

이것 때문에 플레이어가 죽었는데 서 있는 채로 죽는 모션이 출력되어 한동안 씨름을 헸다. 생각해보니 하반신은 Idle 그대로였던 상태였기 때문에 발생한 문제였다.

이런 부분에서 실수하지 않도록 조심하도록 하자.

3. 몬스터의 공격 모션 추가 및 플레이어에게로 데미지 반영

처음에 몬스터의 공격 모션을 추가할 당시 이와 같이 추가했었다.

하지만 가만 생각해보니 몬스터의 공격모션은 아래와 같이 만드는 게 맞는 것 같아서 IsAttack을 bool 변수 대신 Trigger를 써서 구현해보기로 했다.

몬스터가 플레이어와 가까워졌을 때 공격을 해야 하는데 어떻게 반영해야 할까, 곰곰이 생각해 보고 아래처럼 몬스터 공격범위를 설정해 놓고 해당 영역에 들어오면 몬스터가 공격하는 방식을 생각해 보았다.

OntriggerEnter, Stay, Exit을 사용하여 아래와 같이 코드를 작성하였다.

using UnityEngine;
using UnityEngine.AI;

public class MonsterAttack : MonoBehaviour
{
    private Animator _animator;
    private NavMeshAgent _navMeshAgent;
    public float _attackTime;

    [SerializeField] private int _monsterDamage;

    private void Awake() => Init();

    private void Update()
    {
        if (_attackTime < 0)
        {
            _attackTime += Time.deltaTime;
            if (_attackTime >= 0)
            {
                _attackTime = 0;
                _navMeshAgent.speed = 2;
            }
        }
    }

    private void Init()
    {
        _animator = GetComponentInParent<Animator>();
        _attackTime = 0;
        _navMeshAgent = GetComponentInParent<NavMeshAgent>();
    }
    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            _animator.SetTrigger("IsAttack");
        }
    }

    private void OnTriggerStay(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            _attackTime += Time.deltaTime;
            _navMeshAgent.speed = 0;

            if (_attackTime >= 2.2)
            {
                _attackTime = -1.2f;
                _animator.SetTrigger("IsAttack");
            }
            
            if (_attackTime <= 1.77 && _attackTime >= 1.75 && other != null && other.CompareTag("Player"))
            {
                other.GetComponent<IDamageable>().TakeDamage(_monsterDamage);
            }
        }
    }
    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            _attackTime = -2.7f + _attackTime;
        }
    }
}

위와 같은 코드가 나오기 전의 시행 착오를 아래와 같이 정리해 보았다.

  1. 몬스터의 모션이 출력된 후에 데미지가 들어가야 한다.

이 방법을 위해 처음 생각했던 방식은 코루틴을 사용하는 방식이었다. 하지만 생각해보니 몬스터의 공격 모션은 자주 출력될 상황인데, 코루틴을 계속 할당했다 해제하는 방식은 별로 좋지 않을 거란 생각이 들었다.
그러면 위와 같이 Time.deltaTime을 이용한 방식으로 구현해야지 부담이 덜 한 것은 확실한데, 문제는 데미지를 주는 방식에 있었다.

처음에는 단순하게 애니메이션 길이보다 약간 짧은 시간으로 선정하여 _attackTime > (일정 시간) 이상 지났을 경우로만 조건을 설정했는데, 그 시간으로부터 초기화 될 때까지 데미지가 들어가다 보니 순식간에 데미지가 들어와 죽어버리는 현상이 발생했다.

그래서 수치를 세밀하게 조율해본 결과, Time.deltaTime이 게임 시간 0.02초마다 호출된다는 점을 이용하여 0.02초 간격의 홀수 소수점 시점으로 설정해두면 해당 데미지 계산이 한 번만 호출되는 것을 확인했다.
ex) 1.51 < _attackTime < 1.53 - 한 번 호출

지금 하는 방식이 정석적인 것 같지는 않으나, 우선은 세밀하게 조정하여 얼추 모션이 출력되고 난 후의 데미지 계산이 들어가는 시간은 위와 같이 세팅되었으며 아래와 같이 결과가 나왔다.

이 부분에 대해서는 조금 더 공부가 필요해 보인다. 다만 이와 같이 구현하면 몬스터의 공격을 피한 상황도 만들 수 있는 점이 좋았다.

  1. 몬스터가 공격 중에 움직이지 않도록 설정

최종적으로 위와 같은 코드가 나오긴 했지만, 몬스터가 공격 중에도 계속 플레이어를 쫓는 현상이 발견되었었다.
그래서 공격을 취하는 동안에는 네비메쉬 상의 속도를 0으로 만들고 다시 복구하는 방식을 취해야 했으나, 생각보다 타이밍 조절이 어려웠다.
그래서 결국 아이디어를 낸 방법이 Trigger영역을 exit 한 순간에 _attackTime을 음수로 만들고, 다시 Update에서 0으로 만든 다음 속도를 다시 원상태로 복구하는 방식을 취했다.

  1. 몬스터가 다시 공격이 안 됨

이 문제는 금방 해결 되었다. MonsterAttack 트리거 영역이 Monster로 설정되어 있다 보니, TakeDamage가 호출되지 않는 문제였다. MonsterAttack 트리거 영역을 Default로 설정해놓자.

업로드중..

  1. 몬스터가 죽고 나서도 계속 공격하려고 함

몬스터가 죽고 나서는 공격해서는 안 된다. 하지만 몬스터가 죽고 나서 다가가면 몬스터가 들썩들썩하는 현상이 발견되었다. 이를 해결하기 위해 몬스터가 죽을 때 공격 트리거 영역이 꺼지도록 하였다.

  • NormalMonster
using DesignPattern;
using UnityEngine;
using UnityEngine.AI;

public class NormalMonster : Monster, IDamageable
{
...

    [SerializeField] private GameObject _monsterAttack;
    
...

    public void TakeDamage(int value)
    {
        CurrentHp.Value -= value;
        if (CurrentHp.Value <= 0)
        {
            IsAlive.Value = false;
            Death(IsAlive.Value);
            _monsterAttack.SetActive(false);
        }
    }
    
    private void Death(bool value)
    {
        _animator.SetBool("IsAlive", value);
        if (value == false)
        {
            _isActivateControl = false;
            _navMeshAgent.speed = 0;
        }
    }
}
profile
게임 만들러 코딩 공부중

0개의 댓글