오늘 한 일
- 팀프로젝트 진행하기 ( 토끼 만들기 )
- AI 내비게이터를 좀 더 깊게 파보기
오늘은 AI 내비게이터를 활용한 도주 AI를 만들어볼 생각이다.
- 기본적으로는 플레이어로 부터 잘 멀어져야한다.
- 모서리에 붙었을 경우에 대한 이동 처리도 생각해줘야한다.
- 도주에 어느 정도 랜덤성을 부여해줘야한다.
// 필요한 컴포넌트 protected Animator animator; protected NavMeshAgent agent; // 플레이어 거리 구하기 protected float playerDistance; // 상태 protected AIState aiState; // SO public AnimalSO statSO; void Awake() { agent = GetComponent<NavMeshAgent>(); animator = GetComponent<Animator>(); } void Start() { ChangeState(AIState.Idle); } protected virtual void Update() { if (aiState == AIState.Dead) { return; } // 플레이어 거리 감지 playerDistance = (transform.position - CharacterManager.Instance.Player.transform.position).sqrMagnitude; // sqr을 활용했으니 제곱을 하는 걸 잊지 맙시다. animator.SetBool("Moving", aiState != AIState.Idle); switch (aiState) { case AIState.Idle: // IdleState(); break; case AIState.Flee: FleeState(); break; } animator.speed = agent.speed / statSO.walkSpeed; } // 상태 변환 protected virtual void ChangeState(AIState state) { aiState = state; switch (aiState) { case AIState.Idle: agent.isStopped = true; agent.speed = statSO.walkSpeed; break; case AIState.Flee: agent.isStopped = false; agent.speed = statSO.runSpeed; break; case AIState.Dead: // 초기화 작업 agent.isStopped = true; agent.speed = 0f; animator.speed = 1f; agent.ResetPath(); animator.SetBool("Dead", true); _collider.isTrigger = true; // 통과 시키게 하기 위해서 Invoke("Destroy", 30f); break; } } ... IdleState 부분 생략... // 도주 상태 시 void FleeState() { if (agent.remainingDistance < 1f) { agent.SetDestination(NewFleePoint()); } else { ChangeState(AIState.Idle); } } // 도주 목적지 설정 Vector3 NewFleePoint() { 목적지 설정 코드 } // 술래잡기 테스트용 private void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag("Player")) { if (aiState == AIState.Dead) { return; } ChangeState(AIState.Dead); } } protected virtual void Destroy() { Destroy(this.gameObject); }
- 스파르타 코딩클럽에서 제공된코드를 살짝 고쳐줬다.
반대 방향으로만 도망
코드
void FleeState() { if (agent.remainingDistance < 1f) { agent.SetDestination(NewFleePoint()); } else { ChangeState(AIState.Wandering); } } Vector3 NewFleePoint() { Vector3 dir = transform.position - CharacterManager.Instance.Player.transform.position.normalized; return transform.position + dir; }
- 모서리를 만나면 너무 약하다.
랜덤으로 돌리지만 특정 부분만 거르기
코드
void FleeState() { if (agent.remainingDistance < 1f) { agent.SetDestination(NewFleePoint()); } else { ChangeState(AIState.Wandering); } } Vector3 NewFleePoint() { NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * statSO.safeDistance), out NavMeshHit hit, statSO.safeDistance, NavMesh.AllAreas); int i = 0; while (GetDestinationAngle(hit.position) > 90 || playerDistance < statSO.safeDistance) { NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * statSO.safeDistance), out hit, statSO.safeDistance, NavMesh.AllAreas); i++; if (i == 30) break; } return hit.position; } float GetDestinationAngle(Vector3 targetPos) { return Vector3.Angle(transform.position - CharacterManager.Instance.Player.transform.position, transform.position + targetPos); }
- 전보다 낫지만 뭔가 보충이 좀 더 필요해보인다.
플레이어에게 꼴아박는 경우가 있다;
최종 개선
- FleeState에 조건을 추가하여 좀 더 능동적인 도주 패턴을 덧붙여줬다.
코드
void FleeState() { float destinationDistance = (agent.destination - CharacterManager.Instance.Player.transform.position).sqrMagnitude; // 조건에다가 목적지가 플레이랑 가깝지 않은 지 그리고 당장 플레이어와 너무 가깝지 않은지에 따른 조건을 추가해뒀다. if (agent.remainingDistance < 1f || destinationDistance < statSO.safeDistance * statSO.safeDistance || playerDistance < 1f) { agent.SetDestination(NewFleePoint()); } else { ChangeState(AIState.Wandering); } } ... 아랫 부분은 랜덤 방향 도주와 동일하다.
- 빨간색이 랜덤 도주, 파란색이 추가적인 조건을 덧붙여준 토끼다
- 체감상 느끼기로는 그냥 뭉쳐있으면 도주 능력이 어느 정도 약해질 수 밖에 없는 것 같다.
- 실제 적용으로는 다를 수도 있으니, 되도록이면 코드는 간결하게 만들수록 좋을 것이다.
Vector
.sqrMagnitude
- 루트를 씌우지 않은 벡터의 스칼라값으로 기존 magnitude보다 연산은 빠르지만 조건 덧붙여줄 때에는 조건을 제곱해줘야한다.
NavMeshAgent
.FindClosetEdge(out NavMeshHit edgeHit)
- 현재 위치에서 가장 가까운 NavMesh의 바깥 테두리 위치를 가져온다.
확실히 한 사람이 여러 역할을 분담받는 것보다는 파트를 나눠 한 사람씩 역할을 맡으니까. 좀 더 세부적인 사항을 신경 쓸 수 있어서 좋은 것 같다.