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
을 사용하는 방식은 다음과 같은 단점이 존재한다.
- 확장성이 부족하다. 상태를 많이 추가할수록
Update
와ChangeState
둘 다 가독성이 떨어지게 된다. - 여러 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++ 도서를 학습하며 요약정리한 내용입니다.