본문으로 바로가기

4장 인공지능 - ① 상태 기계 행위

인공지능(AI, Artificial Intelligence) 알고리즘은 게임에서 컴퓨터가 제어하는 오브젝트의 행위를 결정하는 데 사용한다. 4장에서는 3가지의 유용한 AI 게임 테크닉을 다룬다.

💡 4장의 목차

  • 상태 기계로 행위 변경하기
  • 오브젝트가 세계 주변을 돌아다니도록 경로 계산하기(길 찾기)
  • 2인용 턴 기반 게임(미니맥스와 게임 트리)에서 의사결정 내리기

상태 기계 행위(State Machine Behaviors)

게임에서 AI의 행위는 시점에 따라서 다르게 행동한다.
이러한 행위의 변화를 표현하기 위해 각 행위가 하나의 상태를 가지는 상태 기계(State Machine)라는 개념을 활용한다.

상태 기계 설계하기

상태는 그 자체는 부분적으로 하나의 상태 기계만 정의하기 때문에 각각의 상태 기계 간의 전이(transition)가 중요하다.
스텔스 게임의 기본 가드 캐릭터를 예로들면 다음과 같다.

  • 가드 캐릭터의 상태
    • 순찰
    • 공격
    • 사망
  • 가드 캐릭터의 전이
    • 치면적인 데미지를 받을 경우 사망 상태로 전이
    • 순찰 상태에서 플레이어를 발견할 경우 공격 상태로 전이
  • 상태와 전이의 결합 결과

▲ 상태 기계와 전이

물론 게임을 더욱 풍부하게 만들기 위해서는 이보다 더 많은 종류의 상태전이가 정의될 필요가 있다.
하지만 상태와 전이의 수에 상관없이 AI 상태 기계를 설계하는 원리는 같다.

기본 상태 기계 구현

열거형을 활용한 구현

열거형enum을 사용한다면 다양한 상태를 유연하게 구현할 수 있는 좋은 접근 법이다.

enum AIState
{
    Patrol,
    Death,
    Attack
}

그리고 별도의 Update함수에서 switch문으로 현재 상태에 해당하는 갱신 함수를 호출할 수 있다.

void AIComponent::Update(float deltaTime)
{
    swithc (mState)
    {
    case Patrol:
        UpdatePatrol(deltaTime);
        break;

    case Death:
        UpdateDeath(deltaTime);
    // ...(생략)...

추가로 상태 전이를 위한 ChangeState함수로 상태기계의 전이를 다룰 수 있을 것이다.

하지만 이렇게 enum을 사용하는 방식은 다음과 같은 단점이 존재한다.

  • 확장성이 부족하다. 상태를 많이 추가할수록 UpdateChangeState둘 다 가독성이 떨어지게 된다.
  • 여러 AI 간에 기능을 혼합하고 일치시키는 것도 쉽지 않다.

클래스를 활용한 구현

열거형보다 더 나은 접근법으로는 클래스를 사용하는 방법이 있다. 다음은 AIState라는 모든 상태에 대한 기본 클래스의 정의이다.

AIState.h의 선언

class AIState
{
public:
    AIState(class AIComponent * owner)
        :mOwner(owner)
    {}
    // 각 상태의 구체적인 행동
    virtual void Update(float deltaTime) = 0; // 상태 갱신
    virtual void OnEnter() = 0;     // 상태가 시작될 때 호출
    virtual void OnExit() = 0;      // 상태가 종료되기 전에 호출

    // 상태의 이름 얻기
    virtual const char* GetName() const = 0;
protected:
    class AIComponent* mOwner;
};

기본 클래스는 상태를 제어하고자 몇 가지의 가상 함수를 포함한다.

  • Update(): 상태를 갱신
  • OnEnter(): 상태 진입 시의 전이 코드를 구현
  • OnExit(): 종료 전이 코드를 구현
  • GetName(): 함수는 상태에 대해 사람이 읽을 수 있는 이름 문자열을 반환

또한 AIState 클래스는 class AIComponent* 타입의 mOwner 멤버 변수를 사용해서 AIComponent와 연관시킨다.

앞서나온 예제를 활용해서 Patrol, Attack, Death상태를 만들기 위해서는 아래와 같이 AIState를 상속받아서 기본 클래스를 활용한다.

AIPatrol.h의 선언

class AIPatrol : public AIState
{
public:
    AIPatrol(class AIComponent* owner);

    // 이 상태에 대한 행위를 재정의
    void Update(float deltaTime) override;
    void OnEnter() override;
    void OnExit() override;

    const char* GetName() const override
    {
        return "Patrol";
    }
};

그리고 AIComponent class에서는 해쉬 맵을 통해서 액터의 상태를 관리한다.

AIComponent.h의 선언

class AIComponent : public Component
{
public:
    AIComponent(class Actor* owner);

    // mCurrentState의 Update 함수를 호출한다.
    void Update(float deltaTime) override;

    // 각 상태의 Update 함수에서 상태 전이가 발생 조건이 형성되면 
    // 상태 전이를 위해서 호출된다.
    void ChangeState(const std::string& name);

    // 새 상태를 맵에 추가한다.
    void RegisterState(class AIState* state);

private:
    // 상태의 이름과 AIState 인스턴스를 매핑한다.
    std::unordered_map<std::string, class AIState*> mStateMap;
    // AI의 현재 상태
    class AIState* mCurrentState;
};

클래스를 활용해서 상태 기계를 구현한 것을 도표로 나타내면 아래와 같다.

▲ 클래스를 활용한 상태 기계 구현 도표

마지막으로 상태를 활용하고자 하는 엑터에서는 아래와 같이 상태를 등록하여 활용한다.

Actor* a = new Actor(this);
AIComponent* aic = new AIComponent(a);
aic->RegisterState(new AIPatrol(aic));
aic->RegisterState(new AIDeath(aic));
aic->RegisterState(new AIAttack(aic));

클래스를 활용해서 상태 기계를 관리하는 접근법에는 다음과 같은 이점이 있다.

  • 각 상태가 별도의 서브클래스에 구현돼서 재사용 혹은 수정이 용이하다.
  • 이를 통해 AIComponent는 간결함을 유지할 수 있기 때문이다.

 

 


본 게시글은 에이콘 출판사의 Game Programming in C++ 도서를 학습하며 요약정리한 내용입니다.