Chapter 7-6. AI : 추격하는 동물
카테고리: Unity Lesson 3
태그: Unity Game Engine
인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
Chapter 7. 동물들의 공격형 AI, 도망형 AI 구현
🚖 사자
돼지와 마찬가지로 애니메이션, 컴포넌트(📜Lion.cs, 📜FieldOfViewAngle.cs, Nav Mesh Agent, Box Collider, Rigidbody)들 다 적용해주었다. 태그는 Strong_Animal
. 애니메이션도 새로 만들었다! 근데 사자가 계속 진동하면서 움직여서 ㅠㅠ 원인 찾느라 오랫동안 고생했는데 간단하게도 원인은 콜라이더 위치에 있었다.. 사자의 Box Collider가 땅에 파묻혀있는 바람에 땅과 마구 충돌하는 중이라 발생했던 일.. 교훈 하나 얻었다 ㅠ 움직임이 버벅거린다면 콜라이더가 땅에 묻혀있는지는 않은지 살펴보기.. 😭
🚖 추격하는 동물 (String Animal)
- 📜Animal
- 시야각 내에서 플레이어를 발견했다면
- 📜WeakAnimal 👉 Run
- 플레이어의 반대 방향으로 도망간다.
- 📜Pig.cs
- 📜StrongAnimal 👉 Chase
- 플레이어 방향으로 플레이어를 따라와 추격한다.
- 📜Lion.cs
- 📜WeakAnimal 👉 Run
- 시야각 내에서 플레이어를 발견했다면
📜FieldOfViewAngle
private PlayerController thePlayer;
void Start()
{
thePlayer = FindObjectOfType<PlayerController>();
}
public Vector3 GetTargetPos()
{
return thePlayer.transform.position;
}
public bool View()
{
Collider[] _target = Physics.OverlapSphere(transform.position, viewDistance, targetMask);
for (int i = 0; i < _target.Length; i++)
{
Transform _targetTf = _target[i].transform;
if (_targetTf.name == "Player")
{
Vector3 _direction = (_targetTf.position - transform.position).normalized;
float _angle = Vector3.Angle(_direction, transform.forward);
if (_angle < viewAngle * 0.5f)
{
RaycastHit _hit;
if(Physics.Raycast(transform.position + transform.up, _direction, out _hit, viewDistance))
{
if (_hit.transform.name == "Player")
{
Debug.Log("플레이어가 시야 내에 있습니다.");
Debug.DrawRay(transform.position + transform.up, _direction, Color.blue);
return true;
}
}
}
}
}
return false;
}
- GetTargetPos()
- 플레이어의 위치 리턴 (📜PlayerController가 붙어있는
Player
에서 가져옴)
- 플레이어의 위치 리턴 (📜PlayerController가 붙어있는
- View() 를
Bool
리턴 타입 함수로 고쳣다.- 플레이어가 시야 각도 내에 어떤 장애물도 가리지 않은 체 잘 보인다면 True 를 리턴하고
- 시야 각도 내에서 플레이어가 안보인다면 False 를 리턴한다.
- public 으로 고침.
📜Animal.cs
protected bool isChasing; // 추격중인지 판별
protected FieldOfViewAngle theFieldOfViewAngle;
protected void Start()
{
currentTime = waitTime; // 대기 시작
isAction = true; // 대기도 행동
theAudio = GetComponent<AudioSource>();
nav = GetComponent<NavMeshAgent>();
theFieldOfViewAngle = GetComponent<FieldOfViewAngle>();
}
protected virtual void Update()
{
if (!isDead)
{
Move();
ElapseTime();
}
}
protected void ElapseTime()
{
if (isAction)
{
currentTime -= Time.deltaTime;
if (currentTime <= 0 && !isChasing) // 랜덤하게 다음 행동을 개시
ReSet();
}
}
protected void RandomSound()
{
int _random = Random.Range(0, sound_Normal.Length);
PlaySE(sound_Normal[_random]);
}
- Start()
theFieldOfViewAngle
로딩
- Update()
- 가상 함수로 고쳐 자식들이 오버라이딩 하게 하였다.
- 📜Pig 의 경우
- 매 프레임마다 👉 플레이어가 시야 내에 있다면 플레이어로 부터 Run 도망가는 처리를 추가로 해줄 것이기 때문에
- 📜Lion 의 경우
- 매 프레임마다 👉 플레이어가 시야 내에 있다면 플레이어를 Chase 추격하는 처리를 추가로 해줄 것이기 때문에
- 📜Pig 의 경우
- 가상 함수로 고쳐 자식들이 오버라이딩 하게 하였다.
- ElapseTime()
isChasing
이 False일 때, 즉 플레이어를 추격하는 상태가 아닐 때만 다음 행동을 개시하도록 한다.- 📜StrongAnimal 이 플레이어 추격 중일땐 계속 플레이어가 시야 내에 있는 한 계속 쫓도록 하기 위하여
- RandomSound()
- 3 으로 하드 코딩 되어 있던 것을
sound_Normal.Length
로 고쳤다. 다른 동물들의 일상 소리가 꼭 3 개라는 보장은 없으므로
- 3 으로 하드 코딩 되어 있던 것을
📜StrongAnimal.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StrongAnimal : Animal
{
[SerializeField]
protected float chaseTime; // 총 추격 시간
protected float currentChaseTime;
[SerializeField]
protected float chaseDelayTime; // 추격 딜레이
public void Chase(Vector3 _targetPos)
{
isChasing = true;
destination = _targetPos;
nav.speed = runSpeed;
isRunning = true;
anim.SetBool("Running", isRunning);
nav.SetDestination(destination);
}
public override void Damage(int _dmg, Vector3 _targetPos)
{
base.Damage(_dmg, _targetPos);
if (!isDead)
Chase(_targetPos);
}
}
📜StrongAnimal 은 플레이어를 추격한다.
- 플레이어가 시야 내에서 사라져도 일정 시간동안은 계속 추격을 유지하다가 추격을 그만두는게 더 자연스럽다.
- 항상 시야내에 있다면 Chase 하되
- 한번 추격할 때마다
currentChaseTime
시간이chaseTime
을 초과할 때까지 대기 시간 간격chaseDelayTime
을 두면서(코루틴) 유지하도록 하면, 만약 플레이어가 시야 내에서 사라져서 더 이상 추격 함수 호출이 되지 않더라도 한 동안은 추격을 유지하게 된다.
- 한번 추격할 때마다
- 항상 시야내에 있다면 Chase 하되
- Chase(Vector3 _targetPos)
- Run 과 다르게
- 목적지
destination
는_targetPos
플레이어 위치이다. currentTime
을 사용하지 않고 플레이어가 시야 내에 있는 한 추적을 유지해야 하므로currentChaseTime
로 따로 사용- 그 외엔 Run 과 같다.
- 목적지
- Run 과 다르게
- Damage(int _dmg, Vector3 _targetPos)
- 맞으면 Chase 추격
📜Lion.cs
Lion
에 붙으며, 📜StrongAnimal 상속
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lion : StrongAnimal
{
protected override void Update()
{
base.Update();
if (theFieldOfViewAngle.View() && !isDead)
{
StopAllCoroutines();
StartCoroutine(ChaseTargetCoroutine());
}
}
IEnumerator ChaseTargetCoroutine()
{
currentChaseTime = 0;
Chase(theFieldOfViewAngle.GetTargetPos());
while(currentChaseTime < chaseTime)
{
Chase(theFieldOfViewAngle.GetTargetPos());
yield return new WaitForSeconds(chaseDelayTime);
currentChaseTime += chaseDelayTime;
}
isChasing = false;
isRunning = false;
anim.SetBool("Running", isRunning);
nav.ResetPath();
}
protected override void ReSet()
{
base.ReSet();
RandomAction();
}
private void RandomAction()
{
RandomSound();
int _random = Random.Range(0, 2); // 대기, 걷기
if (_random == 0)
Wait();
else if (_random == 1)
TryWalk();
}
private void Wait() // 대기
{
currentTime = waitTime;
Debug.Log("대기");
}
}
사자의 기본 상태 2 가지 (플레이어 발견한 상태가 아닐 때의 랜덤 상태) 👉 1️⃣ 대기 2️⃣ 걷기
protected override void Update()
{
base.Update();
if (theFieldOfViewAngle.View() && !isDead)
{
StopAllCoroutines();
StartCoroutine(ChaseTargetCoroutine());
}
}
IEnumerator ChaseTargetCoroutine()
{
currentChaseTime = 0;
Chase(theFieldOfViewAngle.GetTargetPos());
while(currentChaseTime < chaseTime)
{
Chase(theFieldOfViewAngle.GetTargetPos());
yield return new WaitForSeconds(chaseDelayTime);
currentChaseTime += chaseDelayTime;
}
isChasing = false;
nav.ResetPath();
}
- Update()
base.Update()
- 매프레임마다 Move(움직임), ElapseTime(다음 행동 결정)
- 플레이어가 시야 각도 내에 있고 and 사자가 죽은 상태가 아니라면
- 플레이어를 추격해야 한다.
- 기존에 진행되던 코루틴은 멈추고 새롭게 ChaseTargetCoroutine() 코루틴 함수 실행
- 플레이어가 시야 각도내에 있다면 계속 해서 코루틴이 멈췄다가 새롭게 다시 시작되므로 계속 추적 상태를 유지하게 된다.
- 플레이어가 시야 각도 내에서 사라진다면 새롭게 코루틴이 실행되진 않고 기존의 코루틴이 시간이 다 되어 멈추면 끝남 (즉, 시야 각도 내에서 사라져도 일정 시간동안은 추적을 유지)
- 플레이어를 추격해야 한다.
- ChaseTargetCoroutine() 코루틴
currentChaseTime
을 0 부터 업뎃 하기 시작- Chase(theFieldOfViewAngle.GetTargetPos()); 플레이어 추격.
currentChaseTime
가chaseTime
에 도달할 때까지 반복- Chase(theFieldOfViewAngle.GetTargetPos()); 플레이어 추격.
chaseDelayTime
동안 대기currentChaseTime
에chaseDelayTime
을 더해주기
- 추적 시간이 끝났다면 (이때까지도 StopAllCoroutines이 되지 않고 여기까지 왔다는 얘기이니, 여기까지 왔다는건 플레이러가 시야 각도 내에서 사라졌을 확률이 높다.)
- 이제
isChasing
을 False로 업뎃하고 - Nav Mesh Agent 의 목적지 없애기
- 달리기 애니메이션 멈추기
- 이제
📜Pig.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Pig : WeakAnimal
{
protected override void Update()
{
base.Update();
if (theFieldOfViewAngle.View() && !isDead)
{
Run(theFieldOfViewAngle.GetTargetPos());
}
}
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("두리번");
}
}
protected override void Update()
{
base.Update();
if (theFieldOfViewAngle.View() && !isDead)
{
Run(theFieldOfViewAngle.GetTargetPos());
}
}
- Update()
base.Update()
- 매프레임마다 Move(움직임), ElapseTime(다음 행동 결정)
- 플레이어가 시야 각도 내에 있고 and 돼지가 죽은 상태가 아니라면
- 돼지는 플레이어를 피해 도망간다. Run
📜PickaxeController.cs
else if (hitInfo.transform.tag == "Weak_Animal")
{
SoundManager.instance.PlaySE("Animal_Hit");
hitInfo.transform.GetComponent<WeakAnimal>().Damage(currentCloseWeapon.damage, transform.position);
}
else if (hitInfo.transform.tag == "Strong_Animal")
{
SoundManager.instance.PlaySE("Animal_Hit");
hitInfo.transform.GetComponent<StrongAnimal>().Damage(currentCloseWeapon.damage, transform.position);
}
“Strong_Animal” 태그 붙은 강한 동물 (사자)를 곡괭이로 때릴 때도 처리.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기