Chapter 7-2. AI : 동물 사냥, 3 D 입체 음향

Date:     Updated:

카테고리:

태그:

인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

Chapter 7. 동물들의 공격형 AI, 도망형 AI 구현

🚖 사운드

돼지의 소리 Audio Source : 3 D 음향

image

Pig 돼지 오브젝트에게 Audio Source 를 붙여 준다.

  • Play On Awake 체크 해제
    • 돼지가 활성화 될 때 재생되게 하면 안되고 원할 때 소리를 재생시킬 것이라서
  • 3D Sound Settings
    • Min Distance
      • 이 컴포넌트가 붙은 오브젝트를 기준으로 하여 Min Distance 내에서는 최고 음량을 유지한다. (이 거리 내에선 돼지 소리가 최대로 들림)
      • 이 컴포넌트가 붙은 오브젝트를 기준으로 하여 Min Distance 을 벗어나면 서서히 음량이 감소한다. (벗어나면 돼지 소리가 거리에 따라 서서히 작게 들리기 시작)
    • Max Distance
      • 이 컴포넌트가 붙은 오브젝트를 기준으로 하여 Max Distance 내에서는 서서히 음량이 감소한다. (이 거리 내에선 거리에 따라 돼지 소리가 서서히 작게 들리기 시작)
      • 이 컴포넌트 오브젝트를 기준으로 하여 Max Distance 을 벗어나면 소리가 완전히 들리지 않는다. (벗어나면 돼지 소리가 아예 들리지 않음)
    • 커브 설정
      • Max Distance를 15로 설정했다면 돼지로부터 거리가 15 이상 떨어지면 돼지의 소리를 들을 수 없게 된다.
      • 거리 15 Max Distance 에 가까울 수록 볼륨이 서서히 0 에 가까워 지도록 커브 모양을 위와 같이 변경 하였다.

image

작은 원이 Min Distance 범위이고 큰 원이 Max Distance 범위다. Min Distance 거리 내에 있을 땐 돼지의 Audio Source 가 재생하는 소리를 최대 볼륨으로 들을 수 있으며 Min Distance ~ Max Distance 범위 안의 거리에서는 돼지의 소리가 거리에 반비례하여 들린다. 멀 수록 돼지 소리가 작게 들림. Max Distance 거리 밖에서는 돼지의 소리를 아예 들을 수가 없게 된다.

image

image

3D Sound Settings 에서 커브와 음향 거리를 조정했는 데도 불구하고 거리가 멀어져도 소리가 바로 앞에 있는 것 마냥 크게 들린다면

  • Pig 오브젝트 - 디버그 모드
    • Pan Level Custom Curve 를 1 에 위치하도록 설정한다.
      • 1 에 가까울 수록 3 D 음향에 가까워 지고
      • 낮은 값일 수록 거리에 따른 볼륨 차별화가 없는 2 D 음향에 가까워 진다.

Sound Manager가 있음에도 불구하고 돼지 오브젝트에 직접 Audio Source 컴포넌트를 추가한 이유.

돼지는 월드를 돌아다니는 NPC이기 때문에 돼지와 멀리 있다면 소리도 작게 재생해야 하고 가까이 있다면 소리도 크게 재생해야 한다. 돼지 자체에 Audio Source 소리 재생기를 달아주면 돼지가 멀어질 때 소리 재생기도 멀어지는 것이므로 이렇게 돼지 자체에 Audio Source 를 달아주어야 3 D 입체 음향을 구현할 수 있게 된다. 자연스레 거리에 따른 볼륨 감소가 생기게 된다.


효과음 Sound Manager

때리는 효과음은 언제나 대상과 가까이 있을 때 재생될테니 3 D 음향으로 재생할 필요는 없다. 따라서 Sound Manager에서 재생하게 한다.

사냥 효과음 클립 추가.

image


🚖 돼지 공격 처리

📜 Pig.cs

Pig 돼지 트랜스폼을 담당하는 부모 오브젝트에 붙인다.

public class Pig : MonoBehaviour
{
    [SerializeField] private float walkSpeed;  // 걷기 속력
    [SerializeField] private float runSpeed;  // 달리기 속력
    private float applySpeed;

    private bool isRunning; // 달리는지 판별
    private bool isDead;   // 죽었는지 판별

    [SerializeField] private float runTime;  // 뛰기 시간

    private AudioSource theAudio;

    [SerializeField] private AudioClip[] sound_Pig_Normal; // 돼지의 일상 소리. 여러개라 배열로
    [SerializeField] private AudioClip sound_Pig_Hurt;  // 돼지가 맞을 때 소리
    [SerializeField] private AudioClip sound_Pig_Dead; // 돼지가 죽을 때 소리

    void Start()
    {
        currentTime = waitTime;   // 대기 시작
        isAction = true;   // 대기도 행동
        theAudio = GetComponent<AudioSource>();  // 오디오 컴포넌트 가져오기
    }

    void Update()
    {
        if (!isDead)
        {
            Move();
            Rotation();
            ElapseTime();
        }
    }

    private void Move()
    {
        if (isWalking || isRunning)
            rigidl.MovePosition(transform.position + transform.forward * applySpeed * Time.deltaTime);
    }

    private void Rotation()
    {
        if (isWalking || isRunning)
        {
            Vector3 _rotation = Vector3.Lerp(transform.eulerAngles, new Vector3(0f, direction.y, 0f), 0.01f);
            rigidl.MoveRotation(Quaternion.Euler(_rotation));
        }
    }

    private void ReSet()  // 다음 행동 준비
    {
        // 초기화
        isAction = true;
        isWalking = false;
        anim.SetBool("Walking", isWalking);
        isRunning = false;
        anim.SetBool("Running", isRunning);
        applySpeed = walkSpeed;

        // 랜덤한 방향으로 초기화
        direction.Set(0f, Random.Range(0f, 360f), 0f);

        // 다음 행동 결정하기
        RandomAction();
    }

    private void RandomAction()
    {
        // 다음 행동을 결정하기에 앞서 랜덤하게 돼지의 일상 소리 재생 
        RandomSound();

        int _random = Random.Range(0, 4); // 대기, 풀뜯기, 두리번, 걷기

        if (_random == 0)
            Wait();
        else if (_random == 1)
            Eat();
        else if (_random == 2)
            Peek();
        else if (_random == 3)
            TryWalk();
    }

    private void TryWalk()  // 걷기
    {
        currentTime = walkTime;
        isWalking = true;
        anim.SetBool("Walking", isWalking);
        applySpeed = walkSpeed;
        Debug.Log("걷기");
    }

    private void Run(Vector3 _targetPos)
    {
        direction = Quaternion.LookRotation(transform.position - _targetPos).eulerAngles;

        currentTime = runTime;
        isWalking = false;
        isRunning = true;
        applySpeed = runSpeed;

        anim.SetBool("Running", isRunning);
    }

    public void Damage(int _dmg, Vector3 _targetPos)
    {
        if(!isDead)
        {
            hp -= _dmg;

            if (hp <= 0)
            {
                Dead();
                return;
            }

            PlaySE(sound_Pig_Hurt);
            anim.SetTrigger("Hurt");
            Run(_targetPos);
        }
    }

    private void Dead()
    {
        PlaySE(sound_Pig_Dead);

        isWalking = false;
        isRunning = false;
        isDead = true;

        anim.SetTrigger("Dead");
    }

    private void RandomSound()
    {
        int _random = Random.Range(0, 3);  // 돼지의 일상 사운드는 3 개
        PlaySE(sound_Pig_Normal[_random]);
    }

    private void PlaySE(AudioClip _clip)
    {
        theAudio.clip = _clip;
        theAudio.Play();
    }
}
  • 돼지가 공격 받으면 Damage(int _dmg, Vector3 _targetPos)
    • 근데 돼지가 죽은 상태가 아니라면
      • hp 를 깎는다.
        • hp 가 0 에 도달 했다면 죽는다. 👉 Dead()
      • 돼지가 다치는 소리 재생
      • 돼지가 다치는 애니메이션 재생
      • 플레이어(_targetPos)의 반대 방향으로, isRunning 시간 동안, “달리기” 상태를 가진다. 👉 Run(_targetPos)
  • 돼지는 공격을 받으면 플레이어의 반대 방향으로 도망간다. Run(Vector3 _targetPos)
    • 실제로 달리기에 의한 이동 처리는 걷기와 마찬가지로 Move() 에서 한다.
      • 따라서 이제 Move() 에서는 속력 스칼라로 walkSpeed를 쓰지 않고, walkSpeedrunSpeed 두 값을 할당 받아 그때 그때에 맞는 속력으로 이동할 수 있는 applySpeed를 사용하게 되었다.
          private void Move()
          {
              if (isWalking || isRunning)
                  rigidl.MovePosition(transform.position + transform.forward * applySpeed * Time.deltaTime);
          }
        
    • (플레이어 👉 돼지인 자기 자신) 방향 쪽으로 달려가야 한다.
      • 돼지가 달려가야할 방향
        direction = Quaternion.LookRotation(transform.position - _targetPos).eulerAngles;
        
    • 달리기에 맞게 초기화
          currentTime = runTime;
          isWalking = false;
          isRunning = true;
          applySpeed = runSpeed;
      
    • 달리기 애니메이션 재생
  • 돼지가 죽으면
      private void Dead()
      {
          PlaySE(sound_Pig_Dead);
    
          isWalking = false;
          isRunning = false;
          isDead = true;
    
          anim.SetTrigger("Dead");
      }
    
    • 더 이상 데미지 받지 않는다.
    • 더 이상 이동하지도, 회전하지도, 어떤 행동을 취하지도 않는다.
      void Update()
      {
          if (!isDead)
          {
              Move();
              Rotation();
              ElapseTime();
          }
      }
      
  • 돼지는 일상 소리들을 랜덤하게 재생한다.
    • RandomAction() 에서 다음 행동을 결정하기에 앞서 랜덤하게 돼지의 일상 소리 재생
      • 👉 RandomSound() 돼지의 일상 사운드는 3 개 쓸거라서 0, 1, 2 중에 랜덤으로 뽑아서 재생
          private void RandomSound()
          {
              int _random = Random.Range(0, 3);  // 돼지의 일상 사운드는 3 개
              PlaySE(sound_Pig_Normal[_random]);
          }
        
  • 돼지의 회전
    • 걸을 때 방향 ✨ 0 ~ 360 도 에서 랜덤한 각도로 y 축 회전
      direction.Set(0f, Random.Range(0f, 360f), 0f);
      
    • 달릴 때 방향 ✨ 플레이어의 반대 방향. 즉 (플레이어가 👉 돼지인 자기 자신을 향하는) 방향으로 달려가야 한다.
      direction = Quaternion.LookRotation(transform.position - _targetPos).eulerAngles;
      
      • 단, 이 때도 y 축 회전 방향만 고려해야 하기 때문에 아래와 같이 수정해주었다. 그냥 direction으로 Lerp 에게 넘기면, (플레이어가 👉 돼지인 자기 자신을 향하는) 방향의 x, z 방향의 회전값도 함께 들어가는 것이기 때문에 돼지가 땅을 향해 달려가는 등 문제가 생긴다. 따라서 y값만 넘겨줄 수 있도록 아래와 같이 수정
          private void Rotation()
          {
              if (isWalking || isRunning)
              {
                  Vector3 _rotation = Vector3.Lerp(transform.eulerAngles, new Vector3(0f, direction.y, 0f), 0.01f);
                  rigidl.MoveRotation(Quaternion.Euler(_rotation));
              }
          }
        


📜 PickaxeController.cs

public class PickaxeController : CloseWeaponController
{
    // 활성화 여부
    public static bool isActivate = false;

    void Update()
    {
        if (isActivate)
            TryAttack();
    }

    protected override IEnumerator HitCoroutine()
    {
        while (isSwing)
        {
            if (CheckObject())
            {
                if (hitInfo.transform.tag == "Rock")
                    hitInfo.transform.GetComponent<Rock>().Mining();
                else if (hitInfo.transform.tag == "Twig")
                    hitInfo.transform.GetComponent<Twig>().Damage(this.transform);
                else if (hitInfo.transform.tag == "NPC")
                {
                    SoundManager.instance.PlaySE("Animal_Hit");
                    hitInfo.transform.GetComponent<Pig>().Damage(currentCloseWeapon.damage, transform.position);
                }
                isSwing = false;
                Debug.Log(hitInfo.transform.name);
            }
            yield return null;
        }
    }

    public override void CloseWeaponChange(CloseWeapon _closeWeapon)
    {
        base.CloseWeaponChange(_closeWeapon);
        isActivate = true;
    }
}
                else if (hitInfo.transform.tag == "NPC")
                {
                    SoundManager.instance.PlaySE("Animal_Hit");
                    hitInfo.transform.GetComponent<Pig>().Damage(currentCloseWeapon.damage, transform.position);
                }

곡괭이와 충돌된 것이 있을 때 충돌된 것의 태그가 “NPC” 라면, 동물 AI 이므로 사운드 매니저로 하여금 효과음을 재생하고 📜Pig.cs 의 Damage 함수를 호출하여 해당 돼지에게 데미지를 입힌다. Pickaxe에 붙어 있는 📜CloseWeapon.cs 의 곡괭이의 데미지 damage 값을 넘겨주고 플레이어의 위치로는 그냥 곡괭이 위치를 넘긴다. transform.position


설정

image

돼지에게 “NPC” 태그를 붙여 준다.

image



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

댓글남기기