TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역] Enemy AI in Unity

jplee 2024. 5. 26. 17:05

역자의 말.
최근 어떤 계기로 리니지M 스러운 캐릭터 콘트롤 및 제어와 간단한 몬스터의 AI 를 구현 해야 할 일이 생겼습니다. 그러한 계기로 요즘 현업에서 사용 될 법한 AI의 처리에 좀 친숙해 져 보려고 노력 하던 차에... 좋은 아티클이 보여서 공유 해 봅니다.


저자
https://x.com/JohnLFrench
https://gamedevbeginner.com/author/john/

 

John French, Author at Game Dev Beginner

I’m John, I write all of the Unity tutorials on Game Dev Beginner. I've been working as a professional game composer for the last 10 years and I've been writing about game development in Unity for the last 6. I started by writing audio-related tutorials

gamedevbeginner.com

게임에서 AI는 무엇인가요?

  • 적이나 NPC와 같은 자동으로 움직이는 오브젝트를 제어하는 데 사용됩니다.
  • 주변 환경에 따라 스스로 생각하고 행동하도록 합니다.

Unity에서 AI를 구축하는 방법:

  • 상태 머신: 오브젝트의 현재 상태에 따라 행동을 결정합니다.
  • 비헤이비어 트리: 오브젝트가 수행해야 할 작업 목록을 정의합니다.
  • 스크립팅: 간단한 규칙에 따라 오브젝트의 행동을 제어합니다.

AI 구축 시 고려 사항:

  • 오브젝트의 목적은 무엇인가요?
  • 오브젝트는 어떻게 생각해야 하나요?
  • 어떤 환경에서 행동해야 하나요?

적합한 방법 선택:

  • 오브젝트의 복잡성과 필요한 행동 수준에 따라 적합한 방법을 선택합니다.

참고:

  • 이 글은 Unity에서 AI를 구축하는 기본적인 개념만 다룹니다.
  • 더 복잡한 AI 시스템을 구축하려면 추가적인 학습이 필요합니다.

How does enemy AI in Unity work?

적 AI는 플레이어가 아닌 오브젝트가 게임 상황에 따라 행동을 변화시키는 것을 의미합니다. 적, 아군, NPC, 심지어 카메라까지 모든 오브젝트가 AI로 작동할 수 있습니다.
하지만 일반적으로 적 AI는 플레이어와 대결하거나 협력하도록 설계됩니다. 이 때문에 게임에서 AI 시스템을 구축하는 것은 복잡한 과정이 될 수 있습니다.
하지만 걱정하지 마세요! 복잡한 AI를 제외하고는 일반적으로 AI 오브젝트에는 세 가지 기본 요소만 존재합니다.

  1. 상태: 현재 AI의 목표 또는 수행하려는 작업을 나타냅니다.
  2. 입력: AI의 상태를 변화시키는 요소 (예: 플레이어 공격, 주변 환경 변화)
  3. 작업: AI가 목표를 달성하기 위해 수행하는 행동 (예: 공격, 이동, 방어)

간단히 말해, AI 오브젝트는 다음과 같은 기능을 수행해야 합니다.

  • 자신의 목표를 파악: 현재 상태를 인지해야 합니다.
  • 상황에 반응: 주변 환경 변화에 따라 목표를 변경해야 합니다.
  • 목표 달성: 목표를 달성하기 위한 행동을 수행해야 합니다.

이를 구현하기 위해 다음과 같은 기술적 사항을 고려해야 합니다.

  • 상태 머신: AI의 상태와 행동을 제어하는 데 사용됩니다.
  • 비헤이비어 트리: AI가 수행해야 할 작업 목록을 정의합니다.
  • 스크립팅: 간단한 규칙에 따라 AI의 행동을 제어합니다.
  • 센서: 주변 환경을 감지합니다.
  • 경로탐색: AI가 목표 지점까지 이동할 수 있도록 합니다.

적절한 AI 시스템을 선택하는 것은 AI 오브젝트의 복잡성과 필요한 기능 수준에 따라 달라집니다.

AI 개체의 상태를 관리하는 방법

AI 오브젝트는 항상 특정 상태를 가지고 있으며, 이는 현재 달성하려는 목표를 나타냅니다.

  • 상태는 간단하거나 복잡할 수 있으며, 다른 변수, 코드 블록, 상태 머신, 행동 트리 등으로 구현될 수 있습니다.
  • 중요한 것은 AI가 무엇을 하고 싶은지, 그리고 어떤 상황에서 목표를 변경할지 명확하게 정의하는 것입니다.

예를 들어, 상태는 다른 변수 값에 따라 다른 코드 블록을 실행하는 if 조건이나  스위치 문과 같이 간단할 수 있습니다.

  • 상태 관리 방식: if 조건, 스위치 문, 상태 머신, 행동 트리 등 다양한 방식으로 구현 가능
  • 상태 변경 요인: 플레이어 공격, 주변 환경 변화 등 다양한 입력에 의해 발생
  • 상태에 따른 행동: 공격, 이동, 방어 등 다양한 작업 수행

핵심: AI는 현재 상태에 따라 행동하며, 이는 목표입력에 의해 결정됩니다.
상태:

  • 다양한 방식으로 구현 가능 (if-문, 상태 머신, 행동 트리 등)
  • 간단하거나 복잡할 수 있음
  • 핵심: AI가 무엇을 하고 싶은지 명확하게 정의해야 함

변화 요인:

  • 플레이어 공격, 주변 환경 변화 등 다양한 입력
  • 핵심: AI가 상황에 따라 목표를 변경할 수 있도록 해야 함

결과:

  • AI는 상황에 맞게 적절하게 행동
  • 핵심: AI가 어떻게 행동하고, 언제 목표를 변경하는지 명확하게 정의해야 함

그렇다면 어떻게 작동할까요?

AI 객체의 상태를 변경하는 방법

여기서 '입력'은 AI 시스템의 상태를 변경할 수 있는 외부 요인을 의미합니다.
간단히 말해, 인공지능이 다른 일을 하고 싶게 만드는 것은 세상이나 대상에서 어떤 변화가 일어날 수 있는지입니다.
이는 AI 시스템에서 가장 중요한 부분이라고 할 수 있습니다.
상태는 AI가 수행하고자 하는 작업을 결정하지만, 기술적으로는 AI가 다양한 작업을 수행할 수 있도록 하는 프레임워크에 지나지 않습니다. 서로 다른 코드 블록을 분리하고 실행하는 데 사용하는 실용적인 방법입니다.
상태가 실제로 어떻게 변경되는지, 그리고 그 원인이 무엇인지를 관리하는 것은 본질적으로 AI가 의사 결정을 내리는 방식입니다.
실제로 지능을 갖춘 인공지능의 일부입니다.
그렇다면 어떻게 작동할까요?
AI가 결정을 내리는 데 사용할 수 있는 입력은 일반적으로 플레이어가 자신의 행동을 변경하는 데 사용할 수 있는 정보와 유사합니다.
예를 들어, 월드의 이벤트, 체력이나 탄약 부족과 같은 자체 데이터의 변화 또는 게임에서 일어나는 일을 보거나 들음으로써 반응할 수 있습니다.
입력이 객체의 동작을 변경하는 데 어떻게 사용되는지에 따라 달라질 수 있으며, 이는 일반적으로 상태를 관리하는 데 어떤 방법을 사용하느냐에 따라 달라집니다.
예를 들어 값을 읽고 그 결과로 발생하는 결과를 변경하는 가장 간단한 방법 중 하나는  스위치 문을 사용하는 것입니다.

스위치 문으로 AI 만들기

AI 시스템이 반드시 복잡할 필요는 없습니다.
때로는 간단한 조건부 검사를 기반으로 객체가 한 가지 또는 다른 작업을 수행하도록 하고 싶을 수도 있습니다.
이를 수행하는 쉬운 방법은 조건이나 변수의 값에 따라 실행할 코드 블록을 지정할 수 있는  스위치 문을 사용하는 것입니다.
Like this:

public class SwitchExample : MonoBehaviour
{
    public State currentState = State.Idle;

    void Update()
    {
        switch (currentState)
        {
            case State.Idle:
                Debug.Log("Waiting...");
                break;

            case State.Attack:
                Debug.Log("Attacking!");
                break;

            case State.Retreat:
                Debug.Log("Run Away!");
                break;
        }
    }
}

[System.Serializable]
public enum State { Idle, Attack, Retreat}

일반적으로 추적하기가 조금 더 쉽고 간단한 방법으로 여러 실행 상태를 설정할 수 있다는 점을 제외하면 if 문과 비슷한 방식으로 작동합니다.
기본적으로 스위치 문은 일종의 상태 머신과 유사하지만, 스위치 문은 일반적으로 공통 외부 변수를 사용하여 제어되는 반면 상태 머신은 자체 변경 조건을 관리한다는 점이 다릅니다.

Creating AI with State Machines

상태 머신은 일반적으로 여러 개의 유한한 실행 상태를 번갈아 가며 실행하며, 각 상태는 해당 상태에서 가능한 종료 경로를 평가할 책임이 있습니다.
따라서 상태 머신은 예를 들어  유휴 상태,  순찰 상태,  공격 상태와 같이 여러 유형의 활동으로 쉽게 분리되는 단순한 AI 동작에 이상적입니다.

  • State Machines in Unity (how and when to use them)

그러나 복잡한 동작의 경우 상태 머신이 때때로 제한적일 수 있습니다.
이는 일반적으로 상태가 하나의 작업을 나타내기 때문입니다.
시스템이 흐름도처럼 미리 정해진 여러 가지 상태 중 하나로 이동할 수 있는 하나의 존재 상태로, 다음에 일어날 일은 전적으로 현재 상태에 대한 해석에 따라 달라집니다.
그러나 이 접근 방식의 문제점은 더 복잡한 의사 결정 과정을 반영하지 못한다는 점입니다;
 계층적 상태 머신의 경우 하위 상태가 부모로부터 공유 동작을 상속할 수 있지만, 상태 머신의 주요 문제는 일반적으로 머신 자체가 아닌 각 개별 상태가 다음에 일어날 일을 결정한다는 점입니다.

일반적으로 상태는 자체적으로 전환을 관리하며, 각 상태는 게임의 이벤트를 해석하고 다음에 들어갈 상태를 선택합니다.

그렇다면 대안은 무엇일까요?
상태 머신은 상태를 관리하는 데 유용할 수 있지만, AI가 실제로 의사 결정을 내리게 하려면 여러 가지 가능한 목표를 고려하고 우선순위를 정할 수 있어야 합니다.
예를 들어  행동 트리와 같은 경우입니다.

Behaviour Trees in Unity

행동 트리는 보다 자연스러운 의사 결정 프로세스를 만드는 데 사용할 수 있는 일종의 모듈형 AI 시스템입니다.
상태 머신은 AI가 다음에 수행할 작업을 결정하기 전에 여러 가지 조건을 순서대로 평가한다는 점에서 상태 머신과 다릅니다.
이를 통해 AI는 작업을 완료하려고 할 때 경직된 진입 및 종료 경로로 미리 정해진 몇 가지 상태에서 단순히 전환하는 것이 아니라 여러 가지 요소를 지속적으로 고려할 수 있습니다.
예를 들어, 인공지능 객체가 잠긴 상자를 열려고 한다고 가정해 보겠습니다.
목표를 이해하지만 이를 달성하기 위해서는 열쇠를 찾아야 합니다.
열쇠는 상자를 열기 위한 전제 조건으로, AI 객체가 열쇠를 발견한 후에도 상자를 여는 목표를 완수하기 위해 열쇠를 계속 가지고 있어야 한다는 의미입니다.

하지만 인공지능은 보물보다 자신의 생명을 더 소중히 여깁니다.
공격으로부터 자신을 보호하는 것이 상자를 여는 것보다 당연히 더 중요하므로 위험하지 않은 경우에만 상자를 열려고 시도해야 합니다.
행동 트리를 사용하면 이 작업을 비교적 쉽게 수행할 수 있습니다.
약탈 행동에 앞서 대상의 안전 여부를 확인하는 프로세스를 통해 한 목표의 우선순위를 다른 목표보다 우선시할 수 있습니다.

이 경우 AI는 상자를 열기 전에 안전해질 때까지 기다렸다가 어느 시점에서든 상황이 바뀌면 수행 중인 작업을 전환합니다.
하지만 스테이트 머신을 사용하여 이와 같은 시스템을 구축한다면, 각 상태가 로직의 다음 단계를 담당하기 때문에 플레이어가 두 가지 논리 상태 모두에서 공격을 받고 있는지 평가할 수 있어야 두 상태 사이를 이동할 수 있습니다.
상태 머신이 단순할 때는 문제가 되지 않지만, AI가 다양한 요소를 고려할 수 있도록 하려면 일반적으로 상태가 직접 연결되어 있기 때문에 유한 상태 머신을 사용하여 이와 같은 시스템을 구축하는 것이 금방 어려워질 수 있습니다.
그러나 행동 트리는 우선순위에 따라 평가됩니다.
즉, 한 가지 작업이 다른 작업보다 더 중요하다는 사실을 제외하면 각 작업은 서로 연결되어 있지 않습니다.
따라서 다른 로직에 영향을 주지 않고 동작 트리에서 실행 상태를 훨씬 쉽게 추가, 제거 및 재사용할 수 있습니다.
따라서 기본 상태 집합보다 더 복잡하지만 프로젝트 전체에서 변경되거나 재사용될 수 있는 무언가가 필요하다는 것을 알고 있다면 동작 트리 또는 이와 유사한 것이 이상적일 수 있습니다.
하지만 실제로는 어떻게 작동할까요?

How Behaviour Trees in Unity work

비헤이비어 트리 는 일반적으로  노드로 구성되며, 각 노드는  평가된 경우 세 가지 가능한 상태 중 하나를 반환할 수 있습니다: 성공실패, 또는 실행.
노드의 정의와 노드가 반환할 수 있는 상태의 유형은 동작 트리에서 보편적이며 상호 교환이 가능하므로 다양한 유형의 노드를 조합하여 전체 트리를 구성하는 완전한 로직 세트를 구성할 수 있습니다.
하지만 트리의 로직은 어떻게 평가되며, 객체의 동작을 결정하는 데 어떻게 사용될 수 있을까요?
일반적으로 노드는 다른 함수와 마찬가지로 작업을 수행하는 데 사용할 수 있으며, 작업을 수행하면 성공, 실패 또는 실행 중 상태를 반환합니다.
예를 들어 액션 노드를 사용하여 객체를 찾을 수 있습니다.
검색하는 동안 노드는  실행 중이며, 이는 단순히 작업이 아직 성공하지 않았음을 의미하는 것이 아니라 완료되지 않았기 때문일 뿐입니다.
찾고 있는 객체를 찾으면 노드는  성공  상태를 반환하고, 그 후 다른 노드를 처리하여 정보에 따라 조치를 취할 수 있습니다.
또는 객체를 찾는 데 시간이 너무 오래 걸려서 작업을 완료할 수 없는 경우 대신  실패를 반환할 수 있습니다.
이를 통해 AI의 작업을 프로젝트에서 쉽게 변경하고 재사용할 수 있는 작은 모듈식 조각으로 나눌 수 있습니다.
하지만 AI의 행동을 이렇게 작은 작업으로 나누면 공격, 방어, 도주 등 더 넓은 상태를 관리하기가 더 어려워질 수 있습니다.
여기서  컨트롤 노드가 등장합니다.

Behaviour Tree Control Nodes

제어 노드를 사용하면 여러 작업 및 기타 조건부 노드를 포함 노드인 제어 노드로 구성하여 하위 노드의 상태에 따라 일반 작업의 완료 여부를 결정할 수 있습니다.
이 기능은 전체 작업 집합을 행동의 한 가지로 정리한 다음 성공 여부를 결정할 수 있으므로 유용합니다.
일반적으로 제어 노드에는 두 가지 유형, 즉  선택자와  시퀀스가 있습니다.
다른 노드와 마찬가지로 평가 시  성공,  실패  또는  실행 중 응답과 같은 동일한 종류의 상태도 반환합니다.
그 외에는 자식 노드의 상태에 따라 반환되는 상태가 달라집니다.
예를 들어,  선택자 노드 는 일반적으로 해당 노드 중 하나라도 성공 또는 실행을 반환하는 경우 성공 또는 실행을 반환합니다.


일반적으로 선택기 노드는 해당 노드 중 하나라도 있는 경우 성공 또는 실행 중을 반환합니다.

이는 조건 중 하나만 참이어야 계속할 수 있는 if/또는 문과 비슷한 방식으로 작동합니다.
그 후에는 일반적으로 완전히 다른 로직의 응답을 처리할 수 있습니다.
시퀀스 노드는 일반적으로 모든 자식 노드가 성공할 경우에만 성공을 반환하고, 그렇지 않으면 실패하거나 실행 중으로 돌아갑니다.

일반적으로 시퀀스 노드는 포함된 모든 작업도 성공해야만 성공으로 간주합니다.

이는 여러 작업을 순서대로 지정할 때 유용할 수 있으며, AI가 작업을 완료한 것으로 간주하기 전에 한 가지 작업을 차례로 수행해야 합니다.
제어 흐름 노드 내부의 각 작업은 그 자체로 시퀀스 또는 선택기가 될 수 있으므로 중첩된 로직의 복잡한 브랜치를 쉽게 만들 수 있습니다.
행동 트리를 구성할 때는 일반적으로 덜 중요한 작업이 더 중요한 작업 다음에 실행되도록 노드를 정렬하는 것이 좋습니다.
즉, AI 객체의 기본 동작(다른 작업을 수행할 수 없을 때 수행하는 작업)은 일반적으로 트리의 오른쪽에 있는 마지막에 평가됩니다.
이를 통해 가능하면 더 중요한 동작을 먼저 평가하고 처리할 수 있습니다.
예를 들어, AI 오브젝트가 전리품을 찾고 있다면 여러 가지 작업을 수행할 수 있습니다.
먼저 열 컨테이너를 찾아야 합니다.
그런 다음 잠겨 있는 경우 열 수 있는 키를 찾아야 합니다.
일단 열리면 AI는 더 이상 열어야 할 대상이 없어지고 시퀀스가 다시 시작됩니다.
즉, 전체 전리품 획득 시퀀스에 앞서 AI 오브젝트에 애초에 전리품이 필요한지 여부를 고려하는 전제 조건 검사를 생성하지 않는다면 말입니다.

이 기본 예시에서는 AI가 보물 상자를 찾아서 여는 데 필요한 단계를 반복해서 수행하면서 전리품을 찾습니다.

행동 트리의 핵심은 작업과 조건부 검사를 모듈식으로 구성하고 우선순위를 정할 수 있다는 점입니다.
다른 스크립트를 변경할 필요 없이 AI가 수행하는 작업과 원하는 작업을 쉽게 변경할 수 있습니다.
그러나 행동 트리는 매우 유용할 수 있지만 구축하기 어려울 수 있습니다.
특히 처음부터 자체 행동 트리 시스템을 구축하는 경우, 그리고 시각적으로 트리를 보고 수정할 수 없는 경우에는 더욱 그렇습니다.
따라서 직접 구축하지 않는 한, 행동 트리 시스템을 구현하는 가장 좋은 방법은 다음과 같은 기성 옵션을 사용하는 것입니다 노드 캔버스, 또는 행동 디자이너.
인공지능의 모든 행동은 궁극적으로 인공지능이 무엇을 하고 싶다고 생각하는지에 따라 달라지기 때문에 인공지능에게 원하는 것에 대해 마음을 바꿀 수 있는 능력을 부여하는 것은 인공지능 시스템에서 가장 중요한 부분 중 하나가 될 수 있습니다.
하지만...
인공지능에게 원하는 것을 결정할 수 있는 기능을 제공한다고 해도 실제로 그 결정에 따라 행동할 수단이 없다면 아무 소용이 없습니다.
그렇다면 AI가 실제로 어떤 일을 하도록 만들려면 어떻게 해야 할까요?

Enemy AI actions

AI 객체가 하고 싶은 일을 알고 그 일을 하기로 결정한 후에는 목표를 완수하기 위해  행동을 수행해야 합니다.
여기에는 물체를 다른 물체 쪽으로 이동하거나, 틈새를 뛰어넘거나, 무기로 플레이어를 쏘려고 하는 등의 행동이 포함될 수 있습니다.
그렇다면 Unity의 AI 액션은 어떻게 작동할까요?

How to make enemy AI movement in 3D

AI 객체가 해야 할 가장 일반적인 일 중 하나는 세계를 돌아다니는 것입니다.
개방된 공간에서는 비교적 간단할 수 있습니다.
다음과 같은 이동 함수를 사용하여 객체를 대상 쪽으로 이동하면 됩니다.  이동 방향과 같은 이동 기능을 사용하거나 대상에 대한 방향을 계산한 다음 개체를 수동으로 이동합니다.
Like this:

void Update()
{
    Vector3 direction = (target.position - transform.position).normalized;
    transform.Translate((direction * moveSpeed) * Time.deltaTime);
}
  • How to move objects in Unity (3 methods with examples)

하지만 씬이 단순히 넓은 공간이 아닐 수도 있습니다.
게임에는 AI가 탐색해야 하는 장애물이 있을 수 있습니다.
이 작업을 수행하는 방법은 만들려는 동작 유형에 따라 다릅니다.
예를 들어, AI가 단순히 위험을 피해야 하는 경우, 간단한 감지 시스템을 만들어  레이캐스트 만 있으면 충분할 수 있습니다.
하지만 AI가 목표 위치에 도달하기 위해 게임의 지오메트리를 횡단하는 방법을 알아내야 한다면 일종의 경로 탐색 시스템을 사용해야 할 가능성이 높습니다.
예를 들어,  내비게이션 메시와 같은 것입니다.

How to use the AI Navigation package in Unity

 내비 메시 란  에이전트라고 하는 AI 객체가 이동할 수 있는 미리 정해진 영역을 말합니다.
보행 가능한 표면과 장애물 주변에서 물체가 자동으로 목표물을 향해 가는 경로를 찾을 수 있습니다.

 내비 메시  컴포넌트는 씬에서 오브젝트 유형인  에이전트가 이동할 수 있는 위치를 파악하여 걷기 가능한 영역으로 매핑합니다.

이는 3D 씬에서 적과 플레이어가 아닌 오브젝트의 움직임을 생성할 때 매우 유용하며, AI 오브젝트가 이동할 수 있는 위치를 계산할 수 있기 때문입니다.
이를 통해 아이들은 자연스럽게 세상의 특정 위치에 도달하는 방법을 알아낼 수 있습니다.
내비 메시 컴포넌트는 수년 동안 Unity의 공통 기능으로 사용되어 왔습니다.
이전 버전의 내비 메시 시스템은 유니티의 Git Hub 리포지토리에서 사용할 수 있었습니다.
하지만 이제 Unity 2022.2에 기본으로 포함된  AI 내비게이션 패키지 의 일부로 제공됩니다.
내비 메시 표면은 특정 유형의 AI 오브젝트가 이동할 수 있는 위치를 선택할 수 있는 컴포넌트입니다.
오브젝트에 배치하거나 씬에 별도로 배치할 수 있으며, 콜라이더 또는 렌더 메시를 사용하여 게임 내 지오메트리를 식별하고 걷기 가능한 영역을 매핑합니다.

오브젝트에 걷기 가능한 영역을 생성하려면 먼저 내비 메시를  베이크 해야 합니다.

보행 가능 여부는 내비게이션 메시를 사용할 에이전트의 유형에 따라 정의됩니다.
기본 유형인 휴머노이드는 AI 오브젝트의 크기, 올라갈 수 있는 경사, 벽으로 간주되기 전에 오브젝트를 얼마나 높이 올라갈 수 있는지를 지정합니다.
그러나 씬을 다른 방식으로 이동하도록 설계된 다양한 유형의 에이전트를 만들 수 있습니다.

 내비 메시 에이전트 설정은 표면에서 움직일 오브젝트의 유형과 크기를 결정합니다.

표면 컴포넌트는 씬의 모든 오브젝트 또는 선택 사항으로 지정된 볼륨 내의 영역을 평가하고 AI 오브젝트가 이동할 수 있는 걷기 가능한 영역을 매핑합니다.
일반적으로 에이전트 유형이 통과할 수 있는 충분한 여유 공간이 있는 하위 레이어를 포함하여 개체의 최상위 레이어가 됩니다.
하지만 내비게이션 메시가 반드시 게임의 물리 시스템을 사용하여 오브젝트의 상단이 어디에 있는지 계산하는 것은 아니라는 점을 기억하는 것이 중요합니다.
예를 들어 내비게이션 메시 자체의 방향은 씬의 중력이 아닌 방향에 따라 결정됩니다.
즉, 내비 메시 컴포넌트의 회전을 그 기반이 되는 오브젝트로 개별적으로 이동하거나 변경하면 아래로 간주되는 것은 월드가 아닌 내비 메시를 기준으로 합니다.
예를 들어 벽이나 천장에 걷기 좋은 공간을 만들고자 할 때 유용하게 사용할 수 있습니다.
하지만 내비 메시를 자유롭게 이동할 수 있기 때문에 내비 메시 컴포넌트를 계산하는 데 사용된 오브젝트에서 실수로 오프셋이 발생할 수도 있습니다.
따라서 일반적으로 내비 메시 표면을 다시 구우지 않더라도 모든 방향 변경이 내비 메시에도 반영되므로, 내비 메시가 사용하는 지오메트리의 루트 오브젝트에  내비 메시 표면 컴포넌트를 배치하는 것이 좋습니다.

내비게이션 메시 데이터는 이동 및 회전이 가능하므로 생성에 사용한 지오메트리에 부착된 상태로 유지하는 데 도움이 될 수 있습니다.

 Agent  컴포넌트를 사용하면 오브젝트가 장면에서 호환되는 내비게이션 메시를 사용하여 장애물을 통과하여 각각의 새로운 타깃으로 이동할 수 있습니다."
이를 통해 오브젝트의 일부 이동 특성을 지정할 수 있으며, 오브젝트가 얼마나 멀리 떨어지거나 점프할 수 있는지와 같은 다른 특성은 에이전트 유형에 따라 결정됩니다.

내비 메시 에이전트 컴포넌트는 내비 메시에서 오브젝트를 움직일 수 있게 해주는 컴포넌트입니다.

상담원이 이동할  대상 은  대상 설정하기 기능을 사용하여 코드에서 설정할 수 있습니다.
Like this:

using UnityEngine.AI;

public class SetNavTarget : MonoBehaviour
{
    NavMeshAgent agent;

    void SetNewTarget(Vector3 targetPosition)
    {
        agent.SetDestination(targetPosition);
    }
}

그러면 에이전트가 내비게이션 메시에서 목표 위치를 향해 이동합니다.
 AI 내비게이션  시스템은 모든 종류의 비플레이어 오브젝트가 3D 월드에서 자동으로 움직일 수 있도록 하려는 경우에 이상적입니다.
하지만 3D에서 인공적인 움직임을 만드는 것은 의외로 간단할 수 있지만 2D에서는 훨씬 더 어려울 수 있습니다.

How to make enemy AI movement in 2D

안타깝게도 Unity에는 2D 경로 찾기 시스템이 없습니다.
적어도 이미 내장되어 있는 것은 아닙니다.
즉, 2D 씬에서 오브젝트를 탐색하려면 자체 경로 탐색 시스템을 만들거나, 기성 에셋을 사용하거나, 2D 게임에서 작동하도록 Unity의 3D 내비게이션 메시 시스템을 강제로 사용해야 합니다.
예를 들어 2D 월드를 만들 수 있지만 3D 물리 시스템을 사용하여 조합하는 것이 가능합니다.

How to use the Nav Mesh component in 2D

Unity의 내비게이션 메시가 2D에서는 작동하지 않지만 2D 게임에 3D 피직스를 빌드하여 마치 2D처럼 사용할 수 있습니다.
이렇게 하려면 스프라이트의 2D 콜라이더 대신 3D 콜라이더를 사용하면 됩니다.
그런 다음 내비게이션 메시가 렌더링 메시가 아닌 물리 충돌기를 감지하도록 설정하여 생성한 지오메트리를 인식할 수 있도록 합니다.

2D 스프라이트와 함께 3D 콜라이더를 사용하면 Unity의 내비 메시가 2D 지오메트리로 작동하도록 강제할 수 있습니다.

이 방법의 장점은 에이전트 유형의 설정에 따라 Unity의 내비게이션 메시가 이미 갭과 드롭을 인식할 수 있다는 점입니다.
이는 경로를 계산하는 것이 매우 어려울 수 있는 2D 플랫폼 게임에서 인공지능을 탐색하는 데 유용할 수 있습니다.
기본적으로 이것은 3D 세계가 측면에 표시되지만 기술적으로는 2D가 아닌 2.5D 게임을 만드는 것과 유사한 접근 방식입니다.
유일한 차이점은 이 경우 오브젝트가 움직이고 보이지 않는 3D 물리 시스템을 사용하여 서로 충돌하지만 2D 스프라이트를 사용하여 월드를 표현한다는 점입니다.
완벽한 솔루션은 아니지만, '2D' 오브젝트의 회전 가능성과 깊이를 고려해야 하므로 훨씬 쉽게 시작할 수 있습니다.
보이지 않는 3D 물리학을 사용하여 내비게이션 메시를 계산하는 하향식 게임에서 2D 경로를 찾는 데에도 동일한 접근 방식을 사용할 수 있습니다.
하지만 하향식 경로 찾기의 경우 사용할 수 있는 대체 옵션이 있습니다.
예를 들어,  별 경로 찾기가 있습니다.

A Star Pathfinding in Unity

스타는 2D와 3D 모두에서 작동하는 빠르고 효율적인 Unity의 경로 찾기 시스템입니다.
장애물 주변을 탐색하기 위해 다양한 방법을 사용하며, 그 결과 매우 유연합니다.
2D 게임에서 경로 탐색이 작동하도록 하기 위해 2D 오브젝트에 3D 피직스를 추가하는 등 특이한 방식으로 게임을 빌드할 필요가 없다는 것이 Unity의 내비 메시 대신 A Star를 사용할 때의 장점입니다.
프리미엄 도구는  유니티 에셋 스토어에서 구매할 수 있지만, 개발자 Aron Granberg는 자신의 웹사이트에서 제한된 무료 버전도 제공합니다.
하지만 A Star는 매우 유연하고 다양한 경로 찾기 애플리케이션의 시작점으로 사용할 수 있지만, 기본적으로 지원하지 않는 몇 가지 유형의 움직임이 있습니다.
예를 들어, 하향식 이동에는 이상적이지만 기본적으로 2D 플랫폼 이동은 지원하지 않습니다.
하지만 그렇다고 해서 A Star로 플랫폼 기반 경로 탐색을 만들 수 없다는 의미는 아닙니다.

How to create platformer movement with A Star

A Star는 기본적으로 플랫포머 스타일의 이동을 지원하지 않지만, A Star의 경로 찾기 기능과 로컬 장애물 회피 기능을 결합하여 계산된 경로를 사용하여 목표를 향해 이동하지만 장애물 자체는 피하는 일종의 플랫포머 AI를 만들 수 있습니다.
이는 별이 생성하는 경로를 평가하고 해당 경로의 다음 목표 위치로 개체를 이동하면서 개체가 점프해야 하는지 확인하여 수행할 수 있습니다.
Like this:

using Pathfinding;

public class EnemyMovement : MonoBehaviour
{
    [Header("Movement Controls")]
    public Transform target;
    public LayerMask obstacleLayer;
    public float moveSpeed = 5;
    public float jumpForce = 4;
    public float jumpCooldown = 0.25f; 

    [Header("Gap Calculation")]
    public float calculationInterval = 0.25f; // How often should the path be recalculated
    public int lookAhead = 6; // How many nodes to use when checking for gaps. Larger values may mean gaps are recognised too early.
    public int gapDetection = 3; // the number of unsupported nodes required to recognise a gap. A larger value means that smaller gapes will be ignored.
    public float heightTolerance = 0.5f; // How much higher should a node be to require a jump
    public float unsupportedHeight = 1; // How far down to check before deciding there's nothing there

    public Seeker seeker;
    public Rigidbody2D rb;
    Path currentPath;

    Vector3 moveDirection;
    bool followPath;
    int nextNode;
    float jumpTimer;
    float timer;

    private void Awake()
    {
        timer = calculationInterval + 1; // Forces a first time calculation
    }

    private void Update()
    {
        CalculatePath();
        FollowPath();
    }

    void CalculatePath()
    {
        if (timer > calculationInterval)
        {
            followPath = false; // prevents the object from trying to follow the path before it's ready
            seeker.StartPath(transform.position, target.position, OnPathComplete);
            timer -= calculationInterval;
        }
        timer += Time.deltaTime;
    }

    void OnPathComplete(Path path)
    {
        currentPath = path;
        nextNode = 0;
        followPath = true;
    }

    void FollowPath()
    {
        if (followPath)
        {
            Vector3 targetPosition = (Vector3)currentPath.path[nextNode].position;
            targetPosition.z = transform.position.z;
            moveDirection = targetPosition - transform.position;

            if (Vector2.Distance(transform.position, targetPosition) < .5f)
            {
                nextNode++;
                if (nextNode >= currentPath.path.Count)
                {
                    followPath = false;
                }
            }

            if (CanJump() && ShouldJump())
            {
                rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
                jumpTimer = 0;
            }
        }
    }

    private void FixedUpdate()
    {
        rb.AddForce(new Vector2(moveDirection.x, 0) * moveSpeed);
    }

    bool CanJump()
    {
        jumpTimer += Time.deltaTime;
        return (jumpTimer > jumpCooldown && Physics2D.Raycast(transform.position, Vector2.down, transform.localScale.y / 2 + 0.01f, obstacleLayer));
    }

    bool ShouldJump()
    {
        int unsupportedNodes = 0;

        for (int i = 0; i < lookAhead; i++)
        {
            bool unsupported = false;

            if (nextNode + i < currentPath.path.Count)
            {
                Vector3 nodePosition = (Vector3)currentPath.path[nextNode + i].position;
                unsupported = !Physics2D.Raycast(nodePosition, Vector2.down, unsupportedHeight, obstacleLayer);
                unsupported = !((nodePosition.y + heightTolerance) < transform.position.y);

                if (unsupported)
                {
                    unsupportedNodes++;
                }
            }
        }

        return (unsupportedNodes >= gapDetection);
    }
}

이 방법은 수평축의 개체를 다음 사용 가능한 점의 방향으로 이동하는 방식으로 작동합니다.
그런 다음 오브젝트가 선반에서 떨어지거나 계산된 경로에서 벗어나면 변경될 수 있는 목표까지의 경로를 주기적으로 다시 계산합니다(이 예에서는 0.25초마다).
경로는 일련의 점으로 이루어져 있으므로 각 위치의 지오메트리와 객체와 관련된 위치를 검사하여 객체가 통과해야 하는 위치를 테스트할 수 있습니다.
이 방법은 경로의 제한된 수의 위치에서 앞을 보고 그 중 상당수가  지지되지 않음, 즉 아래에 아무것도 없고 물체보다 낮지 않은지(점프하는 대신 아래로 내려가야 하는 경우) 확인하는 방식으로 작동합니다.
이는 일종의 자동 장애물 회피 기능을 생성하여 적의 전방에 무엇이 있는지 파악하고 이에 대응하여 행동할 수 있도록 합니다.

이 예에서 물체는 지속적으로 경로를 다시 계산하고 경로의 다음 위치로 이동하려고 시도합니다. 이때 앞에 있는 점 중 3개 이상이 더 높거나 그 아래에 아무것도 없는 경우 개체는 점프해야 한다는 것을 알 수 있습니다.

이펙트는 기본이지만 게임에서 지오메트리를 식별하는 출발점으로 사용할 수 있습니다.

그 후에는 장애물에 도달했을 때 AI가 어떻게 대처할지 결정하기만 하면 됩니다.

How to fire at the player using AI (with prediction) 

게임에서 적들이 플레이어에게 발사체를 발사하는 경우, 플레이어가 발사할 때 조준할 위치를 파악할 수 있는 방법이 필요합니다.

많은 게임에서 발사체는 실제 총알과 달리 속도가 상당히 느리기 때문에 플레이어가 실제로 피할 수 있는 기회가 많지 않습니다.

즉, 움직이는 물체를 조준하는 느린 발사체는 도달할 때까지 놓칠 가능성이 높기 때문에 발사 위치를 선택하는 것은 단순히 플레이어의 현재 위치를 사용하는 것보다 조금 더 복잡할 수 있습니다.

게임을 더 어렵게 만들려면 플레이어가 총을 쏠 때와 마찬가지로 적도 플레이어가 다음에 어디로 갈지 예측하여 총알이 적중할 확률을 높여야 합니다.

물체의 이동 델타와 발사체가 물체에 도달하는 데 걸리는 대략적인 시간을 계산하면 플레이어가 주어진 시간에 어디에 있을지를 예측하여 적에게 정확하게 사격을 가할 수 있습니다.

그렇다면 어떻게 할 수 있을까요?

발사체의 현재 궤적에 발사체가 도달하는 데 걸리는 시간을 곱하면 물체의 위치를 예측할 수 있습니다.

이는 현재 오브젝트의 위치와 이전 프레임의 위치를 비교하는 방식으로 작동합니다.

Like this:

public GameObject projectile;
public Transform target;
public float marginOfError = 1.5f; // By up to how many units could the projectile miss
float projectileSpeed = 25;

Vector3 targetLastPosition;
float interval = 5;
float timer;

private void LateUpdate()
{
    Vector3 trajectory = (target.position - targetLastPosition) / Time.deltaTime;
    targetLastPosition = target.position;
}

현재 위치에서 마지막 위치를 빼면 이전 프레임의 방향 벡터가 생성됩니다. 이 값을 델타 시간으로 나누면 초당 단위의 궤적이 생성됩니다.
현재 궤적을 기반으로 몇 초 후 개체의 위치를 계산하는 데 사용할 수 있습니다.
발사체가 목표물에 도달하는 데 걸리는 시간은 발사체가 이동해야 하는 거리를 이동하는 속도로 나누어 계산할 수 있습니다.
Like this:

float TimeToReach()
{
    float distance = Vector3.Distance(transform.position, target.position);
    return distance / projectileSpeed;
}
 

그러면 향후 대상의 위치를 계산하는 데 사용할 수 있는 값(초)이 반환됩니다.
그런 다음 적의 발사체를 그 위치로 조준하기만 하면 됩니다.
Like this

public GameObject projectile;
public Transform target;
public float marginOfError = 1.5f; // By up to how many units could the projectile miss
float projectileSpeed = 25;

Vector3 targetLastPosition;
float interval = 5;
float timer;

private void Start()
{
    targetLastPosition = target.position;
}

private void LateUpdate()
{

    Vector3 targetPosition = target.position + (Trajectory() * TimeToReach());

    if (timer > interval)
    {
        ResetTimer();
        FireProjectile(targetPosition);
    }

    timer += Time.deltaTime;
}

void ResetTimer()
{

    timer -= interval;
    interval = Random.Range(1f, 4f);
}

Vector3 Trajectory()
{
    Vector3 trajectory = (target.position - targetLastPosition) / Time.deltaTime;
    targetLastPosition = target.position;

    return trajectory;
}

float TimeToReach()
{
    float distance = Vector3.Distance(transform.position, target.position);
    return distance / projectileSpeed;
}

void FireProjectile(Vector3 targetPosition)
{
    GameObject newProjectile = Instantiate(projectile, transform.position, Quaternion.identity);
    projectile.GetComponent<Projectile>().speed = projectileSpeed;
    newProjectile.transform.LookAt(targetPosition + AimError(marginOfError));
}

Vector3 AimError(float amount)
{
    return Random.insideUnitSphere * amount;
}
 

기본적으로 이 스크립트는 타겟이 있을 것으로 예상되는 월드의 위치를 정확히 조준합니다.

그러나 이는 다소 비현실적일 수 있으므로 목표물 주변에 적을 놓칠 수 있는 오차 범위를 만드는 것이 유용할 수 있습니다.

이것은  단위 구 내부 무작위 함수를 사용하면 반지름이 1인 구 내부의 위치를 반환합니다.

그런 다음 이 값은 적이 놓칠 수 있는 최대 거리를 단위로 반영하여 조정한 다음 발사체가 발사될 때 목표 위치에 추가할 수 있습니다.

How to find the player

AI 오브젝트가 플레이어를 향해 이동하는 경우, 플레이어의 위치를 알아야 합니다.

일반적인 방법:

  • 플레이어와의 충돌 또는 시야 감지 등 상호 작용을 통해 플레이어 참조를 얻습니다.

문제점:

  • 많은 적들이 있는 경우 비효율적일 수 있습니다.

대안:

  • 플레이어 위치를 정적 값으로 설정하고, AI 오브젝트가 플레이어와 얼마나 가까운지 계산합니다.

장점:

  • 간단하고 효율적입니다.

Enemy AI best practices

Unity에서 비플레이어 AI 오브젝트를 제작하는 방법에는 여러 가지가 있습니다.

그리고 모든 경우에 가장 좋은 방법은 어떤 종류의 행동을 만들려고 하느냐에 따라 크게 달라집니다.

하지만...

일반적으로 말해서, AI 시스템이 효과적이기 위해 복잡할 필요는 없으며, 어떤 용도로 사용하려는지에 따라 실제로는 생각보다 쉽게 구축할 수 있습니다.

그렇기 때문에 Unity에서 AI 오브젝트를 만드는 가장 좋은 방법 중 하나는 문제, 즉 AI가 달성하기를 원하는 목표부터 시작하여 거꾸로 작업하는 것입니다.

인공지능이 조금 더 쉽게 생각할 수 있도록 원하는 내용을 결정할 수 있습니다.

Now it’s your turn

이제 여러분의 의견을 듣고 싶습니다.

게임에서 AI를 어떻게 활용하고 있나요?

상태 머신, 동작 트리 또는 다른 것을 사용하고 계신가요?

다른 사람이 유용하게 사용할 수 있는 Unity의 AI에 대해 배운 점이 있다면 무엇인가요?

무엇이든 댓글을 남겨서 알려주세요.


원문

 

Enemy AI in Unity - Game Dev Beginner

Learn the different methods for building an AI system in Unity, including pathfinding, targeting, movement prediction and decision-making.

gamedevbeginner.com