아래에 링크한 이전 글에서는 유니티에서 제공하는 NavMeshAgent 를 이용해 몬스터가 플레이어를 따라다니도록 했습니다.
이번 글에서는 한단계 더 나아가서 처음엔 가만히 있다가 플레이어가 근처에 오면 따라가고 공격 범위안에 들어오면 공격을 하고 다시 멀어지면 다시 따라가다가 더 멀어지면 추적을 포기하고 그 자리에 가만히 있는 몬스터의 AI 를 만들어 보도록 하겠습니다.
* 이전 포스팅 하단에서 현 시점까지의 작업 내용이 포함된 unitypackage 도 다운로드 받으실 수 있습니다.
▶ State Machine
State Machine을 만드는 방법에는 여러 가지가 있지만 이 글에서는 Coroutine 을 이용해 만들어 보도록 하겠습니다. State 는 너무 복잡하지 않게 [IDLE / CHASE / ATTACK / KILLED] 이렇게 4 가지 상태를 이용하겠습니다.
IDLE | 아무것도 하지 않고 대기 |
CHASE | 플레이어 캐릭터를 따라다님 |
ATTACK | 플레이어 캐릭터를 공격 |
KILLED | 사망 |
아래는 State Machine의 다이어그램입니다.
▶ Monster Animator 만들기
이 예제는 Monster 스크립트에서 StateMachine 에 따른 animation 을 직접 제어하기 때문에 Animator 설정은 간단하게 됩니다.
Project 창에서 Asset 폴더를 선택하고 마우스 우클릭 [Create > Animator Controller] 를 선택해 새로운 Animator Controller를 생성한 다음 이름을 Monster-Slime 으로 변경합니다. Monster-Slime 을 더블클릭해서 Animator 창을 열어 줍니다.
Slime의 애니메이션을 animator 에 등록하기 위해 Project 창에서 경로 [Assets > RPG Monster Duo PBR Polyart > Animations > Slime] 으로 이동합니다. 이곳에 Slime 과 관련된 animation 들이 있습니다.
이 중 "IdleNormal_Slime_Anim" 을 드래그해서 Animator 창에 놓으면 다음과 같이 같은 이름의 state 가 만들어집니다. 오른쪽 Inspector 창의 붉은색 테두리 위치에서 이름을 아래와 같이 변경해 줍니다.
이와 같은 방법으로 "WalkFWD_Slime_Anim", "Attack01_Slime_Anim", "IdleBattle_Slime_Anim" 을 등록하고 아래와 같아 지도록 이름을 변경합니다.
Attack01 을 선택하고 마우스 우클릭 [Make Transition]을 선택한 다음 IdleBattle 을 선택해 transition을 만드는 것도 잊으시면 안 됩니다.
▶ Slime 에 Sphere Collider 추가
Slime 객체를 선택하고 Inspector 창에서 [Add Component > Physics > Sphere Collider] 를 선택해 추가하고 다음과 같이 설정해 줍니다.
이렇게 하면 몬스터 주변에 반지름 5의 크기를 갖는 구형태의 Collider 가 생성됩니다. 이 Collider 는 플레이어 캐릭터가 근처에 왔는지 감지하기 위한 목적으로 사용됩니다. 몬스터가 플레이어를 감지할 수 있는 범위는 Radius의 크기를 조절하시면 됩니다.
▶ Monster Script 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Monster : MonoBehaviour
{
Transform target;
NavMeshAgent nmAgent;
Animator anim;
float HP = 0;
public float lostDistance;
enum State
{
IDLE,
CHASE,
ATTACK,
KILLED
}
State state;
// Start is called before the first frame update
void Start()
{
anim = GetComponent<Animator>();
nmAgent = GetComponent<NavMeshAgent>();
HP = 10;
state = State.IDLE;
StartCoroutine(StateMachine());
}
IEnumerator StateMachine()
{
while (HP > 0)
{
yield return StartCoroutine(state.ToString());
}
}
IEnumerator IDLE()
{
// 현재 animator 상태정보 얻기
var curAnimStateInfo = anim.GetCurrentAnimatorStateInfo(0);
// 애니메이션 이름이 IdleNormal 이 아니면 Play
if (curAnimStateInfo.IsName("IdleNormal") == false)
anim.Play("IdleNormal", 0, 0);
// 몬스터가 Idle 상태일 때 두리번 거리게 하는 코드
// 50% 확률로 좌/우로 돌아 보기
int dir = Random.Range(0f, 1f) > 0.5f ? 1 : -1;
// 회전 속도 설정
float lookSpeed = Random.Range(25f, 40f);
// IdleNormal 재생 시간 동안 돌아보기
for (float i=0; i< curAnimStateInfo.length; i+=Time.deltaTime)
{
transform.localEulerAngles = new Vector3(0f, transform.localEulerAngles.y + (dir) * Time.deltaTime * lookSpeed, 0f);
yield return null;
}
}
IEnumerator CHASE()
{
var curAnimStateInfo = anim.GetCurrentAnimatorStateInfo(0);
if (curAnimStateInfo.IsName("WalkFWD") == false)
{
anim.Play("WalkFWD", 0, 0);
// SetDestination 을 위해 한 frame을 넘기기위한 코드
yield return null;
}
// 목표까지의 남은 거리가 멈추는 지점보다 작거나 같으면
if (nmAgent.remainingDistance <= nmAgent.stoppingDistance)
{
// StateMachine 을 공격으로 변경
ChangeState(State.ATTACK);
}
// 목표와의 거리가 멀어진 경우
else if (nmAgent.remainingDistance > lostDistance)
{
target = null;
nmAgent.SetDestination(transform.position);
yield return null;
// StateMachine 을 대기로 변경
ChangeState(State.IDLE);
}
else
{
// WalkFWD 애니메이션의 한 사이클 동안 대기
yield return new WaitForSeconds(curAnimStateInfo.length);
}
}
IEnumerator ATTACK()
{
var curAnimStateInfo = anim.GetCurrentAnimatorStateInfo(0);
// 공격 애니메이션은 공격 후 Idle Battle 로 이동하기 때문에
// 코드가 이 지점에 오면 무조건 Attack01 을 Play
anim.Play("Attack01", 0, 0);
// 거리가 멀어지면
if (nmAgent.remainingDistance > nmAgent.stoppingDistance)
{
// StateMachine을 추적으로 변경
ChangeState(State.CHASE);
}
else
// 공격 animation 의 두 배만큼 대기
// 이 대기 시간을 이용해 공격 간격을 조절할 수 있음.
yield return new WaitForSeconds(curAnimStateInfo.length * 2f);
}
IEnumerator KILLED()
{
yield return null;
}
void ChangeState(State newState)
{
state = newState;
}
private void OnTriggerEnter(Collider other)
{
if (other.name != "Player") return;
// Sphere Collider 가 Player 를 감지하면
target = other.transform;
// NavMeshAgent의 목표를 Player 로 설정
nmAgent.SetDestination(target.position);
// StateMachine을 추적으로 변경
ChangeState(State.CHASE);
}
// Update is called once per frame
void Update()
{
if (target == null) return;
// target 이 null 이 아니면 target 을 계속 추적
nmAgent.SetDestination(target.position);
}
}
코드의 길이가 조금 길긴하지만 요약하면 다음과 같습니다.
- enum 을 이용해 state 정의
- Coroutine 을 이용해 StateMachine 가동
- 각 state의 이름을 이용해 해당 코루틴 함수를 호출 (state.ToString())
- 각 state의 코루틴 함수에서 state 에 맞는 행동을 할 수 있는 코드 작성
▶ 실행
실제 플레이 화면입니다. 글 초반에 있는 State Machine의 다이어그램과 같이 몬스터가 동작하고 있습니다. 이제 여기에 플레이어가 공격을 당했을 때 모션을 추가하고 플레이어도 몬스터를 공격할 수 있는 코드를 추가하면 간단한 전투 시스템이 만들어지게 됩니다.
지금까지 작업한 내용이 포함된 패키지입니다.
※ 프로젝트에서 사용한 무료 에셋을 먼저 다운로드 -> Import 한 다음 위의 패키지를 import 하셔야 합니다.
▶ 무료 에셋 목록
플레이어 캐릭터
몬스터
발소리
'게임 프로그래밍 > 유니티 활용' 카테고리의 다른 글
[유니티 활용] ParticleSystem 으로 초간단 총알 패턴 만들기 (10) | 2023.01.18 |
---|---|
[유니티 활용] 액션 RPG 스타일의 실시간 전투 구현하기 (0) | 2023.01.14 |
[유니티 활용] NavMeshAgent 사용법 (Simple Monster AI) (26) | 2023.01.11 |
[유니티 활용] Character Controller 로 캐릭터 이동과 점프 (22) | 2023.01.11 |
[유니티 활용] 캐릭터 애니메이션과 이벤트 - 발자국 소리 (21) | 2023.01.09 |
댓글