배틀블루의 문서가 너무 길어져서 이렇게 분리하게 되었습니다.
게임 스타트 씬은, 로비에서 임무시작 버튼을 클릭하면 전환되는 씬입니다.
원하는 미션을 선택하고, 게임을 시작하는 중간다리 역할을 하고 있씁니다.
다만, 게임 스타트에 앞서 스쿼드를 선제적으로 편성해야 하기 때문에, 편성창으로 이어질 수 있는 버튼을 삽입하였습니다.
인게임의 프로세스와 매니징 역할을 총괄하는 "02_SceneIngame"씬을 씬로더를 통하여 호출하고, Additive형태로 게임의 배경 및 틀 형태를 담당하는 "03_Stage1_Scene"을 호출하여, 게임을 연결합니다.
다만, 선제적인 조건으로 스쿼드의 존재 유무를 파악하며.
만약 스쿼드가 null인 경우라면, 텍스트를 출력하여 파티를 먼저 편성해야할 것을 알려줍니다.
스쿼드씬을 로드하는 버튼은 Additive 형태로 신을 호출하여, 스쿼드 씬에서 나가기를 누를 경우, 다시 StartMission 씬으로 돌아올 수 있도록 코드를 구성하였습니다.
그외는 각 버튼에 설정된 인자를 인덱스로 받아와, SelectMissionLevel로 변수에 저장.
플레이어가 선택한 임무를 코드가 인지할 수 있도록 만들었습니다.
수집형 모바일 RPG게임에는 다양한 캐릭터가 존재합니다.
하나, 실제로 전투에 돌입할 때 플레이어가 보유하고 있는 모든 캐릭터를 전장에 투입할 수는 없습니다.
그렇기에 임무에 투입할 캐릭터를 정하는 절차를 필요로 합니다.
스쿼드 씬은 이러한 과정을 위하여 만든 씬입니다.
위 사진을 보면 알 수 있다시피, 스쿼드는 네 명의 캐릭터가 최대이며, 선택한 캐릭터의 데이터가 출력됩니다.
'SQUAD_Characters'는 정적 변수로써, 게임 플레이에 사용하는 파티 자체를 가리킵니다.
StartGame에서 요구하는 스쿼드가 바로 해당 변수입니다.
'scrollView'와 'buttonSet'은 각각 플레이어의 모든 캐릭터 객체를 보여주는 배경 툴인 스크롤 UI, 스크롤 UGUI 내부를 채울 버튼 오브젝트의 origin 프리팹 객체입니다.
'partyInfoField'는 캐릭터 데이터 정보를 출력할 텍스트들의 부모 객체로써, 자식 객체에 접근하는 형식으로 캐릭터 데이터를 출력할 것입니다.
'selectButtons'은 플레이어가 원하는 파티 번 째에 캐릭터를 편성할 수 있도록하는 버튼 배열입니다.
Dictionary 타입의 'buttonSetArray'는 클래스 내의 데이터를 관리하기 위해 필요로 하는 변수입니다.
캐릭터의 데이터를 가리키는 캐릭터 클래스와 해당하는 캐릭터가 보유하는 버튼Set 프리팹을 한 쌍으로 묶어서 관리합니다.
'indexStack'는 SQUAD_Characters에서 캐릭터를 삽입할 순서를 관리하는 스택입니다.
스쿼드 씬에서는, 자동으로 파티 순서 할당. 플레이어 임의 파티 순서 할당. 이렇게 두 가지 방법으로 캐릭터를 순서를 관리하며, 이럴 제어하기 위해 선택한 자료구조가 스택이었습니다.
UI셋팅은 씬 상에서 표현되는 UI들을 세팅하는 함수입니다.
기본적인 구조의 골자는 탐험씬과 유사합니다.
세트 프리팹을 생성하고, 생성한 프리팹을 캐릭터 클래스와 함께 쌍을 지어 Dictionary에 저장.
버튼 프리팹에 UI 작업을 담당하는 함수에, 생성한 버튼 객체를 인자로 넘겨서 처리합니다.
또한 버튼 세트 프리팹에 클릭할 시에 호출되는 함수를 연동하고, 생성된 UI 컴포넌트 객체를 스크롤 뷰 상에서 위치를 조정한다.(우측최후열에 배치합니다.)
SquadSelect() 함수는 스크롤뷰에 생성된 프리팹에 연동된 함수입니다.
사용자가 클릭한 버튼을 'EventSystem.current.currentSelectedGameObject' 메서드를 이용하여 인식합니다.
클릭한 버튼에 저장된 캐릭터 데이터는 Dictionary 변수를 이용하여 가져올 수 있으며, 이러한 캐릭터 데이터는 추후, UI 세팅 및 오브젝트 관리 등에 사용됩니다.
foreach (ChracterClass item in SQUAD_Characters)
{
if(item == null)
continue;
if (item.characterName == selectCharac.characterName)
return;
}
상단의 반복문은 스쿼드 파티 내에 같은 이름을 가지는 캐릭터 있는지 검사하는 코드입니다.
이는, 중복되는 이름을 가지는 캐릭터를 제거하기 위해 필요로 합니다.
int _index = Index();
if(indexStack.Count > 0)
{
_index = indexStack.Pop();
}
인덱스는 'SQUAD_Character'내의 파티 순서를 지시하는 변수입니다.
Stack내에 데이터가 존재한다면, 인덱스의 숫자는 스택 내의 데이터로 대체되며, 존재하지 않을 겨우에 기존의 인덱스를 이용합니다.
인덱스 함수는, 스쿼드의 전체 범위를 벗어나지 않도록 모듈러 연산을 통하여 보정한 인덱스 값을 반환합니다.
이렇게 정해진 인덱스에 partyInfoField(스쿼드 파티 데이터를 출력하는 부모객체 배열)의 순서를 결정짓고 해당 객체가 지닌 모든 자식 객체를 불러와 UI작업을 합니다.
Switch문은 TextMeshPro를 저장한 texts 배열의 인덱스에 따라 분기하여 필요한 UI 문구를 출력합니다.
이렇게 파티원으로 선택된 객체는 스크롤뷰에서 제거하고 Dictionary에서도 삭제되어야 하는 작업이 필요합니다.
foreach문을 이용하여, 선택한 캐릭터 클래스 객체와 동일한 원소를 찾아냅니다.
찾은 원소는 버튼 아이템을 파괴하고 Dictionary 내에서도 제거합니다.
또한 함수를 호출하여, 기존의 스쿼드 데이터 내의 제거된 데이터를 다시 Dicionary에 생성하고, 버튼 프리팹도 추가하는 작업을 합니다.
마지막으로 스쿼드를 관리하는 코드입니다.
스쿼드는 총 4명이기에, 스쿼드 리스트의 크기 역시 4를 벗어나면 안됩니다.
만약, 리스트의 범위를 추가하여 데이터를 추가했을 경우에 기존의 인덱스번째 데이터를 제거하고 해당 인덱스에 선택한 캐릭터 데이터를 Insert합니다.
마지막으로 스택 내에 값이 존재하지 않는다면, 인덱스를 이용하여 데이터 순서를 제어하기에, 값을 증감연산합니다.
제거된 데이터가 없다면 바로 return해줍니다.
그리고, 만약 데이터가 존재한다면, 상단에 설명한 UI셋팅과 마찬가지로, 버튼 프리팹을 생성하고 dictionary에 프리팹과 쌍을 지어, 원소를 추가.
이후, 클릭 함수를 연동하고 버튼 UI 요소 세팅을 마친 후에,
스크롤뷰 내의 객체 위치를 조정합니다.
해당 함수는, 사용자 임의 포지션 선택 버튼에 연동되어 있는 Event 함수입니다.
버튼 클릭시 indexStack내의 원소 유무를 검사하여, 존재한다면 인덱스에 선택한 버튼의 순서(index값)를 push하고, 존재하지 않는다면 0 번째 원소를 추가하는 것이기에, 기존의 인덱스 값을 스택의 원소로 추가한 후에, 값을 Push합니다.
인게임 씬은 게임매니저, 이펙트 매니저, 적 생성 매니저, 퀘스트 보상 매니저 등을 보유하고 있는 씬입니다.
인게임씬은 게임의 진행 프로세스의 매니징을 총괄하는 씬으로써, Additve로 생성된 스테이지 씬에 존재하는 오브젝트의 태그를 확인하여 필요한 객체를 저장하고, 각각의 객체에 지정된 함수와 변수를 배분하고 호출하여 게임을 진행시키는 씬입니다.

그렇기 때문에, 인게임씬에서는 3D오브젝트를 보유하지 않습니다.
필요한 객체는 전부 Stage씬에서 생성하고, 인게임 씬에서는 함수의 흐름을 제어할 뿐입니다.
현재 배틀블루에는 총 10명의 캐릭터가 존재합니다.
엠버,호두, 리시타, 루나, 루타, 세리카, 스나, 실비아, 테일리, 제트.
이러한 10명의 캐릭터는 플레이어가 뽑기를 통하여 획득할 수 있고, 획득한 캐릭터를 스쿼드 씬에서 분대화하여 인게임으로 투입할 수 있도록 시스템이 되어있습니다.
즉, 10명의 캐릭터 객체를 구현할 필요가 있었습니다.

3D 캐릭터 에셋을 이용하여 캐릭터 오브젝트를 구현했습니다.
box콜라이더와 NavMesh가 적용된 캐릭터는 충돌 감지와 Navigation.AI를 활용하여 길찾기 인공지능이 Set되어 있습니다.

캐릭터는 모두 캐릭터의 프로세스를 관리하는 CharacterManager와 ViewRange라는 적을 탐지하는 클래스를 보유하고 있으며,

애니메이터에서는 인트형 변수를 이용하여 애니메이션이 제어 됩니다.

인트형으로 제어되는 캐릭터 애니메이터는 캐릭터의 현재 State에 따라 크게 다섯 가지의 동작을 구동합니다.
첫 번째로 Idle : 캐릭터의 대기 상태를 나타냅니다.
두 번째로 Run : 이동 중인 캐릭터의 달리기 동작 애니메이션입니다.
세 번째로 Attack : 기본적인 캐릭터의 공격 애니메이션입니다.
네 번째로 Skill : 각 캐릭터가 가지고 있는 고유한 스킬용 애니메이션입니다.
다섯 번째, Death : 캐릭터 사망 시 출력되는 애니메이션입니다.
CharacterViewRange

CharacterViewRange클래스는 일정 범위 내의 적을 탐지하고, 탐지한 적 객체를 데이터로 저장하는 클래스입니다.
GameManager와 CharacterManager 클래스에서 ViewRange가 탐색한 해당 enemyDatas를 필요로하며, 이는 옵저버 패턴을 이용하여 데이터를 주고 받습니다.
CharacterViewRange클래스는 Subject로써, enemy를 탐지할 때마다 구독자인 Observer들에게 데이터들을 넘겨주고, Observer들은 이렇게 받은 데이터를 이용하여 전투 AI를 구현합니다.

ViewCastInfo는 raycast의 결과를 저장하는 구조체입니다. 시야 영역의 레이캐스트 결과를 저장하여 후속 처리에 사용됩니다.
Edge는 두 개의 간선(선분)을 저장하는 역할을 합니다. 각 간선은 시작점과 끝점의 좌표를 가지고 있습니다. 이 구조체는 주로 시야 영역의 가장자리를 나타내는 두 개의 간선을 저장하고 반환하는 데 사용됩니다.

ViewRange클래스를 구현하는데 필요한 변수들입니다.
시야 영역의 반지름과 각도는 SerializedField로 직렬화하여, 에디터 상에서 조절할 수 있도록 하였습니다.
targetMask와 obstacleMask는 각각 목표물 레이어와 장애물 레이어로써, 장애물에 부딪칠 경우 레이캐스트가 무시되고 부딪치지 않을 경우에, targetMask 레이어를 가진 객체를 선택할 수 있도록 레이어 마스크를 변수화합니다.
visibleTargets은 범위 내에서 감지한 target 객체들을 저장하는 List타입의 변수입니다.
meshResolution은 시야 영역을 구성하는 메시의 해상도를 조절하는 값입니다. 레이 각도를 기반으로 시야 영역을 구성할 때, 이 값을 사용하여 레이의 개수를 결정합니다. 값이 클수록 더 부드러운 시야 영역을 생성합니다.
viewMesh는 시야 영역을 그리기 위해 사용되는 메시(Mesh)입니다. 메시는 정점(Vertex)와 삼각형(Triangle)으로 구성되며, 이를 사용하여 시야 영역을 시각화합니다.
ViewMeshFilter는 시야 영역 메시를 표시하기 위한 MeshFilter 컴포넌트입니다. MeshFilter는 메시를 가지고 있으며, 이를 통해 메시를 렌더링할 수 있습니다.
edgeResolveIterations는 가장자리를 결정하기 위한 이진 탐색의 반복 횟수를 나타내는 값입니다. 시야 영역의 가장자리를 결정할 때, 이진 탐색을 사용하여 보간할 부분을 찾습니다.
edgeDstThreshold는 가장자리를 결정할 때 사용되는 레이의 길이 임계치를 나타내는 값입니다.

Start에서는 viewMesh 변수를 새로운 Mesh 객체로 초기화합니다. 그리고 ViewMeshFilter 컴포넌트의 mesh 속성을 초기화된 viewMesh로 설정합니다. 이렇게 함으로써 viewMesh를 렌더링할 수 있는 MeshFilter를 준비합니다.
FindTargetWithDelay 코루틴 함수를 0.2초 간격으로 호출하기 위해 StartCoroutine 함수를 사용합니다. 이 함수는 일정 시간 간격으로 FindVisibleTargets 함수를 호출하여 주변의 타겟을 찾습니다.

visibleTargets 리스트를 초기화합니다.
Physics.OverlapSphere 함수를 사용하여 시야 반경 내에 있는 targetMask 레이어에 속하는 콜라이더를 가져옵니다.
이렇게 가져온 콜라이더 배열을 순회하면서 각 타겟에 대해 다음 과정을 수행합니다.
해당 타겟과 플레이어 캐릭터 사이의 방향 벡터를 계산하고 플레이어 캐릭터의 Forward 벡터와 방향 벡터 사이의 각도를 계산합니다.
만약 계산된 각도가 시야 각도의 절반보다 작은 경우에만 아래 과정을 수행합니다.
플레이어 캐릭터 위치에서 타겟 방향으로 레이캐스트를 발사하여 장애물 마스크에 걸리지 않는 경우에만 해당 타겟을 visibleTargets 리스트에 추가합니다
이렇게 얻은 visibleTargets 리스트를 Subject를 오버라이딩한 클래스들에게 데이터를 전송합니다.
DirFromAngle 함수는 주어진 각도를 기반으로 3차원 공간에서의 방향 벡터를 반환하는 함수입니다.
매개변수로 받은 angleDegrees를 이용하여 각도를 라디안으로 변환합니다.
이때, anglesGlobal 매개변수가 true인 경우에는 변환된 각도에 현재 오브젝트의 transform.eulerAngles.y 값을 더하여 전역 각도로 보정합니다.
방향 벡터를 계산하기 위해 라디안으로 변환된 보정된 각도를 사용하여 코사인과 사인 값을 계산합니다.
계산된 코사인 값은 x 축 성분이 되고, 사인 값은 z 축 성분이 되는 3차원 방향 벡터를 생성합니다.
최종적으로 생성된 방향 벡터를 반환합니다.


시야각을 구성하는 레이의 개수를 결정하기 위해 stepCount를 계산합니다.
이는 시야 각도를 meshResolution으로 나눈 후 반올림하여 정수로 만들어진 값입니다.
(앞서 설명했다시피 meshResolution의 갯수에 따라 레이의 개수가 결정되기에, 값이 크면 해상도가 좋아지고 부드러운 시야영역이 만들어집니다.)
시야각을 구성하는 각 레이의 크기를 결정하기 위해 stepAngleSize를 계산합니다.
이는 시야 각도를 stepCount로 나눈 값입니다.
시야에 포함되는 정점 좌표를 저장하기 위한 viewPoints 리스트를 생성하고 이전 레이캐스트의 결과를 저장하기 위한 prevViewCast 변수를 초기화합니다.
stepCount 만큼 반복하면서 각 레이의 각도를 계산하고 ViewCast 함수를 호출하여 해당 각도에 대한 레이캐스트 정보를 얻습니다.
이전 레이캐스트 정보와 현재 레이캐스트 정보를 비교하여 보간해야 할 부분을 찾습니다.
(시야 캐스트에서의 보간은 최소 시야 캐스트 결과와 최대 시야 캐스트 결과 사이에서 보간을 수행하여 시야 캐스트의 경계를 정확히 결정하는 것을 의미합니다. )
이를 위해 이전 레이와 현재 레이의 충돌 여부, 거리 차이, 그리고 edgeDstThreshold 값을 비교합니다.
보간해야 할 부분을 찾은 경우, 해당 부분의 좌표를 찾아 viewPoints 리스트에 추가합니다.
현재 레이캐스트의 충돌한 지점 좌표를 viewPoints 리스트에 추가하고 다음 레이캐스트를 위해 현재 레이캐스트 정보를 prevViewCast에 저장합니다.
모든 레이에 대한 처리가 완료되면, viewPoints 리스트를 기반으로 폴리곤 메시를 생성합니다.
메시의 정점 좌표와 삼각형 정보를 설정하고, 메시의 법선 벡터를 재계산합니다.

ViewCast함수는 특정 각도에 대한 레이캐스트를 수행하여 시야 내의 객체 또는 장애물을 검출하는 역할을 합니다.
주어진 각도(globalAngle)를 기준으로 레이캐스트 방향 벡터(dir)를 계산합니다.
이는 DirFromAngle 함수를 사용하여 각도로부터 방향 벡터를 구합니다.
레이캐스트를 쏴서 충돌 정보를 얻습니다.
레이캐스트가 충돌한 경우, hit 정보를 기반으로 시야 내의 객체 또는 장애물을 감지한 것으로 판단하고, 해당 정보를 반환합니다.
이때, 캐릭터의 위치 좌표를 고려하여 0.4f만큼 y축을 올려 ray를 쏩니다.
만약 보정하지 않는다면, 장애물을 인식하지 않아 폴리곤이 그대로 obstacleMask 객체와 부딪쳐도 그대로 통과하는 문제가 발생합니다.
레이캐스트가 충돌하지 않은 경우, 시야 내에 아무것도 감지하지 않은 것으로 판단하고, 최대 시야 거리까지의 레이캐스트 정보를 반환합니다.

FindEdge함수는 시야 캐스트 결과의 최소 각도와 최대 각도 사이에서 보간하여 가장자리(edge)를 찾는 역할을 가지고 있습니다.
최소 시야 캐스트 결과와 최대 시야 캐스트 결과를 매개변수로 받고, 최소 각도와 최대 각도를 설정합니다. 또한 최소 포인트와 최대 포인트를 초기화합니다.
edgeResolveIterations 변수에 지정된 반복 횟수만큼 가장자리(edge)를 해결하기 위한 반복문을 실행합니다.
중간 각도를 계산합니다, 이때 중간 각도는 최소 각도와 최대 각도 사이의 가운데 각도입니다.
계산한 중간 각도에 대한 시야 캐스트 정보를 가져옵니다.
ViewCast 함수를 호출하여 중간 각도에 대한 시야 캐스트 결과를 얻습니다.
현재 시야 캐스트 결과와 이전 시야 캐스트 결과의 충돌 여부 및 거리 차이, edgeDstThreshold 값을 비교하여 보간할 부분을 찾습니다.
가장자리가 아니라고 판단되는 경우, 최소 각도와 최소 포인트를 업데이트하고 가장자리인 경우, 최대 각도와 최대 포인트를 업데이트합니다.
반복문이 완료되면 최소 포인트와 최대 포인트로 Edge 객체를 생성하고 반환합니다.
CharacterManager
캐릭터 매니저는 캐릭터 프리팹의 애니메이션과 행동 State를 제어하고 관리하는 클래스입니다.
CharacterManager와 CharacterViewRange는 옵저버 패턴을 이용하여 상호 데이터를 교환합니다.
이는 CharacterViewRange클래스에서 탐지한 적 객체를 CharacterManager 클래스에 보내어, 캐릭터의 공격을 제어하는 데 필요로 하기 때문입니다.
1. 변수 선언 및 초기화


변수의 선언과 초기화는 위와 같습니다.
ChracterClass 타입의 변수인 myCharacter는 게임매니저로부터 전달받은 캐릭터 데이터이며, 해당 변수가 가지고 있는 캐릭터 데이터를 그대로 변수화하여, 게임터의 데이터를 Set합니다.
target은 캐릭터 객체가 현재 공격해야 하는 적 객체를 저장하는 변수이며, CharacterObject는 현재 캐릭터매니저 클래스를 가지고 있는 오브젝트, 즉. 캐릭터 프리팹을 가리킵니다.
2. 애니메이션 제어

캐릭터매니저 클래스는 캐릭터의 직접적인 애니메이터 제어를 담당합니다.
애니메이터는 캐릭터의 현재 상태. 즉, ChracterClass에 선언된 열거형 변수인 eState에 의거하여 판단합니다.
애니메이터의 제어 변수는 캐릭터 프리팹의 이름입니다.
0번은 idle, 1번은 run, 2번은 attack, 3번은 skill, 4번은 death로 앞서 설명한 다섯가지 애니메이션 동작이 이러한 방식으로 구동됩니다.
public void setAniControl_Int()는 eChracter_State를 인자로 받아서, 캐릭터 매니저의 state를 변경, 컨트롤러 함수를 호출합니다.
즉, Setter함수와 유사한 기능을 하고 있습니다.
3. 전투 프로세스
배틀블루의 전투는 AI로 이루어집니다.
게임매니저에서 전 캐릭터의 이동을 제어, 이동 중 enemyDatas에 적이 포착된다면, 전투 페이즈로 전환.
이후 전투 페이즈는 캐릭터 매니저 클래스에서 담당하게 됩니다.


AI_Move는 Update함수에서 이뤄지므로, 코루틴 함수를 반복해서 호출하는 문제가 발생합니다.
그렇기에, 전투 프로세스를 제어하는 SkillLoop함수는 플래그 변수를 이용하여 중복 호출을 하지 않도록 안전장치를 걸어두었습니다.
기본적으로 Attack상태와 Skill상태가 반복되며, 스킬 동작시간 + 스킬 쿨타임 시간 만큼 코루틴으로 Wiat하여 그 시간 동안에 일반 공격이 이뤄집니다.

공격 애니메이션의 EndFrame에서는 해당 함수를 호출합니다.
적 개체에게 데미지를 입히기 위한 피격 데미지 계산 함수에 캐릭터의 power를 인자로 넘깁니다.
또한, EffectManager의 EffectCreate함수를 호출하여, 필요한 이펙트 파티클을 생성합니다.

target은 캐릭터가 공격하는 대상이 되는 enemy객체 입니다.
그렇기 때문에, target은 공격 프로세스 동안 널이 되어서는 안됩니다.
이 함수는 만약, target이 죽거나, 범위를 벗어나서 null이 된다면 Range내에 존재하는 다른 enemy객체를 타겟으로 지정합니다.
스킬

스킬 역시도 AttackFunc과 마찬가지입니다.
애니메이션의 마무리 프레임에 스킬 호출 이벤트를 이용하여 상단의 함수를 걸어둡니다.
gameObject의 이름을 이용하여 분기하고 이름에 맞는 스킬을 구동합니다.
㉠ 호두 스킬

enemyDatas를 순회하며 탐지한 적 객체가 가지고 있는 enemyManager를 변수화 한다.
그리고, 호두의 위치 자표와 해당 객체의 위치 좌표, 투사체의 스피드와 생성할 이펙트의 인덱스 번호, 데미지계산 식을 위한 power 보정값, 그리고 투사체 이펙트 파티클의 생성주기를 인자로 넘기고, 함수를 돌립니다.
ThrowProjectile() 함수에서는 이렇게 입력받은 파라미터를 이용하여, 빈게임 오브젝트를 생성하고 위치에 배치, 타겟의 방향과 거리를 계산하여 변수화하고 이것을 코루틴 함수에 인자로 넘깁니다.

타겟과의 거리를 감산, 그리고 타겟의 position은 방향 벡터값에 스피드와 시간 값을 곱하여 변환합니다.
이렇게 얻은 3차원 벡터값을 투사체의 새로운 위치 좌표로 바꾸어, 객체를 이동시킵니다.
만약 distance, 즉 타겟과의 거리가 0.1f 이하로 줄어들었다면 파티클을 생성하고 투사체를 파괴하며 반복문을 종료하고 그렇지 않을 경우 투사체의 충돌체크를 돌립니다.
투사체와의 충돌은 Raycast를 이용하여 구현했으며 0.2f 만큼 가까워졌을 경우에 충돌 판정으로 인식했습니다.
Enemy 레이어를 가진 객체를 조건으로 검사하고 충돌한 객체. 즉, 콜라이더에 함수에 들어온 객체들을 반복문으로 돌려서 데미지 계산 후에 투사체를 파괴합니다.

마지막으로는 이펙트 생성주기에 따라 이펙트를 생성해주어, 가시적으로 스킬을 사용하고 있음을 플레이어가 인식하도록 구현했습니다.
호두의 스킬은 범위 내의 전체 적에게 emptyGameObject를 투척하여, 충돌한 객체에게 데미지를 주는 방식입니다.
즉, 광역 타격기라고 짧게 요약할 수 있습니다.
㉡ 세리카 스킬

세리카의 스킬은 단일 타겟에 막대한 양의 피해를 주는 스킬입니다.
호두와는 다르게 투사체를 발사하지 않고, 즉발기로 객체에 데미지를 줍니다.
적 객체. 즉 Target객체에게 캐릭터 power의 401%만큼 값을 보정하여 데미지 계산식에 넘겨서 피해를 주고, offSet값을 조정하여 이펙트를 생성합니다.
㉢ 엠버 스킬

enemyDatas의 변수 범위를 확인하고 함수를 시작합니다.
target의 위치 값과 파워 보정값을 인자로 넘겨서 코루틴 함수를 시작하고, 애니메이터 구동을 잠시 종료합니다.
이는, 엠버의 스킬은 기존의 스킬 애니메이션과 다른 애니메이션을 사용하기 때문입니다.

필요한 변수들을 초기화하고 다음은 엠버의 애니메이션을 바꾸어 동작시킵니다.
엠버의 위치를 적 객체의 z(-1) 위치로 이동한 후에 적을 바라보고 타격합니다.
IsEnemyCheck는 현재 타깃으로 지정된 객체가 존재하는지 검사하고 타깃이 될 적 객체의 Transform값을 반환하는 함수입니다.
적 객체가 있을 경우에는 데미지 계산을 돌리고, 없을 경우에는 코루틴 함수를 즉시 종료합니다.
데미지 계산 후에도 마찬가지로 타깃이 존재하는지 검사하고, 없을 경우 코루틴 함수를 break합니다.

위 로직과 동일하지만, offSet위치 값이 z(1)로 바뀐 채로 엠버 객체를 이동시킵니다.
즉, 적을 기준으로 z축의 -1,1 포인트 좌표값을 이동하며 적을 타격하는 것입니다.

앞서 설명했다시피, 현재 타겟으로 설정된 enemyPos가 null인지 아닌지 판별하고 null일 경우에는 enemyDatas를 순회하며 0번째 객체의 위치값을 반환. Datas가 널일 경우에는 null을 반환합니다.
엠버의 스킬은 기존의 캐릭터 스킬과는 다른 스킬 애니메이션을 사용하기에 스킬 루프 코루틴 함수를 초기화해야 했습니다.
또한, 여러 번 적을 타격하기 때문에 그 사이에 적 객체가 파괴될 가능성도 있었기에, 중간중간 target 객체가 널인지 아닌지 검사하는 코드도 추가하였습니다.
㉣ 실비아 스킬

실비아의 스킬은 호두 스킬팩에서 구현한 ThrowProjectile()함수를 그대로 사용합니다.
다만, 호두의 스킬은 범위 내에 들어온 모든 enemy객체를 타격하는 반면 실비아의 경우 단일 객체(타겟)만을 공격한다는 점에서 찾이가 있습니다.
그렇기에 반복문을 사용하지 않고 타겟을 지정하여 함수를 호출하여 필요한 매개변수를 넘깁니다.
㉤ 루나 스킬

모든 적 객체를 순회하며 해당 객체의 EnemyManager 변수를 DecreaseEnemyDef() 함수의 인자로 넘기고, 파티클 이팩트를 생성합니다.

방어력 감소는 일정시간 동안 디버프를 적용하고, 원래 방어력으로 복원하는 로직을 구현했습니다.
변수로 적용 시간과 진행 시간, enemy의 기존 방어력 값을 저장하고 감소할 방어력을 계산한 후에 감산시킵니다.
방어력이 0보다 작거나 같을 경우에는 방어력을 0으로 고정하고 진행 시간 변수를 이용하여 일정시간 대기한 후에 원래 방어력으로 복원시킵니다.
㉥ 리시타 스킬

리시타는 기존의 캐릭터들과 달리, RisitaSkillCls라는 스킬 구현 전용 클래스를 따로 호출하여 스킬을 진행하는 방식으로 구현했습니다.

리시타의 스킬은 더미를 생성하여, 적의 공격을 대신 받게 해주는 것입니다.
enemy는 Dummy 레이어를 지닌 객체를 최우선적으로 타겟팅하고 공격합니다.
먼저 스킬을 구현하는데 필요한 프리팹과 변수를 선언하고 초기화합니다.
dummyHp는 더미의 체력이며, 0이하가 될 경우 Destroy되기 때문에, RisitaSkillCls 클래스를 보유하고 있는 리시타 객체는 기본적으로 dummyHp에 값을 보유하고 있습니다.

더미가 존재하지 않는다면 더미를 생성합니다.
이는 필드 위에 하나의 더미만을 생성하기 위해서 입니다.
적과 타겟 사이의 거리를 계산한 벡터를 정규화하여 방향 벡터만을 구합니다.
이 값에 객체의 위치를 더하고 4f만큼 곱하여 떨어진 위치를 변수화합니다.
이 변수는 프리팹을 생성할 위치가 됩니다.
이렇게 생성한 더미 객체에 체력을 설정하고 부모 객체(리시타)를 초기화합니다.
만약 hp가 0이하가 될 경우, 부모 객체의 bool 변수값을 바꾸기 위해서입니다.

CallDestroy는 더미hp가 0이하로 줄어들었을 경우 호출됩니다.
부모캐릭터(리시타)에게 자신이 파괴되었음을 알리고, 자기자신을 Destroy하여 제거합니다.
DamageFigures()는 데미지 계산용 함수입니다.
캐릭터 매니저 클래스의 데미지 계산용 함수와 동일하지만, 방어력이 존재하지 않기에 damage값이 그대로 피해량이 된다는 점에서 차이가 있습니다.
㉦ 루타 스킬

루타의 스킬 역시도 리시타의 스킬과 유사하게 스킬 클래스의 함수를 호출하여, 스킬을 구현합니다.
루타의 스킬은 오브젝트 프리팹을 생성하여, 아군은 오브젝트를 통과하여 적을 공격할 수 있지만, 적은 오브젝트를 넘어와야 공격할 수 있는 장애물을 설치합니다.

프리팹의 생성위치를 구하는 것은 리시타 스킬과 동일합니다.
타겟과 루타 오브젝트 사이의 방향 벡터를 구하고 자신의 위치으로부터 4f만큼 떨어진 좌표에 오브젝트 프리팹을 생성합니다.
오브젝트 프리팹은 더미와 다르게 공격으로 파괴되지 않기 때문에, 코루틴함수를 호출하여 일정 시간이 지나면 파괴되도록 해야 합니다.

위 함수가 바로 일정 시간 대기 후, 객체가 파괴되는 함수입니다.
35초동안 wait하고, 오브젝트 객체를 파괴합니다.
이후, 부모객체(루타)에게 자신이 파괴되었음을 알려, 새로운 오브젝트를 생성할 수 있도록 합니다.
㉧ 스나 스킬

스나 스킬 클래스의 함수를 호출합니다.
스나의 스킬은 블랙홀 객체를 생성하여, 범위 내의 적을 빨아들입니다.

매개변수로 입력받은 enemyDatas을 순회하며, 리스트 내의 적 객체의 위치를 참조하여 일정 거리(4)만큼 떨어진 곳에 블랙홀 프리팹을 생성합니다.
이후, enemyDatas와 블랙홀 프리팹의 transform을 파라미터로 받는 코루틴 함수를 호출합니다.

BlackHoleSkill()함수는 코루틴 함수로써, 일정시간 동안 적 객체를 끌어당기는 블랙홀 기능을 실질적으로 구현하는 함수입니다.
매개변수로 받은 enemyDatas를 순회하며, 적 객체들의 위치와 블랙홀의 위치를 이용하여 방향 벡터를 구하고 해당 방향 벡터를 노멀라이즈하여, moveSpeed만큼 translate시켜, 블랙홀 방향으로 적 객체를 이동하게 만듭니다.
이후, 일정 시간이 지난다면 블랙홀 객체를 파괴하고 코루틴 함수를 종료합니다.
㉨ 테일리 스킬

테일리의 스킬은 파티원의 체력을 회복시키는 기술입니다.
SQUAD_Characters의 범위만큼 함수를 순회하며, 해당 객체가 널이 아닐 경우에, squadCharac 배열에 접근하여, 캐릭터 매니저 클래스를 가져옵니다.
이는, SQUAD_Characters와 squadCharac의 순서와 범위가 동일하기 때문에 가능한 로직입니다.
GameManager에서 squadCharac의 배열을 채울 때, SQUAD_Characters의 데이터 값과 순서를 그대로 가져와서 저장하였기에 가능했습니다.
이렇게 가져온 캐릭터 매니저 클래스를 이용하여 전체 파티원의 hp를 회복시킵니다.
maxHp를 초과한 경우에는 hp를 maxHp로 고정하고, 그렇지 않을 경우에는 hp에 maxHp의 10%값만큼 체력을 회복시킵니다.
㉩ 제트 스킬

제트는 파티원의 공격력을 20%만큼 증가시켜주는 버퍼입니다.
테일리와 마찬가지로 SQUAD_Characters를 순회하며 캐릭터 매니저 클래스 컴포넌트를 가져와 변수에 저장합니다.
이렇게 저장한 변수를 IncreaseAttackPower() 코루틴 함수의 매개변수로 넘기고 해당 객체의 위치에 파티클 이펙트를 생성합니다.

적용 시간과 공격력 증가량을 변수 선언하고, 파티원의 기존 공격력 값을 저장합니다.
그리고 setPower를 이용하여, 파티원의 공격력을 증가시킵니다.
elapsedTime은 버프가 들어간 시간을 셈하는 변수입니다.
위 변수가 duration을 초과했다면, 10초가 지난 것이므로 다시 원래 공격력으로 값을 돌려 놓고, 코루틴 함수를 빠져나옵니다.
5. 데미지 계산

데미지 계산은 캐릭터의 방어력을 %로 계산하여 파라미터로 받은 데미지 값을 감산하고 hp를 깎는 방식으로 구현했습니다.
예컨대, 방어력이 1이라면 1%만큼 데미지를 깎고, 방어력이 20이라면 20%, 방어력이 80이라면 80%만큼 데미지를 감산합니다.
옵저버 패턴을 이용하면 별도의 함수 추가 없이 다른 객체의 상태 변화를 곧바로 알 수 있다는 장점이 있습니다.
이렇게 자동으로 데이터를 갱신하고 일대다 관계성을 지니는 디자인 패턴이 바로 옵저버 패턴입니다.
또한 주체에 필요한 만큼의 객체를 관찰자로 추가할 수 있으며 런타임에 동적으로 제거할 수도 있는 장점이 있습니다.
게임 프로그래밍에서 자주 쓰이는 기법 중에 하나이기도 한 옵저버 패턴으로 캐릭터의 Enemy 탐지 결과 및 데이터를 CharacterManager와 GameManager에 송신하도록 하였습니다.

Observer의 역할은 '관찰자'입니다.
즉, Subject가 송신하는 데이터를 받습니다.
위 코드는 옵저버 인터페이스를 구현하여, 인터페이스 맴버를 구현하고 Subject가 제공한 데이터를 필요에 따라 가공하여 정보처리합니다.
Norify와 FindEnemyData 함수는 각각 enemyList와 CharacterPrefab객체를 파라미터로 받습니다.
게임매니저_사용

Norify()함수는 GameManager가 아닌 CharacterManager에서 사용하는 함수이기에 스킵합니다.
FindEnemyData는 게임매니저의 특성상 전 캐릭터를 조율하고 관리해야하기 때문에, 어떤 캐릭터의 List에 데이터가 들어온 것인지 판별해야 합니다.
그렇기 때문에 캐릭터 객체의 프리팹과 Enemy객체의 Transform을 저장한 리스트가 동시에 필요하기에 Dictionary형태로 데이터를 저장하고 관리합니다.
charac을 key로 사용하여 data를 저장하고, 만약 킷값이 변수내에 존재한다면, data를 최신화하고 킷값이 없다면 킷값과 data를 함께 추가합니다.

Detach()는 Subject에게 데이터를 보내지 않아도 됨을 알리는 함수입니다.
사실 GameManager가 파괴된다면 인게임씬 자체가 종료되는 것이기에 생략해도 상관 없지만, 일단 코드상으로 구현했습니다.

Attach()는 해당 객체를 Subject의 ArrayList에 자기자신을 Add시켜, 구독했음을 알립니다.
위 함수는 인게임 상에 캐릭터 데이터를 읽어서 필요한 데이터와 프리팹 등을 세팅하는 로직입니다.
추후, 게임매니저 씬에서 자세히 적어보도록 하겠습니다.
캐릭터매니저

GameManager와 다르게 캐릭터 매니저는 캐릭터 자체에 붙어있는 클래스이기 때문에, FindEnemyData를 사용할 필요가 없습니다.
그렇기에 RangeView가 찾은 EnemyData만을 저장하는 List 변수인 enemyDatas에 data를 저장하여 관리합니다.
그리고 Detatch()의 경우, GameManager와 마찬가지로 객체가 Destroy되는 순간에 구독을 취소합니다.

Attach()의 경우, 클래스를 Awake할 때, Subject에 자기 자신이 구독함을 알려줍니다.

Subject는 자신을 구독한 관찰자에게 데이터를 송신하는 역할을 합니다.
'_observer'는 관찰자들을 저장한 ArrayList로, 해당 변수를 순회하며 데이터가 변할 때마다, 데이터를 송신합니다.
NorifyObservers_enemyFind()와 Notify_List()는 각각 게임매니저와 캐릭터매니저가 사용하는 Observer 인터페이스의 함수에 데이터를 보내줍니다.
CharacterViewRange

ViewRange클래스는 캐릭터 프리팹에 부착된 클래스로써 일정 각도와 범위 내에 Enemy 레이어를 가진 객체를 탐지하여 visibleTargets 리스트에 저장하는 클래스입니다.
이렇게 저장된 리스트 데이터를 상속받은 Subject의 맴버함수인 NorifyObservers_enemyFind()와 Norify_List()에 파라미터로 넘겨 Attach()된 구독자들에게 알립니다.
게임매니저는 게임 프로세스를 관리하는 클래스입니다.
먼저 스테이지 씬에 존재하는 오브젝트들을 사용할 수 있도록 태그에 맞춰 분류하고 저장합니다.
그리고 스쿼드 씬에서 선택한 파티의 데이터를 이용하여 객체를 생성하고, 해당 객체들을 통합하여 관리하며 AI 이동 및 전투 페이즈 전환 등을 관리합니다.
또한, 전투의 승리와 패배, 카메라의 이동을 관리합니다.
뿐만아니라 EnemyCreateSetManager를 호출하여 적 객체를 생성시키는 등. 필요에 따라 적절하게 클래스 파일을 호출합니다.
1. 초기화 및 선언

cameraPos, pointPos, spawnPos 리스트는 각각 스테이지에서 CameraPos와 Point, SpawnPoint 태그를 가지고 있는 객체들을 저장하는 리스트입니다.
스테이지 오브젝트를 저장하는 GameObject 변수인 _Stage.
초기 스쿼드 맴버 생성 위치 좌표를 저장할 squadField.
모든 캐릭터 프리팹을 저장하는 allCharacterArr.
생성한 캐릭터 오브젝트를 저장하는 squadCharac과 각 캐릭터들을 제어하는 데 필요한 targetEnemys, navMeshAgents, nDestinationIndex. 등이 있습니다.
마지막으로 enemyDatas는 ViewRange 클래스에서 송싱받은 enemy객체와 해당 ViewRange 클래스를 보유한 Character(캐릭터) 객체를 키쌍으로 저장하는 Dictionary 타입의 변수입니다.

squadCharac, targetEnemys, navMeshAgents, nDestinationIndex는 Squad의 캐릭터들을 제어하기 위해 필요한 배열이기에, 배열의 크기는 4로 정합니다
Start 함수에서 호출한 SceneFinder()함수는 인게임씬을 제외한 다른 씬을 찾아내 변수에 저장하는 함수이고,
ObjectSet 함수는 이렇게 찾아낸 스테이지에서 오브젝트들을 필요에 따라 추출하고 저장하는 역할을 합니다.

현재 Active된 씬을 변수에 저장합니다.
그리고 로드되어 있는 모든 씬들을 가져와 배열에 저장하고, 해당 배열을 순회하며 currentScene과 대조하여 일치하지 않은 첫 번째 씬을 stageScene에 저장합니다.

추출한 stageScene에서 가장 상위 오브젝트들을 추출하고, 그중에서 Stage태그를 가지고 있는 게임 오브젝트를 '_Stage'에 저장합니다.
이후 '_Stage'에서 Transform을 가진 자식 객체를 모두 호출하고 Select() 메서드를 이용해서 Transform에 대응하는 GameObject들을 선택합니다.
최종적으로 stageChilds에 ToArray() 메서드를 호출하여서 GameObject 배열로 변환하고 저장합니다.
이렇게 스테이지에 존재하는 모든 오브젝트들을 저장하는 배열이 완성됩니다.
완성된 배열에서 필요한 태그를 가진 객체들만을 뽑아서 List, 혹은 배열에 저장하여 필요할 때마다 해당 변수를 호출하여 사용할 수 있도록 합니다.

메인 카메라 위치를 Set합니다.
그리고 squadField가 자식으로 가지고 있는 객체들을 분리하여 파티원들이 생성될 자리를 Transform 변수에 저장합니다.
allCharacterArr는 모든 캐릭터 프리팹을 저장한 배열로써, 플레이어가 선택한 파티원 List인 SquadSceneManager를 순회하는 이중 반복문을 구현하여 양쪽의 editorName이 일치하는 프리팹을 생성하면서, (Clone)이라는 이름을 Replace하여 프리팹의 원래 이름으로 객체를 생성합니다.
그리고 필요한 Component들을 get하여 배열 변수에 저장합니다.
CharacterManager의 myCharacter 변수는 캐릭터의 데이터를 초기화하는 데 가장 중요한 변수입니다.
SquadSceneManager의 캐릭터 클래스 데이터를 그대로 붙여넣어서, 캐릭터의 데이터를 만들어줍니다.
그리고 Subject객체인 CharacterViewRange를 Get하여 GameManager를 구독시켜서, Observer로 인식하도록 합니다.
마지막으로 enemyDatasDictionary에 킷값으로 객체를 추가하고 데이터를 초기화하여 세팅을 마칩니다.

"EnemyCreateSetManager"라는 이름을 가진 오브젝트를 Find하여 객체가 가지고 있는 EnemyCreateSetManager 클래스를 변수에 저장합니다.
그리고 해당 클래스에서 CreateMonsterFunc 함수를 호출하고 필요한 파라미터를 인자로 넘겨서 몬스터 객체를 생성시킵니다.
앞서 분리한 스쿼드 포지션에 squadCharac 객체들을 조건문에 따라 분기하여 position 세팅을 마칩니다.
2. AI
배틀 블루의 AI는 기본적으로 NavMesh 시스템을 이용하여 구축되었습니다.
탐지 범위 내에 적이 존재하지 않을 경우에는 Move 모드가 되어 이동하고 적을 조우할 경우에는 Attack 모드가 되어 움직임을 멈추고 적 개체를 파괴할 때까지 전투를 이어갑니다.
아래 코드는 AI 파트를 설명한 문서입니다.

전투 페이즈에서는 아군이 targetting을 완료한 경우에, 본인의 탐지 범위에 적이 존재하지 않을 경우에도 파티원의 적을 target으로 삼아서 전투를 지속합니다.
그렇기 때문에 만약 개체가 파괴되어 널이 되었음에도 불구하고 target 배열이 null로 초기화되지 않는다면, 캐릭터들은 연속해서 전투 페이즈에 머무르는 에러가 발생합니다.
그렇기에 만약 객체가 파괴되어서 널이 되었을 경우에는 해당하는 target도 null로 초기화해야 합니다.
이는 update에서 반영되며, 이후에 AI_Move함수를 호출하여 이동을 지속합니다.

먼저 이동 함수에서 필요한 지역변수들을 호출합니다.
이후, navMesh함수를 순회하며 널이 아닌 경우에만 이하 로직에 진입하여 구동합니다.
먼저 캐릭터가 보유하고 있는 캐릭터 매니저 클래스를 변수화합니다.
첫 번째 조건문은 자신의 enemyDatas의 값이 널이 아니고 Count가 0을 초과할 경우에 isBattle 변수를 참으로 돌리고, 이동을 멈춘 후에 곧바로 AI_Attack()함수를 호출하여 전투 페이즈로 전환하는 것입니다.
두 번째 조건문은 자신의 enemyDatas가 널이거나 Count가 0일 때, 아군의 targetEnemys 배열을 순회하면서 적이 존재할 경우에, 배틀변수를 참으로 바꾸고 해당 타겟을 자신의 타겟으로 설정하고 target으로 삼는 분기입니다.

다음 세 번째 조건문은 지정한 타깃의 위치를 SetDestination하여 이동하고, 사정거리(탐지범위)에 들어왔을 경우에, 전투 함수를 호출하는 로직입니다.
이는, 앞 조건문에서 아군의 타겟을 자신의 타겟으로 설정하더라도 사정권에 벗어났을 경우에는 전투를 할 수 없기에 구현한 코드입니다.

이러한 조건문들을 모두 순회했음에도 불구하고 적이 존재하지 않는다면 캐릭터는 이동을 해야합니다.
다만, 전투 로직을 돌다보면 뜻하지 않게 SetDestination이 변경되어, 인덱스 변수가 Point배열의 최댓값을 초과하는 문제가 발생합니다.
이는, 타깃을 추적하는 과정에서 Destination에 도착했음을 판별하는 로직에 들어오면 무조건적으로 인덱스 배열의 값을 가산하기 때문입니다.
그렇기에, 최종 목적지에 도착했음을 검사하는 조건문 내에 인덱스가 Point배열의 최댓값을 넘었음에도 불구하고 최종 Detination에 도착하지 못했다면, 각 포인트들을 순회하며 가장 가까운 목적지에 해당하는 인덱스로 변수값을 보정합니다.
만약 최종목적지에 도착한 게 아니라면 destination을 새롭게 set하고 인덱스 값을 증가시킵니다.

AI 공격 함수는 Move에 비하여 비교적 간단합니다.
이는, 실질적인 전투 파트는 게임매니저가 아닌 캐릭터 매니저 클래스에서 이뤄지기 때문입니다.
즉, 게임매니저에서 AI_Attack 함수의 역할은 캐릭터 매니저의 코루틴 함수가 끝난다면 다시 함수를 재호출하고, State를 Attack 상태로 유지시켜주는 것입니다.
3. 카메라 무빙
카메라의 동선관리 및 이동 역시 게임 매니저에서 관리하는 기능 중 하나입니다.
아래는 메인 카메라를 이동시키는 함수입니다.

카메라를 제어하는 함수는 보통 LateUpdate에서 이뤄집니다.
이는 유니티 엔진의 생명주기는 Update->LateUpdate 순으로 이뤄지며,
에셋과 오브젝트의 이동 및 구현 등을 Update에서 끝마치고, LateUpdate를 이용해 플레이어에게 위화감 없는 화면을 보여주기 위함입니다.

카메라 무빙 함수는 카메라의 이동을 제어하는 함수입니다.
Lerp를 이용하여 헌재 pos에서 다음 pos 위치까지 부드럽게 이동하도록 구현하였고,
캐릭터를 화면에서 벗어나지 않도록 하기 위하여, z축을 캐릭터의 position.z 값과 일치시켜 보정하였습니다.
그리고 이렇게 구한 좌표값과 rotation값을 보정하여 최종적으로 mainCamera의 위치좌표를 세팅했습니다.
그리고 다음 pos 위치와 충분히 가까워진다면, 인덱스 값을 변환하여 다음 위치와 rotation을 받을 수 있도록 하였습니다.
4. 게임 엔딩
게임의 엔드 판정 또한 게임 매니저에서 이뤄집니다.
다만, 게임의 엔딩은 승리와 패배로 나뉘어 구분 됩니다.
모든 플레이어의 캐릭터가 null이라면, 즉. 모든 캐릭터 객체가 파괴되었다면 패배.
반대로, 엔드 포인트에 도달한다면 승리합니다.

Update에서 매 프레임마다 호출되어 엔딩 조건을 만족했는지 확인하는 함수입니다.
스쿼드 캐릭터를 순회하며, 모든 캐릭터가 널 상태인지 체크합니다.
만약 널이라면, 변수를 제어하고 클리어 상태를 false, 게임 엔딩을 true로 합니다.

게임 엔드 변수가 true가 되었을 경우, 호출되는 함수입니다.
isClear가 false라면 게임 클리어를 실패했기 때문에, 텍스트 출력 후에 바로 씬은 Down됩니다.
만약 클리어가 true라면, 스테이지를 판별하고 보상을 줘야하기 때문에, Stage 오브젝트에서 문자를 추출하여, 클리어한 레벨을 판별하고 변수에 저장합니다.
또한, UI_Manager의 전역변수인 currentStageLevel에 현재 레벨값에 1을 더하여, 다음 스테이지 레벨을 해금시켜줍니다.
마지막으로 퀘스트 진행 단계를 관리하는 QuestReward_Manager 클래스를 탐색하고 호출하여, 현재 레벨을 인자로 넘겨서, 관련된 퀘스트를 처리하도록 합니다.

보상 함수에서는 현재 스테이지 레벨을 파라미터로 입력받고, 스테이지에 따라 골드량을 차등 지급하고, 스쿼드 캐릭터를 순회하며 미션에 참가한 캐릭터들을 함수의 파라미터로 넘겨서, exp를 증가하고 만약 레벨업을 한다면 캐릭터의 스테이터스를 조정하도록 합니다.

캐릭터와 스테이지 레벨을 인자로 받은 캐릭터 보상 함수입니다.
스테이지 레벨x100 만큼의 경험치를 증가시키고, 캐릭터의 maxExp가 배열의 범위를 벗어나지 않도록 처리합니다.
만약 레벨을 초과한다면, 배열의 마지막 값으로 고정시켜줍니다.
반복문을 이용하여 레벨업 이후에도 경험치가 남아서 추가 레벨업을 했을 경우에도 스테이터스 처리를 할 수 있도록 해줍니다.
마지막으로 maxExpIndex가 배열의 범위를 벗어나지 않도록 조정하고 목표 경험치로 설정합니다.

패배와 승리, 모두 모든 로직이 종료된 후에 Invoke함수를 이용하여 일정시간 UI를 출력 후에 StartMissionScene을 로드하여, 현재 인게임씬과 스테이지 씬을 종료시킵니다.
적 객체 역시, 플레이어 캐릭터와 동일하게 ViewRange를 이용하여 적을 인식하고 NavMesh 시스템으로 AI가 제어됩니다.
캐릭터가 CharacterManager로 제어된다면, Enemy는 EnemyManager로 제어된다는 점에서 차이가 있습니다.
그렇기에 EnemyManager 또한, CharacterManager와 마찬가지로 ViewRange에 옵저버로써 구독을 하고 enemyData를 송신 받습니다.

enemy는 총 6개의 애니메이션이 존재하며, 8개의 상태로 FSM 로직을 완성했습니다.

Enemy에는 콜라이더 박스가 존재하며, 이 박스는 히트박스로써의 역할을 합니다.
Enemy의 공격 동작의 중간 프레임에 도달했을 경우, 콜라이더 박스의 SetActive를 true로 하고, 콜라이더 박스에 Character나 Dummy 레이어를 가진 객체가 존재할 경우에는 데미지 판정을 하고, 콜라이더 박스를 다시 False상태로 변환시킵니다.

Start에서는 Attach로, ViewRangeSubject에 자기 자신을 알립니다.
Update에서는 자기 자신의 체력이 0이하일 경우에는 Death단계로 고정하고, 사망처리를 합니다.
만약 그렇지 않을 경우에는 AI_EnemyProcess()함수를 호출하여, AI프로세스를 구동합니다.
target은 적이 공격 대상으로 설정한 객체이며, 만약 널일 경우에는 isTargetBettle 변수를 false로 설정합니다.
target이 필요한 이유는 몬스터가 피격당했을 경우, 자신을 공격한 객체를 추적하고 공격하기 위함입니다.

애니메이터의 파라미터를 현재 상태값을 이용하여 제어하는 함수입니다.
enemy의 상태값이 변경될 때마다, 위 함수를 호출하여 애니매이션 처리까지 마칩니다.

AI 프로세스 입니다.
AI의 우선 순위는 다음과 같습니다.
Hit 단계 > 타겟 전투 > 일반 전투 > 이동 상태.
Hit 단계는 Enemy객체가 피격을 당했을 경우, 일정 확률로 구현됩니다.
타겟 전투는 Enemy 객체를 공격한 객체를 타겟으로 삼아서 전투 페이즈로 돌입하는 것입니다.
그리고 일반 전투는 Enemy의 사정거리, 즉. ViewRange가 인식한 EnemyDatas가 널이 아닐 경우, 시작되는 로직입니다.
마지막이 이동 상태인데, 이동은 Coroutine 함수를 이용하여 구현합니다.

Enemy 피격 시에 이뤄지는 작업은 총 세 가지로 구분됩니다.
첫 째, 타겟 설정 및 타겟 배틀 활성화.
둘 째, 데미지 계산.
셋 째, 일정 확률로 피격 상태로 변경.

타겟 전투 함수입니다.
반복문을 돌리며 주기적으로 상태를 확인하여, Hit상태인지 아닌지 확인합니다.
만약 Hit 상태가 되었다면, 코루틴 함수를 빠져나오도록 합니다.
스나의 스킬은 적 객체를 끌어당기는 블랙홀이기 때문에 주기적으로 enemy객체는 캐릭터 객체와의 거리를 파악하고, 만약 공격 사정거리를 벗어났을 경우 캐릭터를 다시 추격하고 공격하도록 해야 합니다.
이것은 Distance 메서드로 구한 flaot값으로 현재 캐릭터와의 거리가 최대 사정거리를 벗어났을 경우, 적을 쫓고 거리를 좁혔을 경우 다시 공격 상태로 돌립니다.
이 코루틴 함수는 타겟이 죽거나 Hit상태가 되거나, Enemy객체의 체력이 0이하가 될 때까지 반복합니다.

일반 전투 함수입니다.
먼저 타겟이 존재하는지 검사합니다.
타겟이 null이라면 enemyDatas의 0번째 원소를 타겟으로 지정하고 적을 추격합니다.
적이 사정거리 내에 들어왔다면, 공격상태로 전환하고 공격 상태로 바꿉니다.
일반 전투 함수는 타겟 전투와 달리 코루틴 함수가 아니기 때문에, Hit상태와 체력을 조건문으로 검사할 필요가 없습니다.
AI 프로세스에서 선제적으로 히트상태와 체력 조건을 확인하고 AI_Attack함수의 루프로 들어가기 때문입니다.

리시타의 스킬은 더미를 생성합니다.
즉, ViewRange 내에 더미가 존재하는지 파악하고, 더미가 있을 경우 최우선적으로 더미를 공격 대상으로 적용해야 합니다.
위 함수는 DummyFinder 함수로써, 더미가 존재할 경우에 target를 dummy로 바꾸는 기능을 가지고 있습니다.
더미 파인더 함수는 각 전투 단계에서 선제적으로 호출되기 때문에, 코루틴 함수인 AI_beHittedAttack 함수에서도 적용이 됩니다.
이는 AI_beHittedAttack함수 또한 타겟을 공격 대상으로 설정하기 때문입니다.

AI프로세스에서 히트와 전투 조건을 만족하지 못 했을 경우, AI 순찰 및 이동 파트로 넘어갑니다.
이동은 코루틴 함수를 통하여 제어되며, 매 루프마다 Attack, Hit, Death 상태를 확인하여 코루틴 함수를 탈출할 수 있도록 합니다.
이러한 조건을 모두 빠져나가면, idle과 Petrol을 랜덤한 확률로 시행시킵니다.

순찰 중에는 주기적으로 도착지에 도착했는지 확인해야합니다.
또한, 최초 Spawn위치를 기억해두어 일정 거리 이상 멀어지면 제자리로 돌아오도록 Destination을 수정합니다.

위 함수가 패트롤과 아이들을 구현하는 함수입니다.
순찰 함수는 랜덤 DestinationPoint를 함수로 받아서 설정하고 stop을 해제하며 move상태로 전환하고 애니메이터의 파라미터도 변환합니다.
반면 이동함수는 isStopped를 true로 전환하고 velocity를 zero로 바꾸어, 모든 운동값을 제거합니다. 또한 순찰과 마찬가지로 idle상태로 전환하고 애니메이터의 파라미터를 변환합니다.

패트롤 함수에서 랜덤한 DestinationPoint를 얻을 수 있도록 Vector3 값을 반환하는 함수입니다.
객체 주변의 랜덤한 범위를 설정하고, 해당 포인트 지점을 현재 위치에 더한 값을 SamplePosition 메서드의 인자로 이용하여 NavMesh의 범위를 벗어나지 않는 랜덤한 목적지 지점을 획득하여 반환합니다.

위의 아이들 무빙 프로세스에서 순찰 상태에서 주기적으로 확인하는 도착지 도착 여부를 판단하는 함수입니다.
1.3f는 Enemy객체의 유효 사정거리이며, 만약 조건을 만족할 경우 true를 반환하여 다음 동작을 랜덤하게 결정할 수 있도록 합니다.

최초 스폰 지점으로부터 일정 범위 이상을 벗어났을 경우, 다시 제자리로 돌아오는 코드 또한 필요합니다.
너무 먼 지역으로 enemy객체가 이동했을 경우, 플레이어 캐릭터가 전투를 모두 회피하거나 불합리할 정도로 많은 적을 조우할 수 있기 때문입니다.
그렇기에, HasEceededDistanceThreshold()함수는 최초 스폰지점은 StartPosition과 현재 위치를 비교하여 일정 거리 이상 멀어졌을 경우 트루를, 그렇지 않을 경우 false를 반환하여 ReturnToStart()의 구동 여부를 결정합니다.
ReturnToStart함수는 SetDestination의 위치를 스폰 지점으로 변경하는 기능을 가진 함수입니다.

애니메이션의 일정 프레임에 Event를 걸어, 함수를 호출할 수 있습니다.
위 함수들은 각각 Hit와 Death, Attack 단계해서 호출되는 함수들입니다.
Hit조건을 해제하지 않으면 무한히 Hit 판정을 유지하는 에러가 발생하기 때문에, 히트 조건을 해제합니다.
그리고 객체 파괴 역시 Death 애니메이션에서 일정 프레임에 도달할 경우, 객체를 파괴하고 이에 관련되어 필요한 퀘스트 처리를 해야하므로 함수를 호출합니다.
Enemy는 ColliderBox를 이용하여 충돌 판정 처리후 데미지 계산을 합니다.
그렇기에 콜라이더 박스를 활성화 하는 함수를 호출합니다.
히트박스가 되어줄 콜라이더 박스는 Enemy객체의 자식 컴포넌트로써 존재합니다.
AttackFinished함수가 setActive한 콜라이더 박스에는 해당 클래스가 부착되어, 충돌 판정을 합니다.

데미지 계산의 우선권은 Dummy 레이어를 가진 객체가 가지고 있습니다.
충돌한 객체의 레이어를 확인하고 조건을 만족할 경우, 콜라이더 박스의 부모 객체인 Enemy의 EnemyManager 클래스에서 Power를 가져와 충돌체인 other의 DamageFigures 함수에 power를 인자로 넘기며 데미지 계산을 합니다.
이후, Invoke함수를 이용하여, ColliderBoxOff 함수를 호출하여, 박스를 SetActive, False합니다.
이러한 로직 구조는 충돌체가 Character인 경우에도 동일합니다.
다만, GetComponent하여 가져오는 클래스가 다를 뿐입니다.

Invoke하여 가져오는 함수입니다.
콜라이더 박스를 SetActive(flase)로 전환하는 간단한 함수입니다.
이펙트 매니저는 이펙트 파티클의 생성과 유지, 파괴 등을 관리하는 클래스입니다.
스킬과 공격 등에서 이펙트 효과를 생성하기 위해서 호출되는 클래스입니다.

이펙트 파티클을 GameObject 배열의 형태로 저장합니다.
그리고 매니저 역할을 하는 클래스이기 때문에, 싱글턴 패턴으로 자기 자신을 전역변수화합니다.

이펙트 생성 함수는 위와 같습니다.
오버로딩을 이용하여, vector3와 flaot 인수의 케이스를 분리하였으며, 이렇게 입력받은 파라미터를 이용하여, 인덱스 번째의 이펙트 파티클을 생성하고, 위치와 크기를 조정하며, 이펙트의 duration을 설정하여 해당 시간 후에 객체를 파괴합니다.
즉, 이펙트에 관련된 모든 생명 주기를 관리합니다.
게임매니저에서 게임에 필요한 변수와 오브젝트들을 초기화한 이후에 EnemyCreateSetManager 클래스에 접근합니다.
이는, 적 객체의 생산과 관련된 파트는 해당 클래스가 담당하고 있기 때문입니다.

GameManager로부터 스테이지와 spwanPoint를 파라미터로 입력받습니다.
각각의 데이터를 변수에 초기화한 후에, StageNameForDistribute()함수를 호출하여, 스테이지 이름에 따라 알맞는 생성 함수를 구동합니다.

SetMonsterAtSpawnPoint_Stage1() 함수의 구조는 기본적으로 위와 같습니다.
스폰포인트 위치를 저장한 리스트를 순회하며 각 포인트에 프리팹을 생성하고 스테이지 씬의 스테이지, 자식 객체로 생성합니다.
그리고 생성한 Enemy객체의 이름에서 (Clone)을 제거하여 적절한 파라미터 이름으로 애니메이터를 제어할 수 있도록 합니다.
마지막으로 Enemy객체의 EnemyManager를 GetComponent하여 데이터를 세팅합니다.
private void SetMonsterAtSpawnPoint_Stage2()
{
// 스폰 포인트를 순회하며 enemy 프리팹 생성
for (int i = 0; i < enemySpawnPos.Count; i++)
{
// Enemy 프리팹 생성, 스테이지의 자식 컴포넌트화
GameObject monster = Instantiate(monsterPrefabs[0], enemySpawnPos[i].position, Quaternion.identity, GameManager.Instance._Stage.transform);
string prefabName = monster.name.Replace("(Clone)", "");
monster.name = prefabName;
// EnemyManager를 GetComponent하여 스테이터스를 초기화
EnemyManager monster_mng = monster.GetComponent<EnemyManager>();
monster_mng.SetEnemyHp(2000);
monster_mng.SetEnemyDef(20);
monster_mng.SetEnemyPower(20);
}
}
private void SetMonsterAtSpawnPoint_Stage3()
{
// 스폰 포인트를 순회하며 enemy 프리팹 생성
for (int i = 0; i < enemySpawnPos.Count; i++)
{
// Enemy 프리팹 생성, 스테이지의 자식 컴포넌트화
GameObject monster = Instantiate(monsterPrefabs[0], enemySpawnPos[i].position, Quaternion.identity, GameManager.Instance._Stage.transform);
string prefabName = monster.name.Replace("(Clone)", "");
monster.name = prefabName;
// EnemyManager를 GetComponent하여 스테이터스를 초기화
EnemyManager monster_mng = monster.GetComponent<EnemyManager>();
monster_mng.SetEnemyHp(3000);
monster_mng.SetEnemyDef(30);
monster_mng.SetEnemyPower(30);
}
}
스테이지 2와 스테이지 3 역시도 이와 유사한 방식으로 구현됩니다.
다만, Enemy객체의 스테이터스를 조정하여 강하게 만든다는 차이가 있습니다.

퀘스트 씬은 인게임의 시스템을 이용하여, 일정 수준의 목표를 달성하고 보상을 얻을 수 있도록 가이드 라인을 잡아주는 역할을 합니다.
또한, 플레이어에게 적절한 양의 재화를 주어서, 성장의 재미를 느끼게 할 수도 있습니다.
퀘스트 씬에는 총 두 개의 캔버스와 하나의 매니저 클래스로 이루어져 있습니다.
퀘스트는 일일임무, 주간임무, 일반임무로 구분되며, 각각 퀘스트를 버튼을 눌러서 불러올 수 있습니다.
조건을 만족하여 보상을 얻을 수 있는 경우, 일괄처리하여 한번에 보상을 받을 수도 있습니다.

퀘스트 클래스는, 캐릭터 클래스와 마찬가지로 퀘스트를 관리하는 클래스 타입의 자료형입니다.
enum 타입의 열거형 변수로 퀘스트의 타입을 구분짓고,
현재 달성 수치와 목표 달성 수치를 비교하여, 퀘스트의 클리어 유무를 결정할 수 있도록 합니다.

스크롤뷰에 표시할 퀘스트 데이터는 기본적으로 일일임무가 defualt가 됩니다.
SubCanvas는 획득한 퀘스트의 보상을 보여줄 UI입니다. 기본적으로는 SetActive를 false로 두어, 보이지 않게 합니다.
start에서는 json데이터로 관리되는 플레이어의 퀘스트 데이터 파일을 불러와 세팅합니다.
isSet은 전역변수로 관리되며 씬 로드시 최초 1회만 함수를 호출할 수 있도록 유지합니다.
이후, 임무의 날짜 주기를 확인 후에 리셋하고 스크롤뷰의 콘텐츠를 세팅하는 함수를 차례대로 호출합니다.

데이터 세팅 함수는 모든 퀘스트 데이터를 가지고 있는 오리지널 데이터인 AllQuest.json파일과 플레이어의 퀘스트 데이터를 관리하는 myQuestList를 비교하여 추가되는 값이 있을 경우, 리스트에 데이터를 반영하여 퀘스트 데이터를 최신 데이터로 유지하도록 하는 함수입니다.
myQuestList를 순회하며, allQuests 원소의 Number(id)값을 비교하여, 일치하지 않는다면 해당 원소를 Add하여 데이터를 최신화합니다.

마지막으로 추가된 퀘스트 데이터의 경우,
일일임무나 주간임무임에도 불구하고 Date 변수가 DateTime.MinValue로 세팅되어 있기 때문에,
분기를 나누어 각각 다음날 새벽 4시나 다음 주 월요일 새벽 4시로 리셋 시간을 변경해줍니다.

일일임무의 시간 데이터를 리셋하기 위해서는 리셋 시간과 현재 시간을 대조하여, 현재 시간이 리셋 시간을 초과했을 경우 해당 퀘스트를 리셋하여 다시 퀘스트를 클리어할 수 있도록 변경합니다.
또한, 퀘스트 리셋 시간을 다시 다음날 새벽 4시로 변경해줍니다.

주간임무 역시 크게 다르지 않습니다.
현재 시간과 리셋 시간을 대조하고 시간이 초과되었다면, 퀘스트 데이터를 초기화하고 시간을 다음주 월요일 새벽 4시로 변경해줍니다.

ContentsDistribute함수는, 솔직히 말하면 그다지 의미가 없는 함수입니다.
일일 임무, 주간 임무, 일반 임무를 nScrollViewContentsOrder에 따라 구분하여 함수를 호출하고 파라미터를 넘깁니다.
다만, 모든 분기마다 nScrollviewContentsOrder를 파라미터로 넘기는 것은 변하지 않습니다.

ScrollContentsSet()함수는 스크롤뷰 UI를 채우는 함수입니다.
Chracter_UI와 Adventure 스크롤뷰, 스쿼드 스크롤뷰 코드와 구조가 유사합니다.
Vertical 타입의 스크롤 뷰에 오브젝트 세트를 생성하여 채우고 Dictionry에 Add하여 통합관리합니다.


UI오브젝트 세트의 UI처리를 하는 함수입니다.
조건문에 따라 적절히 타이틀 텍스트와 설명문을 출력하고 현재 퀘스트 진행상황에 따라 프로그레스 이미지를 FillAmount합니다.
그리고 퀘스트 클리어 유무와 보상획득 가능 여부에 따라 알파값을 조절합니다.

버튼을 클릭할 시에 기존의 스크롤뷰 데이터를 리셋하고 새로운 obj데이터를 출력하기 위하여, 리셋 함수를 구현했습니다.
퀘스트 클리어 버튼 처리함수는 아래와 같습니다.

각 처리 버튼에 연동되는 함수는 QuestClearBtn함수입니다.
클릭한 객체를 EventSystem을 이용하여 추출합니다.
그후, UI_Manager.myQuestList와 일치하는 퀘스트 데이터를 Find합니다.
이렇게 찾은 퀘스트 데이터를 수정합니다.
Clear를 true로 바꾸고 QuestIncome를 함수를 호출하고 보상 목록 프린트 함수를 호출합니다.
마지막으로 버튼의 알파값을 수정하여, 퀘스트가 클리어 되었음을 알립니다.

AllQuestRewairdClear함수는 현재 보상 획득이 가능한 모든 퀘스트들을 순회하며 완료 처리를 하고 한 번에 보상을 얻도록 만드는 함수입니다.

QuestIncome함수는 퀘스트의 번호.
즉, ID값에 따라서 보상을 산정하고 변수값을 수정하는 함수입니다.
지역변수 gold와 stamina는 유저가 획득한 스테미너와 골드값을 알려주기 위하여 저장하는 변수입니다.

보상 프린트 함수는 QuestIncome함수에서 반영한 gold와 stamina 값을 텍스트로 출력하여, 플레이어가 획득한 보상을 알려주는 함수입니다.
출력용 UI캔버스인 SubCanvas를 적절히 SetActive하고 자식 UI 객체를 가져와 문자열을 출력합니다.
이후, OffCanvas함수를 Invoke함수로 호출하여 다시 SetActive를 false로 바꿉니다.

버튼에 연결된 함수들입니다.
먼저 ScrollView의 데이터와 Dictionary 데이터를 모두 Clear한 후에,
일일임무, 주간임무, 일반임무의 order값을 변경하고
ContentsDistribute함수를 호출하여, 변경한 퀘스트 타입으로 컨텐츠를 다시 나타냅니다.

퀘스트를 클리어하기 이해서는 currentNum. 즉 현재 달성 수치가 목표 달성 수치를 넘겨야합니다.
그렇기에 QuestReward_Manager 클래스를 이용하여, 퀘스트 보상처리를 관리하게 만들었습니다.
ClearRewardFunc는 스테이지 클리어 후에 각 스테이지의 레벨을 인자로 받아서 관련된 퀘스트를 호출하기 위하여 ID인덱스를 인자로 넘겨서 함수를 호출합니다.
이는 EnemyDiedRewardFunc와 AdventrueRewardFunc 함수 역시 마찬가지입니다.
EnemyDiedRewardFunc는 적 객체의 이름을 확인하고 관련된 퀘스트 진행도를 증가시키고 AdventrueRewardFunc는 탐험을 보낸 후에 관련된 퀘스트 진행도를 증가시킨다는 점에서 차이가 있을 뿐입니다.

ClassFinderAtList는 인덱스를 인자로 받은 후에, 파라미터와 일치하는 myQuestList의 원소를 가져와 해당 원소의 nCurrentNum 값을 가산해주는 함수입니다.
이렇게 하여, 퀘스트의 현재 진행도를 올려주고, 퀘스트 매니저에서 이를 이용하여 퀘스트 클리어 유무를 판단할 수 있습니다.
이렇게 우여곡절 끝에 배틀블루의 구현을 모두 마쳤습니다.
최초 설계했던 부분은 대부분 구현하는데 성공하여, 제법 뿌듯한 기분이 들었습니다.
다만, 모바일 이식 과정에서 json 파일을 읽지 못하는 문제가 발생하여, 이를 해결하기 위해서 조금 시간이 걸릴 듯 합니다...
(유니티 에디터 환경에서는 아무런 문제가 없는데 말이죠...)
하나의 프로젝트를 완성한다는 것은 굉장히 보람찬 일인 것 같습니다.
물론 이렇게 배틀블루 프로젝트를 완성했다고 해서 끝은 아닐 것입니다.
어떻게 하면 조금더 깔끔하고 조금 더 가독성과 이식성이 좋은 코드를 만들 수 있을까? 많은 고민을 했었고 지금도 그렇습니다.
수집형 모바일 RPG가 그렇듯이, 기능을 확장하고자 한다면 얼마든지 그 범위를 넓힐 수 있다고 생각하기 때문입니다.
2D 종스크롤 탄막슈팅 게임이었던 스타슈트나 1인칭 플레이 디펜스 게임이었던 심플 디펜스와는 다르게 배틀블루는 비교적 프로젝트의 규모가 크고 방대했기 때문에, 상당한 시간이 걸렸던 것 같습니다.
중간중간 짬을 내며 개발했던 스타슈트가 먼저 완성되었던 것을 생각해보면 말이죠.
하지만 오히려 그렇기에 더욱 시원하고 섭섭한 기분이 들기도 했습니다.
앞으로도 지속적인 성장을 통하여 더욱 재미있고 훌륭한 게임을 개발하도록 노력해야겠습니다.
지금까지 길고 길었던 개발여정을 지켜봐주셔서 감사합니다...