본문 바로가기

portfolio

[리뉴얼] 7- AI의 FSM

 

AI는 유한 상태 기계(finite-state machine, FSM)를 가진다. FSM은 유한한 개수의 상태를 가질 수 있는 추상 기계이며 이 기계는 한 번에 오로지 하나의 상태만을 가지게 된다.

이 FSM이 이번 프로젝트에 AI가 가지는 상태이다. 이 상태 패턴은 MonsterInputComponent에 존재한다.

 

 이전 AI

 

이전 AI에서는 따로 FSM이 존재하지 않았고 단순하게 코드를 작성하였다. 

해당 type이라면 사전에 정의한 Lua Script를 통해 AI가 행동하도록 하였으며 기본적인 이동, 따라가기, 공격 정도를 수행할 수 있었다.

 

 이후 AI

이번에는 MonsterInputComponent를 통해 각자 AI가 자신만의 상태를 가지고 있고 해당 상태에서 상황에 따라 변동하는 형식으로 코드를 구성하였다. 이번에는 전과 다르게 A* 코드를 넣다 보니 Lua가 아닌 오로지 C++로만 작성을 하였고 Lua는 따로 몬스터의 채팅(플레이어가 근처에 오면 적이다! 느낌의 말)을 넣을 때 사용해볼 예정이다.

 

 

슬립 -> 대기

처음으로 볼 상태는 슬립에서 대기로 바뀌는 구간이다. 이 구간은 플레이어가 시야에 들어온 몬스터를 깨우면서 해당 몬스터의 상태를 슬립에서 대기 상태로 변경한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool CSector::WakeUpNearMonster(const ObjectIDType montserID, const ObjectIDType playerID) {
 
    //SLEEP 상태가 아니라면 PASS(어떠한 행동을 하고 있는 중)
    if (gameobjects_[montserID]->GetObjectState() != ObjectState::SLEEP) return false;
 
    //가까우면서 몬스터라면 깨운다.
    if (IsMonster(montserID) && IsNearObject(montserID, playerID)) {
 
        // 상태를 IDEL 상태로 변경
        gameobjects_[montserID]->SetObjectState(ObjectState::IDEL);
 
        //TimerQueue에 몬스터 추가
        CTimerQueueHandle::GetInstance()->queue_.Emplace(
            EVENT_ST{ montserID,playerID,EVENT_TYPE::EV_EXCUTE_MONSTER,high_resolution_clock::now() + 1s });
 
        return true;
    }
    return false;
}

 

대기 -> 이동, 대기 -> 공격

대기 상태에서 들어온 AI는 두 가지 선택지를 가진다. 하나는 이동 또 하나는 공격이다. 이동은 해당 플레이어가 현재 사정거리 안 밖에 있을 때 변경이 되며 공격은 사정거리에 들어왔을 때 바뀌며 둘 다 아닐 경우 다시 슬립 상태로 변경된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case ObjectState::IDEL: {
        /*
        플레이어가 현재 사정거리 안에 있다면 Attack으로 변경
        그렇지 않고 시야안에 있다면 move로 변경
        둘다 아니라면 다시 Sleep
        */
        if (CheckNearPlayer(myObject, targetObject)) {
            myObject.SetObjectState(ObjectState::ATTACK);
            CTimerQueueHandle::GetInstance()->queue_.Emplace(
                EVENT_ST{ npc_id,player_id,EVENT_TYPE::EV_EXCUTE_MONSTER,high_resolution_clock::now() + 1s });
        }
        else if (sector.IsNearObject(npc_id, player_id) == true) {
            myObject.SetObjectState(ObjectState::MOVE);
            CTimerQueueHandle::GetInstance()->queue_.Emplace(
                EVENT_ST{ npc_id,player_id,EVENT_TYPE::EV_EXCUTE_MONSTER,high_resolution_clock::now() + 1s });
        }
        else {
#ifdef _DEBUG
            std::cout << npc_id << ": IDEL To SLEEP " << "\n";
#endif // _DEBUG
            myObject.SetObjectState(ObjectState::SLEEP);
        }
        break;
    }

 

이동 -> 공격, 이동 -> 복귀

이동은 기본적으로 A*을 이용한다. A*을 이용해서 플레이어 근처에 도착하면 공격을 A*에서 찾지 못하면 몬스터는 원래 자리로 돌아간다.

 

이동 -> 공격

150% 속도의 GIF

몬스터가 플레이어에게 다가가고 근처에 도착하면 공격을 하며 다시 플레이어가 공격범위에서 벗어나면 쫒아가서 공격을 하는 것을 확인할 수 있다.

 

 

이동 -> 복귀

 

150% 속도의 GIF

몬스터가 플레이어에게 이동하는 도중 몬스터의 영역을 벗어나게 되면 다시 자기 자리로 돌아가고 IDEL 상태로 변경된다. GIF 마지막에서는 다시 시야 안에 있기 때문에 플레이어에게 다가가는 모습을 확인할 수 있다.

 

되돌아가기 기능은 Stack을 사용했다. 이 기능은 100% 완벽하지 못하다 몬스터가 이동한 좌표를 스택에 저장하다 보니 바로 자기 자리로 돌아가는게 아닌 자기가 왔던 길을 되돌아가기 때문에 조금 부자연스러운 느낌도 난다.

 

 

Network를 통한 클라이언트에서도 동일하게 보이는 몬스터의 행동

다른 클라이언트에서도 동일하게 몬스터가 행동 하는 것을 볼 수 있다. 최초로 자기를 깨운 플레이어를 따라가도록 설계했기 때문에 도중에 다른 플레이어가 더 가까이 있어도 계속 따라가는 것을 확인할 수 있다.

 

150% 속도의 GIF

 

 

 

후기

 

두 번에 걸친 AI의 FSM 과 A*를 끝냈다. 100% 완벽하지는 않다고 생각한다. 찾지 못한 버그가 있을 수 있고 사실 100% 완벽한 코드란 없다고 하니 버그가 생기면 계속해서 고쳐갈 예정이다. 이 이후에는 DB를 붙일 예정이다.

 

 

 

 

리뉴얼 관련 글

--서버--

[portfolio] - [리뉴얼] 4- AcceptEx 및 이동 동기화

[portfolio] - [리뉴얼] 6- AI의 길찾기(A*)

[portfolio] - [리뉴얼] 7- AI의 FSM

--클라이언트--

[portfolio] - [리뉴얼] 3- 공간 분할을 이용한 Sector Class

[portfolio] - [리뉴얼] 2- 경량 패턴을 이용한 World Class

[portfolio] - [리뉴얼] 1- 중재자 패턴을 이용한 Network Class

[portfolio] - [리뉴얼] 개요 및 목표(20.11.02 수정)

--리뉴얼 작업 전 포트폴리오

2019/12/16 - [portfolio] - DirectX & IOCP

 

 

* 리소스는 알피지 만들기 툴 리소스를 사용했으며 영리 목적으로 사용하지 않을 것입니다.