[Jem Clash] 개발일지 #7 - 씬, UI 추가 및 아이템 데이터 연동
씬 및 상점 UI 추가
저번 포스팅 이후 일주일 만에 올리네요
설 연휴라서 개발 속도가 조금 느리기도 했고, 구현한 부분이 난이도가 있어서 애를 조금 먹었습니다
바로 시작해 볼게요
먼저 현재 게임 씬만 만들어준 상태였는데, 다른 씬들을 추가로 만들어주었습니다
간단하게 타이틀 씬부터 만들어주고, 씬을 관리하는 스크립트도 추가해 주었습니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneChanger : MonoBehaviour
{
public void LoadGame()
{
SceneManager.LoadScene(1); // 게임 씬
}
public void QuitGame()
{
Application.Quit();
}
public void LoadOption()
{
SceneManager.LoadScene(2); // 옵션 씬
}
public void LoadMapSelect()
{
SceneManager.LoadScene(3); // 맵 선택 씬
}
}
구글링을 해보니 SceneManager로 클래스 이름을 설정하는 경우 클래스와 메서드가 겹치면서 버그가 난다고 하여 이름을 SceneChanger로 설정했어요
다음으로 게임에서 승리했을 때 플레이어가 게임 결과를 확인하고 아이템을 구매할 수 있는 상점 창을 만들어주었습니다
상점 창은 따로 다른 씬으로 이동하는 것이 아닌, 게임 화면 위에 나타나도록 해줬어요
이런 식으로 첫 번째 창은 클리어 시간, 획득한 골드량, 추가 획득하는 골드량을 표시해 주도록 만들었습니다
여기서 골드는 아이템을 구매할 때 사용되는 재화로, 게임에서 승리할 때 일정량을 지급받을 수 있습니다
또한 특정 아이템을 소유하고 있는 경우 골드를 추가로 획득할 수 있도록 할 예정인데, 나중에 구현해 보겠습니다
첫 번째 창에서 Continue 버튼을 누르면 아이템을 구매할 수 있는 두 번째 창으로 이동합니다
창의 왼쪽은 구매할 수 있는 아이템, 아이템 설명에 대한 내용을 담고 있으며 창의 오른쪽에는 새로고침 버튼, 현재 골드량, Continue 버튼을 넣어주었어요
사실 처음 기획한 UI는 조금 다른 형태였습니다
현재 UI의 중간에 빈 네모 칸은 아이템 설명이 들어갈 자리인데, 원래 저 부분을 없애고 아이템을 가로로 긴 막대 형태로 표현하려고 했어요
구매할 수 있는 아이템들을 담은 부모 오브젝트를 Vertical Layout Group으로 관리하고 있는데, 마우스를 버튼 위에 올리면 RPG 게임처럼 설명창이 뜨도록 하고 싶었습니다
그러나 위의 사진처럼 다른 버튼에 설명 창이 가려지는 버그가 나서 이를 코드로 수정도 해보고, 따로 캔버스를 만들어서 관리할까 생각해봤는데 그건 아닌 거 같아서 그냥 따로 설명 창 공간을 고정적으로 만들었어요
아이템을 막대 형태로 표현은 못하겠지만, 그래도 UI가 조금 덜 난잡해진 느낌이 들어 좋긴 하네요
아이템 데이터 연동
오늘 포스팅의 하이라이트입니다 🔥
사실 그동안 만들었던 게임들은 데이터를 많이 가져와야 할 필요가 없는 규모가 작은 게임들이어서 모든 데이터를 ScriptableObject로 관리하는 선에서 대부분 마무리가 되었습니다
하지만 현재 기획 중인 게임의 방향에 따르면 아이템의 숫자가 아주 많아질 예정이기에(적으면 50개, 많으면 100개를 생각하고 있어요 😂), 잘은 모르지만 일단 기존에 쓰던 방식을 사용하면 안 되겠다는 생각이 들었어요
유니티 에디터에서 ScriptableObject를 하나하나 클릭해 가며 밸런스를 잡거나 수치를 조절하고 있는 제 모습을 상상하니 너무 불행한 것 같아서, 다른 방법을 알아보았습니다
구글링을 해보니 JSON 파일을 만들어서 데이터를 관리하는 방법이 많이 사용되더라고요
실무에서도 JSON 파일을 사용하는 일이 잦다고 하니, 미리 부딪혀가며 공부해 보는 것도 나쁘지 않은 것 같습니다
우선 필요한 스크립트 목록을 나열해 봅시다
- ItemImporter: JSON 파일을 ScriptableObject로 변환하는 스크립트입니다
- ItemDatabase: ScriptableObject를 담는 데이터베이스입니다
- ItemSlot: 아이템 버튼에 달리는 스크립트입니다
- ItemShopUI: 구매할 수 있는 아이템을 관리하는 스크립트입니다
데이터 관리 방식은 다음과 같습니다
- 엑셀 파일에 데이터 테이블을 작성
- 엑셀 파일을 JSON 파일로 변환
- JSON 파일을 ItemImporter 스크립트를 이용해 ScriptableObject로 변환
- 변환된 ScriptableObject들을 ItemDatabase에 수동으로 추가
- ItemShopUI 스크립트를 통해 새로고침 기능 구현
1단계는 엑셀, 2단계는 파이썬 코드를 이용해 만드는 부분이기에 따로 첨부하진 않겠습니다
그럼 3단계부터 살펴보겠습니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Serializable]
public class ItemDataList
{
public List<ItemDataJSON> items;
}
[Serializable]
public class ItemDataJSON
{
public string itemName;
public string itemType;
public string itemRarity;
public string imagePath;
public string description;
public int maxLevel;
public float[] counts;
public int[] energyCosts;
public float cooldownTime;
}
ItemDataJSON 클래스는 JSON 파일에서 가져올 아이템의 데이터 구조를 정의합니다
itemName, itemType 등의 텍스트 정보와 maxLevel, cooldownTime 등의 숫자 정보가 들어있죠
ItemDataList 클래스의 items는 ItemDataJSON 객체들의 리스트로 구성됩니다
JSON 파일에 적힌 모든 아이템이 ItemDataJSON 객체 형태로 이 리스트에 저장되는 겁니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ItemImporter : MonoBehaviour
{
[MenuItem("Tools/Import Items from JSON")]
public static void ImportItems()
{
string path = "Assets/Resources/items.json";
if (!File.Exists(path))
{
Debug.LogError("JSON 파일을 찾을 수 없습니다");
return;
}
string json = File.ReadAllText(path);
ItemDataList itemList = JsonUtility.FromJson<ItemDataList>(json);
foreach (ItemDataJSON item in itemList.items)
{
UpgradeData newItem = ScriptableObject.CreateInstance<UpgradeData>();
newItem.itemName = item.itemName;
newItem.itemType = (UpgradeData.UpgradeType)Enum.Parse(typeof(UpgradeData.UpgradeType), item.itemType);
newItem.itemRarity = (UpgradeData.Rarity)Enum.Parse(typeof(UpgradeData.Rarity), item.itemRarity);
newItem.itemImage = Resources.Load<Sprite>(item.imagePath);
newItem.description = item.description;
newItem.maxLevel = item.maxLevel;
newItem.counts = item.counts;
newItem.energyCosts = item.energyCosts;
newItem.cooldownTime = item.cooldownTime;
AssetDatabase.CreateAsset(newItem, $"Assets/SO/Items/{item.itemName}.asset");
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("ScriptableObject로 변환 완료");
}
}
다음으로 JSON 파일을 읽고 ScriptableObject로 변환하는 부분입니다
ImportItems 함수는 유니티의 Tools 바에서 사용할 수 있도록 속성을 추가해 주고, path 경로에 있는 JSON 파일을 읽는 것으로 시작합니다
만약 없다면 에러 메시지를 리턴하고, 있다면 json 변수에 담아준 후 FromJson을 이용해 문자열을 ItemDataList 객체로 변환합니다
이러면 itemList의 items에 ItemDataJSON 객체들이 리스트로 담기게 됩니다
이후 foreach 문을 돌면서 newItem이라는 ScriptableObject를 새로 만들고, item에 담긴 값들을 속성에 맞게 저장해줍니다
마지막으로 CreateAsset 함수로 newItem을 실제 파일로 생성하여 프로젝트에 저장하고, SaveAssets 함수로 모든 변경 사항을 Unity 에디터의 애셋 데이터베이스에 저장한 후, Refresh 함수를 호출해 에디터에 변경 사항을 바로 반영합니다
임시로 path 경로에 items.json 파일을 직접 만들어서 테스트해 보겠습니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
"items": [
{
"itemName": "Blover",
"itemType": "Blover",
"itemRarity": "Common",
"imagePath": "Assets/Sprites/Items/blover.png",
"description": "바람을 부는 능력입니다",
"maxLevel": 5,
"counts": [4, 6, 8, 10, 12],
"energyCosts": [10, 20, 50, 100, 200],
"cooldownTime": 2
},
{
"itemName": "Meteor",
"itemType": "Meteor",
"itemRarity": "Common",
"imagePath": "Assets/Sprites/Items/meteor.png",
"description": "메테오 능력입니다",
"maxLevel": 5,
"counts": [4, 6, 8, 10, 12],
"energyCosts": [10, 20, 50, 100, 200],
"cooldownTime": 2
},
{
"itemName": "Gravity",
"itemType": "Gravity",
"itemRarity": "Common",
"imagePath": "Assets/Sprites/Items/gravity.png",
"description": "중력 능력입니다",
"maxLevel": 5,
"counts": [4, 6, 8, 10, 12],
"energyCosts": [10, 20, 50, 100, 200],
"cooldownTime": 2
},
{
"itemName": "Fireworks",
"itemType": "Fireworks",
"itemRarity": "Common",
"imagePath": "Assets/Sprites/Items/fireworks.png",
"description": "불꽃 능력입니다",
"maxLevel": 5,
"counts": [4, 6, 8, 10, 12],
"energyCosts": [10, 20, 50, 100, 200],
"cooldownTime": 2
}
]
}
이렇게 json 파일을 작성해 주고 유니티 Tools 바의 버튼을 클릭해 주면,
작성해 준 데이터가 모두 정상적으로 들어간 것을 확인할 수 있습니다 😍
이제 ScriptableObject를 일일이 만들어주는 게 아니라, 그냥 엑셀에서 데이터를 작성하고 JSON 파일로 변환한 다음 유니티 내에서 버튼 하나만 눌러주면 필요한 모든 ScriptableObject가 한 번에 생성됩니다
앞으로 작업을 할 때 정말 큰 도움이 될 것 같은 기능이네요 👍
이제 만들어준 데이터를 ItemDatabase에 추가해 주는 4단계와, 새로고침 기능을 구현하는 5단계가 남았습니다
일단 4단계부터 해보죠
먼저 ItemDatabase 스크립트를 만들어준 다음,
1
2
3
4
5
6
7
8
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "ItemDatabase", menuName = "ScriptableObjects/ItemDatabase")]
public class ItemDatabase : ScriptableObject
{
public List<UpgradeData> items;
}
네 끝났습니다
그냥 ItemDatabase ScriptableObject를 하나 생성해 주고, items 리스트에 모든 아이템 ScriptableObject를 드래그해서 넣어주면 됩니다 😅
5단계로 넘어갑시다
먼저 아이템 버튼에 달리는 ItemSlot 스크립트를 생성해 줍니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class ItemSlot : MonoBehaviour
{
public Image itemImage;
public TMP_Text itemNameText;
public void SetItem(UpgradeData item)
{
if (item == null)
{
itemImage.enabled = false;
itemNameText.text = "";
return;
}
itemImage.enabled = true;
itemImage.sprite = item.itemImage;
itemNameText.text = item.itemName;
}
}
현재는 아이템 이미지와 이름만 적용시켜 줄 거고, 다른 수치들은 이후에 적용할 예정이에요
그다음 구매할 수 있는 아이템을 보여주는 ItemShopUI 스크립트를 만들어줍니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class ItemShopUI : MonoBehaviour
{
private const int DISPLAY_ITEM_COUNT = 4;
public ItemDatabase itemDatabase;
public ItemSlot[] itemSlots;
private List<UpgradeData> currentItems = new();
private void Start()
{
RerollShop();
}
public void RerollShop()
{
currentItems = GetRandomItems(DISPLAY_ITEM_COUNT);
for (int i = 0; i < itemSlots.Length; i++)
if (i < currentItems.Count)
itemSlots[i].SetItem(currentItems[i]);
else
itemSlots[i].SetItem(null);
}
private List<UpgradeData> GetRandomItems(int count)
{
if (itemDatabase.items.Count < count)
{
Debug.LogError("아이템 개수가 부족합니다!");
return new List<UpgradeData>(itemDatabase.items);
}
var items = new List<UpgradeData>(itemDatabase.items);
int n = items.Count;
for (int i = 0; i < count; i++)
{
int randomIndex = Random.Range(i, n);
(items[i], items[randomIndex]) = (items[randomIndex], items[i]);
}
return items.Take(count).ToList();
}
}
itemDatabase 변수와 ItemSlot을 담아줄 itemSlots 배열을 생성해 줍니다
GetRandomItems 메서드는 현재 itemDatabase의 item 안에 있는 아이템 중 count개 만큼 아이템을 랜덤으로 선택해 리스트로 반환하는 함수입니다
랜덤으로 선택하는 알고리즘은 Fisher-Yates Shuffle 방식을 사용했는데, 심플하면서 편향되지 않게 섞어준다고 해서 괜찮은 것 같네요
에러 메시지 보여주는 건 혹시나 제가 items에 아이템들 넣어주는 걸 깜빡했을 때를 위해서입니다
한번에 선택할 수 있는 아이템의 개수는 4개로 고정이어서 const로 만들어줍니다
RerollShop 메서드에서 currentItems 변수에 GetRandomItems 메서드로 리턴한 리스트를 받아주고, SetItem 메서드를 통해 UI에 적용해 줍니다
마지막으로 상점을 관리할 ShopManager 스크립트를 만들어줍니다
1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using UnityEngine.UI;
public class ShopManager : MonoBehaviour
{
public ItemShopUI itemShopUI;
public Button refreshButton;
private void Start()
{
refreshButton.onClick.AddListener(() => itemShopUI.RerollShop());
}
}
이로써 코딩 작업은 모두 끝났네요 🫠
이제 게임 오브젝트들에 각각 스크립트를 달아주고, 함수와 오브젝트를 연결해 주는 일만 남았습니다
모두 연결해 준 다음 새로고침 기능도 테스트해 보겠습니다
아이템을 보여주는 UI에 스크립트를 연결하고, 최상위 오브젝트에 ShopManager 스크립트를 추가해 줍니다
새로고침 버튼까지 연결해서 실행해보면,
아이템들이 잘 바뀌는 것을 확인할 수 있습니다…?
왜인지 이미지가 안 보이는 버그가 발생하네요
스프라이트 경로도 잘 설정되어 있는데, 한번 확인해 봐야겠습니다
그래도 텍스트가 잘 변경되는 걸 보니 작동은 하는 것 같네요 👍
후기
분명히 많이 구현한 것 같은데, 정리해 놓고 보니 눈에 띄는 건 UI와 상점 새로고침밖에 없네요 하하…
그래도 JSON 변환기는 계속 요긴하게 써먹을 것 같아서 잘 만들어준 것 같습니다
처음 공부하는 내용도 많았고 로직을 어떻게 구현할지 고민할 때 삽질도 많이 해서 여러모로 고생을 많이 한 파트였으나 그만큼 많이 배운 것 같아 뿌듯하네요
특히 JSON은 공부할 내용이 정말 많아서 앞으로도 고생깨나 할 것 같네요 😅
게임을 만들 땐 어떤 데이터를 어떻게 관리할지 초기 단계에서 계획을 세워 진행하는 게 바람직하다는 교훈을 얻으며 이만 마무리하겠습니다 👋