[Project] FPS Prototype(11)
in Unity
적 캐릭터의 행동 중 대기, 배회, 추적하기에 대해 만들어보자.
적 캐릭터 이동 경로 설정 (Navigation Mesh)
맵 정보가 바뀌면 바뀐 맵에 대해 Navigation 설정을 다시하면 된다.
(1) Navigation 뷰를 열고 경로, 장애물로 사용되는 오브젝트들을 선택한 후 Object탭에 있는 Navigation Static을 체크해준다.

(2) Bake탭으로 가서 Bake버튼을 눌러서 맵 데이터를 생성해준다.

이제 이동가능, 이동불가능 구역이 나타난다.
적 캐릭터 FSM - 배회하기
(1) 적 캐릭터의 행동을 제어하는 EnemyFSM스크립트를 만든다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public enum EnemyState { None=-1,Idle = 0, Wander,}
public class EnemyFSM : MonoBehaviour
{
private EnemyState enemyState = EnemyState.None; // 현재 적 행동
private Status status; // 이동속도 등의 정보
private NavMeshAgent navMeshAgent; // 이동 제어를 위한 NavMeshAgent
private void Awake()
{
status=GetComponent<Status>();
navMeshAgent=GetComponent<NavMeshAgent>();
// NavMeshAgent 컴포넌트에서 회전을 업데이트하지 않도록 설정
navMeshAgent.updateRotation = false;
}
private void OnEnable()
{
// 적이 활성화될 떄 적의 상태를 "대기"로 설정
ChangeState(EnemyState.Idle);
}
private void OnDisable()
{
// 적이 비활성화될 때 현재 재생중인 상태를 종료하고, 상태를 "None"으로 설정
StopCoroutine(enemyState.ToString());
enemyState=EnemyState.None;
}
public void ChangeState(EnemyState newState)
{
// 현재 재생중인 상태와 바꾸려고 하는 상태가 같으면 바꿀 필요가 없기 때문에 return
if (enemyState == newState) return;
// 이전에 재생중이던 상태 종료
StopCoroutine(enemyState.ToString());
// 현재 적의 상태를 newState로 설정
enemyState = newState;
// 새로운 상태 재생
StartCoroutine(enemyState.ToString());
}
private IEnumerator Idle()
{
// n초 후에 "배회" 상태로 변경하는 코루틴 실행
StartCoroutine("AutoChangeFromIdleToWander");
while (true)
{
// "대기" 상태일 떄 하는 행동
yield return null;
}
}
private IEnumerator AutoChangeFromIdleToWander()
{
// 1~4초 시간 대기
int changeTime = Random.Range(1, 5);
yield return new WaitForSeconds(changeTime);
// 상태를 "배회"로 변경
ChangeState(EnemyState.Wander);
}
private IEnumerator Wander()
{
float currentTime = 0;
float maxTime = 10;
// 이동 속도 설정
navMeshAgent.speed = status.WalkSpeed;
// 목표 위치 설정
navMeshAgent.SetDestination(CalculateWanderPosition());
// 목표 위치로 회전
Vector3 to = new Vector3(navMeshAgent.destination.x,0,navMeshAgent.destination.z);
Vector3 from = new Vector3(transform.position.x,0,transform.position.z);
transform.rotation = Quaternion.LookRotation(to - from);
while (true)
{
currentTime += Time.deltaTime;
// 목표위치에 근접하게 도달하거나 너무 오랜시간 배회하기 상태에 머물러 있으면
to = new Vector3(navMeshAgent.destination.x, 0, navMeshAgent.destination.z);
from = new Vector3(transform.position.x, 0, transform.position.z);
if((to-from).sqrMagnitude<0.01f || currentTime >= maxTime)
{
// 상태를 "대기"로 변경
ChangeState(EnemyState.Idle);
}
yield return null;
}
}
private Vector3 CalculateWanderPosition()
{
float wanderRadius = 10; // 현재 위치를 원점으로 하는 원의 반지름
int wanderJitter = 0; // 선택된 각도 (wanderJitterMin ~ wanderJitterMax)
int wanderJitterMin = 0; // 최소 각도
int wanderJitterMax = 360;
// 현재 적 캐릭터가 있는 월드의 중심 위치와 크기 (구역을 벗어난 행동을 하지 않도록)
Vector3 rangePosition = Vector3.zero;
Vector3 rangeScale=Vector3.one*100.0f;
// 자신의 위치를 중심으로 반지름(wanderRadius) 거리, 선택된 각도(wanderJitter)에 위치한 좌표를 목표지점으로 설정
wanderJitter=Random.Range(wanderJitterMin,wanderJitterMax);
Vector3 targetPosition=transform.position+SetAngle(wanderRadius,wanderJitter);
// 생성된 목표위치가 자신의 이동구역을 벗어나지 않게 조절
targetPosition.x = Mathf.Clamp(targetPosition.x, rangePosition.x - rangeScale.x * 0.5f, rangePosition.x + rangeScale.x * 0.5f);
targetPosition.y = 0.0f;
targetPosition.z = Mathf.Clamp(targetPosition.z, rangePosition.z - rangeScale.z * 0.5f, rangePosition.z + rangeScale.z * 0.5f);
return targetPosition;
}
private Vector3 SetAngle(float radius,int angle)
{
Vector3 position = Vector3.zero;
position.x = Mathf.Cos(angle) * radius;
position.z = Mathf.Sin(angle) * radius;
return position;
}
private void OnDrawGizmos()
{
// "배회" 상태일 때 이동할 경로 표시
Gizmos.color = Color.black;
Gizmos.DrawRay(transform.position,navMeshAgent.destination-transform.position);
}
}
(2) Enemy프리팹에 status, EnemyFSM스크립트와 NavMeshAgent컴포넌트를 추가해준다.

이제 위와 같이 처음에는 가만히 있다가 몇 초후에 배회상태가 되는 걸 볼 수 있다.
적 캐릭터 FSM - 추적하기
(1) EnemyFSM스크립트를 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public enum EnemyState { None=-1,Idle = 0, Wander, /*●*/Pursuit,}
public class EnemyFSM : MonoBehaviour
{
/*●*/[Header("Pursuit")]
/*●*/[SerializeField]
/*●*/private float targetRecognitionRange = 8; // 인식 범위 (이 범위 안에 들어오면 "PurSuit" 상태로 변경)
/*●*/[SerializeField]
/*●*/private float pursuitLimitRange = 10; // 추적 범위 (이 범위 바깥으로 나가면 "Wander" 상태로 변경)
private EnemyState enemyState = EnemyState.None; // 현재 적 행동
private Status status; // 이동속도 등의 정보
private NavMeshAgent navMeshAgent; // 이동 제어를 위한 NavMeshAgent
/*●*/private Transform target; // 적의 공격 대상 (플레이어)
//private void Awake()
public void Setup(Transform target)
{
status=GetComponent<Status>();
navMeshAgent=GetComponent<NavMeshAgent>();
/*●*/this.target=target;
// NavMeshAgent 컴포넌트에서 회전을 업데이트하지 않도록 설정
navMeshAgent.updateRotation = false;
}
private void OnEnable()
{
// 적이 활성화될 떄 적의 상태를 "대기"로 설정
ChangeState(EnemyState.Idle);
}
private void OnDisable()
{
// 적이 비활성화될 때 현재 재생중인 상태를 종료하고, 상태를 "None"으로 설정
StopCoroutine(enemyState.ToString());
enemyState=EnemyState.None;
}
public void ChangeState(EnemyState newState)
{
// 현재 재생중인 상태와 바꾸려고 하는 상태가 같으면 바꿀 필요가 없기 때문에 return
if (enemyState == newState) return;
// 이전에 재생중이던 상태 종료
StopCoroutine(enemyState.ToString());
// 현재 적의 상태를 newState로 설정
enemyState = newState;
// 새로운 상태 재생
StartCoroutine(enemyState.ToString());
}
private IEnumerator Idle()
{
// n초 후에 "배회" 상태로 변경하는 코루틴 실행
StartCoroutine("AutoChangeFromIdleToWander");
while (true)
{
// "대기" 상태일 떄 하는 행동
/*●*/// 타겟과의 거리에 따라 행동 선택 (배회, 추격, 원거리 공격)
/*●*/CalculateDistanceToTargetAndSelectState();
yield return null;
}
}
private IEnumerator AutoChangeFromIdleToWander()
{
// 1~4초 시간 대기
int changeTime = Random.Range(1, 5);
yield return new WaitForSeconds(changeTime);
// 상태를 "배회"로 변경
ChangeState(EnemyState.Wander);
}
private IEnumerator Wander()
{
float currentTime = 0;
float maxTime = 10;
// 이동 속도 설정
navMeshAgent.speed = status.WalkSpeed;
// 목표 위치 설정
navMeshAgent.SetDestination(CalculateWanderPosition());
// 목표 위치로 회전
Vector3 to = new Vector3(navMeshAgent.destination.x,0,navMeshAgent.destination.z);
Vector3 from = new Vector3(transform.position.x,0,transform.position.z);
transform.rotation = Quaternion.LookRotation(to - from);
while (true)
{
currentTime += Time.deltaTime;
// 목표위치에 근접하게 도달하거나 너무 오랜시간 배회하기 상태에 머물러 있으면
to = new Vector3(navMeshAgent.destination.x, 0, navMeshAgent.destination.z);
from = new Vector3(transform.position.x, 0, transform.position.z);
if((to-from).sqrMagnitude<0.01f || currentTime >= maxTime)
{
// 상태를 "대기"로 변경
ChangeState(EnemyState.Idle);
}
/*●*/// 타겟과의 거리에 따라 행동 선택 (배회, 추격, 원거리 공격)
/*●*/CalculateDistanceToTargetAndSelectState();
yield return null;
}
}
private Vector3 CalculateWanderPosition()
{
float wanderRadius = 10; // 현재 위치를 원점으로 하는 원의 반지름
int wanderJitter = 0; // 선택된 각도 (wanderJitterMin ~ wanderJitterMax)
int wanderJitterMin = 0; // 최소 각도
int wanderJitterMax = 360;
// 현재 적 캐릭터가 있는 월드의 중심 위치와 크기 (구역을 벗어난 행동을 하지 않도록)
Vector3 rangePosition = Vector3.zero;
Vector3 rangeScale=Vector3.one*100.0f;
// 자신의 위치를 중심으로 반지름(wanderRadius) 거리, 선택된 각도(wanderJitter)에 위치한 좌표를 목표지점으로 설정
wanderJitter=Random.Range(wanderJitterMin,wanderJitterMax);
Vector3 targetPosition=transform.position+SetAngle(wanderRadius,wanderJitter);
// 생성된 목표위치가 자신의 이동구역을 벗어나지 않게 조절
targetPosition.x = Mathf.Clamp(targetPosition.x, rangePosition.x - rangeScale.x * 0.5f, rangePosition.x + rangeScale.x * 0.5f);
targetPosition.y = 0.0f;
targetPosition.z = Mathf.Clamp(targetPosition.z, rangePosition.z - rangeScale.z * 0.5f, rangePosition.z + rangeScale.z * 0.5f);
return targetPosition;
}
private Vector3 SetAngle(float radius,int angle)
{
Vector3 position = Vector3.zero;
position.x = Mathf.Cos(angle) * radius;
position.z = Mathf.Sin(angle) * radius;
return position;
}
/*●*/private IEnumerator Pursuit()
/*●*/{
/*●*/ while (true)
/*●*/ {
/*●*/ // 이동 속도 설정 (배회할 때는 걷는 속도로 이동, 추적할 때는 뛰는 속도로 이동)
/*●*/ navMeshAgent.speed = status.RunSpeed;
/*●*/
/*●*/ // 목표위치를 현재 플레이어의 위치로 설정
/*●*/ navMeshAgent.SetDestination(target.position);
/*●*/
/*●*/ // 타겟 방향을 주시하도록 함
/*●*/ LookRotationToTarget();
/*●*/
/*●*/ // 타겟과의 거리에 따라 행동 선택 (배회, 추격, 원거리 공격)
/*●*/ CalculateDistanceToTargetAndSelectState();
/*●*/
/*●*/ yield return null;
/*●*/ }
/*●*/}
/*●*/private void LookRotationToTarget()
/*●*/{
/*●*/ // 목표 위치
/*●*/ Vector3 to = new Vector3(target.position.x,0,target.position.z);
/*●*/ // 내 위치
/*●*/ Vector3 from = new Vector3(transform.position.x,0,transform.position.z);
/*●*/
/*●*/ // 바로 돌기
/*●*/ transform.rotation = Quaternion.LookRotation(to-from);
/*●*/
/*●*/ // 서서히 돌기
/*●*/ //Quaternion rotation = Quaternion.LookRotation(to - from);
/*●*/ //transform.rotation = Quaternion.Slerp(transform.rotation, rotation, 0.01f);
/*●*/}
/*●*/private void CalculateDistanceToTargetAndSelectState()
/*●*/{
/*●*/ if (target == null) return;
/*●*/
/*●*/ // 플레이어(target)와 적의 거리 계산 후 거리에 따라 행동 선택
/*●*/ float distance = Vector3.Distance(target.position, transform.position);
/*●*/
/*●*/ if (distance <= targetRecognitionRange)
/*●*/ {
/*●*/ ChangeState(EnemyState.Pursuit);
/*●*/ }
/*●*/ else if(distance >= pursuitLimitRange)
/*●*/ {
/*●*/ ChangeState(EnemyState.Wander);
/*●*/ }
/*●*/}
private void OnDrawGizmos()
{
// "배회" 상태일 때 이동할 경로 표시
Gizmos.color = Color.black;
Gizmos.DrawRay(transform.position,navMeshAgent.destination-transform.position);
/*●*/// 목표 인식 범위
/*●*/Gizmos.color = Color.red;
/*●*/Gizmos.DrawWireSphere(transform.position, targetRecognitionRange);
/*●*/
/*●*/// 추적 범위
/*●*/Gizmos.color = Color.green;
/*●*/Gizmos.DrawWireSphere(transform.position, pursuitLimitRange);
}
}
(2) EnemyMemoryPool 스크립트를 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyMemoryPool : MonoBehaviour
{
/*●*/[SerializeField]
/*●*/private Transform target; // 적의 목표 (플레이어)
[SerializeField]
private GameObject enemySpawnPointPrefab; // 적이 등장하기 전 적의 등장 위치를 알려주는 프리팹
[SerializeField]
private GameObject enemyPrefab; // 생성되는 적 프리팹
[SerializeField]
private float enemySpawnTime = 1; // 적 생성 주기
[SerializeField]
private float enemySpawnLatency = 1; // 타일 생성 후 적이 등장하기까지 대기 시간
private MemoryPool spawnPointMemoryPool; // 적 등장 위치를 알려주는 오브젝트 생성, 활성/비활성화 관리
private MemoryPool enemyMemoryPool; // 적 생성, 활성/비활성화 관리
private int numberOfEnemiesSpawnedAtOne = 1; // 동시에 생성되는 적의 숫자
private Vector2Int mapSize = new Vector2Int(100, 100);
private void Awake()
{
spawnPointMemoryPool = new MemoryPool(enemySpawnPointPrefab);
enemyMemoryPool = new MemoryPool(enemyPrefab);
StartCoroutine("SpawnTile");
}
private IEnumerator SpawnTile()
{
int currentNumber = 0;
int maximumNumber = 50;
while (true)
{
// 동시에 numberOfEnemiesSpawnAtOne 숫자만큼 생성되도록 반복문 사용
for(int i = 0; i < numberOfEnemiesSpawnedAtOne; i++)
{
GameObject item = spawnPointMemoryPool.ActivatePoolItem();
item.transform.position = new Vector3(Random.Range(-mapSize.x * 0.49f, mapSize.x * 0.49f), 0,
Random.Range(-mapSize.y * 0.49f, mapSize.y * 0.49f));
StartCoroutine("SpawnEnemy", item);
}
currentNumber++;
if(currentNumber >= maximumNumber)
{
currentNumber = 0;
numberOfEnemiesSpawnedAtOne++;
}
yield return new WaitForSeconds(enemySpawnTime);
}
}
private IEnumerator SpawnEnemy(GameObject point)
{
yield return new WaitForSeconds(enemySpawnLatency);
// 적 오브젝트를 생성하고, 적의 위치를 point의 위치로 설정
GameObject item = enemyMemoryPool.ActivatePoolItem();
item.transform.position=point.transform.position;
/*●*/item.GetComponent<EnemyFSM>().Setup(target);
// 타일 오브젝트 비활성화
spawnPointMemoryPool.DeactivatePoolItem(point);
}
}
(3) EnemyMemoryPool오브젝트의 target변수에 Player오브젝트를 넣어준다.

이제 추적 범위가 나오고 플레이어가 빨간선 안으로 들어가면 추적을 하고 초록선 밖으로 나가면 배회하는 모습을 볼 수 있다.
