유니티 19버전에 추가된 New Input System을 사용하였습니다.
New Input System
간편한 컨트롤 설정과 하나의 코드로 다양한 플랫폼의 입력 환경을 지원
패키지 매니저에서 Input System을 설치하고,
플레이어 오브젝트에 Player Input 컴포넌트를 추가한 뒤,
Actions를 생성하여 연결하면 Input System을 준비합니다.
Vector.Normalize의 대각선 이동 보정은 Move - Processors에서 다룰 수 있습니다.
물리 동작은 FixedUpdate에서 호출하였습니다.
deltaTime을 사용할 때 FixedUpdate에서는 fixedDeltaTime을 사용하는 것이 좋다고 합니다.
(deltaTime을 사용해도 알아서 보정되지만 캐싱과 같은 이유에서?)
캐릭터의 반전 기능은 SpriteRenderer의 flip을 이용하였는데,
이동 중이면서 inputVec.x가 0보다 작다면 왼쪽을 바라보는 것과 동일한 개념이기 때문에 반전을 하게 됩니다.
결과값에 boolean 대신 조건식을 넣어도 동작하게 됩니다.
LateUpdate
다음 프레임이 넘어가기 직전에 실행되는 Update 함수
대기, 이동, 사망 애니메이션을 추가하는 과정에서 트랜지션의 설정이 필요합니다.
부드러운 전환 효과를 연출해주는 Transition Duration은 3D 애니메이션에서 유용하게 사용되지만,
2D 픽셀아트로 구성된 에셋에서는 불필요하기 때문에 0 또는 0.01로 설정하며,
Has Exit Time은 true일 시 현재 트랜지션의 재생이 끝난 뒤 이동하게 되는 기능이므로
즉시 전환을 위해 체크 해제하여 false 상태로 둡니다.
SetFloat의 매개변수로 파라미터 이름과 value를 전달받습니다.
Speed 파라미터는 이동하는 벡터의 크기에 따라 트랜지션을 전달하는 것이 목적이므로,
매개변수 value에 해당하는 부분에는 inputVec.magnitude(벡터의 순수한 크기 값)을 입력하여 이동 여부를 확인하게 됩니다.
Animator Override Controller를 사용하면 원하는 애니메이터를 오버라이딩하여
다른 캐릭터를 만들 때마다 새로 애니메이션을 등록하고 트랜지션을 연결할 필요 없이 로직이 같은 기존 애니메이터를 재활용할 수 있습니다.
삽 스프라이트에 콜라이더와 트리거를 추가하고 태그를 입혀 프리팹으로 만들었습니다.
Bullet 스크립트는 프리팹 오브젝트에 입혀주어 발사체 역할을 하게 됩니다.
플레이어가 레벨업하여 강화될 때 발사체의 공격력과 관통 수치를 초기화하도록 Init 함수를 만들어둡니다.
풀 매니저에 삽 프리팹을 등록한 다음
Enemy 스크립트에 몬스터를 처치하는 코드를 추가합니다.
OnTriggerEnter(Collider collision)
콜리전과 충돌했을 때 발동되는 함수
무기를 생성하고 관리할 스크립트가 필요하여 기본적인 데이터를 담을 변수를 만들었습니다.
풀 매니저에 추가한 근접 무기 프리팹은 Bullet 0이고 풀 매니저 배열의 1번째 원소에 있으니
ID는 0, 프리팹 ID는 1이 됩니다.
Update에서 Switch-case문으로 무기마다 로직을 분리할 것인데요,
현재는 근접 무기만 추가하였으니 case 0에서 작성하였습니다.
0번 무기는 Transform.Rotate 함수를 사용하여 오브젝트를 회전하고 Vector3.back에 무기의 속도, DeltaTime을 곱하여 프레임마다 정한 속도만큼 z값이 -1로 회전하게 됩니다. (시계방향)
Init 함수는 무기의 ID에 맞게 초기화 해주는 함수입니다.
무기의 회전 속도를 초기화 해주었고, Batch 함수를 호출합니다.
Batch 함수는 무기를 특정한 위치에 생성하는 함수입니다.
구간별로 설명을 첨언하였습니다.
이 부분은 무기가 소환될 때 위치와 각도를 조절해줍니다.
Vector3.forward에 360도를 곱하고 index / count를 곱하였는데요,
index / count는 i번째 생성되는 무기 위치를 정의합니다.
총 무기 수(count)가 4라면 i번째 무기 프리팹은 각각 90도, 180도, 270도, 360도(=0도)에 배치됩니다.
플레이어에게 인접한 적을 탐색하기 위해서 스캐너 스크립트를 만들었습니다.
foreach문에서 적 오브젝트 배열(targets)을 순차적으로 체킹하여 데이터를 target 변수에 하나씩 담은 뒤 자신의 위치와 적의 위치를 비교하는 것을 반복합니다.
Physics.CircleCast()
원형의 캐스트를 쏘고 모든 결과를 반환하는 함수 (Raycast와 유사)
사용한 오버로딩의 매개변수
1. 캐스팅 시작 위치
2. 원의 반지름
3. 캐스팅 방향
4. 캐스팅 길이
5. 대상 레이어
근접무기와 동일하게 플레이어 자식 오브젝트로 무기 정보를 담은 오브젝트를 만들어 줍니다.
풀 매니저의 배열에 추가하여 오브젝트 풀에 등록하였습니다.
Weapon 스크립트에 발사 함수를 작성합니다.
이 곳에서 발사체의 위치, 방향 목적지를 인접한 적으로 설정하였고
풀 매니저의 Get 함수를 통해 오브젝트를 생성하고 위치와 회전값을 지정한 뒤
Weapon 1 오브젝트에 지정한 데이터를 Bullet 스크립트의 Init 함수로 전달해주었습니다.
Quaternion.FromToRotation(Vector3 fromDirection, Vector3 toDirection)
fromDirection에서 toDirection으로 오브젝트를 회전하여 발사체의 머리 방향을 조절하는 함수입니다.
작성한 발사 함수는 Update swtich문의 default case에서 사용됩니다.
타이머 변수 추가하여 딜레이를 조절하였고,
0.3초마다 무기를 발사하게 됩니다.
Bullet의 Init 함수입니다.
원거리 무기는 매개변수로 Vector3 방향을 전달받으니, if문을 통해 근접 무기와 관통에 대한 조건식을 걸어주고
원거리 무기의 경우 리지드바디 velocity에 방향과 속도를 곱하여 발사하는 힘을 늘렸습니다.
스크립터블 오브젝트를 만들어서 아이템에 관한 데이터를 관리하였습니다.
아이템 타입은 플레이어가 선택하여 올릴 수 있는 5가지 유형의 스킬입니다.
근접, 원거리 무기와 공격속도, 이동속도, 그리고 체력 회복이 있습니다.
기본 데미지나 기본 개수, 레벨업 시 데미지 상승치, 개수 증가, 프리팹 등 여러 데이터들을 미리 작성하였는데요, 이처럼 스크립터블 오브젝트로 관리하면 다른 스크립트에서 각 아이템에 대한 데이터를 쉽게 접근하고 참조할 수 있다는 장점이 있습니다.
그리고 캔버스에서 스킬 UI가 될 버튼을 만들었습니다.
Item 스크립트입니다.
이 스크립트는 스킬 UI의 컴포넌트로 부착해서 아이템으로 만들어둔 스크립터블 오브젝트를 받아와 UI에 연동을 해주는 역할을 합니다.
그리고 OnClick 함수로 유저가 스킬 UI의 버튼을 클릭한다면 적어둔 상승치를 기반으로 업그레이드가 됩니다.
아이템 타입을 열거형으로 선언했기 때문에 Switch-case 문으로 아이템 타입에 따라 분류하여 관리 및 유지보수에 중점을 두었습니다.
아이템 타입이 무기라면 게임을 처음 시작할 때 아무 무기가 없는 플레이어에게 선택한 무기 오브젝트를 추가하며,
아이템 타입이 장갑, 신발이라면 속도가 상승하게 되는데요, 이러한 값들을 사전에 초기화해주는 것이 Item 스크립트의 핵심 기능입니다.
마지막으로 아이템 타입이 회복이라면 플레이어는 체력을 최대치까지 회복하게 됩니다.
최대 레벨에 달성했다면 버튼의 interactable(활성화) 여부를 false로 전환해 더이상 스킬업을 할 수 없도록 만들어주었습니다.
Gear 스크립트는 Item 스크립트에서 무기 오브젝트를 생성한 것과 유사하게 동작하는 장비 전용 코드입니다. (무기와 장비는 분류가 다르기 때문에 따로 작성하였습니다.)
이 곳에서 선언한 RateUp, SpeedUp 함수는 사전에 구성해둔 스크립터블 오브젝트의 데이터를 기반으로 플레이어의 속도 상승을 도맡습니다.
다음은 Hand 스크립트입니다.
처음 플레이어는 맨 손으로 시작을 하고, 레벨업 했을 때 선택한 무기를 장착할 수 있도록 해줍니다.
변수 rightPos, rightPosReverse의 벡터값은, 무기를 장착한 스프라이트를 플레이어에 사전에 부착시켜서 배치해둔 로컬 좌표입니다.
Awake에서 초기화를 해준 뒤 (0번은 자기자신이므로) LateUpdate에서 flip을 처리합니다.
if (isLeft) ... 코드는 왼쪽 무기 (근접 무기)에 해당되는 로직입니다.
삼항 연산자를 사용하여 플레이어 스프라이트 flip에 따라 위치와 회전값을 교체해줍니다.
else if (GameManager...) 코드는 원거리 무기가 적을 바라보게끔 근처의 적 좌표를 받아와 방향 벡터를 구해 무기를 회전시키는 역할을 합니다.
else ... 코드는 오른쪽 무기 (원거리 무기)에 해당되는 로직입니다.
처음 if문과 마찬가지로, 삼항 연산자를 사용하여 플레이어 flip에 따라 무기 좌표가 교체되는 방식입니다.
그리고, 각 메소드마다 스프라이트가 반전이 될 때 Order in Layer을 조절하여 플레이어 뒤에 있는 손의 무기가 가려지고, 바깥 쪽 스프라이트가 잘 보이도록 삼항 연산자를 사용하여 디테일을 강조하였습니다.
이 무기를 장착한 손 스프라이트는 Weapon 스크립트에서 함수의 Init(초기화 함수)가 호출될 때(=레벨업이 됐을 때) 무기 타입에 맞추어 SetActive를 키는 것으로 기획하였습니다.
하단에 BrodcastMessage("ApplyGear"...) 함수는 기어를 먼저 업그레이드 하고 무기가 뒤늦게 추가되어도 기어에 따른 속도 변화를 무기에 적용될 수 있도록 함수를 호출해주는 코드입니다.
근접 무기 (장착 및 업그레이드) 테스트
원거리 무기 (장착 및 업그레이드)
& 장갑 기어 (공격속도 증가) 테스트
신발 기어 (이동속도 증가)
& 체력 회복 기어 테스트
캐릭터 해금 기능입니다.
조건을 달성하면 현재 잠겨있는 오브젝트가 비활성화되고,
해금된 캐릭터 오브젝트가 활성화 됩니다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AchieveManager : MonoBehaviour
{
public GameObject[] lockCharacter; // 잠긴 캐릭터
public GameObject[] unlockCharacter; // 언락된 캐릭터
public GameObject uiNotice; // 해금될 시 알림 UI
enum Achieve { UnlockPotato, UnlockBean }
Achieve[] achieves; // 업적 데이터를 저장할 변수
WaitForSecondsRealtime wait;
void Awake()
{
// 열거형의 모든 데이터를 가져오는 GetValues 함수로 업적 데이터에 초기화
achieves = (Achieve[])Enum.GetValues(typeof(Achieve));
wait = new WaitForSecondsRealtime(5);
// 유저의 데이터가 없으면 초기 데이터 초기화 및 저장
if (!PlayerPrefs.HasKey("MyData"))
{
Init();
}
}
void Init() // 초기화 함수
{
// 유저에게 키 부여
PlayerPrefs.SetInt("MyData", 1);
// 업적 데이터에 들어있는 모든 캐릭터를 0 (잠김) 상태로 초기화
foreach (Achieve achieve in achieves)
{
PlayerPrefs.SetInt(achieve.ToString(), 0); // 0은 잠김 상태
}
}
void Start()
{
UnlockCharacter();
}
void UnlockCharacter() // 해금을 담당하는 함수
{
// 잠긴 배열을 순회하며 인덱스에 해당하는 업적 이름 가져오기
for (int index = 0; index < lockCharacter.Length; index++)
{
// 업적 데이터의 현재 해금 상태를 (0과 1) 가져옴
string achieveName = achieves[index].ToString();
// 가져온 값을 비교. 1이면 해금, 0이면 잠김
bool isUnlock = PlayerPrefs.GetInt(achieveName) == 1;
// 캐릭터의 해금 유무에 따른 버튼 활성화 여부 조정
lockCharacter[index].SetActive(!isUnlock);
unlockCharacter[index].SetActive(isUnlock);
}
}
void LateUpdate()
{
foreach (Achieve achieve in achieves)
{
CheckAchieve(achieve);
}
}
void CheckAchieve(Achieve achieve) // 업적을 담당하는 함수
{
// 해금한 지 판단할 변수
bool isAchieve = false;
// 캐릭터 업적에 관한 데이터
switch (achieve)
{
case Achieve.UnlockPotato:
if (GameManager.instance.gameTime != GameManager.instance.maxGameTime)
{
isAchieve = GameManager.instance.kill >= 800; // 800킬 이상 시 해금
}
break;
case Achieve.UnlockBean:
isAchieve = GameManager.instance.gameTime == GameManager.instance.maxGameTime; // 끝까지 생존 시 해금
break;
}
if (isAchieve && PlayerPrefs.GetInt(achieve.ToString()) == 0) // 두가지 조건을 만족할 경우 해금
{
PlayerPrefs.SetInt(achieve.ToString(), 1); // 1은 해금 상태
// 어떤 해금을 완료했는지 알림 UI에 적합한 데이터를 전송
// 알림 UI의 자식 오브젝트를 순회하며 순번이 맞으면 활성화
for (int index = 0; index < uiNotice.transform.childCount; index++)
{
bool isActive = index == (int)achieve;
uiNotice.transform.GetChild(index).gameObject.SetActive(isActive);
}
StartCoroutine(NoticeRoutine());
}
}
IEnumerator NoticeRoutine() // 해금 알림 UI
{
uiNotice.SetActive(true);
// 사운드 재생
AudioManager.instance.PlaySfx(AudioManager.Sfx.LevelUp);
yield return wait;
uiNotice.SetActive(false);
}
}
잠긴 캐릭터와 해금된 캐릭터의 오브젝트를 배열에 등록하여 초기화 합니다.
해금될 경우 해금된 캐릭터의 알림창이 활성화 됩니다.
유니티 엔진 내에도 PlayerPrefs의 데이터를 초기화 하는 기능이 있습니다. (Edit - Clear All PlayerPrefs)
낫 무기입니다.
몇 부분을 제외하면 삽과 로직이 비슷합니다.
차이가 있다면 count에 따라 회전체가 늘어나는 것이 아닌, 프리팹의 스케일이 커지도록 만들었습니다.
스크립터블 오브젝트로 데이터를 관리하면서 무기의 기본 세팅과 로직만 작성하면 새로운 무기라도 추가와 수정이 비교적 편하다는 점을 몸소 느꼈습니다.
다음은 샷건입니다.
for문의 인덱스 값이 탄환의 개수 역할을 합니다.
샷건도 권총과 로직이 동일합니다.
타겟의 위치를 반환하고 목적지로 설정하고, 대신 목적지에 랜덤 방향값을 곱하여 샷건의 탄퍼짐을 구현하였습니다.
타일맵을 그림판으로 비유한다면 위 사진과 같습니다.
배경 이미지가 될 스프라이트를 준비하고,
Window - 2D - Tile Palatte
먼저 팔레트를 생성합니다.
Project - Create - 2D - Tiles - Rule Tile
타일을 생성합니다.
Number of Tiling Rules - 1
Output - Random
Size - 10
랜덤 타일을 사용하면 일일히 타일을 배치하는 것보다 편하게 타일맵을 만들 수 있습니다.
만든 타일을 드래그하여 팔레트에 등록하고,
도화지가 되어줄 사각형 그리드를 생성합니다.
라인 브러쉬와 채우기 기능을 이용해 플레이어를 중심으로 20 x 20 크기의 타일을 그렸습니다.
랜덤으로 배치된 타일이 마음에 들지 않을 경우 Noise를 조절하여 다른 형태의 랜덤한 타일을 확인할 수 있습니다.
하이라이키에서 만든 타일맵을 선택하고 콜라이더를 추가합니다.
타일맵 콜라이더와 컴포지트 콜라이더를 사용할 것인데,
생소할 수도 있는 컴포지트 콜라이더는 다수의 2D 콜라이더를 하나로 합쳐주는 역할을 합니다.
만약 타일맵 콜라이더만 사용한다면 좌측 사진과 같이 모든 타일들에 대해 콜라이더가 무수히 많이 입혀지지만,
컴포지트 콜라이더를 함께 사용한다면 타일맵의 외곽에만 콜라이더가 입혀지게 됩니다.
Used by Composite를 활성화하면 기존 콜라이더의 제어권은 컴포지트 콜라이더로 위임되므로 트리거 전환은 이곳에서 해야합니다.
Composite Collider 컴포넌트를 사용할 때 Rigidbody가 자동으로 추가되는 이유:
물리 엔진과 충돌 처리에 대한 요구 사항 때문
게임매니저 스크립트에서 게임매니저를 동적으로 만들고 플레이어 클래스 변수를 추가합니다.
로직을 작성합니다. 이동 거리와 방향를 계산하여 오브젝트가 이동하게 됩니다.
플레이어 이동 방향의 반대 쪽에 있는 두 사분면 타일맵이 이동 방향에 따라 두 배 거리만큼 재배치 됩니다.
캔버스에서 5가지의 HUD를 제작하였습니다.
HUD는 하나의 총괄 스크립트를 작성한 뒤 열거형과 switch문을 이용해 각각의 캔버스 오브젝트에게 전달됩니다.
HUD와 UI의 차이
차로 예를 들자면...
![]()
- HUD (Head Up Display)
유저가 개입할 수 없고 시스템 상 필요한 정보를 표시해주는 것 (ex. 체력바, 시계)
- UI (User Interface)
유저와 상호작용 할 수 있는 것 (ex. 상점, 설정, 인벤토리)
스크립트에서 열거형을 선언한 뒤, 5종류의 HUD를 추가하고,
텍스트와 슬라이더를 초기화 해주었습니다.
Exp와 Health는 슬라이더를 사용하며 Level, Kill, Timer는 텍스트를 사용합니다.
선언한 열거형을 LateUpdate에서 switch문으로 HUD의 종류에 따라 분류하였습니다.
Exp, Health
슬라이더는 최소값이 0, 최대값이 1로 표현됩니다. 따라서 Exp는 현재 경험치를 최대 경험치에서 나눈 뒤 슬라이더에 전달하여 경험치가 누적될수록 슬라이더가 늘어나게 됩니다.
Health도 마찬가지로 현재 체력을 최대 체력에서 나눈 값을 전달하여 체력을 표기합니다.
Level, Kill
게임매니저에 인스턴스하여 레벨, 킬 데이터를 받아온 뒤 표기합니다.
Time
게임의 매커니즘은 생존이므로 게임 최대 시간에서 남은 시간을 뺀 값을 분, 초로 표기합니다.
게임에서 사용되는 시간을 (int형 데이터) 시분초 형식으로 표현하는 방법은 다음과 같습니다.
int hour = data / 3600; int min = (data % 3600) / 60; int sec = (data % 3600) % 60;
타임 스케일을 조정하여 2배속으로 녹화하였습니다.
최종 시간까지 생존할 경우 UI가 나타납니다.
모바일 조작을 위해 Input System의 On-Screen Controls를 임포트하고
조이스틱의 스프라이트를 추가한 뒤,
On-Screen Stick 스크립트를 추가하여 조이스틱과 연동이 완료되었습니다.
포스트 프로세싱은 씬에 다양한 화면 효과를 추가하는 기능입니다.
기존 컨텐츠를 수정할 필요 없이 즉시 시각적인 효과를 구현할 수 있습니다.
빈 게임오브젝트를 하나 만들어 Volume 컴포넌트를 추가하고 세가지 효과를 Override 하였습니다.
Bloom
블룸은 광원에서 빛이 주위 물체에 새는 것처럼 보이게 하는 효과입니다.
Film Grain
필름 그레인은 사진 필름의 임의의 광학 질감을 시뮬레이션하는 것으로, 필름에 존재하는 작은 입자에 의해 발생합니다.
Vignette
이미지의 가장자리를 향해 진하게 하거나 불포화시킵니다.
포스트 프로세싱 On / Off 차이
배경음과 효과음을 관리하는 AudioManager의 주요 기능과 동작은 다음과 같습니다.
싱글톤 패턴을 구현하여 다른 클래스에서 접근이 가능합니다.
열거형으로 변수를 만들었기 때문에 매개변수에 enum 값을 전달해주어 쉽게 재생할 수 있습니다.
애니메이터 오버라이드 컨트롤러와 박스 콜라이더, 리지드바디를 추가한 뒤 프리팹으로 등록했습니다.
몬스터의 이동 및 추적 로직입니다.
몬스터가 화면 밖으로 벗어나면 재배치 시키는 기능을 추가했습니다.
무한맵에 사용된 재배치 스크립트에서 콜라이더가 활성화 중이라면 Enemy 태그인 오브젝트를 재배치하게 됩니다.
오브젝트 풀링을 사용하여 몬스터를 관리할 것입니다.
오브젝트 풀링이란?
게임 오브젝트가 많이 생성(Instantiate)되고 제거(Destroy)되면 메모리 할당, 초기화, 해제의 과정이 여러번 반복하게 되므로 프레임 드랍과 같은 성능 문제를 초래하게 됩니다.
따라서 할당과 호출을 최소화시켜 메모리를 절약할 수 있는 오브젝트 풀링 기법을 사용하는 것이 좋습니다.
두 종류의 몬스터를 프리팹으로 등록합니다.
오브젝트 풀 로직입니다.
저장해둔 프리팹을 받아와 리스트에 등록하고 풀 배열과 오브젝트 리스트를 초기화합니다.
Get 함수는 몬스터 프리팹을 꺼내 생성시키고 인덱스의 풀에 대기중인 오브젝트가 있는지 확인합니다.
예를 들어 100마리의 몬스터가 생성되었을 때 플레이어가 몬스터를 모두 처치한다면 몬스터는 Destroy가 아닌 SetActive(false)가 됩니다.
그리고 다시 몬스터를 생성하는 Get 함수가 호출될 경우 풀에 해당 몬스터 오브젝트가 있다면 SetActive(true)로 기존 메모리를 재사용하여 절약하는 것입니다.
PoolManager 오브젝트에 풀링으로 관리할 프리팹 두 종류를 등록했습니다.
화면 밖에 SpawnPoint를 추가하고 몬스터가 이곳에서 스폰되게 할 것입니다.
몬스터 스폰을 담당하는 스포너 로직입니다.
GetComponentsInChildren<T>();
자식 오브젝트들의 컴포넌트를 반환하는 함수
여러 Point 좌표 중 랜덤한 곳에 Get (오브젝트 풀링에서 만든 몬스터 소환) 함수를 호출하여 몬스터를 스폰합니다.
몬스터를 처치하는 기능을 추가하기 위해 Enemy 스크립트를 수정하였습니다.
else문의 로직이 몬스터가 사망했을 때 실행되는 코드입니다.
기존에 사용했던 Dead 함수는
애니메이션에 이벤트를 통하여 작동시켰기 때문에 코드에서 빠졌습니다.
(엔진의 기능을 이용하여 코드 사용을 최소화)
플레이어의 공격에 피격될 때 실행되는 넉백 코루틴 함수입니다.
자신의 위치에서 플레이어의 위치를 빼 방향 벡터를 구하고 AddForce로 힘을 가해 밀어내는 효과를 줍니다.
하지만 몬스터는 FixedUpdate에서 물리적인 이동을 하고 있는데 이 때문에 넉백이 작동하지 않기 때문에 조건을 걸어주어야 합니다.
애니메이터의 Get CurrentAnimatorStateInfo 함수는 지금 실행되고 있는 애니메이션 상태의 정보를 가져오는 함수입니다.
Hit 애니메이션이 재생 중이라면 이동 코드가 담긴 FixedUpdate를 탈출하도록 조건을 걸어서 넉백의 우선순위를 당겨주었습니다.
그리고 몬스터 처치 시 경험치가 상승하는 부분은 게임 매니저에서 변수를 추가하였습니다.
임의로 설정해둔 경험치 최대치를 초과하면 다음 레벨로 넘어가게 됩니다.
경험치와 레벨은 추후에 플레이어의 무기 업그레이드 혹은 레벨 디자인 등에 사용될 것입니다.
게임 흐름 시간에 따라 난이도를 조절하기 위해 게임 매니저, Enemy, Spawner 스크립트를 수정합니다.
변경된 코드의 전체적인 흐름을 안내하기 앞서 몬스터 소환의 매커니즘 방식을 설명하겠습니다.
전에는 Enemy A와 B로 두 종류의 프리팹을 만들었지만,
몬스터 데이터를 추가하여 코드를 통해 하나의 프리팹에 데이터를 변경하여 소환하는 방식으로 바뀌었습니다.
시간에 따라 몬스터가 강화되는 구조에 맞추어 이제 하나의 프리팹을 사용할 것입니다.
몬스터 스포너 스크립트에 새로운 클래스 Spawn Data를 추가하였습니다.
이 클래스는 몬스터의 스폰 주기, 종류, 체력, 속도 등을 외부에서 설정하는 것을 목적으로 직렬화하여 사용됩니다.
변수는 배열로, 접근제한자는 public으로 두어 인스펙터 창에서 쉽게 수정할 수 있습니다.
두 종류의 몬스터를 소환하기 위해 데이터를 위와 같이 분류하였습니다.
그리고, 두가지 프리팹을 받아왔던 기존 풀 매니저는
변경점에 맞추어 하나의 프리팹만 남겨놓았습니다.
게임매니저의 변경점입니다.
게임 진행시간 변수(gameTime)를 추가하였습니다.
MaxGameTime 변수는 테스트를 위해 대략적으로 설정한 수치인데요,
몬스터의 강화를 10초 간격으로 총 2번 진행할 것이기 때문에 임시로 2 * 10f로 설정해두었습니다.
Enemy 스크립트의 변경점입니다.
speed, health, maxHealth 변수는 Spawner 스크립트에서 만들 몬스터 데이터를 초기화 하기 위해 사용됩니다.
프리팹을 하나로 압축시켰기 때문에 사용할 몬스터 종류의 애니메이터 컨트롤러를 등록하여 초기화할때 컨트롤러를 변경해줄 것입니다.
조금 뒤에 서술할 Init 함수를 통해 애니메이션이 변경되게 됩니다.
Spawner 스크립트의 변경점입니다.
새로 만든 int level 변수는 게임 진행 단계를 맡습니다.
이 변수는 Update문 초기화 하는데, 현재는 게임 흐름에 맞추어 10초마다 게임 레벨이 상승하게 됩니다.
나중에 몬스터 처치를 통해 레벨이 상승하도록 변경할 예정입니다.
Mathf.FloorToInt()
실수의 소수점 아래를 버리고 Int로 형변환 하는 함수
Mathf.CeilToInt()
실수의 소수점 아래를 올리고 Int로 형변환 하는 함수
Spawn 함수 내에선 몬스터 소환 함수(Get)의 인자값으로 0을 입력했는데요,
풀 매니저에 등록한 프리팹은 1개 뿐이며 0번째 원소이기 때문입니다.
그리고 Spawn 함수 내에서 Enemy 스크립트의 Init 함수를 실행시킵니다.
Init 함수는 Enemy 스크립트에서 작성한 설정한 스폰 데이터를 가져와 초기화하는 함수입니다.
현재 레벨 구성과 스폰 데이터가 0, 1로 동일하기 때문에 Init 함수의 인자값으로 레벨을 넘겨주었습니다.
이 초기화 과정을 거쳐 몬스터의 데이터와 애니메이션이 변경되게 됩니다.
텍스트를 추가하여 작동을 확인합니다.
테스트를 위해 단순하게 10초 전까지는 0단계인 좀비가 소환되고 11초 이후로 1단계인 스켈레톤이 소환되는 것을 확인합니다.
이 값을 조절하면 플레이어의 레벨 혹은 게임 진행 시간에 따라 세밀하게 난이도 조절을 할 수 있습니다.
레벨업 시 유저가 원하는 스킬을 선택할 수 있도록 레벨업 UI를 추가하였습니다.
몬스터 처치
↓
플레이어 EXP 상승
↓
플레이어 레벨업
↓
스킬선택 UI 노출
레벨업 시스템은 위와 같은 동작 구조를 지니고 있습니다.
스킬선택 UI에서 플레이어가 고를 수 있는 항목은 최대 3개이며,
최대까지 올렸을 경우 체력 회복 아이템이 대신 노출되게 기획하였습니다.
UI가 나타날 때 호출되는 OnEnable 함수에서
아이템의 레벨, 설명 등의 텍스트를 데이터에 따라 변경해줍니다.
아이템 데이터는 스크립터블 오브젝트에 작성한 데이터를 받아왔습니다.
레벨업 스크립트 입니다.
UI가 나타나는 것은 Rect Transform의 Scale을 0과 1로 조절하는 것으로 (Show, Hide 함수) 구현했습니다.
(캔버스는 좌표계가 Transform이 아닌 Rect Transform)
Select 함수는 플레이어가 1레벨일 때 기본 무기인 삽을 지니고 시작하게 설정하기 위해 추가한 함수입니다.
레벨업 시 선택할 수 있는 항목을 랜덤으로 출력하기 위한 로직입니다.
모든 아이템을 비활성화합니다.
랜덤으로 뽑기 위해 items 배열에 있는 모든 아이템의 활성화 상태를 비활성화로 변경합니다.
랜덤하게 세 개의 아이템을 활성화합니다.
중복되지 않는 세 개의 랜덤 인덱스를 선택하여 해당 인덱스에 있는 아이템을 활성화합니다.
만렙 아이템의 경우 소비 아이템으로 대체합니다.
활성화된 각 아이템의 레벨이 최대 레벨과 같을 경우, 랜덤한 다른 아이템을 활성화하여 대체합니다.
타임스케일이 0이 되는 것은 '레벨업 했으며 스킬 선택을 완료하기 전' 이라는 것을 의미하므로
다른 스크립트에서 사용하는 Update 함수들의 코드가 동작하는 것을 막기 위해 bool 변수인 isLive 트리거를 사용하여 업데이트 함수를 빠져나오는 것으로 제지합니다.
몬스터가 죽으면 생성되는 5종류의 아이템을 만들었습니다.
먼저 몬스터를 처치해서 exp를 얻는 기존 시스템에서,
몬스터를 처치하고 나온 코인을 주워야 exp가 쌓이도록 시스템을 변경하였습니다.
그에 따른 아이템이 윗줄의 코인 3종입니다. 각각 드랍률과 exp 획득량에 차이가 있습니다.
자석 아이템은 맵에 남아있는 필드 아이템을 흡수하는 기능입니다.
경광등 아이템은 잠시동안 플레이어를 제외하고 게임 시간을 느리게 합니다.
DropItem 클래스입니다.
각 필드 아이템들에 이 클래스를 입혀 열거형으로 데이터를 분류하고,
아이템의 추적 속도와 범위를 설정하여 플레이어와 인접하면 플레이어 쪽으로 이동하게 됩니다.
아이템은 기본적으로 자석 기능을 내장하고 있으나
자석 아이템을 획득한다면 ApplyMagnetBuff 함수가 호출되어
매개변수로 traceSpeed, traceRange에 더 높은 값을 일시적으로 부여받아 기본값 대비 확연한 차이를 느낄 수 있습니다.
아이템들은 플레이어 클래스에서 충돌 체크를 하여 충돌한 오브젝트의 열거형에 따라 다른 로직이 실행됩니다.
현재는 아이템이 적은 만큼 경험치 획득량을 (1, 5, 25) 하드코딩 하였지만,
만약 아이템의 종류가 많다면 DropItem 클래스에서 데이터를 관리하는 쪽이 나을 것 같습니다.
몬스터 클래스에서 실행될 Drop 함수입니다.
몬스터가 죽으면 이 함수가 호출되고 설정한 확률에 따라 아이템을 드롭하게 됩니다.
필드 아이템 프리팹을 모두 풀로 등록하고 사용하여 메모리를 절약할 수 있었습니다.
풀의 Get 함수 매개변수에도 하드코딩을 하였지만,
필드 아이템은 가지고 있는 데이터가 적기 때문에 굳이 스크립터블 오브젝트로 관리하지 않았습니다.
하지만 필드 아이템이 늘거나 번거로움을 감내하더라도,
게임을 장기적으로 유지보수 하게 된다면 객체지향의 원칙을 중요시 하는 것이 퀄리티 높은 개발자가 되는 길이라고 생각하고 정진해야겠습니다.
몬스터를 처치하고 떨어진 코인을 먹으면 경험치가 쌓이게 됩니다.
슬로우 아이템의 로직입니다.
간단하게 설명하자면,
아이템 발동 시 2초 동안 타일맵의 색상을 흐리게 해 배경에 암전 효과를 주고, 타임 스케일을 0.5로 낮추게 됩니다.
이 동작은 1초 동안 지속되며, 1초가 지나면 모든 값들이 1초 동안 원점으로 복구됩니다.
이 기능을 구현하기 위해 Lerp 함수와 Clamp01 함수를 사용하였는데요,
Lerp 함수는 시작값에서 끝값을 보간값으로 선형 보간하는 함수이며,
Clamp01 함수는 최소값과 최대값을 0과 1로 보정시키는 함수입니다.
보정된 값은 Lerp 함수의 보간값에 사용됩니다.
하지만 타임스케일을 조절하면 플레이어 또한 느려지는 영향을 받아서 아이템의 기획과는 다른 결과가 연출됩니다.
따라서 플레이어 인풋 클래스의 FixedUpdate에 이동 방법을 보정하던 코드를 수정할 필요가 있었습니다.
기존에는 인풋 벡터와 속도에 deltaTime을 곱하였는데,
삼항연산자를 이용해 타임스케일이 1이 아닐 때는 타임스케일에 영향받지 않도록
unscaledDeltaTime을 사용하였습니다.
그리고 1이 되면 다시 fixedDeltaTime으로 교체됩니다.
얼핏 알기로, FixedUpdate에 사용하는 deltaTime은
자동으로 fixedDeltaTime으로 보정된다고 하는데, unscaledDeltaTime도 영향을 받는지에 대해선 확실치 않아서 불필요한 코드가 될 수도 있습니다.
마지막으로, 게임 시간에 따라 오디오도 영향받도록 오디오 소스의 피치를 조절하는 코드를 추가하였습니다.
결과 영상
사운드에도 변화가 있어서 GIF 대신 영상을 첨부하였습니다.
Ctrl+Shift+B로 빌드 세팅에 들어가 플랫폼을 모바일로 변경하였습니다.
이외에 어플리케이션 버전, 타이틀 이미지 등을 수정할 수 있습니다.
velog는 영상 첨부가 불가능하여 이 곳에 들어가면 확인할 수 있습니다.