Chapter 7-4. AI : 동물의 이중 상속

Date:     Updated:

카테고리:

태그:

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

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

🚖 이중 상속

📜Pig 는 📜WeakAnimal 을 상속 한다. 📜WeakAnimal 은 📜Animal 를 상속 하므로, 📜Pig 는 📜WeakAnimal, 📜WeakAnimal 두 가지 내용을 동시에 가진다.

  • 상속 구조
    • 📜Animal
      • 📜WeakAnimal
        • 📜Pig
  • 📜Animal
    • 모든 동물들이 가지는 공통적인 부분
      • 이름, 체력 등등
      • 애니메이션
      • 걷기, 데미지, 죽음
  • 📜WeakAnimal
    • 약한 동물들이 가지는 공통적인 부분
      • 데미지 입으면 👉 도망감
      • 플레이어를 절대 공격하지 않음
    • 📜Pig, 사슴, 토끼 등등..
  • 📜StrongAnimal
    • 강한 동물들이 가지는 공통적인 부분
      • 데미지 입으면 👉 공격함
      • 플레이어를 먼저 공격함
    • 악어, 호랑이 등등..
  1. 완전히 동일하게 공통적인 부분은 그냥 상속.
  2. 동일하게 가지는 부분이나, 내용이 좀 다르다면 virtual, override로 재정의
  3. 완전 다른, 그 동물만의 새로운 특성이라면 새롭게 정의해주면 됨.


📜Animal.cs

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

public class Animal : MonoBehaviour
{
    [SerializeField] protected string animalName; // 동물의 이름
    [SerializeField] protected int hp;  // 동물의 체력

    [SerializeField] protected float walkSpeed;  // 걷기 속력
    [SerializeField] protected float runSpeed;  // 달리기 속력
    [SerializeField] protected float turningSpeed;  // 회전 속력
    protected float applySpeed;

    protected Vector3 direction;  // 방향

    // 상태 변수
    protected bool isAction;  // 행동 중인지 아닌지 판별
    protected bool isWalking; // 걷는지, 안 걷는지 판별
    protected bool isRunning; // 달리는지 판별
    protected bool isDead;   // 죽었는지 판별

    [SerializeField] protected float walkTime;  // 걷기 시간
    [SerializeField] protected float waitTime;  // 대기 시간
    [SerializeField] protected float runTime;  // 뛰기 시간
    protected float currentTime;

    // 필요한 컴포넌트
    [SerializeField] protected Animator anim;
    [SerializeField] protected Rigidbody rigid;
    [SerializeField] protected BoxCollider boxCol;
    protected AudioSource theAudio;

    [SerializeField] protected AudioClip[] sound_Normal;
    [SerializeField] protected AudioClip sound_Hurt;
    [SerializeField] protected AudioClip sound_Dead;

    protected void Start()
    {
        currentTime = waitTime;   // 대기 시작
        isAction = true;   // 대기도 행동
        theAudio = GetComponent<AudioSource>();
    }

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

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

    protected void Rotation()
    {
        if (isWalking || isRunning)
        {
            Vector3 _rotation = Vector3.Lerp(transform.eulerAngles, new Vector3(0f, direction.y, 0f), turningSpeed); // turningSpeed 만큼 보간 됨
            rigid.MoveRotation(Quaternion.Euler(_rotation));
        }
    }

    protected void ElapseTime()
    {
        if (isAction)
        {
            currentTime -= Time.deltaTime;
            if (currentTime <= 0)  // 랜덤하게 다음 행동을 개시
                ReSet();
        }
    }

    protected virtual 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();
    }

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

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

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

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

    protected void Dead()
    {
        PlaySE(sound_Dead);

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

        anim.SetTrigger("Dead");
    }

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

    protected void PlaySE(AudioClip _clip)
    {
        theAudio.clip = _clip;
        theAudio.Play();
    }
}

자식 클래스가 사용할 수 있도록 public을 제외한 모든 private 을 protected 로 보호 수준 올리기

  • 저 많은 변수 들은 모든 동물들이 공통적으로 가지는 멤버 변수이다.
    • 📜WeakAnimal, 📜Pig 가 그대로 가지게 됨
  • 모든 동물들이 내용 그대로를 공통적으로 가지는 기능
    • Start
    • Update
    • 이동 Move
    • 회전 Rotation
    • 행동 타이밍 + 다음 행동 개시 ElapseTime
    • 걷기 TryWalk
    • 사망 Dead
    • 랜덤 사운드 재생 RandomSound
    • 사운드 재생 PlaySE
  • 모든 동물들이 공통적으로 가지고 있기는 하나 내용은 조금 다르게 가질 수 있는 기능 👉 자식 클래스에서 재정의 해야할 가상 함수 virtual
    • 다음 행동 준비를 위한 초기화 Reset()
    • 데미지 입었을 때 처리 Damage


📜WeakAnimal.cs

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

public class WeakAnimal : Animal
{
    public void Run(Vector3 _targetPos)
    {
        direction = Quaternion.LookRotation(transform.position - _targetPos).eulerAngles;

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

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

    public override void Damage(int _dmg, Vector3 _targetPos)
    {
        base.Damage(_dmg, _targetPos);
        if (!isDead)
            Run(_targetPos);
    }
}

자식 클래스가 사용할 수 있도록 protected 이상으로 보호 수준 올리기

  • 모든 약한 동물들만이 내용 그대로를 공통적으로 가지는 기능
    • 도망가기 Run
      • 약한 동물들만 도망가기 때문
  • 모든 동물들이 가지나 약한 동물일 땐 내용이 조금 다른 기능 👉 재정의 override
    • 데미지 입었을 때 처리 Damage
      • 약한 동물일 경우, 데미지를 입을 때 모든 동물들이 데미지를 입을 때 하는 행동에다가
        • 추가로 도망가는 기능을 덧붙여 한다.
          base.Damage(_dmg, _targetPos);
          if (!isDead)
              Run(_targetPos);
          


📜Pig.cs

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

public class Pig : WeakAnimal
{
    protected override void ReSet()
    {
        base.ReSet();
        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 Wait()  // 대기
    {
        currentTime = waitTime;
        Debug.Log("대기");
    }

    private void Eat()  // 풀 뜯기
    {
        currentTime = waitTime;
        anim.SetTrigger("Eat");
        Debug.Log("풀 뜯기");
    }

    private void Peek()  // 두리번
    {
        currentTime = waitTime;
        anim.SetTrigger("Peek");
        Debug.Log("두리번");
    }
}

  • 모든 동물들이 가지나 돼지일 땐 내용이 조금 다른 기능
    • 다음 행동 준비를 위한 초기화 Reset()
      • 돼지 만의 다음 행동 결정 함수인 RandomAction() 을 추가로 실행한다.
  • 오로지 돼지들만이 가지는 기능
    • 돼지만의 랜덤 행동들 결정 RandomAction
    • 걷기는 모든 동물들이 가지는 기능이지만, 대기, 풀 뜯기, 두리번 거리는 것은 돼지만의 기능이다. Wait(), Eat(), Peek()


이름이 바뀐 멤버들은 컴파일 되면 할당 되었던게 사라지므로 다시 인스펙터에서 할당해 주어야 한다.


🚖 다형성

📜PickaxeController.cs

    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 == "Weak_Animal")
                {
                    SoundManager.instance.PlaySE("Animal_Hit");
                    hitInfo.transform.GetComponent<WeakAnimal>().Damage(currentCloseWeapon.damage, transform.position);
                }
                isSwing = false;
                Debug.Log(hitInfo.transform.name);
            }
            yield return null;
        }
    }
                else if (hitInfo.transform.tag == "Weak_Animal")
                {
                    SoundManager.instance.PlaySE("Animal_Hit");
                    hitInfo.transform.GetComponent<WeakAnimal>().Damage(currentCloseWeapon.damage, transform.position);
                }

다형성에 의하여 GetComponent<Pig»()… GetComponent<Rabbit>()… 이렇게 나열하지 않아도 모든 약한 동물들을 GetComponent<WeakAnimal>() 하나로 다 받을 수 있다. 다형성에 의하여 !

image

“Weak_Animal” 태그를 새로 만들고 돼지의 태그를 이걸로 바꾸어 주었다.



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

맨 위로 이동하기

댓글남기기