배틀레드_Part1

양규빈·2023년 7월 15일

유니티

목록 보기
5/34

개요

깃허브 주소
구현 영상

수집형 모바일 RPG 게임을 구현했던 배틀블루에 이어서,
시작한 배틀레드는 오픈 월드 모바일 RPG 게임 프로젝트입니다.

모바일 오픈 월드 rpg 게임으로 유명한, 원신과 비슷한 구조로 시스템을 구현해보고자 합니다.
UI 파트와 게임 시스템 전반에서 원신의 시스템을 모작했습니다.

상단의 스크린샷은 개발 화면을 촬영한 이미지입니다.



설계

주요 기능

캐릭터

  1. 조작(Control) : 조이스틱과 터치를 이용한 캐릭터 조작 기능

  2. 공격 및 스킬 : 버튼 컴포넌트를 이용한 캐릭터 공격, 스킬 기능

  3. 상호작용 : 특정 오브젝트에 가까이 접근했을 때, 상호작용 가능

  4. 사망 및 피격 : 적 객체에게 피격당할 경우, 공격 타입에 따라 스턴 상태 부여.
    원소를 이용한 공격의 경우, 캐릭터에게 특정 원소 상태 이상으로 변경.

  5. 원소 효과 : 물/불/풀/번개/바람 속성으로 인챈트하여 속성효과 부여 가능.

  6. 아이템 : 몬스터 사냥 후에, 일정 확률로 떨어진 아이템을 줍기 가능. 주운 아이템은 인벤토리로 들어간다.

몬스터

  1. 추격 AI : 일정 범위 내에 캐릭터 객체가 들어올 경우, 해당 객체를 추격

  2. 공격 : 사정거리 내에 캐릭터 객체가 있을 경우, 공격 AI를 시행한다

  3. 사망 및 피격 : 피격 시에, 캐릭터의 인챈트 상태에 따라 원소 중독 상태 부여. 다른 원소가 부여시, 상태이상 발생한다

  4. 보스 : 보스 타입의 몬스터일 시, 전용 스킬을 시전한다

아이템

  1. 소모 : 일부 아이템의 경우 소모품으로써 퀵슬롯에 이동하여 소모가 가능하다.

  2. 강화 : 무기나 장비의 경우에 강화재료를 사용하여 강화가 가능하다.

  3. 합성 : 특정 장비나 재화를 합성하여, 고위 장비. 혹은 재화로 변환하는 것이 가능하다.

월드맵

  1. 몬스터 찾기 : 특정 재화를 얻기 위해서, 몬스터를 사냥하고 싶을 때, 탐색 기능을 제공하여 맵상으로 표시해준다.
  2. 길찾기 : 특정 지역으로 가고 싶을 때, 월드맵으로 핑을 찍으면, 길 안내를 따라서 이동할 수 있게 해준다.

인벤토리

  1. 정렬 : 아이템의 태그에 따라 정렬하는 기능을 제공.

오브젝트

  1. 속성 효과 : 특정 속성에 상호작용하여, 정해진 프로세스를 진행한다.
    속성을 이용한 퍼즐 등을 풀 수 있다.

게임 매니저

  1. ui 관리 : 인게임에 도움을 주는 ui 화면을 관리하는 기능 제공.

  2. 재화 관리 : 인게임 재화를 관리하는 기능 제공.

  3. 매니저 관리 : 게임의 오브젝트들을 관리하는 매니저들을 총괄하여 관리.

  4. 던전 관리 : 플레이어가 던전에 진입할 수 있도록 씬을 관리한다.

  5. 씬 관리 : 프로젝트에 사용되는 모든 씬을 관리한다.


클래스 다이어그램

주요 기능을 담은 클래스 다이어 그램입니다.

각 매니저 간의 데이터 교환과 상호작용을 표현했습니다.



구현

<7월 15일부터 본격적인 구현 시작>

컨트롤러&버튼

터치패드

배틀레드는 모바일 빌드의 프로젝트입니다.
사용자의 조작은 모두 터치를 기반으로 이루어지며, 버튼과 드래그 등을 이용하여 조작됩니다.

터치패드의 구현에 있어서, 터치의 시작(DownHandler), 드래그 중인 상태(DragHandler), 터치의 끝(UpHandler)을 필요로 합니다.

이를 인터페이스를 이용하여 클래스에 구현합니다.

터치의 시작을 부울 변수로 확인하고 터치의 고유 아이디를 이용해, 시작 위치를 기준으로 벡터 값을 계산해, 값을 정규화합니다.

이렇게 정규화된 방향을 이용하여, 터치의 방향을 리턴합니다.
위의 터치패드는 현재 x축을 기준으로 구현되어 있으며, 플레이어의 좌우 회전에 관여합니다.



조이스틱

조이스틱은 플레이어의 실질적인 이동을 담당하는 부분입니다.
기본적으로는 스타슈트에서 구현했던 조이스틱과 유사하지만, 3D 상에서 활용된다는 점에서, y축이 아닌 x,z축의 이동 벡터를 생성한다는 점에서 차이가 있습니다.

조이스틱 이미지가 x,y축 방향으로 이동하면 이에 따라, 정규화된 좌표 값을 조이스틱 배경 이미지의 피벗(pivot) 값에 따라 변환합니다.

최종적으로 입력 벡터를 정규화하여, 입력 벡터로써 사용할 수 있게합니다.

터치가 시작되면, 상단의 OnDrag 함수를 호출하고, 터치가 끝나면 입력 벡터를 초기화합니다.
이후, Getter를 구현하여, 조이스틱의 이동 벡터를 사용할 수 있게 하였습니다.

버튼 클래스

베틀레드는 버튼 클래스를 구현하여, 오브젝트를 관리하고 있습니다.
백그라운드로 사용되는 이미지와 내부 이미지로 사용되는 이미, FillAmount 유무와 프레스 이벤트의 종류 등을 일종의 Tool로서 지정할 수 있습니다.

버튼 백그라운드 이미지와 버튼 이미지를 public으로 하이어라키 상에서 입력받습니다.
또한, 버튼 입력과 관련된 Event를 관리합니다.

이때, 스킬 쿨타임을 표현하기 위해서 FillAmount 여부를 부울형으로 체크해줍니다.

Awake 단계에서는 이벤트 등록과 이미지 설정, 알파값 설정 등. 버튼 컴포넌트의 인스턴스화와 관련된 내용을 수행합니다.

GameManager를 통해서 게임 타임 일시정지를 하고 관리하게 됩니다.
OnClick의 경우에는 일시정지와 관계없이, 버튼 클릭 이벤트가 호출되도록하고,
나머지는 일시정지 상태에 따라 리턴해줍니다.

이는, 일시정지 해제 또한 버튼으로 제어되기 때문입니다.

FillAmount값을 외부 클래스에서 관리할 수 있도록 Setter를 이용하여, fillAmount를 조절합니다.


이상이 플레이어의 게임 조작과 관련된 클래스입니다.



캐릭터 이동 (옵저버 패턴)

기본적으로 캐릭터는 세 개의 매니저 클래스에 의해서 관리됩니다.
모든 동작 프로세스와 애니메이션을 총괄 관리하는 캐릭터 매니저 클래스.
그리고, 이동과 제어를 담당하는 컨트롤 매니저 클래스.
마지막으로 공격의 흐름을 관리하는 어택 매니저 클래스 입니다.

각 매니저들은 싱글턴 패턴과 옵저버 패턴을 이용하여 유기적으로 연결되어, 데이터를 주고 받으며 유저의 동작 제어를 원활하게 가능케 합니다.

함수 프로세스

캐릭터의 이동은 컨트롤 매니저에서 이뤄집니다.
Update는 순서대로, 중력 -> 컨트롤러 인풋 데이터 -> 캐릭터 회전 -> 이동 -> 점프 함수를 호출합니다.


중력

지면 레이어를 확인하여, 지면에 닿아있는지, PhysicCheckSphere를 이용하여 확인하고, 지면에 닿았다면 y 속도를 조정하여 더이상 떨어지지 않도록 합니다.

또한, 중력 값에 시간 값을 곱하여, 중력이 시간에 비례하여 적용되도록 합니다.
이를 통해 캐릭터가 빠르게 이동할 때 더 강한 중력이 작용하고, 느리게 이동할 때는 약한 중력이 작용하게 됩니다.


컨트롤러 데이터 인풋

조이스틱 컨트롤러 클래스에서 생성한 xy값을 가져옵니다.
이후, 캐릭터가 회피 중이 아니라면, 걷기 혹은 달리기 상태로 변경합니다.


캐릭터 회전

캐릭터의 회전은 현재, 수평 방향의 회전만 구현된 상태입니다.
상하의 회전은 캐릭터가 아닌, 카메라의 조향 각도를 변경하는 것이 옳은 방향이라 생각했기 때문입니다.

터치패드 컨트롤러에서 Horizontal 값을 Get합니다.
방향에 따라서, 캐릭터를 y축을 기준으로 회전합니다.

이는 y축을 끼고 돌아가는 유니티 엔진의 회전각 특성을 고려한 것입니다.


캐릭터 달리기

달리기는, 전투 페이즈에만 사용되는 동작 패턴입니다.
애니메이션을 관리하는 캐릭터 매니저에 z좌표값과 x좌표값을 넘겨줍니다. (컨트롤러 값)
이는, float 값에 의거하여 동작되는 달리기 애니메이션을 위해서 입니다.

다만, 달리기의 경우에는 일정 각도 이상으로 조이스틱을 우측과 좌측 방향으로 당길 경우에 캐릭터 자체를 회전시키는 로직이 구현되어 있습니다.
또한, 조이스틱을 뒤로 당길 경우에는 가드 상태가 되어, 이동을 멈추고 캐릭터 어택 매니저에 송신하여 쉴드 애니메이션을 동작시키도록 합니다.

객체의 이동은, controller.Move를 이용하여 중력 값을 적용하고 이동시킵니다.
만약 조이스틱으로 들어오는 값이 없다면, 이동을 멈추고 캐릭터 상태를 공격 상태로 전환하여, 어택 매니저로 동작 제어를 넘깁니다.


캐릭터 걷기

걷기 행동은, 캐릭터가 전투 상태가 아닐 때. 이동하는 경우에 동작하는 행동입니다.
그렇기에 캐릭터가 battle 상태라면 리턴을 해줍니다.

기본적인 캐릭터의 이동 로직은 달리기와 유사합니다.
controller.Move를 이용하여 중력이 적용된 이동을 구현하고,

조이스틱에서 들어오는 값이 없다면 걷기 상태를 해제합니다.
다만, 걷기는 공격 상태로 전환하는 것이 아니라 대기 상태로 바뀐다는 점에서 달리기와 차이가 있습니다.


캐릭터 점프

만약 캐릭터가 지면에 있고(isGrounded가 true) 점프를 시도할 수 있는(isJump가 true) 상태라면, 점프 동작을 처리합니다.

점프 동작은 가장 마지막에 Update 되는 동작으로 모든 동작의 중간 지점에서 상태를 강탈할 수 있습니다.
그렇기에 어택 매니저의 동작 흐름을 끊을 수 있기 때문에 어택 매니저의 플래그 초기화 함수를 호출합니다. 이후, 캐릭터 클래스의 상태를 점프로 변경합니다.

중력을 시뮬레이션하기 위해 velocity.y에 중력(gravity)을 더해줍니다.
controller.Move를 사용하여 캐릭터를 점프할 높이(jumpHeight)에 따라 이동시킵니다. velocity와 jumpHeight를 곱하고 Time.deltaTime을 곱하여 시간에 따른 이동을 적용합니다.


캐릭터 회피

블링크 버튼을 눌렀을 때, 조이스틱으로 인풋 받은 값에 의거하여 블링크 방향을 정해줍니다.
NotifyBlinkValue(blinkpos); 코드는 옵저버 패턴을 이용하여, 결정된 블링크 방향을 방송하는 기능을 가지고 있습니다.

또한, 캐릭터 매니저 클래스의 싱글턴에 접근하여, 캐릭터 상태를 e_AVOID 상태로 전환하여, 캐릭터가 회피 중임을 알립니다.

GetBlinkStartNotify()와 GetBlinkEndNotify() 함수는 애니메이션 이벤트로 호출되는 함수로, 블링크 제어 변수를 true로 하여, 블링크 중임을 알립니다.
반대로 GetBlinkStartNotify() 함수의 경우, 회피 애니메이션의 마지막 프레임에 호출되는 함수로 회피 동작의 종료를 알려줍니다.

이러한 회피는 2번 연속으로 동작할 수 있으며, 쿨타임을 돌려서 회피 횟수를 채워주는 코루틴 함수가 존재합니다.

즉, 함수의 흐름은 다음과 같습니다.

  • <전제 조건>
    BlinkCoolTimeReset()

  • <동작 흐름>
    BlickClickEvent()GetBlinkStartNotify()ActTumblin()GetBlinkStartNotify()



캐릭터 공격 (옵저버 패턴)

캐릭터의 공격은 현재 스킬 공격과 일반 공격을 구현한 상태입니다.
스킬 공격은 원소를 묻혀서 공격하는 것으로, 적 객체에게 원소 상태를 부착합니다.
이후, 다른 타입의 원소를 묻힐 경우, 정해진 공식에 따라서 상호작용을 일으킵니다.

이러한 공격 프로세스는 CharacterAttackMng 클래스에 의해서 제어됩니다.

평타 공격

평타 공격은 제어용 부울 변수인 isClick와 isAnimationIng, 그리고 nAtkLevel에 의해서 동작 관리됩니다.

평타 공격은 기본적으로 하나의 동작이 아니라, 여러 개의 동작 애니메이션으로 나누어져 있습니다. 이 때문에, nAtkLevel에 따라서 현재 진행 중인 공격 애니메이션 단계를 구분해야 합니다.

또한, isClick 변수는 플레이어가 공격 버튼을 클릭했을 때, 활성화 되는 변수로, 최초 공격 시작 및 공격의 반복 여부를 판단하는 데에 쓰입니다.

isAnimationIng 변수는 공격 애니메이션의 진행 여부와 관련된 변수입니다.
공격 애니메이션 도중 애니메이션 제어 변수가 변경되는 것을 막기 위함입니다.

최초 공격의 시작은 CharaceterAttackCheck()으로부터 시작됩니다.
이 함수는 공격 버튼 클릭 시에 호출되는 함수이기도 합니다.

이후의 연계되는 공격은 ReturnIdle(int num)에 의해서 작동되며, 동작이 끊긴다면 공격 대기모드로 돌아가는 기능도 포함하고 있습니다.

AttackEventNotify(int num)은 동작 애니메이션이 종료 될 때, 호출되며, 공격에 쓰이는 콜라이더를 SetFalse하고 자기 자신의 공격 애니메이션 단계를 매개변수로 넘겨서 다음 공격 레벨을 찾을 수 있게 합니다.

AttackEventStartNotify() 함수는 반대로 애니메이션이 시작될 때 호출되며, 제어 변수인 isClick과 isAnimationIng를 필요에 따라 적절히 수정하고, 공격의 충돌 판정을 위한 콜라이더 객체를 활성화 합니다.


NotifyAtkLevel((e_AttackLevel)nAtkLevel);
characMng.GetCharacterClass().SetState(eCharactgerState.e_ATTACK);
상단의 코드는 캐릭터의 동작 애니메이션을 관리하는 캐릭터 매니저에 현재 공격 애니메이션의 동작 레벨을 옵저버 패턴을 이용해 알리고, 캐릭터 상태를 e_ATTACK으로 변경하는 코드입니다.

이를 통해서, 캐릭터 공격의 애니메이션을 최종적으로 구현합니다.


즉, 평타 공격의 프로세스는 다음과 같습니다.
  • <관련 변수>
    isClick : 플레이어가 공격 버튼을 클릭 했는지 판단, 공격 프로세스(Count Up)의 연계에 관여.
    isAnimationIng : 공격 애니메이션의 진행 중임을 알림, 플레이어가 공격 버튼을 눌러, 함수를 재호출하지 못하도록 방지, 애니메이션 제어 변수 변동 방지.
    nAtkLevel : 공격 동작의 단계를 구분하는 변수.

  • <동작 흐름 - 단일 공격>
    CharaceterAttackCheck()AttackEventStartNotify()AttackEventNotify(int num)ReturnIdle(int num)동작 종료

  • <동작 흐름 - 공격 연계>
    CharaceterAttackCheck()AttackEventStartNotify()AttackEventNotify(int num)ReturnIdle(int num)AttackEventStartNotify()AttackEventNotify(int num)ReturnIdle(int num)반복



스킬 공격

현재 스킬 공격은 캐릭터 매니저에서 관리하는 원소 상태를 이용한, 원소 공격으로 구현하였습니다.

스킬 공격 시에는 조명을 조절하고, 캐릭터 주위를 감싸고 있는 검은 커튼을 활성화 하여, 주변 풍경을 잠시 Off시켜 줍니다.

마찬가지로 스킬 공격에서도 캐릭터의 공격 단계를 관리하는 nAtkLevel 변수를 사용합니다.
이를 통해서, 캐릭터 매니저 함수가 스킬 애니메이션 동작을 구현합니다.

애니메이션 동작 중에는 충돌 감지 콜라이더를 On하고, 옵저버 패턴을 이용하여 조명과 카메라를 관리하는 함수에 캐릭터 스킬의 시작과 종료를 알려주도록 합니다.

마지막으로 스킬 공격의 종료 단계에는 바뀌었던 값들을 원래대로 돌려놓고 공격 상태를 대기로 다시 돌려놓습니다.

스킬 공격은 평타 공격에 우선하기 때문에, 평타 공격 도중 제어 플래그의 순환 구조를 끊어버릴 위험이 있습니다.
이때문에, FlagValueReset() 함수를 호출하여, 제어 변수들을 초기화 시켜줘야 합니다.


  • <동작 흐름>
    AttackSkillStart()AttackSkilAniStart()CurtainOff()AttackSkillAniEnd()



캐릭터 애니메이션 제어

캐릭터 애니메이션은 CharacterManager 클래스에서 제어됩니다.
캐릭터 어택 매니저와 캐릭터 무브 매니저에서 관리하는 공격과 이동 프로세스가 주는 신호에 따라, 캐릭터의 애니메이션을 제어하는 역할을 맡습니다.


애니메이터 제어

캐릭터의 상태에 따라서 스위치문으로 분기하여, 애니메이터를 제어합니다.
캐릭터의 애니메이션 중,
Walk와 Run State는 float 형으로 제어되기 때문에, 정수형 변수인 Controller를 -1로 할당해줍니다.

반면, Attack 상태와 Avoid 상태의 경우에는 각 어택 레벨과 회피 방향에 따른 열거형 변수로 추가 분기를 필요로 합니다.

다만, 현재는 히트와 죽음 상태를 구현하지 않은 상황입니다.


실수형 애니메이터 변수인 zPos와 xPos, RunX, RunZ는 Walk와 Run 애니메이션을 제어하는 제어 변수입니다.

Setter 함수를 이용하여, 플레이어가 조작한 조이스틱 값을 전달 받아 매니저 클래스 내의 지역 변수에 할당합니다.

이때, 달리기 상태는 캐릭터가 Battle 상태에서만 동작되므로, if문을 이용하여, 분기하여 값을 보정하고 보정한 값들을 애니메이터에 전달합니다.

이렇게 구한 제어 변수 값들은 Update 단계에서 처리됩니다.
CharacterStateActor()는 캐릭터 상태에 따라서 분기하여 정수형 애니메이터 변수를 제어하는 함수이고,
FloatAnimatorValueFunc는 전투 상황에 따라서, Float값을 보정하고 각 값을 애니메이터에 전달하는 함수입니다.


  • <관련 변수>
    zPos : 조이스틱의 y축 이동값을 저장하는 변수, 3차원 상의 z축 이동을 담당. (Walk)
    xPos : 조이스틱의 x축 이동값을 저장하는 변수, 3차원 상의 x축 이동을 담당. (Walk)
    runX : xPos와 동일하나, Run 애니메이션을 관리한다.
    runY : yPos와 동일하나, Run 애니메이션을 관리한다.
    isBattle : 캐릭터가 Attack 상태일 때, 전투 상태에 돌입했음을 알리는 변수.
    blinkValue : 열거형 변수, e_BlinkPos 값을 저장하는 변수. 구르기 방향을 가리킨다.
    atkLevel : 열거형 변수, e_AttackLevel 값을 저장하는 변수. 상세 공격 동작을 제어하는, 공격 레벨을 가리킨다.

애니메이터 제어의 프로세스는 다음과 같습니다.
  • <동작 흐름 - Idle, Avoid, Attack>
    CharacterStateActor애니메이션 동작

  • <동작 흐름 - Run, Walk>
    AnimatorFloatValueSetterCharacterStateActorFloatAnimatorValueFunc애니메이션 동작



원소 스위칭

배틀 레드의 원소는 총 다섯 가지가 존재하며, 평타 공격이 아닌 스킬 공격을 했을 시에 반영됩니다.
스킬을 사용해, 적에게 원소를 묻히고 이 위에 다시 스킬로 원소를 묻혀, 몬스터가 중독된 원소의 상태에 반응하여 특정 이벤트나 효과가 발생하는 것이 원소 효과의 골자입니다.

이를 위해서는 원소 스위칭 시스템을 구현해야했습니다.
원신의 경우에는 여러 캐릭터를 번갈아가면서, 각 캐릭터가 지닌 원소를 묻혀 반응을 유도하는 방식으로 구현했지만, 배틀레드의 경우에는 단일 캐릭터이므로 원신보다는 젤다의 전설 - 야생의 숨결과 같은 방식을 생각하고 시스템을 짜보았습니다.


UI 버튼 객체와 연결되어 호출되는 함수입니다.
직접적인 원소 스위칭 기능을 담당하고 있습니다.

먼저 쿨타임을 체크하고, 쿨타임이 지나지 않았음에도 플레이어가 버튼을 클릭한 경우엔 Return합니다.

조건을 만족했다면, 로직에 들어갑니다.
현재 원소 상태를 나타내기 위한 객체인 SatelliteObj를 활성화하여 플레이어에게 보여주고, 인덱스 값을 증가시킵니다.

정수형 변수인 인덱스를 이용해 현재 원소 상태를 인트형으로 가져온 후에 증가 연산(++)합니다.
이때, 증가한 값이 열거형 변수인 e_Element의 값을 넘어서지 않도록 MAX의 값만큼 모듈러 연산하여 보정해줍니다.

이렇게 보정한 인덱스 값을 원소 상태로 바꾸어, 스위칭 구현을 마칩니다.
이후에는, 캐릭터 객체의 SetCurrentElement 함수를 이용해, 스위칭한 원소 상태를 저장해줍니다.

마지막으로, 쿨타임을 구현하기 위해서, 하이어라키에서 ElementSwitchButton 컴포넌트를 가져와, 해당 버튼 객체를 매개변수로 넘겨서, 쿨타임 관리 코루틴 함수를 호출합니다.


버튼 클래스는 버튼 UI를 관리하는 클래스 객체입니다.

해당 클래스 객체의 SetInsideImageFillAmount()함수를 이용하여, 이미지의 FillAmount를 채웁니다.

쿨타임 값은 매개변수인 time값을 duration으로 지정하고, time값 동안, Time.deltaTime 값을 이용해 구현합니다.

최종적으로 쿨타임을 지났을 경우에, SatelliteObj 객체를 비활성화하고, 제어 플래그인 isClickedCoolCheck를 false로 바꾸는 것으로 로직을 마칩니다.

Update로 호출되는 함수로, 현재 원소 상태를 기준으로, SatelliteObj의 색상 값을 바꾸는 역할을 합니다.

원소 스위칭 후, SatelliteObj가 활성화 될 때,
플레이어가 가시적으로 현재 선택된 원소 상태를 확인할 수 있도록 하기 위함입니다.



  • <관련 변수>

SatelliteObj : Animation이 내장되어 있는, 이펙트 파티클 객체. 캐릭터 주위를 회전하며, 현재 원소상태를 알림.

isClickedCoolCheck : 새틀라이트 오브젝트의 Animation 타임 값만큼 쿨타임을 보장하기 위한, 플래그 변수. 코루틴 함수를 통해서 true/false가 제어 된다.

element : 플레이어블 캐릭터의 현재 원소 상태를 나타내는 변수


원소 스위칭의 프로세스는 다음과 같습니다.
  • <동작 흐름>

ElementSwitch애니메이션 동작SatelliteParticleColorSwitchButtonClickedCoolTimeElementSwitch



몬스터 AI - 흐름

몬스터는 몬스터는 크게 이동과 순찰, 공격으로 구분됩니다.
각 AI는 FSM에 의하여 돌아가며, 열거형으로 구분되는 상태 조건에 따라서 분기하여 작동합니다.

이동 파트의 경우에는 몬스터 매니저에 의해서 관리제어됩니다.
캐릭터와 마찬가지로 몬스터 매니저가 캐릭터 애니메이션을 제어하는 역할도 하고 있습니다.


AI 종합 제어

AI의 흐름은 다음과 같습니다.
피격 원소 상태 확인hp와 상태 최신화AI 종합 제어 함수 호출애니메이터 제어 함수 호출사망 여부 확인몬스터 hp 바 UI 위치 포지션 수정

AI 총괄 함수의 AI 우선 순위는 아래와 같습니다.
Dead > Hit > Attack > PatolAndIdle

이러한 우선순위로 나누어진 각 상태에 따라 분기하여, 적절한 함수를 호출합니다.


애니메이션 제어

애니메이션 제어 함수는 위 로직으로 구현되어 있습니다.
캐릭터 애니메이션 제어와 마찬가지로 몬스터의 상태를 기준으로 우선하여, 애니메이터 변수 값을 설정하고, 이후 세부 동작이 필요하다면, 열거형 변수를 이용해 추가로 분기해줍니다.

이동과 공격-추격 단계에서는 정수형 변수로 애니메이터를 제어하지 않습니다.
실수형 변수인 zPos와 xPos를 이용하여, 실수형으로 애니메이터를 관리합니다.

블랜드 트리를 이용하여, 입력받는 x좌표, z좌표 값을 이용하여 몬스터의 이동 애니메이션을 제어하는 것입니다.

공격 단계에서는 추격과 연속 공격을 구현하기 위해서 추가적인 열거형 변수 e_MonsterAttackLevel를 이용하여, 세부 컨트롤러 값을 나누어 줍니다.

이후, 공격 단계와 주위 관찰에 대한 애니메이션도 해당하는 인트형 변수 값을 넘겨주는 것으로 구현합니다.

다만, 히트 상태와 스턴 상태는 아직 구현되지 않은 상태입니다.


  • <관련 변수>

isBattle : 몬스터의 전투 페이즈 진입 여부를 담는 부울 변수
isHit : 몬스터의 피격 애니메이션 동작 트리거 역할을 하는 부울 변수



몬스터 AI - 이동

몬스터의 대기와 순찰 로직은 정해진 대기/순찰 시간 값에 의거하여, 주위 순찰과 제자리 멈춤 동작을 합니다.
이동 AI는 유니티에서 제공하는 NavMeshAgent를 이용하여 구현하였으며,

이를 관리하는 PatrolAndIdle 함수는 Update에서 호출됩니다.

관리 함수

몬스터는 두 개의 타입으로 나누어져 있습니다.
선공형과 반격형.
반격형의 경우에는, 순찰 없이 제자리에서 대기만 하게 되며,
선공형의 경우 isIdle 변수의 true/false 값에 따라 순찰 함수와 대기 함수를 호출합니다.

단, 최초 리스폰 지역에서 너무 먼 거리까지 벗어났을 경우엔 본래 스폰 장소로 돌아오도록 했습니다.


순찰 함수

순찰 함수는 navMeshController의 경로 도착 여부를 판단하여, 도착지를 갱신하고 객체마다 public으로 정해진 순찰 시간(fElapsedTime)에 도달했을 경우, isIdle을 true로 반환하여 제자리에서 멈추도록 하여, 순찰 - 대기 모션을 반복할 수 있도록 구현했습니다.

또한 NavMeshAgent의 velocity값을 이용하여, 애니메이션 제어 변수인 fPosZ와 fPosX 값을 바꾸어, 적절한 이동 모션을 구현할 수 있도록 했습니다.


대기 함수

대기 함수의 경우, 실수형 애니메이터 변수를 0으로 재할당하고 상태 변수롤 Idle로 바꾸어 대기 동작을 할 수 있도록 합니다.

이후, 순찰 함수와 마찬가지로 fElapsedTime 변수를 이용하여, 대기 시간을 만족했을 때, 재어 플래그를 false로 하여 다시 순찰로 행동을 바꿔줍니다.



관련 기타 함수

랜덤 위치를 반환하는 함수는, public 변수로서 객체마다 설정되는 fMovePointRange(순찰 최대 범위) 값 내에서 랜덤한 navMesh Point를 반환하는 함수입니다.

ReturnToStart()와 HasExceededDistanceThreshold() 함수는 객체가 순찰로 인해서, 너무 먼 거리를 벗어났을 경우에 스폰 포인트로 다시 복귀하는 기능을 가지고 있습니다.

PatrolAndIdle() 함수에서 주기적으로, 최대 순찰 범위를 벗어났는지 판단하고 순찰 범위를 벗어났다면, 즉시 Return함수를 호출해 스폰 포인트로 돌아오도록 합니다.


  • <관련 변수>

isIdle : 몬스터의 순찰-이동의 동작 순서를 담는 부울 변수.
navMeshController : 몬스터 객체에 부착된 NavMeshAgent를 가리키는 변수.
fElapsedTime : 객체에 지정된 최대 순찰/이동 시간
fPatrolTimeNumber : 객체의 현재 순찰 진행 시간
fIdleTimeNumber : 객체의 현재 대기 진행 시간
fMovePointRange : 객체에 설정된 최대 순찰 범위 보정값
fChaseRange : 몬스터의 최대 추격 범위 / 최대 순찰 범윌로서 사용됨

  • <동작 흐름>

PatrolAndIdleisIdle == falseHasExceededDistanceThreshold() == falseMonsterPatrolGetRandomPositionPatrolAndIdleisIdle == trueMonsterIdle

HasExceededDistanceThreshold() == trueReturnToStartPatrolAndIdleisIdle == true ? MonsterPatrol : MonsterIdle



몬스터 AI - 공격

몬스터의 공격 AI는 몬스터 어택 매니저에 의해서 관리됩니다.
다만, 몬스터는 캐릭터와 다르게 단일 객체가 아닌, 여러 타입의 객체가 존재하기 때문에, 하나의 매니저로 통합하여 관리하기에는 다소 상이한 부분이 존재합니다.

그렇기에, 공격 매니저의 종합적인 부분을 MonsterAttack 클래스로 구현한 후에, 해당 객체를 부모로 상속 받아서 각 몬스터 객체의 특성에 따라 추가적인 로직을 더하는 방향으로 공격 매니저를 구현했습니다.


탐지와 전투 돌입 AI

Update는 목표물을 추적하는 함수를 호출합니다.
공격은 추적 - 접근 - 공격 루프의 순서로 이루어지며,
특히 이동 파트는 모든 몬스터 객체가 동일하게 NavMesh를 이용하여 움직이기 때문에 부모 클래스에서 객체를 추적하고 쫓도록 구현했습니다.

공격의 시작은 MonsterAttackStart() 함수를 통해서, 시작 되며
isBattle 변수와 isChase 변수를 true로 바꾸는 것으로 공격 로직이 작동하게 됩니다.

공격 매니저는 공격을 담당하는 만큼, isBattle이 false일 경우에는 Update의 최상단에서 return하여 로직이 구동하지 않도록 막습니다.


몬스터가 타깃을 추적하는 상태임을 저장하는 isChase 변수가 false가 되었을 경우에는, 공격해야 하는 상대가 없음으로 판단, isBattle을 false로 바꾸어, 더이상 Update 함수에서 전투 속행을 하지 못하도록 막습니다.

이후, 몬스터의 이동을 총괄하는 몬스터 매니저에 배틀 페이즈가 중단되었음을 알리고, 관련 변수 변수를 수정합니다.

이후, AttackRange 클래스의 구독을 해제하여 옵저버 패턴을 Off합니다.


여기서 AttackRange 클래스의 역할에 대해서 설명하기 전에, 먼저 몬스터의 전투 시작 흐름에 대해서 알아두어야 합니다.

몬스터의 전투는 다음 절차에 따릅니다.

CharacterViewRange가 탐지 거리 내의 적을 파악탐지한 적 객체를 리스트 형태로 몬스터 매니저와 몬스터 어택 매니저에 송신송신된 데이터를 이용하여, 전투 페이즈로 돌입하고, 가까운 객체를 추적함사정거리를 탐지하는 AttackRange 객체가 타깃이 사정권에 들어왔을 경우, 캐릭터 객체를 송신추적을 중지하고 공격 패턴으로 전환

<몬스터 매니저의 캐릭터 탐지 옵저버 패턴> : CharacterViewRange 클래스가 탐지한 캐릭터 객체를 List형태로 받는 역할을 한다.

이러한 CharacterViewRange 클래스를 이용한 데이터 수집은 어택 매니저가 아닌 몬스터 매니저에서 이뤄집니다.

이는, 어택 매니저는 배틀 페이즈 전환 이후, 순수한 전투 파트만을 담당하기 때문입니다.

즉, 몬스터 매니저가 옵저버 패턴으로 적을 탐지했다면, 전투 페이즈로 전환하고 어택 매니저가 전투 로직을 돌리기 시작하는 방식으로 AI를 구현했습니다.


  • <관련 변수>

isChase : 추적 중인 타깃의 유무를 저장하는 부울 변수
isBattle : 전투 페이즈에 돌입했음을 나타내는 부울 변수
TargetList : 몬스터 매니저 클래스에서 송신 받는, 적 객체 데이터 리스트
target : 직접적인 타깃이 되는 객체 (리스트 내의 0번 째 데이터)



추적 AI

추적 기능을 구현한 함수입니다.
target이 널이 아니고 전투 상태일 경우에 해당 로직을 반복합니다.

먼저 공격권 사정거리 내에 적이 있는지 유무를 저장하는 변수인 isTargetInRange의 값이 true라면, 즉. 사정 거리에 적이 있다면 이동 애니메이션을 종료하고 관련 변수를 수정한 후에 return 하는 것으로 추적을 끝냅니다.

이후엔 최대 추적 거리를 타깃이 벗어났는지를 검사하고, 해당 조건문 마저 통과한 후에 타깃을 추적합니다.

isTargetInRange 변수는 AttackRange 클래스를 옵저버 패턴으로 구독하여, 데이터를 갱신하는 방식으로 관리됩니다.
Exists 메서드를 이용해, 타깃이 리스트 내에 존재하는지 bool 형으로 반환하고, 이렇게 반환한 값이 isTargetInRange 변수로 사용되는 방식입니다.


  • <관련 변수>

isTargetInRange : 공격 사정거리 내에 캐릭터가 있음을 나타내는 변수
navAgent : 몬스터 객체의 NavMeshAgent를 저장하는 변수
fSetChaseRange : 몬스터의 최대 추적 범위를 나타내는 변수
isAtkAnimationConrolFlag : 몬스터의 공격 애니메이션과 코루틴을 제어하기 위한 플래그 변수, 공격 단계에서 사용됨

  • <프로세스 흐름 - 추적>

몬스터 매니저에서 전투 페이즈 시작을 알림TargetInputterMonsterAttackStartTargetChaseMoveRotateTowardsTargetTargetPositionRetrun전투 종료 시EndPageChaseCheck

  • <프로세스 흐름 - 공격 전환>

TargetChaseMoveisTargetInRange == true추적 종료공격 시작



공격 AI

몬스터 객체는 오브젝트 풀 기법으로 관리됩니다.
그렇기 때문에, OnEnable과 OnDisable을 이용하여 적절히 변수를 초기화해주어야 합니다.

Update에서는 부모 클래스인 MonsterAttack 클래스의 Update 함수 내용을 그대로 사용하고,
AttackProcessAI() 함수와 부모 객체의 EndPageChaseCheck() 함수를 추가로 호출합니다.

즉 로직의 흐름은 다음과 같습니다.
탐지추적공격종료

atkMaxLevel은 몬스터의 최대 공격 애니메이션 횟수를 저장하는 변수입니다.
이는, 각 몬스터 객체마다 다르기 때문에, 부모 객체인 AttackMonster를 상속 받아, 필요한 값만큼 수정하는 방식으로 구현했습니다.

이러한 애니메이션 개수에 의거하여, switch문의 e_MonsterAttackLevel 단계 또한, 바뀝니다.
상단의 그림은 같은 부모객체를 상속받는 MobAngryMushroomAttack 클래스와 MobCactusAttack 클래스가 보유하고 있는 AttackProcessAI 메서드를 보여줍니다.


프레임 단위로 호출되는 Update의 특성 상, 애니메이션의 재생 시간을 보장하기 위해서는 코루틴을 이용하여 함수를 제어해야 합니다.

앞전에 설명했던 isAtkAnimationConrolFlag 변수가 바로 여기서 사용됩니다.
애니메이션 클립의 재생 시간을 가져와, 코루틴으로 대기하고 이후, isAtkAnimationConrolFlag 값을 false로 바꾸어 AttackProcessAI() 함수가 다시 공격 단계를 진행할 수 있도록 합니다.

현재까지 구현된 파트는 이러하며, 추후 각 몬스터 객체의 특징에 따라서 새로운 함수를 추가하여 행동 패턴을 구현할 수 있을 것입니다.


  • <관련 변수>

currnetAtkLevel : 현재 진행 중인 공격 단계
atkMaxLevel : 객체가 구현할 수 있는 최대 공격 단계
isAtkAnimationConrolFlag : 몬스터의 공격 애니메이션과 코루틴을 제어하기 위한 플래그 변수

  • <프로세스 흐름>

base.Update()AttackProcessAIWaitForAnimationGetAnimationTime애니메이션 시간만큼 대기 후,AttackProcessAI



몬스터 - 스폰 (오브젝트 풀)

배틀레드의 몬스터 객체는 오브젝트 풀 기법을 이용하여 관리되고 있습니다.
오브젝트 풀(Object Pool)은 프로그램에서 자주 생성되고 삭제되는 객체(오브젝트)를 미리 생성해 두고 필요할 때 재사용하는 디자인 패턴입니다.

이러한 오브젝트 풀을 이용하여 캐릭터의 위치를 기준으로, 일정 거리 내에 스폰 포인트 지점에 도달했을 경우, 몬스터를 생성하는 방식으로 구현했습니다.


오브젝트 풀 클래스

오브젝트 풀 클래스입니다.
제너릭 기법을 이용하여 여러 컴포넌트 타입의 객체를 데이터 유형으로 받아, 오브젝트 풀로서 사용하는 방법으로 구현했습니다.

생성자에서는 프리팹과 리스트에 저장할 객체의 개수, 하이어라키 상에서 저장할 객체의 부모 위치를 매개변수로 받아서, 지역변수에 저장합니다.

이후, SetActive를 적절히 이용하여 Active된 객체를 return하거나, 매개변수로 받은 객체를 SetFalse로 전환하여 객체를 관리하였습니다.


객체 초기화

오브젝트 풀 객체는 모두 게임매니저에서 관리합니다.
초기 데이터를 Awake 단계에서 인스턴스화하여 저장하고, 싱글턴 패턴을 이용해, 다른 객체에서 접근할 수 있게 하였습니다.

오브젝트 풀을 이용하여, 몬스터 객체만을 관리하는 것이 아니라 빈번하게 사용하는 UI 오브젝트 또한 관리할 수 있습니다.


스폰 포인트 탐지

캐릭터 매니저 클래스의 World_InCharacterCheck() 함수는 캐릭터의 위치를 기준으로 10만큼의 범위 내에 SpawnPoint 레이어를 가진 객체가 있는지 체크 하고, 만약 존재한다면 게임매니저를 통해서, 스폰 함수를 호출합니다.

이러한 World_InCharacterCheck() 함수는 FixedUpdate() 단계에서 호출됩니다.

최초에는 Update 단계에서 함수를 호출하도록 만들었는데, 이 경우 스폰과 관련된 버그가 빈번하게 발생하는 문제가 발생하였습니다.

다만, 몬스터 생성에 관련하여 보완할 부분이 있다면
FixedUpdate() 단계에서 주기적으로 포인트를 탐지하여, 물리 프레임마다 호출하여 조건문을 검출하기보다는, 캐릭터 매니저에서 범위 내의 적 객체를 저장하여, 적 객체가 없을 때만 함수를 호출하도록 하는 것이 좋을 것입니다.

이는, ViewRange 객체를 이용해서, 구현할 수 있습니다.
또한, 이러한 ViewRange를 이용한 객체 탐지는 NPC나 상호작용 오브젝트를 찾는 데도 이용할 수 있습니다.


몬스터 생성

몬스터 스폰 함수입니다.
오브젝트의 이름을 Substring 메서드로 파싱하여, 스폰 지역의 번호에 따라 분기하여, 스폰 포인트에 해당하는 몬스터 객체를 호출할 수 있도록 분기하여, 관련 함수를 호출합니다.

이때, 스폰 포인트로부터 20 만큼의 범위를 탐지하여 Monster 레이어를 가진 객체를 콜라이더 배열에 저장하는데, 이는 일정 범위 내에 조건에 일치하는 몬스터가 이미 존재할 경우, 몬스터 스폰을 방지하기 위함입니다.

먼저 콜라이더 배열을 순회하며, 몬스터가 이미 존재할 경우에는 리턴하여 몬스터 생성을 막습니다.
몬스터가 없다면, 오브젝트 풀에 접근하여 해당하는 몬스터를 GetPool하여 활성화시킵니다.
단, 이때 몬스터 클래스는 새롭게 생성하여 초기화해주어야 합니다.

이미, 사망한 몬스터일 경우에는, 몬스터가 스폰됨과 동시에 제거되기 때문입니다.

또한, 몬스터의 HP바 UI도 오브젝트 풀로 Get하여, 몬스터에 연결시켜줍니다.


몬스터 제거

이러한 몬스터는 일정 범위 내에 캐릭터가 존재하지 않는다면 스스로 Return Pool하여 제거됩니다.
몬스터 제거 함수인, WorldPlayerCheck() 함수는 캐릭터와 마찬가지로 FixedUpdate 단계에서 호출되는 함수입니다.

함수를 실행하기에 앞서, 범위 내 탐지한 적을 저장하는 리스트인 taretList의 Count값이 0인지를 먼저 검사하여 확인해줍니다.

이는, OverlapSphere()메서드를 FixedUpdate 단계마다, 함수로 호출하여 반복해서 사용하는 것을 막기 위해서입니다.

이후, 몬스터를 기준으로 지름 40에 해당하는 구를 그려 Player 개체가 존재하는지 확인하고 없다면, 오브젝트 풀에 리턴합니다.


  • <프로세스 흐름>

: World_InCharacterCheckMonsterSpawnSpawnMonstercolliders 내에 타깃 몬스터가 없을 경우,몬스터 Get Pool몬스터 생성플레이어가 몬스터의 최대 생존 범위에서 벗어났을 경우WorldPlayerCheck몬스터 Return Pool

profile
훌륭한 개발자를 꿈꾸는 중입니다

0개의 댓글