본문 바로가기
대학생활/수업

게임프로그래밍고급 14주차 - 게임인공지능

by se.jeon 2023. 6. 13.
728x90
반응형

게임 인공지능

게임 내에서 제어되고 있는 캐릭터나 NPC를 정의하여, 게임 플레이의 연속성을 이어가는 것.

스스로 게임 상황과 게임 데이터들에 대해서 적응하고 학습을 통해서 지능적으로 자율성을 가지고 행동하는 게임 관련 에이전트를 총칭한다.

- 인공지능의 발전으로 게임과 관련된 인공지능의 연구가 활발해졌다.

- 고전적인 게임에서 사용했던 기술부터, 딥러닝을 이용하는 방법까지 지속적으로 발전 해 왔다.

- 바둑이나, 장기 같은 보드판에서 이루어지는 곳의 인공지능은 이미 사람의 한계를 뛰어넘어, 사람이 이길 수 없는 상황까지 확대됨.

- 스타크래프트 같은 RTS 장르에서도, 최고 프로게이머의 실력까지 도달해 있어서 사람들에게 많은 기대를 받고 있다.

 

사람은 학습을 하는 것에 어느정도까지 확장의 맥시멈이 있는데, 인공지능은 맥시멈이 없을 수도 있다.

프로세스, 메모리를 추가하는 식으로 발전할 수 있고, 장기적으로 사람을 뛰어넘게 될 수 있다.

 

 

게임 인공지능의 범위는 게임과 관련된 다양한 범위로 확대된다.

- 기존 : 게임플레이와 관련된 내용이 대부분.

- 최근 : 게임플레이 뿐만 아니라, 게임 리소스 제작, 플레이테스팅, 레벨 디자인, 게임 유저 분석 부분에서 폭넓게 사용.

 

게임은 기술집약적 산업.

 

게임 인공지능의 예시

1. Candy Crush Saga

- 사용자의 플레이 데이터를 이용해서 레벨 디자인에 활용.

- 기존의 인공지능 방식에서 벗어나 딥러닝을 이용해서 학습하고 새로운 레벨에 대해서 에이전트를 통해서 플레이 테스팅을 진행.

- 적합한 레벨로 구성되어 있는지에 대한 평가 진행.

 

2. NC

- 능숙한 사냥터 통제

- 사람 혈맹처럼 자연스럽게 전투하는 AI

- PvP AI - 무한의 탑 B&S AI v1.0, 월드챔피언십 B&S AI v2.0

- Lineage Group AI - 거울 전쟁/ 전설 vs 현역

 

게임 인공지능 기술

- FSM

- Path Finding

- Flocking (군집)

- 유전자 알고리즘

- 강화 학습

 

FSM (Finite State Maching, 유한 상태 머신)

가장 많이 사용되고 있는 기술 중에 하나로, 정해진 상태를 통해서 게임 행동을 결정하는 방법

 

상태 (State)

- 기본적으로 행동에 대한 기본 단위

- 개별 상태는 조건에 따라서 다른 상태로 변경 가능

- 상태는 게임 기획에 의해서 다양한 상태로 표현 가능

- 게임 개발 초창기부터 사용되었던 전통적인 인공 지능 기술

 

- RPG 게임에서 특정 몬스터의 상태 정의

- RPG에서 몬스터가 가진 행동 패턴들은 FSM으로 구현되는 경우가 많다.

- 플레이어의 지속적인 게임 플레이 시, 몬스터의 행동 패턴이 노출되어 예측 가능한 움직임들은 게임의 재미를 반감시키는 요소이다.

- 상태가 늘어나면, 특정 그룹으로 상태들을 묶고 나누어서 계층적으로 FSM을 처리하는 방법으도 존재한다.

- 그룹으로 묶어서 처리하는 방식은 Fuzzy(퍼지 이론)과 FSM을 접조기켜서 상태의 입력과 처리가 관련된 부분을 퍼지 함수를 통해서.

예측하기 어려운 행동 제시.

 

Path Finding

특정 게임 개체를 현재의 위치에서 목표로 하는 위치로 이동하게 하기 위해서 주변 환경을 고려하여 최적의 경로를 찾는 기술.

- 대표적인 기술은 A* 알고리즘

- 가장 많이 사용되는 길 찾기 알고리즘 중에 하나.

- 중간에 주변 환경이 변하거나 하면 처리하기가 어렵다.

- 길을 찾아야 하는 거리가 너무 길면, 탐색해야 할 공간이 너무 많아서 메모리 공간의 증가와 탐색 비효율 처리 가능성이 높다.

 

Flocking

새나 물고기 집단의 움직임을 유사하게 표현하기 위해서 만들어진 방법

- 분리 : 주변의 객체들과 적당한 거리를 두고 충돌되지 않도록 거리를 유지한다.

- 정렬 : 주변의 객체들과 동일한 속도와 방향을 동일하게 유지한다.

- 응집 : 분산되지 않고 하나의 무리로써 평균 위치로 이동하도록 처리한다.

 

유전자 알고리즘

자연 생태계에 존재하고 있는 진화 이론을 바탕으로 한 알고리즘.

우리가 해결해야 하는 대부분의 자연 문제들은 수식으로 해결되지 않는다.

- 인위적인 선택을 통해 다양한 객체들을 생성 가능하다.

- 가장 좋은 객체를 다음 세대에 전달함으로써 진화 능력과 학습 능력을 가지고 있다.

- 탐색 공간이 큰 경우에도 처리가 가능하다.

- 주어진 문제가 정확한 수식으로 정의가 되어있지 않아도 적용 가능하다.

- 반드시 최적한 해를 요구하지 않는 경우에 적당하다.

- 게임이 달라지거나 하면 구현 방식이 달라져 처음부터 새롭게 작성해야 하는 문제점이 있다.

 

강화학습

주어진 환경 속에서 에이전트가 현재 상태와 선택 가능한 행동을 바탕으로 "보상을 최대화 하는 행동을 선택하는 방법"이다.

딥마인드 알파고의 등장으로 유명해진 머신 러닝 중에 한 종류이며, 게임업체들이 가장 관심 가지고 있는 게임 인공지능 기술 중에 하나이다.

- 알파스타 : 스타크래프트2

- 알파고 : 바둑

- NC : 리니지 리마스터

https://youtu.be/v3UBlEJDXR0

 

감독 학습 : 학교와 같이 계속 반복해서 알려주는 것. (인지해야 가능하다.)

비감독 학습 : 아이들이 입에 가져다대며 먹을 수 있는 것과 없는 것을 느끼는 것과 같은 과정. 감독 학습 이전에 우선적으로 거쳐야 하는 과정이다.

 

게임 인공지능 기술 구현

FSM (Finite State Machine)

특정 객체를 지정한 상태로 유지하고 있다가, 조건이 맞으면 다른 상태로 바뀌어서 관련 행동을 취하게 한다.

가장 간단한 FSM은 if, else, switch 등으로 표현이 가능하다.

다른 상태로 변경할 수 있는 조건을 해당 조건문을 통해서 구현

 

FPS 게임에서 게임의 Mission 플레이로 NPC와 플레이어가 대전한다고 했을 때,

해당 Mission에 등장하는 NPC는 다음과 같은 상태를 가지고 있다.

- idle

- attack

- die

switch문을 통해서 구한다면 enumeration을 통해서 표현 가능

enum State
{
    NONE = 0,
    IDLE,
    ATTACK,
    DIE
}
State npcState = State.NONE;
void Update()
{
    switch (npcState)
    {
        case State.IDLE:
            // 관련 내용
            break;
        case State.ATTACK:
            // 관련 내용
            break;
        case State.DIE:
            // 관련 내용
            break;
    }
}

 

FSM을 구현하기 위한 클래스의 필요한 조건

- 상태 관리 클래스 : 현재 상태의 인스턴스 관리

- 추상 클래스 : 주어진 상태를 처리하기 위한 메서드를 정의

- 개별 상태 클래스 : 추상 클래스를 상속 받아 개별 상태에 대한 내용을 정의

 

 

제작 실습 진행

상태 추상 클래스 : 추상클래스를 바탕으로 개별 상태를 정의 한다.

public abstract class NPCBaseState
{
    public abstract void Begin(NPCFSMMgr mgr);
    public abstract void Update(NPCFSMMgr mgr);
    public abstract void OnCollisionEnter(NPCFSMMgr mgr);
    public abstract void End(NPCFSMMgr mgr);
}

 

개별 상태 클래스 (Idle State)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NPCIdleState : NPCBaseState
{
    public override void Begin(NPCFSMMgr mgr)
    {
        Debug.Log("NPCIdleState Begin");     
    }

    public override void Update(NPCFSMMgr mgr)
    {
        if (!mgr.IsAlive())
        {
            mgr.ChangeState(NPCFSMMgr.DeadState);
            return;
        }

        if (mgr.CheckInTraceRange())
        {
            mgr.ChangeState(NPCFSMMgr.TraceState);
            return;
        }
    }

    public override void OnCollisionEnter(NPCFSMMgr mgr)
    {
        Debug.Log("NPCIdleState End");   
    }

    public override void End(NPCFSMMgr mgr)
    {
        Debug.Log("NPCIdleState End");       
    }
}

개별 상태 클래스 (Trace State)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NPCTraceState : NPCBaseState
{
    public override void Begin(NPCFSMMgr mgr)
    {
        Debug.Log("NPCTraceState Begin");       
    }

    public override void Update(NPCFSMMgr mgr)
    {
        if(!mgr.IsAlive())
        {
            mgr.ChangeState(NPCFSMMgr.DeadState);
            return;
        }

        if(!mgr.IsAliveTarget())
        {
            mgr.ChangeState(NPCFSMMgr.IdleState);
            return;
        }

        if (!mgr.CheckInTraceRange())
        {
            mgr.ChangeState(NPCFSMMgr.IdleState);
            return;
        }

        if (mgr.CheckInAttackRange())
        {
            mgr.ChangeState(NPCFSMMgr.AttackState);
            return;
        }

        mgr.moveTarget();
    }

	// target을 지정하면 target을 향해서 움직인다.
    public override void OnCollisionEnter(NPCFSMMgr mgr)
    {
        Debug.Log("NPCTraceState OnCollisionEnter");
    }

    public override void End(NPCFSMMgr mgr)
    {
        Debug.Log("NPCTraceState End");        
    }
}

개별 상태 클래스 (Attack State)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NPCAttackState : NPCBaseState
{
    public override void Begin(NPCFSMMgr mgr)
    {
        Debug.Log("NPCAttackState Begin");
    }

    public override void Update(NPCFSMMgr mgr)
    {
        if (!mgr.IsAlive())
        {
            mgr.ChangeState(NPCFSMMgr.DeadState);
            return;
        }

        if (!mgr.IsAliveTarget())
        {
            mgr.ChangeState(NPCFSMMgr.IdleState);
            return;
        }

        if (!mgr.CheckInAttackRange())
        {
            mgr.ChangeState(NPCFSMMgr.TraceState);
            return;
        }
        mgr.npcHP--;
    }

	// attack range 안에 타겟이 들어왔을 경우
    public override void OnCollisionEnter(NPCFSMMgr mgr)
    {
        Debug.Log("NPCAttackState OnCollisionEnter");
    }

    public override void End(NPCFSMMgr mgr)
    {
        Debug.Log("NPCAttackState End");
    }
}

개별 상태 클래스 (Dead State)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NPCDeadState : NPCBaseState
{
    public override void Begin(NPCFSMMgr mgr)
    {
        Debug.Log("NPCDeadState Begin");      
    }

    public override void Update(NPCFSMMgr mgr)
    {
        Debug.Log("NPCDeadState Update");
    }

    public override void OnCollisionEnter(NPCFSMMgr mgr)
    {
        Debug.Log("NPCDeadState OnCollisionEnter");
    }

    public override void End(NPCFSMMgr mgr)
    {
        Debug.Log("NPCDeadState End");       
    }
}

 

상태 관리 클래스 : (NPCFSMMgr)

- 상태 및 현재 관련 파라미터 지정 관리

- Trace 범위 지정

- Attack 범위 지정

- Target Object의 Transform 지정

- 상태에 대한 초기화 (IdleState)

using System;
using System.Collections.Generic;
using UnityEngine;

//public static class NPCState
//{
//    public static readonly NPCIdleState IdleState = new NPCIdleState();
//    public static readonly NPCTraceState TraceState = new NPCTraceState();
//    public static readonly NPCAttackState AttackState = new NPCAttackState();
//    public static readonly NPCDeadState DeadState = new NPCDeadState();
//}

public class NPCFSMMgr : MonoBehaviour
{   
    private NPCBaseState currentState;
    private NPCBaseState prevState;
    
    public float TraceRange = 100.0f;
    public float AttackRange = 30.0f;    
    public float npcHP = 2000.0f;
    public Transform targetTransform;
    public Transform traceRangeTransform;
    public Transform attackRangeTransform;

    public NPCBaseState CurrentState
    {
        get { return currentState; }
    }
        
    public readonly static NPCIdleState IdleState = new NPCIdleState();
    public readonly static NPCTraceState TraceState = new NPCTraceState();
    public readonly static NPCAttackState AttackState = new NPCAttackState();
    public readonly static NPCDeadState DeadState = new NPCDeadState();

    private void Awake()
    {
        traceRangeTransform.localScale = new Vector3(TraceRange*2, TraceRange * 2, TraceRange * 2);
        attackRangeTransform.localScale = new Vector3(AttackRange*2, AttackRange * 2, AttackRange*2);
        currentState = IdleState;
    }

    private void Start()
    {
        ChangeState(IdleState);
    }

    private void Update()
    {
        currentState.Update(this);
    }

    private void OnCollisionEnter(Collision collision)
    {
        currentState.OnCollisionEnter(this);
    }

    public void ChangeState(NPCBaseState state)
    {
        currentState.End(this);
        prevState = currentState;
        currentState = state;
        currentState.Begin(this);
        ChangeStateColor();
    }

    private void ChangeStateColor()
    {
       if(currentState == IdleState)  transform.gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color.white);
       else if (currentState == TraceState) transform.gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color.green);
       else if (currentState == AttackState) transform.gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color.red);
       else if (currentState == DeadState) transform.gameObject.GetComponent<Renderer>().material.SetColor("_Color", Color.black);

       //Handles.DrawWireDisc(transform.position, new Vector3(0, 1, 0), TraceRange);
    }

    public float CalcTargetDistance()
    {
        return (targetTransform.position - transform.position).magnitude;
    }

    public bool CheckInTraceRange()
    {
        return ((CalcTargetDistance()< TraceRange) ? true : false);
    }

    public bool CheckInAttackRange()
    {
        return ((CalcTargetDistance() < AttackRange) ? true : false);
    }

    public bool IsAlive()
    {
        return (npcHP > 0) ? true : false;
    }

    public bool IsAliveTarget()
    {
        if (targetTransform == null) return false;

        return true;
    }

    public void moveTarget()
    {
        Vector3 velo = Vector3.zero;
        transform.position = Vector3.SmoothDamp(transform.position, targetTransform.position, ref velo, 0.5f);
    }
}

 

FSM의 장점

- 모듈화 해서 사용하기 쉽다.

- 쉽게 유지보수가 가능하다.

- 디버깅하기 편리하다.

- 직관적으로 상태의 파악이 가능하다.

- 상태의 확장이 용이하다.

 

FSM의 단점

- 상태가 많아질 수록 복잡해진다.

- 단순한 상태일 때에는 불필요하다.

- 패턴을 파악하기 용이하다.

- 중복된 상태에 처리가 어렵다.

728x90
반응형