3D 액션 게임 만들기(2)
캐릭터의 애니메이션을 구현하기 위해 MainCharacter라는 Animator를 만들고 거기에 Asset에서 제공한 Animation을 넣는다.

위 사진은 MainCharacter Animator의 Animation과 Parameter를 설정한 것이다. 그리고 CharController에서 Character의 자식 오브젝트에 Animator 컴포넌트를 찾아서 저장한다. 그리고 문자열 데이터를 중복되지 않는 정수형값으로 지정해줘서 값을 비교할 때 성능을 더 좋게 하는 방식이 있는데 StringToHash 함수를 이용하는 것이다. 그래서 아래 코드처럼 IsMove를 Hash값으로 바꿔서 저장한다.
private static int animParam_IsMove = Animator.StringToHash("IsMove");
그리고 Anim에서 SetBool을 해쉬값으로 비교해서 이동할 때 애니메이션을 바꿔준다.
Anim?.SetBool(animParam_IsMove, moveDelta != Vector3.zero);
이제 게임의 해상도에 대해서 다뤄본다. 지금 게임은 16:9 비율로 되어있는데 기기마다 해상도가 다를 수 있다. 그렇게 되면 UI를 표시함에 있어서 애로사항이 발생하게 되는데 레터박스를 이용해서 해상도를 고정할 수 있다.

위 사진처럼 16:9를 유지하기 위해서 양 옆에 검은색 박스가 생긴것을 레터 박스라고 한다. 영화나 다른 게임에서도 볼 수 있다. 이것을 구현하기 위해서 CameraResolution이라는 스크립트를 작성해서 Main Camera에 넣는다.

Rect로 카메라의 Rect 값을 가져오고 scaleHeight와 scaleWidth값을 비율에 맞춰서 계산한다. 그래서 scaleHeight 값이 1보다 작다면 화면의 높이가 게임의 높이보다 더 큰 것으로 height를 맞춰주고 y의 값을 중심 값으로 바꿔줘야한다. 아니라면 화면이 아래로 쳐진다. 마찬가지로 scaleWidth가 1보다 작다면 화면의 너비가 게임의 너비보다 넓은 것으로 화면을 비율로 줄이고 x의 값을 바꿔 정렬을 해준다. 그리고 다음으로는 UI를 설정해준다. 3D 게임에서 UI를 제작할때는 Overlay로 했다가 게임이 시작되면 camera 모드로 바꿔서 레터박스를 포함해서 비율에 맞게 설정해준다.
public class CanvasOption : MonoBehaviour
{
private void Awake()
{
if (TryGetComponent<Canvas>(out Canvas canvas))
{
canvas.renderMode = RenderMode.ScreenSpaceCamera;
}
}
}
이 스크립트를 canvas에 넣는다. 그럼 아래 그림과 같이 비율에 잘 맞춰서 UI가 출력된다.

코루틴에서 yield instrcution을 쓸 때 가비지 컬렉션이 발생하기 때문에 전역으로 yield instrcution으로 쓸 값들을 설정해준다.

그리고 UI들을 제어하기 위해 UIManager 오브젝트를 생성하고UIManager 스크립트를 작성해서 넣는다. 현재 게임에 많은 버튼이 있기 때문에 여러개의 버튼에 해당하는 함수를 만들기보다 ButtonType을 인자로 받는 하나의 HandleButtonOnClick 함수를 선언해서 switch문으로 해당 type에 맞는 행동을 실행하도록 한다.

지금 HandleButtonOnClick 함수는 ButtonType을 인자로 받는데 AddListener 인자를 받을 수가 없다. 그래서 람다식으로 함수를 따로 정의하지 않고 바로 함수의 내용을 입력한다. 람다식은 이름이 없는 익명 함수로도 불린다. 그리고 이제 카메라가 캐릭터를 따라 움직이도록하는 CameraMove 스크립트를 작성해서 Main Camera에 넣는다.

스크립트 내에서 Character 오브젝트를 찾아서 오브젝트의 위치를 저장하고 그 위치에 오프셋만큼 떨어져서 카메라의 위치가 계속 바뀌도록 설정한다. 여기서 LateUpdate 유니티 메시지를 사용했는데 LateUpdate는 프레임마다 한 번 Update가 호출된 후에 호출된다. 따라서 캐릭터 이동 다음에 카메라의 이동을 구현하기 위해서 LateUpdate를 사용했다. 이제 엑셀 데이터를 불러와서 scriptable object로 만들어준다. 거기에는 ItemData, TipMess(팁 메세지), Language, MonsterData, LevelEXP가 담겨있다. 그리고 Reimport한 Scriptable Object인 ActionGame을 Resource 폴더에 옮겨준다. 리소스에 있는 폴더들은 런타임에 동적으로 로드할 수 있기 때문에 그것을 위해서 특별하게 사용되는 폴더이다. 이 폴더에 있는 파일들은 빌드 시 자동으로 프로젝트에 포함된다. 그러나 Resource 폴더에 너무 많은 데이터가 있으면 게임을 로드하는데에 부하가 발생할 수 있다. 이제 저장한 데이터들을 DataManager 스크립트에서 관리한다. 그전에 Manager들에 들어갈 함수들을 정의해놓은 IManager 인터페이스를 만들어서 Manager 클래스의 초기화와 시작을 담당하는 함수를 정의한다.
public interface IManager
{
public void InitManager();
public void StartManager();
}

이전에 2D 게임에서 했던 것처럼 List로 되어있는 데이터들을 key값으로 매핑해서 Dictionary에 저장한다. Resource 파일에 있는 것들은 Resource.Load를 통해서 파일명으로 가져올 수 있다. 그래서 InitManager에서 매핑을 해주고 데이터를 반환하는 함수들(GetItemData, GetMonsterData)을 선언해준다. 데이터를 반환하는 함수에서는 return으로 반환할 수 있는 값은 하나로 정해져있기 때문에 out을 사용해서 모든 데이터을 참조해서 가져오도록 한다. 이제 인벤토리를 구현한다. 인벤토리는 인벤토리 자체와 인벤토리 한 칸도 각각 구현해서 인벤토리 슬롯에 들어있는 아이템에 정보를 출력하고, 개수와 이미지를 보여지게 해야한다. 먼저 인벤터리 전체에 대한 구현은 InventoryData 스크립트로 구현한다. 이 스크립트에서는 특정아이템의 개수, 아이템의 ID, 그리고 uid를 설정해준다. 그리고 아이템을 획득했을 때 인벤토리에 추가하고 삭제하는 것, 아이템의 정보를 업데이트하는 것이 구현되어 있따. InventoryData 스크립트 안에서 InventoryItemData 클래스를 선언해서 아이템의 정보를 담아둔다. 그리고 uid는 아이템의 ID가 같다면 아이템을 서로 구별하기가 힘들다. 그래서 어떤 것을 강화해야하는지 잘 모를 수 있다. 그래서 아이템 고유의 아이디인 uid를 설정해준다.

먼저 List로 아이템의 데이터를 담는 items 리스트를 정의한다. 그리고 AddItem에서는 아이템 데이터를 인자로 받아서 아이템의 인덱스를 찾아서 저장한다. 아이템의 인덱스가 0 이상이라면 이미 아이템이 있는 것이고 인덱스가 없다면 해당 아이템이 없는 것이다. DataManager를 통해 아이템의 데이터를 가져와서 아이템이 장비 아이템이라면 겹쳐지지 않는 아이템이므로 인벤토리가 가득차지 않은 상태라면 인벤토리에 아이템을 저장한다. 그리고 물약과 같은 잡화 아이템이라면 인덱스를 비교해서 인덱스가 0보다 작다면 아이템이 현재 인벤토리에 없는 것으로 인벤토리에 새롭게 추가하고 인덱스가 0보다 크다면 이미 인벤토리에 해당아이템이 있다는 것으로 그 아이템의 갯수를 추가한다. 그리고 RemoveItem은 인덱스를 찾아서 해당 인덱스에 해당하는 아이템을 찾아서 없앤다. 그리고 강화가 성공했을때 UpdateItemInfo 함수에서 아이템의 데이터에서 itemID를 1 올려서 강화 단계를 올린다. IsFull 함수는 인벤토리가 가득찼는지 확인하는 함수이고 GetItemList함수는 현재 리스트의 아이템들의 데이터를 가져오는 것이다. 그리고 FindItemIndex 함수는 아이템이 새로 들어왔을때 새로운 아이템의 ID와 지금 인벤토리에 있는 아이템의 ID를 비교해서 같은 것이 있다면 그 인덱스를 반환한다. 없다면 -1을 반환한다. 그리고 이제 GameManager 스크립트를 구현한다. 싱글턴으로 구현한다. 유저의 데이터를 저장하는 PlayerData 클래스를 선언한다.
public class PlayerData
{
public string playerID;
public int level;
public int curEXP;
public int curHP;
public int maxHP;
public int curMP;
public int maxMP;
public int gold;
public int uidCounter; // for create item uid
public InventoryData inventoryData;
}
그리고 CreatePlayerData에서는 초기 플레이어의 값을 설정한다.
public void CreatePlayerData(string playerID)
{
data = new PlayerData();
data.playerID = playerID;
data.level = 0;
data.curEXP = 0;
data.curHP = data.maxHP = 100;
data.curMP = data.maxMP = 100;
data.gold = 10000;
data.uidCounter = 0;
data.inventoryData = new InventoryData();
}
그리고 SaveData는 JsonUtility.ToJson 함수를 이용해서 json 파일 형식으로 data를 변환하고 File.WriteAllText 함수를 이용해서 저장한다. 그리고 LoadData를 이용해서 파일을 불러오고 JsonUtility.FromJson 함수로 json 형식의 파일을 다시 PlayerData로 저장한다. 여기까지가 Player의 데이터를 GameManager에서 다룬 것이고 이제 아이템의 습득에 대해서 처리를 구현한다. LootingItem 함수에서 AddItem을 호출해서 아이템을 저장한다. 그리고 StartManager 함스에서 dataPath를 정해준다.

위 사진은 인벤토리 UI를 구현한 것이다. 각 슬롯은 버튼으로 이루어져있다.