우선 InputAction에서 Generate C# class를 켜주고 Apply해서 코드로 불러낼 수 있도록 해주자.
이렇게 해준다음 Player 클래스를 생성하여 매핑된 Actions가 작동될 때 실행될 함수를 할당해주자.
우선 입력을 받아올 때 입력 이벤트의 context를 Vector2로 읽어드려서 어떤 방향에서의 입력인지 알아내고 이를 저장한다. 저장한 값이 0보다 크다면 해당 방향으로 이동을 시작하게 한다. 이때 키 입력이 끝났을 때 다시 입력값을 0으로 초기화 해주어야 정상적으로 작동한다.
보드는 모든 카드의 위치를 관리해주고 룰을 결정해준다. 카드는 4*5배열로 배치되게 하였다.
Board.cs
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
publicclassBoard : MonoBehaviour
{
[SerializeField] private GameObject cardPrefab;
[SerializeField] private Sprite[] cardSprites; // 카드 앞면 (1~6번 이미지)private List<Card> cards = new List<Card>();
private List<Card> selectedCards = new List<Card>(); // 선택된 카드들privateint rowCount = 5; //세로privateint colCount = 4; //가로privatefloat xStart = -2.1f;
privatefloat yStart = 3.3f;
privatefloat xSpacing = 1.4f;
privatefloat ySpacing = -1.8f;
privatevoidAwake(){
cardPrefab = Resources.Load<GameObject>("Prefabs/Card/Card");
LoadSprites();
ShuffleCards();
InitBoard();
}
voidLoadSprites(){
// Resources 폴더에서 "Sprites/0~9" 로드
cardSprites = new Sprite[10];
for (int i = 0; i < 10; i++)
{
cardSprites[i] = Resources.Load<Sprite>($"Sprites/Back/{i}");
}
}
publicvoidShuffleCards(){
List<Sprite> tempSprites = new List<Sprite>();
// 0~9번 카드 각각 2장씩 추가
foreach (var sprite in cardSprites)
{
tempSprites.Add(sprite);
tempSprites.Add(sprite);
}
// 랜덤 섞기
tempSprites = tempSprites.OrderBy(x => Random.value).ToList();
// cards 리스트에 카드 추가
cards.Clear();
for (int i = 0; i < tempSprites.Count; i++)
{
GameObject newCard = Instantiate(cardPrefab, Vector3.zero, Quaternion.identity, this.transform);
Card card = newCard.GetComponent<Card>();
card.SetCard(tempSprites[i], this);
cards.Add(card);
}
}
publicvoidInitBoard(){
int index = 0;
for (int i = 0; i < rowCount; i++)
{
for (int j = 0; j < colCount; j++)
{
if (index >= cards.Count) return; // 카드 개수 초과 방지// 위치 설정
Vector3 pos = newVector3(xStart + (xSpacing * j), yStart + (ySpacing * i), 0);
// 기존에 생성된 카드 객체를 위치만 변경
cards[index++].transform.position = pos;
}
}
}
publicvoidSelectCard(Card card){
if (selectedCards.Contains(card) || selectedCards.Count >= 2)
return;
selectedCards.Add(card);
if (selectedCards.Count == 2)
{
CheckMatch();
}
}
voidCheckMatch(){
if (selectedCards.Count < 2) return; // 두 장 선택되지 않으면 비교 불가if (selectedCards[0].GetSprite() == selectedCards[1].GetSprite())
{
// 같은 카드라면 유지
selectedCards.Clear();
Managers.Audio.PlaySound("Match"); // 카드 맞추면 효과음 재생!
}
else
{
// 다른 카드라면 1초 후 다시 뒤집기Invoke(nameof(ResetCards), 1f);
}
}
voidResetCards(){
foreach (var card in selectedCards)
{
card.FlipBack();
}
selectedCards.Clear();
}
publicintGetSelectedCardCount()=> selectedCards.Count;
public List<Card> GetCards()=> cards;
}
실제로 배치된 모습을 보면 다음과 같다
이제 GameManager를 통해 이 보드의 초기화 함수인 Init을 호출해주고 코루틴 함수를 통해 모든 카드를 오픈하고 다시 뒤집어서 유저가 게임을 진행할 수 있게 한다. 또한 시간을 관리해주고 게임이 이겼는지 확인해준다.
GamaManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.PlayerLoop;
using UnityEngine.UI;
usingstatic UnityEngine.RuleTile.TilingRuleOutput;
publicclassGameManager
{private Board board;
private List<Card> cards;
privatebool isGameActive = false;
privatefloat gameTime = 60f; // 총 게임 시간privatefloat remainingTime; // 현재 남은 시간private Slider timeSlider;
private Image sliderFill; // 슬라이더의 Fill 색상 변경용private TextMeshProUGUI timeText; // 남은 시간을 표시하는 UIpublic event Action<float> OnTimeUpdated;
publicvoidInit(){
board = GameObject.Find("Board")?.GetComponent<Board>();
timeSlider = GameObject.Find("TimeOutSlider")?.GetComponent<Slider>();
timeText = GameObject.Find("TimeOutText")?.GetComponent<TextMeshProUGUI>();
if (board == null || timeSlider == null || timeText == null)
{
Debug.LogError("GameManager 초기화 실패 - 필수 UI 요소가 없음.");
return;
}
// 게임 시작
isGameActive = true;
sliderFill = timeSlider.fillRect.GetComponent<Image>();
remainingTime = gameTime;
CoroutineHelper.StartCoroutine(StartGameSequence());
}
IEnumerator StartGameSequence(){
// 보드가 초기화될 시간을 기다림yield returnnewWaitForSeconds(0.3f);
cards = board.GetCards();
// 모든 카드 공개 (처음 1초 동안)
foreach (var card in cards)
{
card.FlipCard();
}
yield returnnewWaitForSeconds(1.5f);
// 다시 뒤집기
foreach (var card in cards)
{
card.FlipBack();
}
yield returnnewWaitForSeconds(0.3f);
// 타이머 UI 활성화
timeSlider.gameObject.SetActive(true);
timeText.gameObject.SetActive(true);
Managers.Audio.PlayBGM("BGM");
CoroutineHelper.StartCoroutine(UpdateTimer());
}
IEnumerator UpdateTimer(){
while (remainingTime > 0 && isGameActive)
{
remainingTime -= Time.deltaTime;
timeSlider.value = remainingTime;
OnTimeUpdated?.Invoke(remainingTime); // UI 업데이트 호출if (CheckWinCondition())
{
GameOver(true);
yield returnnewWaitForSeconds(1f);
yield break;
}
yield return null;
}
if (remainingTime <= 0)
{
GameOver(false);
}
}
privateboolCheckWinCondition(){
foreach (var card in board.GetCards())
{
if (!card.IsFlipped()) returnfalse;
}
returntrue;
}
privatevoidGameOver(bool isWin){
isGameActive = false;
Time.timeScale = 0.0f;
CoroutineHelper.StartCoroutine(GameOverSequence(isWin));
}
private IEnumerator GameOverSequence(bool isWin){
yield returnnewWaitForSecondsRealtime(0.5f); // 0.5초 딜레이 후 실행// DOTween의 모든 트위닝을 제거
DG.Tweening.DOTween.KillAll();
if (isWin)
{
Managers.UI.ShowPopupUI<UI_Success>();
}
else
{
Managers.UI.ShowPopupUI<UI_GameOver>();
}
}
}
그리고 게임에 필요한 UI는 UI_Game으로 묶어서 관리해주도록 했다. 옵저버 패턴을 사용하여 시간이 지남에 따라 색깔이 바뀌고 슬라이더의 바가 줄어들도록 구현했다. 이때 각 오브젝트의 이름과 Bind하는 enum 변수들의 이름이 같아야 정상적으로 Bind가 이루어진다.
UI_Game.cs
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
publicclassUI_Game : UI_Scene
{
enumButtons
{
EscapeButton,
}
enumTexts
{
TimeOutText,
}
enumGameObjects
{
TimeOutSlider,
}
privatefloat gameTime = 60f; // 총 게임 시간publicoverridevoidInit(){
base.Init(); // 상위 클래스의 초기화 메서드 호출
Bind<Button>(typeof(Buttons));
Bind<TextMeshProUGUI>(typeof(Texts));
Bind<GameObject>(typeof(GameObjects));
GetObject((int)GameObjects.TimeOutSlider).GetComponent<Slider>().maxValue = gameTime;
GetObject((int)GameObjects.TimeOutSlider).GetComponent<Slider>().value = gameTime;
GetButton((int)Buttons.EscapeButton).gameObject.AddUIEvent(PauseOrResume);
Managers.Game.OnTimeUpdated += UpdateTimeUI;
}
voidPauseOrResume(PointerEventData eventData){
// 1. 뭐든지 열려있으면 다 닫기// 2. 아무것도 없으면 열기if (Managers.UI.GetStackSize() > 0)
Managers.UI.CloseAllPopupUI();
else
Managers.UI.ShowPopupUI<UI_PausePopup>();
}
privatevoidUpdateTimeUI(float time){
GetText((int)Texts.TimeOutText).text = Mathf.CeilToInt(time).ToString();
GetObject((int)GameObjects.TimeOutSlider).GetComponent<Slider>().value = time;
UpdateTimeColor(time);
}
privatevoidUpdateTimeColor(float time){
float normalizedTime = time / gameTime;
Color startColor = newColor(0.96f, 0.55f, 0.0f);
Color endColor = newColor(1.0f, 0.0f, 0.0f);
Color timeColor = Color.Lerp(endColor, startColor, normalizedTime);
GetText((int)Texts.TimeOutText).color = timeColor;
GetObject((int)GameObjects.TimeOutSlider).GetComponent<Slider>().fillRect.GetComponent<Image>().color = timeColor;
}
privatevoidOnDisable(){
Managers.Game.OnTimeUpdated -= UpdateTimeUI;
}
}
그리고 다양한 Mob이 있을 수 있기때문에 Mob이 가지고 있어야 할 함수와 변수를 남은 abstract 객체를 선언해주자. 그리고 기본적인 정보는 FlyWeight 패턴을 사용하여 Scriptable Object를 참조하도록 했다.
BaseMobController.cs
using System.Collections.Generic;
using UnityEngine;
public abstract classBaseMobController : MonoBehaviour
{
protected IMobState currentState;
public MobData mobData;
public IMobState GetCurrentState(){
return currentState;
}
public abstract voidChangeState(IMobState state);
public abstract floatGetDetectionRange();
public abstract floatGetAttackRange();
public abstract floatGetChasableRange();
public abstract Vector2 GetPatrolPointA();
public abstract Vector2 GetPatrolPointB();
public abstract voidSetDestination(Vector2 destination);
public abstract voidSetPatrolPoints(Vector2 pointA, Vector2 pointB);
public abstract boolIsPlayerDetected();
public abstract voidMove(Vector2 target);
}
에디터를 수정해주려면 [CustomEditor(typeof(BaseMobController), true)] 이러한 코드를 클래스전에 선언해주어야한다. 또한 클래스에서 Editor 클래스를 상속받아 코드를 구현해야한다. setDirty를 통해 진행상황이 바로 저장되도록 구현하였다.
MobControllerEditor.cs
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(BaseMobController), true)] // 이제 모든 MobController 파생 클래스에 대해 이 에디터를 사용할 수 있습니다.publicclassMobControllerEditor : Editor
{
privatefloat _handleSize = 5f; // Scene view에서의 핸들 크기를 더 작게 조정publicoverridevoidOnInspectorGUI(){
base.OnInspectorGUI(); // 기존 인스펙터 GUI 요소를 그린다.
BaseMobController mob = (BaseMobController)target;
if (GUILayout.Button("Initialize Patrol Points"))
{
Vector2 center = mob.transform.position;
Vector2 pointA = center + newVector2(2f, 0f);
Vector2 pointB = center + newVector2(-2f, 0f);
mob.SetPatrolPoints(pointA, pointB);
//변경사항 적용
EditorUtility.SetDirty(mob);
}
}
protectedvoidOnSceneGUI(){
BaseMobController mob = (BaseMobController)target;
EditorGUI.BeginChangeCheck();
Vector3 pointAWorld = mob.GetPatrolPointA();
Vector3 pointBWorld = mob.GetPatrolPointB();
// 🔹 Scene 뷰 줌 레벨에 따라 핸들 크기 자동 조정float handleSize = HandleUtility.GetHandleSize(mob.transform.position) * 0.2f;
// 현재 상태에 따라 범위를 다르게 그리기
IMobState currentState = mob.GetCurrentState();
if (currentState is PatrolState)
{
DrawDetectionRange(mob);
DrawAttackRange(mob);
}
elseif (currentState is ChaseState)
{
DrawChasableRange(mob);
DrawAttackRange(mob);
}
elseif (currentState is AttackState)
{
DrawAttackRange(mob);
}
Handles.color = Color.red;
pointAWorld = Handles.FreeMoveHandle(pointAWorld, handleSize, Vector3.zero, Handles.SphereHandleCap);
Handles.Label(pointAWorld, "Patrol Point A");
Handles.color = Color.blue;
pointBWorld = Handles.FreeMoveHandle(pointBWorld, handleSize, Vector3.zero, Handles.SphereHandleCap);
Handles.Label(pointBWorld, "Patrol Point B");
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(mob, "Change Patrol Points");
mob.SetPatrolPoints(pointAWorld, pointBWorld);
EditorUtility.SetDirty(mob);
}
Handles.DrawLine(pointAWorld, pointBWorld);
}
privatevoidDrawDetectionRange(BaseMobController mob){
Handles.color = Color.yellow;
Handles.DrawWireArc(mob.transform.position, Vector3.forward, Vector3.up, 360, mob.GetDetectionRange());
}
privatevoidDrawChasableRange(BaseMobController mob){
Handles.color = Color.yellow;
Handles.DrawWireArc(mob.transform.position, Vector3.forward, Vector3.up, 360, mob.GetChasableRange());
}
privatevoidDrawAttackRange(BaseMobController mob){
Handles.color = Color.red;
Handles.DrawWireArc(mob.transform.position, Vector3.forward, Vector3.up, 360, mob.GetAttackRange());
}
}
이렇게 해주면 Scene창에서 캐릭터의 Patorl Point를 눈으로 보고 직접 수정해줄 수 있으며 초기화해줄 수 있다.
지금 아이템코드는 아이템의 타입에 따라 Switch문을 통해 다른 사용효과를 부여하고 있는데 이렇게 하면 나중에 코드가 복잡해질 수 도 있고 확장성에 좋지 않다고 생각해서 인터페이스를 만들고 이를 상속받는 다양한 아이템 클래스를 만들어주겠다.
IItemBase.cs
using Controllers.Entity;
using UnityEngine;
usingstatic Define;
public interface IItemBase
{
ItemType ItemType { get; } // 아이템 유형
ItemData ItemData { get; set; }
int Quantity { get; }
voidUse(); // 아이템 사용voidInitialize();
}
이를 상속받는 장비 클래스와 소모품 클래스를 만들어주자.
이때 각각의 클래스는 주울 수 있어야하고 실제 프리펩에 부착되어야 하기 때문에 Monobehaviour와 IPickable 또한 상속받도록 해주었다.
Equiment.cs
using Controllers.Entity;
using UnityEngine;
usingstatic Define;
publicclassEquipment : MonoBehaviour, IItemBase, IPickable
{
[SerializeField] private ItemData itemData; // Inspector에서 할당할 수 있는 필드
[SerializeField] privateint quantity;
public ItemType ItemType => ItemType.Equipment;
public ItemData ItemData
{
get => itemData;
set => itemData = value;
}
publicint Quantity
{
get => quantity;
set => quantity = 1;
}
publicvoidInitialize(){
if (ItemData == null)
{
Debug.LogError("ItemData가 설정되지 않았습니다.");
return;
}
// 공통 초기화 로직 (예: 아이콘 설정)
var spriteRenderer = GetComponent<SpriteRenderer>();
if (spriteRenderer != null && ItemData.icon != null)
{
spriteRenderer.sprite = ItemData.icon;
}
}
publicvoidPickup(){
bool addedSuccessfully = Managers.Inventory.TryAddItem(this.ItemData.itemID,quantity);
if (addedSuccessfully)
{
Destroy(gameObject);
}
else
{
Debug.LogWarning("아이템을 인벤토리에 추가할 수 없습니다.");
}
}
publicvoidUse(){
if( Managers.Inventory.EquippedWeapon != null )
{
Managers.Inventory.UnEquipCurrentWeapon();
}
else
{
Managers.Inventory.EquipWeapon(this.ItemData.itemID);
}
}
}
Consumable.cs
using Controllers.Entity;
using UnityEngine;
usingstatic Define;
publicclassConsumable : MonoBehaviour, IItemBase, IPickable
{
[SerializeField] private ItemData itemData; // Inspector에서 할당할 수 있는 필드
[SerializeField] privateint quantity;
public ItemType ItemType => ItemType.Consumable;
public ItemData ItemData
{
get => itemData;
set => itemData = value;
}
publicint Quantity
{
get=> quantity;
set => quantity = value;
}
publicvoidInitialize(){
if (ItemData == null)
{
Debug.LogError("ItemData가 설정되지 않았습니다.");
return;
}
// 공통 초기화 로직 (예: 아이콘 설정)
var spriteRenderer = GetComponent<SpriteRenderer>();
if (spriteRenderer != null && ItemData.icon != null)
{
spriteRenderer.sprite = ItemData.icon;
}
}
publicvoidPickup(){
bool addedSuccessfully = Managers.Inventory.TryAddItem(this.ItemData.itemID,quantity);
if (addedSuccessfully)
{
Destroy(gameObject);
}
else
{
Debug.LogWarning("아이템을 인벤토리에 추가할 수 없습니다.");
}
}
publicvoidUse(){
Debug.Log($"장비 아이템 '{ItemData.itemName}'를 장착합니다.");
Managers.Inventory.UseItem(this.ItemData.itemID);
}
}
이를 실제 프리펩에 부착하고 테스트해보자.
이렇게 나타나는 것을 볼 수 있다. 이에 맞춰 인벤토리 코드도 수정을 해주자. 주요한 수정사항은 아이템 추가시에 맵에 배치된 아이템과 관련된 정보를 가져와서 추가해준다는 점이다. 이때 나는 계속 아이템을 찾는 부하를 줄이기 위해 시작시에 모든 아이템의정보를 캐싱해서 사용했다.
using System;
using System.Collections.Generic;
using UnityEngine;
publicclassInventoryManager : MonoBehaviour
{
private Dictionary<int, ItemSlot> ownedItems = new Dictionary<int, ItemSlot>(); // 소유한 아이템과 수량private Dictionary<int, ItemData> itemDataCache = new Dictionary<int, ItemData>(); // 캐싱된 아이템 데이터private Dictionary<int, IItemBase> ItemCache = new Dictionary<int, IItemBase>(); // 캐싱된 아이템 데이터public Equipment EquippedWeapon { get; private set; }
publicvoidInit(){
CacheItemData();
EquippedWeapon = null;
}
// 1. 아이템 데이터를 캐싱하여 메모리에 저장privatevoidCacheItemData(){
ItemData[] items = Resources.LoadAll<ItemData>("Items");
GameObject[] itemDatas = Resources.LoadAll<GameObject>("Prefabs/Items");
foreach (var item in items)
{
if (!itemDataCache.ContainsKey(item.itemID))
{
//Debug.Log(item.itemName + "아이템 데이터 캐싱 완료");
itemDataCache[item.itemID] = item;
}
}
foreach (var item in itemDatas)
{
IItemBase itemBase = item.GetComponent<IItemBase>();
if (!ItemCache.ContainsKey(itemBase.ItemData.itemID))
{
Debug.Log(itemBase.ItemData.itemName + "아이템 데이터 캐싱 완료");
ItemCache[itemBase.ItemData.itemID] = itemBase;
}
}
}
// 2. 아이템 데이터 검색public ItemData FindItemDataByID(int itemID){
if (itemDataCache.TryGetValue(itemID, out var itemData))
{
return itemData;
}
Debug.LogWarning($"아이템 데이터를 찾을 수 없습니다: ID = {itemID}");
return null;
}
public IItemBase FindItemBaseByID(int itemID){
if (ItemCache.TryGetValue(itemID, out var itemBase))
{
return itemBase;
}
Debug.LogWarning($"아이템 데이터를 찾을 수 없습니다: ID = {itemID}");
return null;
}
// 3. 아이템 추가publicboolTryAddItem(int itemID, int quantity = 1){
// 아이템 데이터를 가져옴if (!itemDataCache.TryGetValue(itemID, out var itemData))
{
Debug.LogWarning($"추가하려는 아이템 데이터(ID: {itemID})가 유효하지 않습니다.");
returnfalse;
}
// 기존 아이템이 있는 경우 수량 증가if (ownedItems.TryGetValue(itemID, out var existingSlot))
{
existingSlot.Quantity += quantity;
Debug.Log($"아이템 '{itemData.itemName}'의 수량이 {existingSlot.Quantity}로 증가했습니다.");
}
else
{
// 새로운 아이템 슬롯 추가
ownedItems[itemID] = newItemSlot(FindItemBaseByID(itemID), quantity);
Debug.Log($"새로운 아이템 '{itemData.itemName}'이(가) {quantity}개 추가되었습니다.");
}
RefreshUI();
returntrue;
}
// 4. 아이템 제거publicvoidRemoveItem(int itemID, int quantity = 1){
if (!ownedItems.TryGetValue(itemID, out var itemSlot))
{
Debug.LogWarning($"제거하려는 아이템(ID: {itemID})이(가) 없습니다.");
return;
}
itemSlot.Quantity -= quantity;
if (itemSlot.Quantity <= 0)
{
ownedItems.Remove(itemID);
Debug.Log($"아이템 '{FindItemDataByID(itemID)?.itemName}'이(가) 인벤토리에서 제거되었습니다.");
}
else
{
Debug.Log($"아이템 '{FindItemDataByID(itemID)?.itemName}'의 수량이 {itemSlot.Quantity}로 줄었습니다.");
}
RefreshUI();
}
// 5. 아이템 사용publicvoidUseItem(int itemID){
if (!ownedItems.TryGetValue(itemID, out var itemSlot))
{
Debug.LogWarning($"사용하려는 아이템(ID: {itemID})이(가) 없습니다.");
return;
}
var itemData = FindItemDataByID(itemID);
if (itemData == null)
{
Debug.LogWarning($"아이템 데이터를 찾을 수 없습니다: ID = {itemID}");
return;
}
Debug.Log($"아이템 '{itemData.itemName}' 사용됨.");
itemSlot.Item?.Use();
// 소비 아이템은 사용 후 제거if (itemData.itemType != Define.ItemType.Equipment)
{
RemoveItem(itemID, 1);
}
RefreshUI();
}
// 6. 무기 장착publicvoidEquipWeapon(int itemID){
if (!ownedItems.TryGetValue(itemID, out var itemSlot))
{
Debug.LogWarning($"장착하려는 무기(ID: {itemID})이(가) 없습니다.");
return;
}
if (EquippedWeapon != null)
{
UnEquipCurrentWeapon();
}
EquippedWeapon = (Equipment)itemSlot.Item;
RemoveItem(itemID, 1);
Debug.Log($"무기 '{EquippedWeapon.ItemData.itemName}' 장착 완료.");
RefreshUI();
}
// 7. 장착 해제publicvoidUnEquipCurrentWeapon(){
if (EquippedWeapon == null)
{
Debug.LogWarning("장착된 무기가 없습니다.");
return;
}
TryAddItem(EquippedWeapon.ItemData.itemID, 1);
Debug.Log($"무기 '{EquippedWeapon.ItemData.itemName}' 해제 완료.");
EquippedWeapon = null;
RefreshUI();
}
// 8. 인벤토리 UI 갱신publicvoidRefreshUI(){
UI_ItemSel uI_ItemSel = FindAnyObjectByType<UI_ItemSel>();
if (uI_ItemSel != null)
{
Managers.UI.ClosePopupUI(uI_ItemSel);
}
UI_Inventory inventoryUI = Managers.UI.GetTopPopupUI() as UI_Inventory;
if (inventoryUI != null)
{
inventoryUI.RefreshUI();
}
}
// 9. 저장publicvoidSaveInventory(ref SaveData saveData){
saveData.inventoryData = new List<SerializedItemSlot>();
foreach (var slot in ownedItems)
{
saveData.inventoryData.Add(new SerializedItemSlot
{
itemID = slot.Key,
quantity = slot.Value.Quantity
});
}
saveData.equippedWeaponID = EquippedWeapon?.ItemData.itemID ?? -1;
}
// 10. 로드publicvoidLoadInventory(SaveData saveData){
if (saveData.inventoryData == null) return;
ownedItems.Clear();
foreach (var serializedSlot in saveData.inventoryData)
{
var itemBase = FindItemBaseByID(serializedSlot.itemID);
if (itemBase != null)
{
ownedItems[serializedSlot.itemID] = newItemSlot(itemBase, serializedSlot.quantity);
}
}
if (saveData.equippedWeaponID != -1)
{
var weaponData = FindItemBaseByID(saveData.equippedWeaponID);
Debug.Log("현재 장착된 무기 : " + weaponData.ItemData.itemName + " 로드완료");
if (weaponData != null)
{
EquippedWeapon = (Equipment)weaponData;
}
}
RefreshUI();
}
public List<ItemSlot> GetOwnedItems(){
returnnew List<ItemSlot>(ownedItems.Values);
}
}
C#으로 사용하는 Unity에서는 제네릭 함수가 많은데 이때 벨류타입의 제네릭을 많이 사용하게 되면 공유 코드를 사용하지 못할 때 JIT(Just In Time)방식, C#에서 IL(Intermediate Language)라고 중간 언어로 변환을 하는데 유저 코드 자체를 실시간으로 파싱하기 때문에 굉장히 느린데 JIT방식은 중간 형태의 .Net Assembly를 가지고 Target device에 맞게 컴파일 하기 때문에 인터프리터보다 빠르다. 이렇게 빌드되기 때문에 빌드 사이즈가 커질 수록 시간이 오래 걸린다. 하지만 Faster builds 방식은 이 사이즈와 시간을 감소 시킬 수 있지만 런타임 퍼포먼스가 떨어진다.
2. Sprite Atlas
sprite가 여러개 있는 경우 Draw Call이 각각의 Sprite마다 실행되는데 Sprite Atlas를 사용하는 경우에는 이 Draw Call이 하나로 묶여서 실행된다.
※Layer로 오브젝트들을 묶어주게 되면 이 또한 하나로 묶어서 DrawCall이 발생한다.
이렇게 만들고 게임 데이터를 가져오는 기능을 만들어주자. 이때 만약 저장데이터가 없다면 기본 데이터를 저장해주고
다른 시스템을 초기화해주는 기능도 넣어주도록 하자.
SaveManager.cs
publicvoidLoadData(){
if (!File.Exists(_defaultPath))
{
Debug.LogWarning("저장된 데이터가 없습니다. 초기 데이터를 생성합니다.");
// 초기 데이터를 생성CreateInitialData();
SaveData(); // 초기 데이터를 저장return;
}
string json = File.ReadAllText(_defaultPath);
SaveData saveData = JsonUtility.FromJson<SaveData>(json);
SpawnPlayer();
Managers.Quest.Init();
Managers.Inventory.LoadInventory(saveData);
Managers.Game.GetPlayer().GetComponent<Player>().LoadPlayer(saveData);
Managers.Quest.LoadQuestsFromSaveFile(saveData);
Debug.Log("게임 데이터가 불러와졌습니다.");
}
privatevoidCreateInitialData(){
SpawnPlayer();
// 인벤토리 초기화
Managers.Inventory.Init();
//Managers.Inventory.AddItem(Resources.Load<ItemData>("Items/Potion"), 5); // 기본 아이템 추가//Managers.Inventory.AddItem(Resources.Load<ItemData>("Items/Sword"), 1); // 기본 무기 추가// 플레이어 초기화
var player = Managers.Game.GetPlayer().GetComponent<Player>();
player.transform.position = Vector3.zero; // 기본 위치
player.CurrentHealth = 100f; // 기본 체력
player.index = 0; // 초기 퀘스트 인덱스// 플레이어 정보 출력
Debug.Log("=== 플레이어 초기화 ===");
Debug.Log($"위치: {player.GetComponent<Player>().transform.position}");
Debug.Log($"체력: {player.GetComponent<Player>().CurrentHealth}");
Debug.Log($"퀘스트 진행 상태: {player.GetComponent<Player>().index}");
// 퀘스트 초기화
Managers.Quest.Init();
}
publicvoidSpawnPlayer(){
GameObject player = Managers.Game.Spawn(Define.WorldObject.Player, "Player");
CinemachineVirtualCamera virtualCamera = GameObject.FindObjectOfType<CinemachineVirtualCamera>();
virtualCamera.Follow = player.transform; // virtual camera의 Follow에 플레이어 등록
}