아래와 같은 유한 상태 머신 구현을 위해 상태를 주기적으로 업데이트하는데 매 프레임 하는 건 부담을 주므로 0.2~0.3초간격으로 해도 된다면 코루틴을 적극 활용하겠다.
적 캐릭터의 상태체크
0.3초 마다 상태를 체크한다. Player와 Monster의 거리를 측정해 추적 사정거리와 공격 사정거리를 판단한 다음 상태를 변경한다. MonsterCtrl 스크립트를 다음과 같이 수정한다.
Enum을 사용하면 인스펙터뷰에 리스트박스로 표시되면 가독성을 높혀준다.
Start()함수를 비롯 2개의 코루틴 CheckMonsterState() MonsterActionState()가 있다.
Start()는 참조조기화및 코루틴을 기동시킨다.
코루틴들은 while(!IsDie) 순환문으로 살아 있는 동안만 감시를 하다가 몬스터가 죽으면 isDie=true로 순환문을 빠져나와 종료한다.
CheckMonsterState() 에서 float distance = Vector3.Distance(playerTr.position, monsterTr.position);거리를 측정해 state를 IDLE, ATTACK, TRACE로 바꾸준다.
MonsterActionState()에서는 state가 Trace라면 agent.SetDestination(playerTr.position);로 목표물을 지정해주고, agent.isStopped = false로 추적을 시작한다.; 다른 state에 대해서도 적절한 처리를 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI; //for Navigation
public class MonsterCtrl : MonoBehaviour
{
//몬스트 상태
public enum State { //인스펙터뷰에 콤보박스로 표시됨
IDLE, PATROL, TRACE, ATTACK, DIE
}
public State state = State.IDLE;
public float traceDist = 10.0f;
public float attackDist = 2.0f;
public bool isDie = false;
private Transform monsterTr;
private Transform playerTr;
private NavMeshAgent agent;
public Vector3 pPos, mPos;
void Start()
{
monsterTr= GetComponent<Transform>();
playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
agent = GetComponent<NavMeshAgent>();
//agent.destination = playerTr.position;
StartCoroutine(CheckMonsterState());
StartCoroutine(MonsterAction());
}
IEnumerator CheckMonsterState() {
while(!isDie) {
pPos = playerTr.position;
mPos = monsterTr.position;
yield return new WaitForSeconds(0.3f);
//Player와 Monster간의 거리측정 스칼라값
float distance = Vector3.Distance(playerTr.position, monsterTr.position);
if(distance <= attackDist) {
state = State.ATTACK; //Attack로 상태변경
} else if(distance <= traceDist) {
state = State.TRACE; //Trace로 상태변경
} else {
state = State.IDLE; //Idle로 상태변경
}
}
}
IEnumerator MonsterAction() {
while (!isDie) {
switch(state) {
case State.IDLE:
agent.isStopped= true;
break;
case State.TRACE:
agent.SetDestination(playerTr.position);
agent.isStopped = false;
break;
case State.ATTACK:
break;
case State.DIE:
break;
}
yield return new WaitForSeconds(0.3f);
}
}
private void OnDrawGizmos() { //상태에 따라 기즈모 색깔 변경
if(state == State.TRACE) {
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, traceDist);
}
if(state == State.ATTACK) {
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackDist);
}
}
}
실행하면서 Monster의 인스펙터의 MonsterCtrl 스크립트뷰를 보면 거리가 좁혀지면 IDEL이 TRACE로 바뀌면 Monster가 추적을 시작하고 더 가가워지면 ATTACK로 바뀐다. 막 도망가서 거리를 벌리면 다시 IDLE상태로 된다.
마지막으로 OnDrawGizmos()는 씬뷰에서만 보인다. 디버그용이다.
트레이스 범위에서 범위에 해당하는 파란색 구를 그려주고 공격범위에서는 범위 크기의 빨간색 구를 그려준다.
애니메이션 동기화
몬스터는 유령처럼 움직임없이 따라오는데 애니메이션을 수행시켜 움직여보자. 애니메이터뷰에 IsTrace 변수를 설정해 놓았다. MonsterCtrl 스크립터에서 Monster의 Animator 컴포넌트에 접근하기 위한 변수를 설정및 할당하고 MonsterAction 함수에 Animator에서 생성한 변숫값을 변경한 변숫값을 변경하는 로직을 추가한다.
switch(state)문안에 case State.IDLE과 State.TRACE 때 IsTrace의 파라메터를false, true로 바꿔주는 anim.SetBool()이 추가되었다. 변경된 IsTrace의 파라메터는 애니메이터의 transition을 일어나게 한다.
IEnumerator MonsterAction() {
while (!isDie) {
switch(state) {
case State.IDLE:
agent.isStopped= true; //추적을 중지
anim.SetBool("IsTrace", false);
break;
case State.TRACE:
agent.SetDestination(playerTr.position); //목표설정
agent.isStopped = false; //추적을 재개
anim.SetBool("IsTrace", true);
break;
MonsterCtrl 전체코드
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI; //for Navigation
public class MonsterCtrl : MonoBehaviour
{
//몬스트 상태
public enum State { //인스펙터뷰에 콤보박스로 표시됨
IDLE, PATROL, TRACE, ATTACK, DIE ,DANCE
}
public State state = State.IDLE; //몬스트 현재상태
public float traceDist = 10.0f; //추적 사정거리
public float attackDist = 2.0f; //공격 사정거리
public bool isDie = false; //몬스터사망여부
private Transform monsterTr; //컴포넌트의 캐시를 처리할 변수
private Transform playerTr;
private NavMeshAgent agent;
private Animator anim;
void Start()
{
monsterTr= GetComponent<Transform>();
playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
agent = GetComponent<NavMeshAgent>();
anim = GetComponent<Animator>();
//agent.destination = playerTr.position;
StartCoroutine(CheckMonsterState());
StartCoroutine(MonsterAction());
}
//일정한 간격으로 몬스터의 행동 상태를 체크
IEnumerator CheckMonsterState() {
while(!isDie) {
yield return new WaitForSeconds(0.3f);
//Player와 Monster간의 거리측정 스칼라값
float distance = Vector3.Distance(playerTr.position, monsterTr.position);
if(distance <= attackDist) {
state = State.ATTACK; //Attack로 상태변경
} else if(distance <= traceDist) {
state = State.TRACE; //Trace로 상태변경
} else {
state = State.IDLE; //Idle로 상태변경
}
}
}
//몬스터의 상태에 따라 몬스터의 동작을 수행
IEnumerator MonsterAction() {
while (!isDie) {
switch(state) {
case State.IDLE:
agent.isStopped= true; //추적을 중지
anim.SetBool("IsTrace", false);
break;
case State.TRACE:
agent.SetDestination(playerTr.position); //목표설정
agent.isStopped = false; //추적을 재개
anim.SetBool("IsTrace", true);
break;
case State.ATTACK: //공격상태
break;
case State.DIE: //사망상태
break;
}
yield return new WaitForSeconds(0.3f);
}
}
private void OnDrawGizmos() { //상태에 따라 기즈모 색깔 변경
if(state == State.TRACE) {
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, traceDist);
}
if(state == State.ATTACK) {
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackDist);
}
}
}
몬스터 공격 루틴
몬스터에 공격 기능을 부여하자. 먼저 프로젝트 뷰의 MonsterAnim을 더블클릭해 애니메이터 뷰를 열고 attack 애니메이션 클립을 드래그해 배치한다. attack와 walk 스테이트간의 Transition 2개를 배치한다.
walk->attack transition은 HasExitTime을 언체크하고 Conditions+를 누루고 IsAttack를 선택하고 true로
attack->walk transition은 HasExitTime을 언체크하고 Conditions+를 누루고 IsAttack를 선택하고 false로
MonsterCtrl 스크립터의 코루틴 MonsterAction() 내부의 Trace와 Attack case문에 IsAttack 파라메터 부분을 넣어준다
IEnumerator MonsterAction() {
while (!isDie) {
switch(state) {
case State.IDLE:
agent.isStopped= true; //추적을 중지
anim.SetBool("IsTrace", false);
break;
case State.TRACE:
agent.SetDestination(playerTr.position); //목표설정
agent.isStopped = false; //추적을 재개
anim.SetBool("IsTrace", true);
anim.SetBool("IsAttack", false);
break;
case State.ATTACK: //공격상태
anim.SetBool("IsAttack", true);
break;
SetBool()은 파라메테명의 오자가 있으면 오류도 발생하고 호출될때 마다 해시테이블을 검색하므로 파라메터 해시값을 미리 추출해 전달하는 방식이 바람직하다. 우선 다음같이 스트링을 해시값으로 미리 바꿔 놓는다 "Hit"는 몬스터가 피격받을때 상태인데 미리 넣어두겠다
public readonly int hashTrace = Animator.StringToHash("IsTrace");
public readonly int hashAttack = Animator.StringToHash("IsAttack");
private readonly int hashHit = Animator.StringToHash("Hit");
코루팅 switch()문안의 스트링을 해시값으로 변경한다.
IEnumerator MonsterAction() {
while (!isDie) {
switch(state) {
case State.IDLE:
agent.isStopped= true; //추적을 중지
anim.SetBool(hashTrace, false);
break;
case State.TRACE:
agent.SetDestination(playerTr.position); //목표설정
agent.isStopped = false; //추적을 재개
anim.SetBool(hashTrace, true);
anim.SetBool(hashAttack, false); //공격파라메터 변경
break;
case State.ATTACK: //공격상태
anim.SetBool(hashAttack, true); //공격파라메터 변경
break;
실행해 보면 이제 몬스터가 걷고 공격한다. 이제부터는 몬스터가 피격받을때 취할 리액션을 붙여 넣겠다.
게임뷰탭옆 Ani탭을 누른후 Hit Trigger 파라메터를 하나 만든후 몬스터 모델에서 gothit을 끌어다 놓고 다음과 transition을 만든다.
Any->gothit transition은 HasExitTime 언체크, condition +를 누르고 Hit로 설정한다
gothit에서 idle 로 단방향 transition을 만든다 HasExitTime는 체크, ExitTime 0.9 isTrace false로 한다.
gothit에서 walk 로 단방향 transition을 만든다 HasExitTime는 체크, ExitTime 0.9 condition + IsTrace true, IsAttack false로 한다.
gothit에서 attack로 단방향 transition을 만든다 HasExitTime는 체크, ExitTime 0.9 condition + IsAttack true로 한다.
몬스터와 총알이 충돌이 나면 총알을 없애주고 hasHit을 Trigger시켜준다(setTrigger) 그러면 Any->gotHit transition이 일어난다.
private readonly int hashHit = Animator.StringToHash("Hit");
private void OnCollisionEnter(Collision coll) {
if (coll.collider.CompareTag("BULLET")) {
Destroy(coll.gameObject); //충돌한 총알을 삭제
anim.SetTrigger(hashHit); //피격 리액셔 애니메이션 실행
}
}
현재까지의 최종코드
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI; //for Navigation
public class MonsterCtrl : MonoBehaviour
{
//몬스트 상태
public enum State { //인스펙터뷰에 콤보박스로 표시됨
IDLE, PATROL, TRACE, ATTACK, DIE ,DANCE
}
public State state = State.IDLE; //몬스트 현재상태
public float traceDist = 10.0f; //추적 사정거리
public float attackDist = 2.0f; //공격 사정거리
public bool isDie = false; //몬스터사망여부
private Transform monsterTr; //컴포넌트의 캐시를 처리할 변수
private Transform playerTr;
private NavMeshAgent agent;
private Animator anim;
public readonly int hashTrace = Animator.StringToHash("IsTrace");
public readonly int hashAttack = Animator.StringToHash("IsAttack");
private readonly int hashHit = Animator.StringToHash("Hit");
void Start()
{
monsterTr= GetComponent<Transform>();
playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
agent = GetComponent<NavMeshAgent>();
anim = GetComponent<Animator>();
//agent.destination = playerTr.position;
StartCoroutine(CheckMonsterState());
StartCoroutine(MonsterAction());
}
//일정한 간격으로 몬스터의 행동 상태를 체크
IEnumerator CheckMonsterState() {
while(!isDie) {
yield return new WaitForSeconds(0.3f);
//Player와 Monster간의 거리측정 스칼라값
float distance = Vector3.Distance(playerTr.position, monsterTr.position);
if(distance <= attackDist) {
state = State.ATTACK; //Attack로 상태변경
} else if(distance <= traceDist) {
state = State.TRACE; //Trace로 상태변경
} else {
state = State.IDLE; //Idle로 상태변경
}
}
}
//몬스터의 상태에 따라 몬스터의 동작을 수행
IEnumerator MonsterAction() {
while (!isDie) {
switch(state) {
case State.IDLE:
agent.isStopped= true; //추적을 중지
anim.SetBool(hashTrace, false);
break;
case State.TRACE:
agent.SetDestination(playerTr.position); //목표설정
agent.isStopped = false; //추적을 재개
anim.SetBool(hashTrace, true);
anim.SetBool(hashAttack, false); //공격파라메터 변경
break;
case State.ATTACK: //공격상태
anim.SetBool(hashAttack, true); //공격파라메터 변경
break;
case State.DIE: //사망상태
break;
}
yield return new WaitForSeconds(0.3f);
}
}
private void OnCollisionEnter(Collision coll) {
if (coll.collider.CompareTag("BULLET")) {
Destroy(coll.gameObject); //충돌한 총알을 삭제
anim.SetTrigger(hashHit); //피격 리액셔 애니메이션 실행
}
}
private void OnDrawGizmos() { //상태에 따라 기즈모 색깔 변경
if(state == State.TRACE) {
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, traceDist);
}
if(state == State.ATTACK) {
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackDist);
}
}
}
'유니티게임강좌 > 적 캐릭터 제작' 카테고리의 다른 글
[Enemy제작] Player - 자동 회전 (0) | 2023.03.08 |
---|---|
[Enemy제작] 혈흔 효과 (4) | 2023.03.08 |
[Enemy제작] 내비게이션 - 적 캐릭터의 순찰 및 추적 (1) | 2023.03.04 |
[Enemy제작] 유한 상태 머신의 정의 (2) | 2023.03.04 |
[Enemy제작] 유한 상태 머신의 정의 (0) | 2023.03.04 |