기능만 만들고 정리를 안 한지 오래 돼서 이번 기회에 중간 정리를 하려고 한다. 주요 기능 설명은 현재 프로젝트 클래스 종류 설명 밑에 있다.
[구현한 기능]
- 인벤토리(아이템 누적, 아이템 설명, 아이템 이동, 아이템 판매, 아이템 종류 별 인벤토리 창, 아이템 칸 부족시 새로 칸 생성)
- 씬 이동 시 인벤토리 매니저에 참조 변수 값 찾아서 넣어주는 기능
- 농장 데이터 저장 기능
[현재 프로젝트 클래스 종류]
1. 농장
FarmingManager: 농사의 전반적인 시스템을 관리하는 클래스
Seed: 씨앗 클래스
Fruit: 과일 클래스(삭제)
SeedContainer: 씨앗 프리팹을 배열에 담아놓고, 씨앗을 생성할 때마다(땅에 심을 때) 풀에 넣어놓는 클래스
FruitContainer: 과일의 개수를 관리하는 클래스
SeedFruitUIManager: 씨앗 구매창, 선택창 UI 를 관리하는 클래스
SlotManager: BuySeedSlotManager 와 PlantSeedSlotManager 의 부모 클래스
BuySeedSlotManager: 씨앗 구매창의 슬롯을 관리하는 클래스
PlantSeedSlotManager: 씨앗 선택창의 슬롯을 관리하는 클래스
SellFruitSlotManager: 과일 판매창의 슬롯을 관리하는 클래스
FarmingData(MonoBehaviour 상속 받지 않는 클래스): 타일이 가지는 농사 데이터 클래스
2. 인벤토리
UIInventoryController: 인벤토리의 UI 와 데이터를 관리하는 클래스
UIInventoryPage: 인벤토리의 전반적인 UI 를 관리하는 클래스
UIInventoryDescription: 인벤토리 아이템의 정보를 관리하는 클래스
UIInventoryItem: 인벤토리 아이템 슬롯을 관리하는 클래스
UIMouseDragItem: 인벤토리 아이템 드래그 관련 클래스
ItemSellPanel: 아이템 판매 관련 클래스
ItemSO: 스크립터블 오브젝트 클래스(SeedItemSO, FruitItemSO 의 부모 클래스)
SeedItemSO: 씨앗 정보를 관리하는 스크립터블 오브젝트 클래스
FruitItemSO: 과일 정보를 관리하는 스크립터블 오브젝트 클래스
InventorySO: 인벤토리 정보를 관리하는 스크립터블 오브젝트 클래스
- FarmingManager
using Inventory.Model;
using JetBrains.Annotations;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Tilemaps;
using UnityEngine.UI;
using static UnityEditor.PlayerSettings;
[Serializable]
// 딕셔너리를 클래스 형식으로 key, value 를 만들어서 구성하가ㅣ..
// 다른 Dictionary 도 고려하여 제네릭 타입으로 만들기..
public class DataDictionary<TKey, TValue>
{
public TKey Key;
public TValue Value;
}
[Serializable]
public class JsonDataArray<TKey, TValue>
{
// 임의로 생성한 딕셔너리 값 저장용 리스트
public List<DataDictionary<TKey, TValue>> data;
}
public static class DictionaryJsonUtility
{
/// <summary>
/// Dictionary를 Json으로 파싱하기
/// </summary>
/// <typeparam name="TKey">Dictionary Key값 형식</typeparam>
/// <typeparam name="TValue">Dictionary Value값 형식</typeparam>
/// <param name="jsonDicData"></param>
/// <returns></returns>
public static string ToJson<TKey, TValue>(Dictionary<TKey, TValue> jsonDicData, bool pretty = false)
{
List<DataDictionary<TKey, TValue>> dataList = new List<DataDictionary<TKey, TValue>>();
DataDictionary<TKey, TValue> dictionaryData;
foreach (TKey key in jsonDicData.Keys)
{
dictionaryData = new DataDictionary<TKey, TValue>();
dictionaryData.Key = key;
dictionaryData.Value = jsonDicData[key];
dataList.Add(dictionaryData);
}
JsonDataArray<TKey, TValue> arrayJson = new JsonDataArray<TKey, TValue>();
arrayJson.data = dataList;
return JsonUtility.ToJson(arrayJson, pretty);
}
/// <summary>
/// Json Data를 다시 Dictionary로 파싱하기
/// </summary>
/// <typeparam name="TKey">Dictionary Key값 형식</typeparam>
/// <typeparam name="TValue">Dictionary Value값 형식</typeparam>
/// <param name="jsonData">파싱되었던 데이터</param>
/// <returns></returns>
public static Dictionary<TKey, TValue> FromJson<TKey, TValue>(string jsonData)
{
JsonDataArray<TKey, TValue> arrayJson = JsonUtility.FromJson<JsonDataArray<TKey, TValue>>(jsonData);
List<DataDictionary<TKey, TValue>> dataList = arrayJson.data;
Dictionary<TKey, TValue> returnDictionary = new Dictionary<TKey, TValue>();
for (int i = 0; i < dataList.Count; i++)
{
DataDictionary<TKey, TValue> dictionaryData = dataList[i];
returnDictionary.Add(dictionaryData.Key, dictionaryData.Value);
//returnDictionary[dictionaryData.Key] = dictionaryData.Value;
}
return returnDictionary;
}
}
[Serializable]
// 아예 따로 클래스 만들어서 값을 저장할 때 Vector3Int 대신 PosInt 써야할 것 같다..
public class PosInt
{
[SerializeField]
public int x;
[SerializeField]
public int y;
[SerializeField]
public int z;
}
// 데이터 저장 클래스
[Serializable]
public class SaveFarmingData
{
[SerializeField]
public bool seedOnTile; // 타일 위에 씨앗이 있는지 여부 확인용
[SerializeField]
public bool plowEnableState; // 밭을 갈 수 있는 상태인지 여부 확인용
[SerializeField]
public bool plantEnableState; // 씨앗을 심을 수 있은 상태인지 여부 확인용
[SerializeField]
public bool harvestEnableState; // 작물이 다 자란 상태인지 여부 확인용
[SerializeField]
public string currentState; // 농사 땅 상태..
// 씨앗 데이터
[SerializeField]
public int seedIdx; // 씨앗 인덱스 저장(종류 저장하기 위함)..
[SerializeField]
public float currentTime; // 씨앗을 심은 뒤로 흐른 시간 저장
[SerializeField]
public bool isGrown; // 씨앗이 다 자랐는지 안자랐는지 여부 저장..
public void PrintData()
{
Debug.Log(seedOnTile + " " + plowEnableState + " " + plantEnableState + " " + harvestEnableState + " " + currentState + " " + seedIdx + " " + currentTime + " " + isGrown);
}
}
// 타일이 가지는 농사 데이터
[Serializable]
class FarmingData
{
[SerializeField]
public Seed seed; // 타일이 가지는 씨앗 정보
//public bool seedOnTile; // 타일 위에 씨앗이 있는지 여부 확인용(씨앗이 있으면 밭을 갈 수 없음)
[SerializeField]
public bool plowEnableState; // 밭을 갈 수 있는 상태인지 여부 확인용(밭이 안 갈린 상태)
[SerializeField]
public bool plantEnableState; // 씨앗을 심을 수 있는 상태인지 여부 확인용
[SerializeField]
public bool harvestEnableState; // 작물이 다 자란 상태인지 여부 확인용
/*
Button과 같은 Unity 엔진 내장 컴포넌트들은 게임 오브젝트나 컴포넌트로 참조되어 있기 때문에 JSON 직렬화에서 제대로 다룰 수 없다고함..
Unity의 직렬화 시스템도 이러한 Unity 엔진 내장 객체들을 처리하는 방식과 JSON 직렬화는 다르기 때문에, 이런 컴포넌트들을 JSON으로 저장할 수는 없다고 함..
=> 일반적인 경우는 UI 요소들은 직렬화에서 제외한다고 함..
*/
[NonSerialized] public Button stateButton; // 타일을 누르면 타일 위에 뜨도록 하는 버튼
[NonSerialized] public Button[] buttons; // [0]: plow 버튼, [1]: plant 버튼, [2]: harvest 버튼
[SerializeField]
public string currentState = "None"; // 현재 상태(초기에는 아무것도 안 한 상태니까 None 으로.. -> plow: 밭 갈린 상태, plant: 씨앗 심은 상태, harvest: 다 자란 상태)
public void SetData()
{
plowEnableState = true;
}
}
public class FarmingManager : MonoBehaviour
{
[Header("Game Data")]
public Camera mainCamera; // 마우스 좌표를 게임 월드 좌표로 변환하기 위해 필요한 변수(카메라 오브젝트 할당해줄 것)
public SeedContainer seedContainer; // 현재 가진 씨앗을 가져오기 위해 필요한 변수(씨앗 컨테이너 게임 오브젝트 할당해줄 것)
public FruitContainer fruitContainer; // 수확한 과일을 저장하기 위해 필요한 변수(과일 컨테이너 게임 오브젝트 할당해줄 것)
public UIInventoryController inventoryController; // 인벤토리 관리하기 위해 필요한 변수(인벤토리 매니저 게임 오브젝트 할당해줄 것)
public Canvas canvas;
// 아이템 스크립터블 오브젝트를 저장해놓기..
public FruitItemSO[] fruitItems; // [0]: 사과, [1]: 바나나, [2]: 체리, [3]: 오렌지, [4]: 딸기
public SeedItemSO[] seedItems; // [0]: 사과, [1]: 바나나, [2]: 체리, [3]: 오렌지, [4]: 딸기
[Header("Tile")]
public TileBase borderTile; // 제한 구역 상태
public TileBase grassTile; // 밭 갈기 전 상태
public TileBase farmTile; // 밭 간 후 상태
public TileBase plantTile; // 씨앗 심은 후 상태
public TileBase harvestTile; // 과일 다 자란 상태
public Vector3Int prevSelectTile; // 이전 클릭된 타일
[Header("Tilemap")]
public Tilemap farmEnableZoneTilemap; // 농사 가능 부지를 나타내는 타일맵
public Tilemap farmTilemap; // 진짜로 현재 타일의 상태에 따라 타일이 변경되는 타일맵
[Header("Farm interaction Button")]
// 버튼을 프리팹으로 만들어 놓은 다음 동적으로 생성해서 쓸 것.
public GameObject[] buttonPrefabs; // [0]: plow 버튼, [1]: plant 버튼, [2]: harvest 버튼
public GameObject buttonParent; // 버튼 생성할 때 부모 지정하기 위한 변수
[Header("Farm interaction Panel")]
public GameObject growTimePanel; // 다 자라기까지 남은 시간 보여주는 판넬
public Text growTimeText; // 다 자라기까지 남은 시간
[Header("Farming Data")]
public Vector2 clickPosition; // 현재 마우스 위치를 게임 월드 위치로 바꿔서 저장
public Vector3Int cellPosition; // 게임 월드 위치를 타일 맵의 타일 셀 위치로 변환
Dictionary<Vector3Int, FarmingData> farmingData;
public int farmLevel = 0; // 농장 레벨. 농장 레벨 업그레이드 함수 호출하면 증가하도록..
public int expansionSize = 1; // 농장 한 번 업그레이트 할 때 얼마나 확장될 건지.. 일단 임시로 1로 해놨다.. 나중에 변경할 것.
[Header("PlantSeed Information")]
public GameObject plantSeedPanel; // 씨앗 선택창
public int selectedSeedIdx; // 현재 심을 씨앗 종류
public bool clickedSelectedSeedButton = false; // 이 값이 true 가 되면 씨앗 심기 함수 호출하도록(씨앗 심기 함수에서는 이 값을 다시 false 로 돌림)..
// 데이터 저장
[Header("Save Data")]
private string farmingDataFilePath; // 농사 데이터 저장 경로..
public void SaveFarmingData()
{
Dictionary<PosInt, SaveFarmingData> tempDic = new Dictionary<PosInt, SaveFarmingData>();
foreach (var item in farmingData)
{
Debug.Log(item.Key + "저장할겁니다!!");
// JSON 저장할 때 Vector3Int 가 직렬화가 안되므로 따로 만든 PosString 이용하가ㅣ..
PosInt pos = new PosInt
{
x = item.Key.x,
y = item.Key.y,
z = item.Key.z
};
SaveFarmingData temp = new SaveFarmingData
{
plowEnableState = farmingData[item.Key].plowEnableState,
plantEnableState = farmingData[item.Key].plantEnableState,
harvestEnableState = farmingData[item.Key].harvestEnableState,
currentState = farmingData[item.Key].currentState
};
// 농사 땅 위에 씨앗이 없을 때 진입..
if (farmingData[item.Key].seed == null)
{
Debug.Log("씨앗 없어여..");
temp.seedOnTile = false;
}
// 농사 땅 위에 씨앗 있을 때 진입..
else
{
Debug.Log("씨앗 있어여..");
temp.seedOnTile = true; // 땅에 씨앗 심어져있는지 여부 판단 정보 저장..
temp.seedIdx = farmingData[item.Key].seed.seedData.seedIdx; // 씨앗 인덱스 저장..
temp.currentTime = farmingData[item.Key].seed.currentTime; // 자라기 까지 남은 시간 저장..
// 만약 씨앗 다 자랐으면..
if (farmingData[item.Key].seed.isGrown)
{
temp.isGrown = true; // 씨앗 다 자란 상태를 변수에 저장..
}
}
tempDic.Add(pos, temp);
tempDic[pos].PrintData();
}
string json = DictionaryJsonUtility.ToJson(tempDic, true);
Debug.Log(json);
Debug.Log("데이터 저장 완료!");
// 외부 폴더에 접근해서 Json 파일 저장하기
// Application.persistentDataPath: 특정 운영체제에서 앱이 사용할 수 있도록 허용한 경로
File.WriteAllText(farmingDataFilePath, json);
}
// 씬 로드 된 후에 SetFarmingData 함수 먼저 호출한 후 호출할 함수..
public void LoadFarmingData()
{
// Json 파일 경로 가져오기
string path = Path.Combine(Application.persistentDataPath, "FarmingData.json");
// 지정된 경로에 파일이 있는지 확인한다
if (File.Exists(path))
{
Debug.Log("파일 있어여!!");
// 경로에 파일이 있으면 Json 을 다시 오브젝트로 변환한다.
string json = File.ReadAllText(path);
Debug.Log(json);
Dictionary<PosInt, SaveFarmingData> tempDic = DictionaryJsonUtility.FromJson<PosInt, SaveFarmingData>(json);
Debug.Log(tempDic.Count + "!!!!!!!!!!!!!!!!!!!!1");
foreach (var item in tempDic)
{
tempDic[item.Key].PrintData();
Vector3Int pos = new Vector3Int(item.Key.x, item.Key.y, item.Key.z);
switch (tempDic[item.Key].currentState)
{
// 현재 농사 땅 상태에 맞는 버튼으로 설정해주기..
case "None":
farmingData[pos].stateButton = farmingData[pos].buttons[0];
farmTilemap.SetTile(pos, grassTile); // 타일을 아무것도 안 한 상태로 변경(키 값이 농사땅의 pos 임)
break;
case "plow":
farmingData[pos].stateButton = farmingData[pos].buttons[1];
farmTilemap.SetTile(pos, farmTile); // 타일을 밭 갈린 모습으로 변경..
break;
case "plant":
farmingData[pos].stateButton = farmingData[pos].buttons[2];
farmTilemap.SetTile(pos, plantTile); // 타일을 씨앗 심은 모습으로 변경..
break;
case "harvest":
farmingData[pos].stateButton = farmingData[pos].buttons[2];
farmTilemap.SetTile(pos, harvestTile); // 타일을 다 자란 모습으로 변경..
break;
}
// 저장해놓은 데이터 가져와서 설정해주기..
farmingData[pos].plowEnableState = tempDic[item.Key].plowEnableState;
farmingData[pos].plantEnableState = tempDic[item.Key].plantEnableState;
farmingData[pos].harvestEnableState = tempDic[item.Key].harvestEnableState;
farmingData[pos].currentState = tempDic[item.Key].currentState;
// 저장당시 농사 땅 위에 씨앗 있었으면 씨앗 데이터 설정해주기..
if (tempDic[item.Key].seedOnTile)
{
// 씨앗 데이터 가져와서 데이터에 맞는 씨앗 생성해주기..
farmingData[pos].seed = seedContainer.GetSeed(tempDic[item.Key].seedIdx).GetComponent<Seed>();
// 기존 씨앗 데이터 적용..
farmingData[pos].seed.currentTime = tempDic[item.Key].currentTime;
farmingData[pos].seed.isGrown = tempDic[item.Key].isGrown;
}
}
}
// 지정된 경로에 파일이 없으면
else
{
Debug.Log("파일이 없어요!!");
}
}
private void Awake()
{
// 데이터 저장 경로 설정..
farmingDataFilePath = Path.Combine(Application.persistentDataPath, "FarmingData.json"); // 데이터 경로 설정..
farmingData = new Dictionary<Vector3Int, FarmingData>(); // 딕셔너리 생성
clickPosition = Vector2.zero;
// 농사 가능 구역만 farmingData 에 저장할 것임.
foreach (Vector3Int pos in farmEnableZoneTilemap.cellBounds.allPositionsWithin)
{
if (!farmEnableZoneTilemap.HasTile(pos)) continue;
SetFarmingData(pos); // FarmingData 타입 인스턴스의 정보를 세팅해주는 함수.
}
// 농사 땅 레벨 데이터 불러오기..
farmLevel = PlayerPrefs.GetInt("FarmLevel");
// 농사 땅 레벨 데이터를 불러온 다음에 레벨 데이터에 맞게끔 땅 업그레이드 해주기..
while (farmLevel > 0)
{
SetFarmSize();
farmLevel--;
}
LoadFarmingData(); // 데이터 가져오기..
}
void Update()
{
//// 모바일용
//if (Input.touchCount > 0)
//FarmingSystemMobile();
// 버튼 눌렀을 때 뒤에 있는 타일 못 누르도록 하기 위한 구문..
// 이거 데스크탑용
if (IsPointerOverUIObjectPC()) return;
// 데스크탑용
// 땅을 왼쪽 마우스키로 누르면..
if (Input.GetMouseButtonDown(0))
{
// 땅을 왼쪽 마우스키로 눌렀을 때, 땅의 현재 상태를 파악한 후 버튼 등 전반적인 UI 조정하는 것과 관련된 로직 함수..
FarmingSystemPC();
}
GrowTimeUpdate(); // 과일이 다 자라기까지 남은 시간 업데이트 해주는 함수..
CheckGrowedFruit(); // 과일이 다 자랐는지 확인하고, 다 자랐으면 그에 맞는 행동을 하도록 해주는 함수..
// 씨앗 선택창에서 버튼 클릭하면 진입하도록..
if (clickedSelectedSeedButton)
{
// 씨앗 심는 함수 호출
PlantTile(cellPosition, selectedSeedIdx);
}
// 확인용 로직..
if (Input.GetKeyDown(KeyCode.W))
SaveFarmingData();
}
private void FarmingSystemPC()
{
growTimePanel.SetActive(false); // 누르면 이전에 켜진 판넬 꺼지도록..
// 땅에 아무것도 안 한 상태는 plow 버튼을 갖고, 갈린 상태는 버튼으로 plant 버튼을 갖는다.
// 다른 땅을 클릭하면 전에 클릭한 땅의 버튼은 안 보여야 하므로 SetActive 로 안보이게 조정한다..
// 수확하기 버튼은 과일이 다 자라면 계속 보여야함..
if (farmEnableZoneTilemap.HasTile(prevSelectTile))
{
if (farmingData[prevSelectTile].currentState == "None" || farmingData[prevSelectTile].currentState == "plow")
{
farmingData[prevSelectTile].stateButton.gameObject.SetActive(false);
}
}
// 현재 마우스 위치를 게임 월드 위치로 바꿔서 저장
clickPosition = mainCamera.ScreenToWorldPoint(Input.mousePosition);
// 게임 월드 위치를 타일 맵의 타일 셀 위치로 변환
cellPosition = farmTilemap.WorldToCell(clickPosition);
foreach (Vector3Int pos in farmingData.Keys)
{
// 저장해놓은 타일 중에 현재 마우스로 클릭한 위치랑 같은 타일이 있으면
if (pos == cellPosition)
{
// 밭이 안 갈린 상태면 눌렀을 때 버튼 뜰 수 있도록
if (farmingData[cellPosition].plowEnableState)
{
farmingData[cellPosition].stateButton.gameObject.SetActive(true);
}
else
{
// 씨앗이 안 심어져 있을 때 또는 씨앗이 다 자랐을 때 버튼 뜰 수 있도록
if (farmingData[cellPosition].seed == null || (farmingData[cellPosition].seed.isGrown))
{
farmingData[cellPosition].stateButton.gameObject.SetActive(true);
}
// 씨앗이 자라는 중이면 남은 시간 나타내는 판넬 뜨도록
else if (!farmingData[cellPosition].seed.isGrown)
{
// 판넬 위치를 현재 클릭한 타일 위치로..
growTimePanel.transform.position = mainCamera.WorldToScreenPoint(farmTilemap.CellToWorld(cellPosition)) + new Vector3(0, 50, 0);
growTimePanel.SetActive(true);
growTimeText.text = "남은시간\n" + (int)(farmingData[cellPosition].seed.seedData.growTime - farmingData[cellPosition].seed.currentTime);
}
}
}
}
prevSelectTile = cellPosition; // 지금 누른 타일을 이전에 누른 타일 위치를 저장하는 변수에 저장..
}
private void FarmingSystemMobile()
{
// 맨 처음 터치만 이용할 것
Touch touch = Input.GetTouch(0);
if (IsPointerOverUIObjectMobile(touch)) return; // 터치한 곳에 UI 있으면 그냥 빠져나오도록..
// 맨 처음 터치 시작시
if (touch.phase == TouchPhase.Began)
{
growTimePanel.SetActive(false); // 누르면 이전에 켜진 판넬 꺼지도록..
// 땅에 아무것도 안 한 상태는 plow 버튼을 갖고, 갈린 상태는 버튼으로 plant 버튼을 갖는다.
// 다른 땅을 클릭하면 전에 클릭한 땅의 버튼은 안 보여야 하므로 SetActive 로 안보이게 조정한다..
// 수확하기 버튼은 과일이 다 자라면 계속 보여야함..
if (farmEnableZoneTilemap.HasTile(prevSelectTile))
{
if (farmingData[prevSelectTile].currentState == "None" || farmingData[prevSelectTile].currentState == "plow")
{
farmingData[prevSelectTile].stateButton.gameObject.SetActive(false);
}
}
// 현재 터치 위치를 게임 월드 위치로 바꿔서 저장
clickPosition = mainCamera.ScreenToWorldPoint(touch.position);
// 게임 월드 위치를 타일 맵의 타일 셀 위치로 변환
cellPosition = farmTilemap.WorldToCell(clickPosition);
Debug.Log(cellPosition);
foreach (Vector3Int pos in farmingData.Keys)
{
// 저장해놓은 타일 중에 현재 마우스로 클릭한 위치랑 같은 타일이 있으면
if (pos == cellPosition)
{
// 밭이 안 갈린 상태면 눌렀을 때 버튼 뜰 수 있도록
if (farmingData[cellPosition].plowEnableState)
{
farmingData[cellPosition].stateButton.gameObject.SetActive(true);
}
else
{
// 씨앗이 안 심어져 있을 때 또는 씨앗이 다 자랐을 때 버튼 뜰 수 있도록
if (farmingData[cellPosition].seed == null || (farmingData[cellPosition].seed.isGrown))
{
farmingData[cellPosition].stateButton.gameObject.SetActive(true);
}
// 씨앗이 자라는 중이면 남은 시간 나타내는 판넬 뜨도록
else if (!farmingData[cellPosition].seed.isGrown)
{
// 판넬 위치를 현재 클릭한 타일 위치로..
growTimePanel.transform.position = mainCamera.WorldToScreenPoint(farmTilemap.CellToWorld(cellPosition)) + new Vector3(0, 50, 0);
growTimePanel.SetActive(true);
growTimeText.text = "남은시간\n" + (int)(farmingData[cellPosition].seed.seedData.growTime - farmingData[cellPosition].seed.currentTime);
}
}
}
}
prevSelectTile = cellPosition; // 지금 누른 타일을 이전에 누른 타일 위치를 저장하는 변수에 저장..
}
}
private void GrowTimeUpdate()
{
// 자라는데 남은 시간이 계속 업데이트 되어야 하므로..
if (farmEnableZoneTilemap.HasTile(cellPosition) && farmingData[cellPosition].seed != null)
{
if (!farmingData[cellPosition].seed.isGrown)
growTimeText.text = "남은시간\n" + (int)(farmingData[cellPosition].seed.seedData.growTime - farmingData[cellPosition].seed.currentTime);
else
growTimePanel.SetActive(false); // 다 자라면 남은시간 나타내는 판넬 꺼지도록..
}
}
private void CheckGrowedFruit()
{
foreach (Vector3Int pos in farmingData.Keys)
{
if (farmingData[pos].seed != null)
{
if (farmingData[pos].seed.isGrown)
{
farmTilemap.SetTile(pos, harvestTile); // 타일을 과일이 다 자란 상태로 변경
farmingData[pos].harvestEnableState = true; // 작물 수확할 수 있는 상태
farmingData[pos].stateButton.gameObject.SetActive(true); // 수확하기 버튼은 항상 떠있어야 함
farmingData[pos].currentState = "harvest";
}
}
}
}
private bool IsPointerOverUIObjectPC()
{
// 컴퓨터용
PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current)
{
// 핸드폰 터치도 mousePosition 으로 이용할 수 있으므로 간단한 건 그냥 이것처럼 mousePosition 쓸 예정..
position = new Vector2(Input.mousePosition.x, Input.mousePosition.y)
};
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventDataCurrentPosition, results);
return results.Count > 0;
}
private bool IsPointerOverUIObjectMobile(Touch touch)
{
// 핸드폰용
PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current)
{
position = new Vector2(touch.position.x, touch.position.y)
};
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventDataCurrentPosition, results);
return results.Count > 0;
}
public GameObject CreateButton(int buttonNumber, Vector3Int pos)
{
// 타일 맵 초기 설정할 때 쓰는 함수
// 타일마다 버튼을 미리 만들어놓고 사용할 것임
GameObject button = Instantiate(buttonPrefabs[buttonNumber], buttonParent.transform);
// 셀 좌표를 월드 좌표로 바꿔서 저장
Vector3 worldPos = farmTilemap.CellToWorld(pos);
// 월드 좌표를 스크린 좌표로 바꿔서 저장
Vector3 screenPos = Camera.main.WorldToScreenPoint(worldPos);
// 버튼의 좌표 설정
button.transform.position = screenPos;
//button.transform.position += new Vector3(0, 50, 0);
return button;
}
public void PlowTile(Vector3Int pos)
{
// 밭을 가는 함수
farmTilemap.SetTile(pos, farmTile); // 타일 모습은 밭 간 상태로 바꿔주기
farmingData[pos].plowEnableState = false; // 갈았으니까 이제 갈 수 없는 상태를 나타내기 위해 false 로 값 변경
farmingData[pos].plantEnableState = true; // 씨앗을 심을 수 있는 상태를 나타내기 위해 true 로 값 변경
farmingData[pos].currentState = "plow"; // 갈린 상태니까 plow 로 바꿔주기
farmingData[pos].stateButton.gameObject.SetActive(false); // 버튼 한 번 눌렀으니까 꺼지도록..
farmingData[pos].stateButton = farmingData[pos].buttons[1]; // plant 버튼으로 변경..
}
public void OpenPlantSeedPanel(Vector3Int pos)
{
// 심기 버튼이랑 연결해줘야함.
farmingData[pos].stateButton.gameObject.SetActive(false); // 버튼 한 번 눌렀으니까 꺼지도록..
plantSeedPanel.SetActive(true); // 심기 버튼 눌렀을 때 씨앗 선택창 뜨도록 하기 위함
}
public void PlantTile(Vector3Int pos, int seedIdx)
{
// 씨앗을 심는 함수
// 이 함수는 씨앗 선택창에서 씨앗 버튼 눌렀을 때 호출되도록..
farmingData[pos].seed = seedContainer.GetSeed(seedIdx).GetComponent<Seed>();
farmTilemap.SetTile(pos, plantTile); // 타일 모습을 씨앗 심은 상태로 바꿔주기
farmingData[pos].plantEnableState = true; // 씨앗을 심을 수 없는 상태를 나타내기 위해 false 로 변경
farmingData[pos].currentState = "plant"; // 씨앗 심은 상태니까 plant 로 바꿔주기
farmingData[pos].stateButton = farmingData[pos].buttons[2]; // harvest 버튼을 가지고 있도록..
clickedSelectedSeedButton = false; // 한 번 심고 난 다음에 바로 변수값 false 로 바꿔주기
}
public void HarvestTile(Vector3Int pos)
{
// 과일을 수확하는 함수
// 생각해보니까 씨앗 인덱스 여기로 안 보내줘도 pos 보내줬으니까, pos 가 가지는 씨앗 인스턴스의 씨앗 인덱스 이용하면 될 듯.
farmingData[pos].plowEnableState = true;
farmingData[pos].currentState = "None"; // 과일을 수확한 상태니까 None 으로 바꿔주기
// 이건 이제 이 함수에서 관리 안 할 것...
//fruitContainer.fruitCount[farmingData[pos].seed.seedData.seedIdx]++; // 씨앗의 인덱스와 같은 과일의 수 증가시키기
// 구매하려는 씨앗의 개수만큼 InventoryItem 구조체의 인스턴스를 만들기..
InventoryItem tempItem = new InventoryItem()
{
item = fruitItems[farmingData[pos].seed.seedData.seedIdx],
quantity = 1,
};
inventoryController.AddItem(tempItem); // 새로 생성한 인벤토리 아이템을 인벤토리 데이터에 추가해주기..
farmingData[pos].stateButton.gameObject.SetActive(false); // 버튼 한 번 눌렀으니까 꺼지도록..
farmingData[pos].stateButton = farmingData[pos].buttons[0]; // plow 버튼을 가지고 있도록..
farmingData[pos].seed = null; // 수확 완료 했으니까 타일의 seed 변수를 다시 null 로 설정해주기..
farmTilemap.SetTile(pos, grassTile); // 타일 모습을 초기 상태의로 바꿔주기
}
public void BuySeed(int count, int idx)
{
// 돈이 부족하면 씨앗 못사!
if (GameManager.instance.money < seedContainer.prefabs[idx].GetComponent<Seed>().seedData.seedPrice * count)
{
Debug.Log("돈 없어!!!");
return;
}
// 구매하려는 씨앗의 개수만큼 InventoryItem 구조체의 인스턴스를 만들기..
InventoryItem tempItem = new InventoryItem()
{
item = seedItems[idx],
quantity = count,
};
inventoryController.AddItem(tempItem); // 새로 생성한 인벤토리 아이템을 인벤토리 데이터에 추가해주기..
// 이건 이제 이 함수에서 관리 안 할 것..
//seedContainer.seedCount[idx] += count; // 씨앗의 개수를 저장하고 있는 배열의 인덱스 요소에 구매한 씨앗의 개수만큼 더해주기
GameManager.instance.money -= seedContainer.prefabs[idx].GetComponent<Seed>().seedData.seedPrice * count; // 가진 돈에서 차감!
}
public void SellFruit(int count, int idx)
{
// 만약 판매하려고 하는 과일의 개수가 현재 과일의 개수보다 적으면 그냥 빠져나가도록..
if (fruitContainer.fruitCount[idx] < count)
{
Debug.Log("과일이 부족해!!!");
return;
}
// 판매하려는 과일의 개수만큼 InventoryItem 구조체의 인스턴스를 만들기..
InventoryItem tempItem = new InventoryItem()
{
item = fruitItems[idx],
quantity = count,
};
inventoryController.MinusItem(tempItem); // 새로 생성한 인벤토리 아이템을 인벤토리 데이터에서 빼주기..
// 이건 이제 이 함수에서 관리 안 할 것..
//fruitContainer.fruitCount[idx] -= count; // 판매할 과일의 수만큼 과일 컨테이너에서 빼주기
GameManager.instance.money += fruitItems[idx].fruitPrice * count; // 가진 돈에 더하기!
PlayerPrefs.SetInt("money", GameManager.instance.money); // 현재 돈 저장
}
public void SetFarmingData(Vector3Int pos)
{
// 이 함수는 FarmingManager 클래스의 Start 함수와 UpgradeFarmSize 함수에서 사용할 것..
// 딕셔너리에 이미 현재 등록하려는 타일이 존재하면 걍 빠져나가도록..
if (farmingData.ContainsKey(pos)) return;
// 아니면 딕셔너리에 등록
// 유니티에서는 new 를 쓰려면 class 가 MonoBehaviour 를 상속 받으면 안 됨.
farmingData[pos] = new FarmingData();
farmingData[pos].SetData();
farmingData[pos].buttons = new Button[3]; // [0]: plow 버튼, [1]: plant 버튼, [2]: harvest 버튼
// 각 타일마다 세 개의 버튼을 가지고 시작하도록..
for (int i = 0; i < buttonPrefabs.Length; i++)
{
// 클로저 문제를 피하기 위해서 값을 변수에 저장해놓고 이 변수를 사용함..
int index = i;
Vector3Int tilePos = pos;
farmingData[pos].buttons[i] = CreateButton(index, tilePos).GetComponent<Button>();
if (index == 0)
{
// 버튼에 함수를 저장해놓음(tilePos 도 같이 저장해놓기)
farmingData[tilePos].buttons[index].onClick.AddListener(() => PlowTile(tilePos));
}
else if (index == 1)
{
//farmingData[tilePos].buttons[index].onClick.AddListener(() => PlantTile(tilePos));
farmingData[tilePos].buttons[index].onClick.AddListener(() => OpenPlantSeedPanel(tilePos)); // 씨앗 선택창 화면에 띄우는 함수 연결시키기
}
else if (index == 2)
{
farmingData[tilePos].buttons[index].onClick.AddListener(() => HarvestTile(tilePos));
}
}
// 맨 처음에는 plow 버튼을 저장하고 있도록
farmingData[pos].stateButton = farmingData[pos].buttons[0];
}
public void SetFarmSize()
{
// Awake 함수에서 호출할 함수
// 불러온 농장 레벨에 따라 호출 횟수가 달라짐..
// 땅의 크기를 업그레이드 하는 함수
BoundsInt bounds = farmEnableZoneTilemap.cellBounds; // 농사 가능 구역 타일맵의 현재 크기 가져오기
// 새로 확장할 영역 좌표 계산 로직..
Debug.Log(bounds.xMin);
int minX = bounds.xMin - expansionSize;
int maxX = bounds.xMax + expansionSize;
int minY = bounds.yMin - expansionSize;
int maxY = bounds.yMax + expansionSize;
for (int i = minX; i < maxX; i++)
{
for (int j = minY; j < maxY; j++)
{
// 테투리 부분만 경계타일 까는 로직
// max 값은 1 이 더 더해져있기 때문에 이를 고려해서 조건식 짜야함.
// 그래서 maxX, maxY 일 때는 i, j 에 1 을 더해줌..
if (i == minX || i + 1 == maxX)
farmTilemap.SetTile(new Vector3Int(i, j, 0), grassTile);
if (j == minY || j + 1 == maxY)
farmTilemap.SetTile(new Vector3Int(i, j, 0), grassTile);
Vector3Int pos = new Vector3Int(i, j, 0);
// 농사 가능 구역 타일맵에 타일이 없으면 진입
if (!farmEnableZoneTilemap.HasTile(pos))
{
farmEnableZoneTilemap.SetTile(pos, grassTile);
}
}
}
// 경계 타일맵 깔기 위한 로직
bounds = farmEnableZoneTilemap.cellBounds; // 업데이트된 농사 가능 구역 타일맵의 현재 크기 가져오기
minX = bounds.xMin - 1;
maxX = bounds.xMax + 1;
minY = bounds.yMin - 1;
maxY = bounds.yMax + 1;
Debug.Log("maxX: " + maxX + " maxY: " + maxY);
Debug.Log("minX: " + minX + " minY: " + minY);
for (int i = minX; i < maxX; i++)
{
for (int j = minY; j < maxY; j++)
{
// 테투리 부분만 경계타일 까는 로직
// max 값은 1 이 더 더해져있기 때문에 이를 고려해서 조건식 짜야함.
// 그래서 maxX, maxY 일 때는 i, j 에 1 을 더해줌..
if (i == minX || i + 1 == maxX)
farmTilemap.SetTile(new Vector3Int(i, j, 0), borderTile);
if (j == minY || j + 1 == maxY)
farmTilemap.SetTile(new Vector3Int(i, j, 0), borderTile);
}
}
// 농사 가능 구역 타일맵의 타일들을 모두 돌면서..
foreach (Vector3Int pos in farmEnableZoneTilemap.cellBounds.allPositionsWithin)
{
SetFarmingData(pos); // 새로운 농사 가능 구역의 타일 정보를 딕셔너리에 저장..
}
}
public void UpgradeFarmSize()
{
// 일단 임시로 만원으로 해놨다..
if (GameManager.instance.money < 10000)
{
Debug.Log("돈 없어!");
return;
}
// 땅의 크기를 업그레이드 하는 함수
BoundsInt bounds = farmEnableZoneTilemap.cellBounds; // 농사 가능 구역 타일맵의 현재 크기 가져오기
// 새로 확장할 영역 좌표 계산 로직..
Debug.Log(bounds.xMin);
int minX = bounds.xMin - expansionSize;
int maxX = bounds.xMax + expansionSize;
int minY = bounds.yMin - expansionSize;
int maxY = bounds.yMax + expansionSize;
for (int i = minX; i < maxX; i++)
{
for (int j = minY; j < maxY; j++)
{
// 테투리 부분만 경계타일 까는 로직
// max 값은 1 이 더 더해져있기 때문에 이를 고려해서 조건식 짜야함.
// 그래서 maxX, maxY 일 때는 i, j 에 1 을 더해줌..
if (i == minX || i + 1 == maxX)
farmTilemap.SetTile(new Vector3Int(i, j, 0), grassTile);
if (j == minY || j + 1 == maxY)
farmTilemap.SetTile(new Vector3Int(i, j, 0), grassTile);
Vector3Int pos = new Vector3Int(i, j, 0);
// 농사 가능 구역 타일맵에 타일이 없으면 진입
if (!farmEnableZoneTilemap.HasTile(pos))
{
farmEnableZoneTilemap.SetTile(pos, grassTile);
}
}
}
// 경계 타일맵 깔기 위한 로직
bounds = farmEnableZoneTilemap.cellBounds; // 업데이트된 농사 가능 구역 타일맵의 현재 크기 가져오기
minX = bounds.xMin - 1;
maxX = bounds.xMax + 1;
minY = bounds.yMin - 1;
maxY = bounds.yMax + 1;
Debug.Log("maxX: " + maxX + " maxY: " + maxY);
Debug.Log("minX: " + minX + " minY: " + minY);
for (int i = minX; i < maxX; i++)
{
for (int j = minY; j < maxY; j++)
{
// 테투리 부분만 경계타일 까는 로직
// max 값은 1 이 더 더해져있기 때문에 이를 고려해서 조건식 짜야함.
// 그래서 maxX, maxY 일 때는 i, j 에 1 을 더해줌..
if (i == minX || i + 1 == maxX)
farmTilemap.SetTile(new Vector3Int(i, j, 0), borderTile);
if (j == minY || j + 1 == maxY)
farmTilemap.SetTile(new Vector3Int(i, j, 0), borderTile);
}
}
// 농사 가능 구역 타일맵의 타일들을 모두 돌면서..
foreach (Vector3Int pos in farmEnableZoneTilemap.cellBounds.allPositionsWithin)
{
SetFarmingData(pos); // 새로운 농사 가능 구역의 타일 정보를 딕셔너리에 저장..
}
farmLevel++; // 농장 레벨 증가
PlayerPrefs.SetInt("FarmLevel", farmLevel); // 농장 레벨 저장..
Debug.Log("농장을 업그레이드 했다!");
}
}
- Seed
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Seed : MonoBehaviour
{
// 생성자.. 그 팜 씬으로 돌아왔을 때, 이전 농장의 상태를 반영해주기 위함..
public Seed(float currentTime, bool isGrown)
{
this.currentTime = currentTime;
this.isGrown = isGrown;
}
// 씨앗은 다 프리팹으로 만들어 놓을 것
// 만들어놓은 프리팹은 SeedContainer 에 저장할 것..
public float currentTime; // 심은 후부터 현재까지 시간
public bool isGrown = false; // 다 자랐는지 여부 확인용 변수
public SeedItemSO seedData; // 씨앗 데이터(씨앗 이름, 씨앗 가격, 성장 시간, 씨앗 인덱스)
private void OnEnable()
{
isGrown = false;
currentTime = 0;
Debug.Log("씨앗을 얻었다!");
}
private void Update()
{
if (currentTime >= seedData.growTime)
{
isGrown = true;
Debug.Log("다 자랐다!");
transform.gameObject.SetActive(!isGrown);
}
currentTime += Time.deltaTime;
}
}
- SeedContainer
using Inventory.Model;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SeedContainer : MonoBehaviour
{
public GameObject[] prefabs; // 씨앗 프리팹을 저장해놓기 위한 배열
public List<GameObject>[] pools; // 씨앗을 심을 때 인스턴스를 생성하면 저장하는데 사용할 배열
public int[] seedCount; // 게임 상에서 어떤 씨앗이 몇 개 있는지 저장할 배열
private void Awake()
{
// [0]:사과, [1]:바나나, [2]:체리, [3]:오렌지, [4]:딸기
seedCount = new int[prefabs.Length]; // 프리팹의 개수만큼 배열 크기 지정
pools = new List<GameObject>[prefabs.Length]; // 프리팹의 개수만큼 리스트 배열 생성
for (int idx = 0; idx < pools.Length; idx++)
{
pools[idx] = new List<GameObject>(); // 리스트 배열 요소에 리스트 생성
}
}
public GameObject GetSeed(int idx)
{
// GetSeed 함수는 씨앗 심기 했을 때 호출되는 함수임.
// 즉, 씨앗을 구매하자마자 인스턴스가 생기는게 아님.
GameObject select = null;
// 선택된 인덱스에 들어있는 게임 오브젝트들을 for 문을 통해 모두 확인함.
foreach (GameObject gameObj in pools[idx])
{
// 만약 놀고 있는 게임 오브젝트가 있다면(활성화 되지 않은 게임 오브젝트가 있다면) 가져오기
if (gameObj.activeSelf == false)
{
select = gameObj;
select.SetActive(true);
break;
}
}
// 놀고 있는 게임 오브젝트가 없다면(다 활성화 되어서 게임상에서 사용되고 있다면) 새로 만들기
if (!select)
{
select = Instantiate(prefabs[idx], transform);
pools[idx].Add(select);
}
return select;
}
public void ResetContainer()
{
// 모든 요소의 값으르 0으로 리셋해주기..
for (int i = 0; i < seedCount.Length; i++)
{
seedCount[i] = 0;
}
}
public void SetContainer(Dictionary<int, InventoryItem> curInventory)
{
// curInventory 에서 키값은, 인벤토리 속에서 해당 아이템의 인덱스 번호임
// 현재 인벤토리의 내용을 가져올 때 비어있는 아이템 칸은 제외하고 가져옴.
// 즉, 인벤토리 속 비어있는 아이템 칸이 있다면 가져온 아이템 딕셔너리의 내용은 [0]: 사과, [2]: 바나나, [5]: 오렌지 이럴 가능성이 있음
// 그래서 key 가 1, 2, 3, 4, 5... 이런식으로 순차적으로 온다는 보장이 없으므로 그냥 키값들을 가져와서 반복문 도는 것..
foreach (int idx in curInventory.Keys)
{
seedCount[((SeedItemSO)(curInventory[idx].item)).seedIdx] += curInventory[idx].quantity; // 해당 아이템의 아이템 인덱스에 맞는 요소의 값을 증가시켜줌..
}
}
}
- FruitContainer
using Inventory.Model;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FruitContainer : MonoBehaviour
{
public FarmingManager farmingManager;
public int[] fruitCount; // 게임 상에서 어떤 과일이 몇 개 있는지 저장할 배열
public int totalFruitCount;
private void Awake()
{
// [0]:사과, [1]:바나나, [2]:체리, [3]:오렌지, [4]:딸기
fruitCount = new int[farmingManager.fruitItems.Length]; // fruitItems 리스트의 크기만큼 크기 설정..
}
public void ResetContainer()
{
// 모든 요소의 값으르 0으로 리셋해주기..
for (int i=0; i<fruitCount.Length; i++)
{
fruitCount[i] = 0;
}
}
public void SetContainer(Dictionary<int, InventoryItem> curInventory)
{
// curInventory 에서 키값은, 인벤토리 속에서 해당 아이템의 인덱스 번호임
// 현재 인벤토리의 내용을 가져올 때 비어있는 아이템 칸은 제외하고 가져옴.
// 즉, 인벤토리 속 비어있는 아이템 칸이 있다면 가져온 아이템 딕셔너리의 내용은 [0]: 사과, [2]: 바나나, [5]: 오렌지 이럴 가능성이 있음
// 그래서 key 가 1, 2, 3, 4, 5... 이런식으로 순차적으로 온다는 보장이 없으므로 그냥 키값들을 가져와서 반복문 도는 것..
foreach (int idx in curInventory.Keys)
{
fruitCount[((FruitItemSO)(curInventory[idx].item)).fruitIdx] += curInventory[idx].quantity; // 해당 아이템의 아이템 인덱스에 맞는 요소의 값을 증가시켜줌..
}
}
}
- SeedFruitUIManager
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
public class SeedFruitUIManager : MonoBehaviour
{
[Header("Seed Info")]
public FarmingManager farmingManager; // 구매하기 버튼이랑 farmingManager 에서 SeedContainer 의 GetSeed 함수랑 연동하기 위해서..
public SeedContainer seedInfo;
public Sprite[] seedImages; // 스프라이트는 미리 배열에 넣어놔야 사용할 수 있음..
[Header("Buy Seed UI")]
public GameObject buySeedPanel;
public GameObject buySlotContainer; // 구매 버튼 슬롯 가지고 있는 게임 오브젝트
public List<Button> buySeedSlots; // 씨앗 구매창의 슬롯들 저장
[Header("Plant Seed UI")]
public GameObject plantSlotContainer; // 씨앗 선택 버튼 슬롯 가지고 있는 게임 오브젝트
public List<Button> plantSeedSlots; // 씨앗 선택창의 슬롯들 저장(씨앗 심기 버튼 눌렀을 때 씨앗 선택창 뜸)
[Header("Sell Fruit UI")]
public GameObject sellFruitPanel;
public GameObject sellSlotContainer; // 과일 판매 버튼 슬롯 가지고 있는 게임 오브젝트
public FruitContainer fruitInfo;
public List<Button> sellFruitSlots; // 과일 판매창의 슬롯들 저장
[Header("Current Button")]
public Button selectSlot;
private void Awake()
{
// 현재 씨앗 구매 판넬에 존재하는 슬롯들을 가져와서 저장함.
// + 현재 씨앗 판매 판넬에 존재하는 슬롯들을 가져와서 저장함.
// 자식만 가져와야 하기 때문에 (자손은 가져오면 안 됨) GetComponentsInChildren 못 씀.
for (int i = 0; i < buySlotContainer.transform.childCount; i++)
{
// 씨앗 구매 판넬에 존재하는 슬롯 저장
Transform child = buySlotContainer.transform.GetChild(i);
buySeedSlots.Add(child.GetComponent<Button>());
// 씨앗 판매 판넬에 존재하는 슬롯 저장
child = sellSlotContainer.transform.GetChild(i);
sellFruitSlots.Add(child.GetComponent<Button>());
}
// 현재 게임 상 존재하는 씨앗 구매 버튼 정보 설정
for (int i = 0; i < buySeedSlots.Count; i++)
{
BuySeedSlotManager slot = buySeedSlots[i].GetComponent<BuySeedSlotManager>();
SeedItemSO seedInfo = farmingManager.seedItems[i];
slot.slotImage.sprite = seedInfo.itemImage; // 스크립터블 오브젝트의 이미지로..
slot.slotName.text = seedInfo.Name;
slot.totalPrice.text = "가격: " + seedInfo.seedPrice;
slot.idx = seedInfo.seedIdx;
slot.countText.text = "1";
}
// 현재 게임 상 존재하는 과일 판매 버튼 정보 설정
for (int i=0; i<sellFruitSlots.Count; i++)
{
SellFruitSlotManager slot = sellFruitSlots[i].GetComponent<SellFruitSlotManager>();
FruitItemSO fruitInfo = farmingManager.fruitItems[i];
slot.slotImage.sprite = fruitInfo.itemImage; // 스크립터블 오브젝트의 이미지로..
slot.slotName.text = fruitInfo.Name;
slot.totalPrice.text = "가격: " + fruitInfo.fruitPrice;
slot.idx = fruitInfo.fruitIdx;
slot.countText.text = "1";
}
// 얘는 그냥 GetComponentsInChildren 써도 되긴 하는데 그냥 통일감 주려고..
// 씨앗 선택 판넬에 존재하는 슬롯 저장
for (int i = 0; i < plantSlotContainer.transform.childCount; i++)
{
Transform child = plantSlotContainer.transform.GetChild(i);
plantSeedSlots.Add(child.GetComponent<Button>());
}
// 현재 게임 상 존재하는 씨앗 선택 버튼 정보 설정
for (int i=0; i < plantSeedSlots.Count; i++)
{
PlantSeedSlotManager slot = plantSeedSlots[i].GetComponent<PlantSeedSlotManager>();
SeedItemSO seedInfo = farmingManager.seedItems[i];
slot.seedImage.sprite = seedInfo.itemImage;
slot.seedNameText.text = seedInfo.Name;
slot.seedCountText.text = farmingManager.seedContainer.seedCount[i] + "";
slot.seedIdx = seedInfo.seedIdx;
}
}
private void Update()
{
// 씨앗 구매 관련
for (int i = 0; i < buySeedSlots.Count; i++)
{
BuySeedSlotManager slot = buySeedSlots[i].GetComponent<BuySeedSlotManager>();
Seed slotSeedInfo = seedInfo.prefabs[i].GetComponent<Seed>();
// BuySlot 이 활성화 되어 있는 슬롯의 정보만 계속해서 변경해줄 것
if (slot.openSlot.activeSelf)
{
// 선택된 씨앗 개수랑 총 가격만 계속해서 업데이트 해주면 됨.
slot.countText.text = slot.curCount + "";
slot.totalPrice.text = "가격: " + (int)(slot.curCount * slotSeedInfo.seedData.seedPrice);
}
}
// 과일 판매 관련
for (int i=0; i<sellFruitSlots.Count; i++)
{
SellFruitSlotManager slot = sellFruitSlots[i].GetComponent<SellFruitSlotManager>();
FruitItemSO fruitInfo = farmingManager.fruitItems[i];
// SellSlot 이 활성화 되어 있는 슬롯의 정보만 계속해서 변경해줄 것
if (slot.openSlot.activeSelf)
{
// 선택된 과일 개수랑 총 가격만 계속해서 업데이트 해주면 됨.
slot.countText.text = slot.curCount + "";
slot.totalPrice.text = "가격: " + (int)(slot.curCount * fruitInfo.fruitPrice);
}
}
// 씨앗 선택 관련
for (int i=0; i<plantSeedSlots.Count; i++)
{
// 씨앗의 개수만 계속해서 업데이트 해주면 됨..
PlantSeedSlotManager slot = plantSeedSlots[i].GetComponent<PlantSeedSlotManager>();
slot.seedCountText.text = farmingManager.seedContainer.seedCount[i] + "";
}
}
public void CloseSlot()
{
for (int i=0; i<buySeedSlots.Count; i++)
{
SlotManager buySlot = buySeedSlots[i].GetComponent<BuySeedSlotManager>();
buySlot.ResetData();
buySlot.openSlot.SetActive(false);
SlotManager sellSlot = sellFruitSlots[i].GetComponent<SellFruitSlotManager>();
sellSlot.ResetData();
sellSlot.openSlot.SetActive(false);
}
}
public void SlotClick()
{
CloseSlot(); // 슬롯 버튼 눌렀을 때, 다른 슬롯의 구매 슬롯이 켜져있으면 다 끄고 시작..
}
public void ExitButton()
{
buySeedPanel.SetActive(false); // 구매 창 없어지도록..
sellFruitPanel.SetActive(false); // 판매 창 없어지도록..
CloseSlot(); // 나가기 버튼 누르면 켜져있던 구매 슬롯 없어지도록..
}
}
- SlotManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
// SellFruitSlotManager 랑 BuySeedSlotManager 랑 겹치는 부분이 많아서 그냥 다형성 이용하는 게 좋을 것 같다.
// 그래서 따로 SlotManager 로 이름지음..
public class SlotManager : MonoBehaviour
{
[Header("FarmingManager")]
public FarmingManager farmingManager;
[Header("Slot Button UI")]
public Image slotImage;
public Text slotName;
public GameObject openSlot; // 슬롯 누르면 슬롯의 뒷모습 보이도록..
public Text totalPrice;
public Text countText;
public Button leftButton;
public Button rightButton;
public Button interactionButton; // 구매하기, 판매하기 버튼으로 사용할 것..
[Header("Slot Imformation")]
public int prevCount = 1; // 이전 카운트
public int curCount = 1; // 현재 카운트
public int maxCount; // 이건 하위 클래스에 따라 달라짐. SellFruitSlotManager 에서는 현재 과일 보유량에따라 달라지고, BuySeedSlotManager 에서는 64 로 고정..
public int minCount = 1;
public int idx; // 해당 슬롯의 (과일||씨앗) 인덱스
protected bool IsPointerOverUIObject()
{
//Touch touch = Input.GetTouch(0);
PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current)
{
// Input.mousePosition 도 모바일에서 작동하니까 이런 간단한 거에서 사용해도 괜찮을 것 같습니다..
position = new Vector2(Input.mousePosition.x, Input.mousePosition.y)
};
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventDataCurrentPosition, results);
return results.Count > 0;
}
public virtual void minusCount() { }
public virtual void plusCount() { }
public void ResetData()
{
curCount = 1;
}
}
- BuySeedSlotManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class BuySeedSlotManager : SlotManager
{
private void Start()
{
farmingManager = FindObjectOfType<FarmingManager>();
interactionButton.onClick.AddListener(() => farmingManager.BuySeed(1, idx)); // 일단 초기 함수 연결 해놓기
}
private void Update()
{
// 부모의 IsPointerOverUIObject 함수 쓸 것..
if (IsPointerOverUIObject()) return;
else
{
// UI 가 아닌 부분을 클릭하면 그냥 꺼지도록..
if (Input.GetMouseButtonDown(0))
{
openSlot.SetActive(false); // 슬롯의 뒷면 꺼지도록..
}
}
}
public override void minusCount()
{
if (curCount <= minCount)
{
// 현재 구매하려고 하는 씨앗 개수가 씨앗 구매 최소 개수보다 작아지는 순간 최댓값으로 넘어가도록
curCount = maxCount + 1;
}
curCount--;
interactionButton.onClick.RemoveAllListeners(); // 현재 선택 씨앗 개수가 변경되었으므로 모든 Listener 를 제거하고 시작
interactionButton.onClick.AddListener(() => farmingManager.BuySeed(curCount, idx));
}
public override void plusCount()
{
// 현재 구매하려고 하는 씨앗 개수가 씨앗 구매 최대 개수보다 커지는 순간 최솟값으로 넘어가도록
if (curCount >= maxCount)
{
curCount = minCount - 1;
}
curCount++;
interactionButton.onClick.RemoveAllListeners(); // 현재 선택 씨앗 개수가 변경되었으므로 모든 Listener 를 제거하고 시작
interactionButton.onClick.AddListener(() => farmingManager.BuySeed(curCount, idx));
}
}
- PlantSeedSlotManager
using Inventory.Model;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlantSeedSlotManager : MonoBehaviour
{
[Header("FarmingManager")]
public FarmingManager farmingManager;
[Header("Slot Information")]
public Text seedNameText; // 씨앗 이름 텍스트
public Text seedCountText; // 씨앗 개수 텍스트
public Image seedImage; // 씨앗 이미지
public int seedIdx; // 씨앗 인덱스
private void Start()
{
farmingManager = FindObjectOfType<FarmingManager>();
transform.GetComponent<Button>().onClick.AddListener(ClickedPlantSeedButton);
}
public void ClickedPlantSeedButton()
{
farmingManager.plantSeedPanel.gameObject.SetActive(false); // 버튼 눌리는 즉시에 판넬 꺼버리기
// 씨앗의 개수가 0보다 작거나 같으면 그냥 빠져나가도록..
if (farmingManager.seedContainer.seedCount[seedIdx] <= 0) {
Debug.Log("씨앗 없어!!!!");
return;
}
farmingManager.selectedSeedIdx = seedIdx; // 현재 심을 씨앗 인덱스를 설정
farmingManager.clickedSelectedSeedButton = true; // 버튼이 클릭됐다는 걸 알려줌..
// 이것도 이제 이 함수에서 관리 안 할 것..
//farmingManager.seedContainer.seedCount[seedIdx]--; // 버튼 클릭하면 씨앗 심는거니까 씨앗 개수 줄어들도록
// 구매하려는 씨앗의 개수만큼 InventoryItem 구조체의 인스턴스를 만들기..
InventoryItem tempItem = new InventoryItem()
{
item = farmingManager.seedItems[seedIdx],
quantity = 1,
};
farmingManager.inventoryController.seedInventoryData.MinusItem(tempItem); // 씨앗 심었으니까 인벤토리에서 개수 줄어들도록..
}
}
- SellFruitSlotManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class SellFruitSlotManager : SlotManager
{
private void Start()
{
farmingManager = FindObjectOfType<FarmingManager>();
interactionButton.onClick.AddListener(() => farmingManager.SellFruit(1, idx)); // 일단 초기 함수 연결 해놓기
maxCount = farmingManager.fruitContainer.fruitCount[idx]; // 해당 슬롯 인덱스에 맞는 현재 과일의 수를 maxCount 에 저장함.
}
private void Update()
{
maxCount = farmingManager.fruitContainer.fruitCount[idx]; // 현재 과일 개수로 계속 업데이트 해주기..
// 부모의 IsPointerOverUIObject 함수 쓸 것..
if (IsPointerOverUIObject()) return;
else
{
// UI 가 아닌 부분을 클릭하면 그냥 꺼지도록..
if (Input.GetMouseButtonDown(0))
{
openSlot.SetActive(false); // 슬롯의 뒷면 꺼지도록..
}
}
}
public override void minusCount()
{
if (curCount <= minCount)
{
// 현재 구매하려고 하는 과일 개수가 과일 구매 최소 개수보다 작아지는 순간 최댓값으로 넘어가도록
curCount = maxCount + 1;
}
curCount--;
if (curCount == 0) curCount = 1;
interactionButton.onClick.RemoveAllListeners(); // 현재 선택 과일 개수가 변경되었으므로 모든 Listener 를 제거하고 시작
interactionButton.onClick.AddListener(() => farmingManager.SellFruit(curCount, idx));
}
public override void plusCount()
{
// 현재 구매하려고 하는 과일 개수가 과일 구매 최대 개수보다 커지는 순간 최솟값으로 넘어가도록
// 구매 최대 개수보다 커지는 순간 최솟값으로 넘어가도록
if (curCount >= maxCount)
{
curCount = minCount - 1;
}
curCount++;
interactionButton.onClick.RemoveAllListeners(); // 현재 선택 과일 개수가 변경되었으므로 모든 Listener 를 제거하고 시작
interactionButton.onClick.AddListener(() => farmingManager.SellFruit(curCount, idx));
}
}
- UIInventoryController
using Inventory.Model;
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using UnityEngine.SceneManagement;
using static UnityEditor.Progress;
using UnityEditor.Experimental.RestService;
// 데이터 저장 클래스
[Serializable]
public class InventoryData
{
public InventorySO seedInventoryData;
public InventorySO fruitInventoryData;
}
public class UIInventoryController : MonoBehaviour
{
// 인벤토리 컨트롤러에서는 인벤토리 UI 와 실제 인벤토리 데이터의 정보를 이용해서 상호작용 하도록 함..
[SerializeField]
public UIInventoryPage inventoryUI; // 인벤토리 창
[SerializeField]
public InventorySO seedInventoryData; // 실제 인벤토리(씨앗 인벤토리)
[SerializeField]
public InventorySO fruitInventoryData; // 실제 인벤토리(과일 인벤토리)
[SerializeField]
public InventorySO curInventoryData; // 현재 보기로 선택한 인벤토리
[SerializeField]
public Button seedButton; // curInventoryData 의 값을 seedInventoryData 의 값으로 설정하는 버튼..
[SerializeField]
public Button fruitButton; // curInventoryData 의 값을 fruitInventoryData 의 값으로 설정하는 버튼..
[SerializeField]
public Button inventoryOpenButton; // 인벤토리를 켜고 닫는 버튼..
[SerializeField]
public static UIInventoryController instance; // 싱글톤 이용하기 위한 변수..
// 농장의 씨앗, 과일 컨테이너 게임 오브젝트 참조
[Header("Farm GameObject")]
[SerializeField]
public SeedContainer seedContainer; // 이 값이 null 이면 현재 씬이 팜이 아닌 것(팜인지 아닌지 여부를 판단해야 인벤토리의 상태를 팜의 컨테이너에 반영할지 말지 선택할 수 있음)..
[SerializeField]
public FruitContainer fruitContainer; // 이 값이 null 이면 현재 씬이 팜이 아닌 것..
[Header("Initial Inventory Items List")]
[SerializeField]
public List<InventoryItem> initialItems = new List<InventoryItem>(); // 처음 시작 인벤토리(이거 그냥 임시로 해놓음..)
[Header("Sell Item Info")]
[SerializeField]
public ItemSellPanel itemSellPanel; // 아이템 판매 판넬
[Header("Save Data")]
private string filePath; // 데이터 저장 경로..
public InventoryData inventoryData = new InventoryData();
/*
Awake와 Start의 호출 시점
Awake는 게임 오브젝트가 생성될 때 호출됩니다. 하지만 싱글톤 패턴처럼 씬 전환 후에도 계속 존재하는 오브젝트의 경우, 씬 전환 시 Awake가 호출되지 않습니다.
Start는 게임 오브젝트가 활성화된 이후 첫 번째 프레임 전에 호출됩니다. 씬 전환 시 새로 생성된 오브젝트가 아니라면 호출되지 않습니다.
*/
// Awake 랑 Start 는 게임오브젝트가 생성될 때 호출됨..
// 만약 씬을 전환해서 다른 씬으로 넘어갔을 때, 이 클래스를 가지는 게임 오브젝트는 삭제되지 않고 그대로 있음
// 새로 생성된게 아니므로 Awake 랑 Start 함수는 호출되지 않음..
// 그래서 씬이 전환되고 난 후, 참조 변수의 값들을 다시 넣어줘야 하는데 이와 관련된 로직을 Awake 와 Start 함수에 쓰면 안됨..
// 즉, 해결 방법은 유니티가 제공하는 UnityEngine.SceneManagement 의 OnSceneLoaded 함수에 관련 로직을 적어주면 됨..
// 그럼 씬이 로드될 때 필요한 모든 참조를 초기화 함..
/*
OnSceneLoaded 활용
OnSceneLoaded는 씬이 로드될 때 호출되는 콜백 함수입니다. 씬 전환 시 기존 씬에서의 참조를 재설정할 때 유용합니다.
SceneManager.sceneLoaded 이벤트를 구독하여 씬이 로드될 때 필요한 초기화 작업을 수행할 수 있습니다.
*/
void Awake()
{
// 싱글톤 변수 instance가 비어있는가?
if (instance == null)
{
// instance가 비어있다면(null) 그곳에 자기 자신을 할당
instance = this;
Debug.Log("인벤토리 매니저가 생성됐습니다");
DontDestroyOnLoad(gameObject); // 씬이 변경되어도 인벤토리가 삭제되지 않도록(인벤토리는 모든 씬에서 이용 가능해야 하기 때문에..)..
}
else
{
// instance에 이미 다른 오브젝트가 할당되어 있는 경우 씬에 두개 이상의 오브젝트가 존재한다는 의미.
// 싱글톤 오브젝트는 하나만 존재해야 하므로 자신의 게임 오브젝트를 파괴
Debug.LogWarning("씬에 두개 이상의 인벤토리 매니저가 존재합니다!");
Destroy(gameObject);
Debug.Log("인벤토리 매니저를 죽입니다");
}
// 델리게이트에 씬 로드 시 참조를 재설정하는 함수 연결..
SceneManager.sceneLoaded += OnSceneLoaded;
filePath = Path.Combine(Application.persistentDataPath, "InventoryData.json"); // 데이터 경로 설정..
//LoadInventoryData();
//PrepareUI();
//PrepareInventoryData();
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 씬이 완전히 로드될 때까지 기다린 후 코루틴 시작..
StartCoroutine(InitializeAfterSceneLoad());
}
private IEnumerator InitializeAfterSceneLoad()
{
// 다음 프레임에 실행 되도록 하는 구문..
yield return null;
Debug.Log("씬 로드됨!!!");
// 씬이 로드될 때 참조 변수 설정
InitializeReferences();
PrepareUI();
PrepareInventoryData();
}
void InitializeReferences()
{
// 씬을 전환하면 이 클래스가 참조하고 있던 게임오브젝트들이 날아감..
// 그래서 현재 씬에서 타입에 맞는 게임오브젝트를 찾아서 연결해줄것..
// 씬에서 필요한 게임 오브젝트 찾기
inventoryUI = FindObjectOfType<UIInventoryPage>();
itemSellPanel = FindObjectOfType<ItemSellPanel>();
// ?. 를 이용해서 null 인지 아닌지 판단함..
// seedContainer 랑 fruitcontainer 는 농장 씬에만 있도록 할거라서 다른 씬에서는 값이 null 로 설정되어 있을 것..
seedButton = GameObject.Find("SeedButton")?.GetComponent<Button>();
fruitButton = GameObject.Find("FruitButton")?.GetComponent<Button>();
seedContainer = GameObject.Find("SeedContainer")?.GetComponent<SeedContainer>();
fruitContainer = GameObject.Find("FruitContainer")?.GetComponent<FruitContainer>();
inventoryOpenButton = GameObject.Find("InventoryOpenButton")?.GetComponent<Button>();
// 일단 다 끈 상태로 시작..
inventoryUI.gameObject.SetActive(false);
seedButton.gameObject.SetActive(false);
fruitButton.gameObject.SetActive(false);
itemSellPanel.gameObject.SetActive(false);
}
public void Update()
{
if (Input.GetKeyDown(KeyCode.I))
{
if (inventoryUI.isActiveAndEnabled == false)
{
SetCurInventoryDataSeed(); // 인벤토리 창 켜질때는 씨앗을 기준으로 켜지도록..
inventoryUI.Show();
}
else
{
inventoryUI.Hide();
}
// 인벤토리 창이 켜졌는지 여부에 따라 씨앗, 과일 인벤토리창 선택 버튼도 켜질지 꺼질지 결정..
seedButton.gameObject.SetActive(inventoryUI.isActiveAndEnabled);
fruitButton.gameObject.SetActive(inventoryUI.isActiveAndEnabled);
inventoryUI.ResetDescription(); // 설명창 리셋해주기..
inventoryUI.sellButtonPanel.gameObject.SetActive(false); // 판매 버튼 판넬도 꺼주기..
}
// 임시 확인 코드
if (Input.GetKeyDown(KeyCode.C))
{
//SaveInventoryData();
//LoadInventoryData();
}
}
public void AddItem(InventoryItem item)
{
switch (item.item.itemType)
{
case 0:
// 씨앗
seedInventoryData.AddItem(item);
break;
case 1:
// 과일
fruitInventoryData.AddItem(item);
break;
case 2:
// 보석
break;
case 3:
// 케이크
break;
}
inventoryData.seedInventoryData = seedInventoryData;
inventoryData.fruitInventoryData = fruitInventoryData;
//SaveInventoryData(); // 데이터 저장!
}
public void MinusItem(InventoryItem item)
{
switch (item.item.itemType)
{
case 0:
// 씨앗
seedInventoryData.MinusItem(item);
break;
case 1:
// 과일
fruitInventoryData.MinusItem(item);
break;
case 2:
// 보석
break;
case 3:
// 케이크
break;
}
inventoryData.seedInventoryData = seedInventoryData;
inventoryData.fruitInventoryData = fruitInventoryData;
//SaveInventoryData(); // 데이터 저장!
}
public void SellItem(int count, int price, int itemType)
{
switch (itemType)
{
case 1:
// 과일
// 현재 마우스로 클릭한 아이템의 인덱스 요소를 판매하려는 아이템의 수만큼 감소시키기..
fruitInventoryData.MinusItemAt(inventoryUI.currentMouseClickIndex, count);
GameManager.instance.money += price; // 판매 가격만큼 돈을 더해줌..
break;
case 2:
// 보석
break;
case 3:
// 케이크
break;
}
itemSellPanel.gameObject.SetActive(false); // 팔고 난 다음에 창 끄기..
inventoryUI.currentMouseClickIndex = -1; // -1 로 다시 바꿔주기..
inventoryUI.ResetDescription(); // 아이템 설명창도 리셋해주기..
inventoryUI.sellButtonPanel.gameObject.SetActive(false); // 판매 버튼 판넬도 꺼주기..
inventoryData.seedInventoryData = seedInventoryData;
inventoryData.fruitInventoryData = fruitInventoryData;
//SaveInventoryData(); // 데이터 저장!
}
private void SetItemSellPanel(int itemIndex)
{
InventoryItem item = curInventoryData.GetItemAt(itemIndex);
itemSellPanel.gameObject.SetActive(true);
itemSellPanel.SetItemInfo(item);
}
// 인벤토리 변경 사항 처리 관련 함수
private void UpdateInventoryUI(Dictionary<int, InventoryItem> curInventory)
{
inventoryUI.ResetInventoryItems(); // 한 번 UI 리셋하고 시작..
// 현재 인벤토리 상태를 매개변수로 받은 후, 그 상태에 맞게 UI 새로 업데이트 해주기
foreach (var item in curInventory)
{
int index = item.Key;
InventoryItem temp = item.Value;
inventoryUI.inventoryUIItems[index].SetData(temp.item.itemImage, temp.quantity);
}
}
private void SetInventoryUI(int inventorySize)
{
inventoryUI.SetInventoryUI(inventorySize);
}
private void SetInventoryToContainer(int itemType)
{
// 이 함수에서 농장씬의 씨앗, 과일 컨테이너에 인벤토리의 정보를 반영해줄 것임..
// 만약 값이 null 이라면 현재 씬이 농장이 아닌 거니까 인벤토리의 정보를 반영해주는 함수를 호출하면 안됨(에러남)..
// 그러니까 그냥 빠져나오도록..
if (seedContainer == null || fruitContainer == null) return;
// 현재 씬이 농장이면 여기로 도달함..
// 여기서 정보 반영 함수 호출!!
Dictionary<int, InventoryItem> curInventory;
switch (itemType)
{
case 0:
// 씨앗
curInventory = seedInventoryData.GetCurrentInventoryState();
// 컨테이너 한 번 리셋해주기..
seedContainer.ResetContainer();
seedContainer.SetContainer(curInventory);
break;
case 1:
// 과일
curInventory = fruitInventoryData.GetCurrentInventoryState();
// 컨테이너 한 번 리셋해주기..
fruitContainer.ResetContainer();
fruitContainer.SetContainer(curInventory);
break;
case 2:
// 보석
break;
case 3:
// 케이크
break;
}
}
private void SetOpenSellButton()
{
if (curInventoryData.inventoryType == 1 || curInventoryData.inventoryType == 2)
{
// 현재 인벤토리 데이터가 가리키는 게 과일이랑 보석이면 판매 버튼이 뜰 수 있도록..
inventoryUI.isPossible = true;
}
else
{
inventoryUI.isPossible = false;
}
}
// 인벤토리 준비 함수
private void PrepareInventoryData()
{
// 인벤토리 각각 초기화해주기..
//seedInventoryData.Initialize();
//fruitInventoryData.Initialize();
// 델리게이트에 UpdateInventoryUI 함수를 연결하기..
// 인벤토리 데이터에 변경사항이 생기면 UpdateInventoryUI 함수를 호출할 수 있도록..
seedInventoryData.OnInventoryUpdated += UpdateInventoryUI;
fruitInventoryData.OnInventoryUpdated += UpdateInventoryUI;
// 델리게이트에 SetInvenetoryToContainer 함수를 연결하기..
// 인벤토리 데이터에 변경사항이 생기면 SetInvenetoryToContainer 함수를 호출할 수 있도록..
seedInventoryData.OnInventoryUpdatedInt += SetInventoryToContainer;
fruitInventoryData.OnInventoryUpdatedInt += SetInventoryToContainer;
// 델리게이트에 SetInventoryUI 함수 연결하기..
// 인벤토리 사이즈에 변경사항이 생기면 호출할 수 있도록..
seedInventoryData.OnInventorySizeUpdated += SetInventoryUI;
fruitInventoryData.OnInventorySizeUpdated += SetInventoryUI;
// 델리게이트에 SetItemSellPanel 함수 연결해놓기..
// 판매 버튼 눌렀을 때, 판매 창 정보를 현재 선택한 아이템의 정보로 설정하기 위함..
inventoryUI.OnItemActionRequested += SetItemSellPanel;
// 델리게이트에 SetOpenSellButton 함수 연결해놓기..
// 아이템 눌렀을 때, 아이템이 과일, 보석이면 인벤토리에서 판매버튼 뜰 수 있도록 하기 위함..
inventoryUI.OpenSellButtonPossible += SetOpenSellButton;
//// 이건 게임 시작할 때 인벤토리에 아이템 몇 개 넣어놓을 때 사용하려고 일단 임시로 쓴 코드..
//// 아예 아무것도 안 준채로 시작할지 아니면 뭐 좀 주고 시작할지 고민..
//foreach (InventoryItem item in initialItems)
//{
// if (item.IsEmpty) continue;
// seedInventoryData.AddItem(item);
//}
}
private void PrepareUI()
{
curInventoryData = seedInventoryData; // 일단 처음 시작은 씨앗 인벤토리로..
// 버튼에 함수 연결
seedButton.onClick.AddListener(SetCurInventoryDataSeed); // 씨앗 버튼에 인벤토리 데이터를 씨앗 인벤토리 데이터로 바꿔주는 함수 연결
fruitButton.onClick.AddListener(SetCurInventoryDataFruit); // 과일 버튼에 인벤토리 데이터를 과일 인벤토리 데이터로 바꿔주는 함수 연결
inventoryOpenButton.onClick.AddListener(OpenInventoryUI); // 버튼에 인벤토리창 여는 로직 함수 연결
// 아이템 판매 판넬 클래스의 델리게이트에 SellItem 함수 연결..
itemSellPanel.sellButtonClicked += SellItem;
inventoryUI.InitializeInventoryUI(curInventoryData.Size); // 씨앗 인벤토리 사이즈만큼 UI 초기화해주기
inventoryUI.OnDescriptionRequested += HandleDescriptionRequest;
inventoryUI.OnSwapItems += HandleSwapItems;
}
private void OpenInventoryUI()
{
if (inventoryUI.isActiveAndEnabled == false)
{
SetCurInventoryDataSeed(); // 인벤토리 창 켜질때는 씨앗을 기준으로 켜지도록..
inventoryUI.Show();
}
else
{
inventoryUI.Hide();
}
// 인벤토리 창이 켜졌는지 여부에 따라 씨앗, 과일 인벤토리창 선택 버튼도 켜질지 꺼질지 결정..
seedButton.gameObject.SetActive(inventoryUI.isActiveAndEnabled);
fruitButton.gameObject.SetActive(inventoryUI.isActiveAndEnabled);
inventoryUI.ResetDescription(); // 설명창 리셋해주기..
inventoryUI.sellButtonPanel.gameObject.SetActive(false); // 판매 버튼 판넬도 꺼주기..
}
// 델리게이트 연결 함수
private void HandleDescriptionRequest(int itemIndex)
{
InventoryItem inventoryItem; // InventoryItem 은 구조체라 null 값을 가질 수 없음(r-value 임..)
// 현재 인벤토리 데이터 변수가 가리키는 값이 씨앗 인벤토리 데이터 값이라면..
if (curInventoryData == seedInventoryData)
{
inventoryItem = seedInventoryData.GetItemAt(itemIndex); // 전달받은 아이템의 인덱스로 인벤토리 아이템을 가져옴..
if (inventoryItem.IsEmpty) // 만약 인벤토리 아이템이 비어있으면 디스크립션 초기화하고 빠져나가도록..
{
inventoryUI.ResetDescription();
return;
}
ItemSO item = inventoryItem.item;
inventoryUI.UpdateDescription(itemIndex, item.itemImage, item.Name, item.Description);
}
// 현재 인벤토리 데이터 변수가 가리키는 값이 과일 인벤토리 데이터 값이라면..
else if (curInventoryData == fruitInventoryData)
{
inventoryItem = fruitInventoryData.GetItemAt(itemIndex); // 전달받은 아이템의 인덱스로 인벤토리 아이템을 가져옴..
if (inventoryItem.IsEmpty) // 만약 인벤토리 아이템이 비어있으면 디스크립션 초기화하고 빠져나가도록..
{
inventoryUI.ResetDescription();
return;
}
ItemSO item = inventoryItem.item;
inventoryUI.UpdateDescription(itemIndex, item.itemImage, item.Name, item.Description);
}
}
private void HandleSwapItems(int index1, int index2)
{
// 현재 인벤토리 데이터 변수가 가리키는 값이 씨앗 인벤토리 데이터 값이라면..
if (curInventoryData == seedInventoryData)
{
seedInventoryData.SwapItems(index1, index2); // 씨앗 인벤토리 데이터의 SwapItems 함수를 호출함!
}
// 현재 인벤토리 데이터 변수가 가리키는 값이 과일 인벤토리 데이터 값이라면..
else if (curInventoryData == fruitInventoryData)
{
fruitInventoryData.SwapItems(index1, index2); // 과일 인벤토리 데이터의 SwapItems 함수를 호출함!
}
}
// 버튼 관련
private void SetCurInventoryDataSeed()
{
// 현재 인벤토리 데이터 값을 Seed 인벤토리 데이터 값으로 설정하는 함수
inventoryUI.sellButtonPanel.gameObject.SetActive(false); // 씨앗은 판매 불가능하니까 씨앗 인벤토리 창으로 전환하면 판매 버튼도 그냥 꺼지도록..
curInventoryData = seedInventoryData;
inventoryUI.SetInventoryUI(curInventoryData.Size); // 인벤토리 UI 를 현재 보려고 선택한 인벤토리 데이터에 맞게 설정..
inventoryUI.ResetDescription(); // 인벤토리 창 변경하면 설명도 꺼지도록..
curInventoryData.InformAboutChange();
}
private void SetCurInventoryDataFruit()
{
// 현재 인벤토리 데이터 값을 Fruit 인벤토리 데이터 값으로 설정하는 함수
curInventoryData = fruitInventoryData;
inventoryUI.SetInventoryUI(curInventoryData.Size); // 인벤토리 UI 를 현재 보려고 선택한 인벤토리 데이터에 맞게 설정..
inventoryUI.ResetDescription(); // 인벤토리 창 변경하면 설명도 꺼지도록..
curInventoryData.InformAboutChange();
}
// 데이터 저장
public void SaveInventoryData()
{
// Json 직렬화 하기
string json = JsonUtility.ToJson(inventoryData, true);
Debug.Log("데이터 저장 완료!");
// 외부 폴더에 접근해서 Json 파일 저장하기
// Application.persistentDataPath: 특정 운영체제에서 앱이 사용할 수 있도록 허용한 경로
File.WriteAllText(filePath, json);
}
public void LoadInventoryData()
{
// Json 파일 경로 가져오기
string path = Path.Combine(Application.persistentDataPath, "InventoryData.json");
// 지정된 경로에 파일이 있는지 확인한다
if (File.Exists(path))
{
// 경로에 파일이 있으면 Json 을 다시 오브젝트로 변환한다.
string json = File.ReadAllText(path);
inventoryData = JsonUtility.FromJson<InventoryData>(json);
seedInventoryData = inventoryData.seedInventoryData;
fruitInventoryData = inventoryData.fruitInventoryData;
Debug.Log(inventoryData.seedInventoryData.Size + "씨앗 인벤토리 사이즈");
Debug.Log(inventoryData.fruitInventoryData.Size + "과일 인벤토리 사이즈");
foreach (var item in seedInventoryData.GetCurrentInventoryState())
{
Debug.Log(item.Key + " 아이템: " + item.Value.item.Name + ", 양: " + item.Value.quantity);
}
foreach (var item in fruitInventoryData.GetCurrentInventoryState())
{
Debug.Log(item.Key + " 아이템: " + item.Value.item.Name + ", 양: " + item.Value.quantity);
}
}
else
{
Debug.Log("파일이 없어용!!");
}
}
}
- UIInventoryPage
using JetBrains.Annotations;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using TMPro;
using Unity.VisualScripting.Dependencies.NCalc;
using UnityEngine;
using UnityEngine.UI;
using static UnityEditor.Progress;
public class UIInventoryPage : MonoBehaviour
{
[SerializeField]
public UIInventoryItem inventoryItemPrefab;
[SerializeField]
public GameObject inventoryItemParent; // 새로 생성된 인벤토리 아이템 UI 가 위치할 부모(스크롤뷰의 content 가 부모가 될 것..)
[SerializeField]
public int initialInventorySize = 10; // 인벤토리 사이즈
[SerializeField]
public List<UIInventoryItem> inventoryUIItems; // 아이템 UI 들을 담아놓을 리스트
[SerializeField]
public UIMouseDragItem mouseDragItem;
[SerializeField]
public int currentMouseDragIndex = -1; // 현재 마우스로 드래그 하고 있는 아이템의 인덱스(아무것도 드래그 안 할때는 -1 로..)
[SerializeField]
public GameObject sellButtonPanel; // 아이템 판매 버튼 판넬
[SerializeField]
public Button sellButton; // 아이템 판매 버튼
[SerializeField]
public int currentMouseClickIndex = -1; // 현재 마우스로 클릭한 아이템의 인덱스..
[SerializeField]
public bool isPossible = false;
[SerializeField]
public UIInventoryDescription inventoryDescription;
public event Action OpenSellButtonPossible;
public event Action<int> OnDescriptionRequested, OnItemActionRequested;
public event Action<int, int> OnSwapItems;
private void Awake()
{
Debug.Log("인벤토리 페이지 생성됐어요..");
mouseDragItem.gameObject.SetActive(false); // 활성화 끈채로 시작..
//transform.gameObject.SetActive(false); // 창 끈채로 시작..
}
private void DeselectAllItems() {
// 모든 아이템의 경계 이미지를 끄는 함수..
foreach (UIInventoryItem item in inventoryUIItems)
{
item.Deselect();
}
}
public void UpdateDescription(int itemIndex, Sprite itemImage, string itemName, string itemDescription)
{
// 클릭한 아이템의 정보로 Description 값 업데이트 해주기...
inventoryDescription.SetDescription(itemImage, itemName, itemDescription);
DeselectAllItems();
inventoryUIItems[itemIndex].Select(); // 현재 디스크립션 대상 아이템의 경계 이미지 켜주기..
}
public void ResetDescription()
{
inventoryDescription.ResetDescription();
}
public void ResetInventoryItems()
{
foreach (UIInventoryItem item in inventoryUIItems)
{
item.ResetData();
}
}
public void CreateInventoryUI()
{
// 인벤토리 데이터에 칸 늘어날 때 이 함수도 호출해야함..
UIInventoryItem item = Instantiate(inventoryItemPrefab, Vector2.zero, Quaternion.identity);
item.Initialize(); // 아이템 초기화..
item.transform.SetParent(inventoryItemParent.transform); // 부모 지정
item.transform.localScale = Vector3.one; // 규모 지정..
// 델리게이트에 함수 연결해주기!!
item.OnItemClicked += HandleItemClicked; // 아이템을 클릭했을 때 로직
item.OnItemDroppedOn += HandleItemDroppedOn; // 아이템을 놓았을 때 로직
item.OnItemBeginDrag += HandleItemBeginDrag; // 아이템 드래그를 시작했을 때 로직
item.OnItemEndDrag += HandleItemEndDrag; // 아이템 드래그를 종료했을 때 로직
inventoryUIItems.Add(item); // 리스트에 아이템 추가..
}
public void InitializeInventoryUI(int inventorySize)
{
sellButton.onClick.AddListener(ClickSellButton); // 판매 버튼에 함수 연결..
inventoryUIItems = new List<UIInventoryItem>(initialInventorySize);
for (int i=0; i<inventorySize; i++)
{
CreateInventoryUI(); // 인벤토리 칸 생성
}
}
public void SetInventoryUI(int inventorySize)
{
int curInventorySize = inventoryUIItems.Count; // 현재 인벤토리 페이지의 아이템칸 사이즈를 가져옴..
if (curInventorySize <= inventorySize) // 새로 설정하려는 인벤토리 사이즈가 현재 인벤토리 사이즈보다 크거나 같다면..
{
for (int i = 0; i < curInventorySize; i++)
{
inventoryUIItems[i].gameObject.SetActive(true); // 새로 설정하려는 사이즈보다 크거나 같을 때, 일단 가지고 있는 인벤토리칸 다 켜기..
}
for (int i = 0; i < inventorySize - curInventorySize; i++) // 부족한 아이템 칸만큼 새로 생성해주기 위한 반복문..
{
CreateInventoryUI(); // 인벤토리 칸 생성
}
}
else if (curInventorySize > inventorySize) //새로 설정하려는 인벤토리 사이즈가 현재 인벤토리 사이즈보다 작다면..
{
for (int i = 0; i < curInventorySize-inventorySize; i++) // 그냥 불필요한 아이템칸 수만큼 활성화 꺼주면 됨..
{
// 뒤에서부터 차례대로 활성화 꺼주기..
inventoryUIItems[curInventorySize - i - 1].gameObject.SetActive(false);
}
}
}
private void HandleItemEndDrag(UIInventoryItem item)
{
mouseDragItem.gameObject.SetActive(false); // 마우스 드래그 아이템 활성화 꺼주기..
}
private void HandleItemBeginDrag(UIInventoryItem item)
{
CloseBorderImage(); // 다른 경계 다 꺼주고 시작..
currentMouseDragIndex = inventoryUIItems.IndexOf(item); // 매개변수로 전달받은 UIInventoryItem 의 인스턴스가 리스트 속 몇 번째 인덱스 요소인지 가져옴..
// 마우스 드래그 아이템의 상태를 현재 드래그 하려고 하는 아이템의 상태로 업데이트..
mouseDragItem.item.itemImage.sprite = inventoryUIItems[currentMouseDragIndex].itemImage.sprite;
mouseDragItem.item.quantityText.text = item.quantityText.text;
//mouseDragItem.item.quantityText.text = "1"; // 일단 임시로 1..
mouseDragItem.gameObject.SetActive(true); // 마우스 드래그 아이템 활성화 켜주기..
HandleItemClicked(item); // 아이템 클릭했을 때 효과를 드래그 시작할 때도 적용되도록..
}
// 이 매개변수로는 아이템을 드롭한 곳의 아이템칸이 들어옴
private void HandleItemDroppedOn(UIInventoryItem item)
{
int index = inventoryUIItems.IndexOf(item);
if (inventoryUIItems[currentMouseDragIndex].empty) return; // 만약 드래그 시작한 아이템이 비어있는 칸이면 그냥 빠져나가도록..
OnSwapItems?.Invoke(currentMouseDragIndex, index); // 아이템 swap 함수 호출
HandleItemClicked(item); // 아이템 클릭했을 때 효과가 드래그 끝난 후에도 적용되도록..
}
// 이 함수로 매개변수가 전달되는 때는, UIInventoryItem 인스턴스가 이 함수가 연결되어 있는 델리게이트를 호출할 때!
private void HandleItemClicked(UIInventoryItem item)
{
CloseBorderImage(); // 다른 경계 다 꺼주고 시작..
if (item.empty)
{
Debug.Log("비어있는디??!!");
// 클릭한 아이템 칸이 비어있으면 경계도 끄고, 설명도 끄고..
inventoryDescription.ResetDescription(); // 설명 초기화..
currentMouseClickIndex = -1; // 현재 아이템칸 클릭 인덱스로 -1로 초기화 해주고..
sellButtonPanel.SetActive(false); // 판매 버튼 활성화 끄기..
return; // 밑에 로직 수행 안하도록 그냥 나가버리기..
}
int index = inventoryUIItems.IndexOf(item); // 매개변수로 전달받은 UIInventoryItem 의 인스턴스가 리스트 속 몇 번 째 인덱스 요소인지 가져옴..
if (index == -1) return;
// 현재 클릭한 아이템칸의 인덱스 저장..
currentMouseClickIndex = index; // 매개변수로 전달받은 UIIventoryItem 의 인스턴스가 리스트 속 몇 번째 인덱스 요소인지 가져옴..
OpenSellButton(); // 판매 버튼 띄우도록..
inventoryUIItems[index].itemBorderImage.enabled = true; // 경계 활성화 키기..
// 아이템이 클릭되었으면 이제 설명 띄워줘야 함.
OnDescriptionRequested?.Invoke(index);
}
public void OpenSellButton()
{
OpenSellButtonPossible?.Invoke(); // 아이템이 과일, 보석, 씨앗이면 판매 버튼이 뜨도록(씨앗은 안뜨도록..) 하기 위함..
// 판매 버튼 판넬 띄우는 함수..
sellButtonPanel.SetActive(isPossible); // isPossible 값은 OpenSellButtonPossible 델리게이트에 연결된 함수가 호출될 때 값이 정해짐..
}
public void ClickSellButton()
{
// 이 함수를 sellButton 에 연결해줘야함..
if (currentMouseClickIndex == -1) return; // 만약 -1 이면 그냥 빠져나가도록..
// 현재 클릭한 아이템칸의 인덱스를 함수의 매개변수로 보내줌..
// 아이템 판매 판넬의 정보를 이 인덱스의 아이템의 정보로 설정해줄것..
OnItemActionRequested?.Invoke(currentMouseClickIndex);
}
private void CloseBorderImage()
{
foreach (UIInventoryItem item in inventoryUIItems)
{
item.itemBorderImage.enabled = false; // 경계 활성화 끄기..
}
}
public void Show()
{
transform.gameObject.SetActive(true);
}
public void Hide()
{
transform.gameObject.SetActive(false);
}
}
- UIInventoryDescription
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
public class UIInventoryDescription : MonoBehaviour
{
[SerializeField]
public Image descriptionItemImage;
[SerializeField]
public Text descriptionTitle;
[SerializeField]
public Text description;
private void Awake()
{
ResetDescription(); // 맨 처음 시작할 때 아이템 설명 칸은 다 비운채로 시작함..
}
public void ResetDescription()
{
descriptionItemImage.gameObject.SetActive(false);
descriptionTitle.text = "";
description.text = "";
}
// 아이템을 클릭했을 때, 아이템에 맞는 설명을 띄우기 위한 함수..
public void SetDescription(Sprite sprite, string itemName, string itemDescription)
{
descriptionItemImage.gameObject.SetActive(true);
descriptionItemImage.sprite = sprite;
descriptionTitle.text = itemName;
description.text = itemDescription;
}
}
- UIInventoryItem
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class UIInventoryItem : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IPointerClickHandler, IDropHandler, IDragHandler
{
[SerializeField]
public Image itemImage;
[SerializeField]
public Text quantityText;
[SerializeField]
public Image itemBorderImage;
[SerializeField]
// delegate 선언
// UIInventoryItem 타입의 매개변수를 받는 함수를 연결할 수 있음.
public event Action<UIInventoryItem> OnItemClicked, OnItemDroppedOn, OnItemBeginDrag, OnItemEndDrag;
[SerializeField]
public bool empty = true;
[SerializeField]
public int touchCount = 0; // 두번 클릭하면 아이템 뒷창 뜨도록..
/*
PointerEventData는 다음과 같은 터치 관련 정보를 포함하고 있습니다:
pointerId: 입력 장치의 ID입니다. 터치 장치에서는 각 터치마다 고유한 ID를 가집니다.
position: 입력 포인터의 현재 스크린 좌표입니다.
delta: 마지막 위치에서 현재 위치까지의 변경량입니다.
pressPosition: 입력이 시작된 위치입니다.
clickTime: 마지막 클릭이 발생한 시간입니다.
clickCount: 클릭 횟수입니다.
*/
// UI 는 매개변수로 넘겨줄 수 없음.
// 그래서 아이템 이미지는 Sprite 로 넘겨줌
public void SetData(Sprite itemImage, int quantity)
{
this.itemImage.sprite = itemImage; // 해당 아이템 이미지로 설정
this.quantityText.text = quantity + ""; // 해당 아이템의 개수로 설정
this.itemImage.gameObject.SetActive(true); // 이제 아이템 이미지 활성화..
empty = false; // 이제 비어있지 않을테니..
}
public void Select()
{
// 경계 이미지 활성화
itemBorderImage.enabled = true;
}
public void Deselect()
{
// 경계 이미지 활성화 끄기
itemBorderImage.enabled = false;
}
public void ResetData()
{
this.itemImage.gameObject.SetActive(false); // 다시 아이템 이미지 비활성화..
empty = true; // 다시 비어있도록..
// 경계 이미지 활성화 끄기
Deselect();
}
// 맨 처음 초기화시 호출 함수
public void Initialize()
{
itemImage.gameObject.SetActive(false);
quantityText.text = "";
Deselect();
}
public void OnBeginDrag(PointerEventData eventData)
{
Debug.Log("드래그 시작!!");
if (empty) return; // 만약 아이템 칸이 비어있으면 걍 빠져나가도록..
OnItemBeginDrag?.Invoke(this);
}
// Drop 은 드래그가 종료된 위치에 드롭 가능한 오브젝트가 있는지 여부에 따라 호출이 결정됨.
// 드롭 가능한 오브젝트가 있으면 호출됨(레이캐스트가 오브젝트를 제대로 감지해야함).
// 드롭 가능한 오브젝트가 IDropHandler 인터페이스를 구현하고 있어야함.
// 이건 드래그가 종료된 위치의 오브젝트에서 발생하는 이벤트임..
// 즉, 0번 아이템을 드래그 시작해서 3번 아이템에 드래그 종료하면 3번 아이템에서 드롭 이벤트 발생!!
public void OnDrop(PointerEventData eventData)
{
Debug.Log("내려놨다!!");
OnItemDroppedOn?.Invoke(this);
}
public void OnEndDrag(PointerEventData eventData)
{
Debug.Log("드래그 끝!!");
OnItemEndDrag?.Invoke(this);
}
public void OnPointerClick(PointerEventData eventData)
{
// 모든 터치는 고유의 아이디를 갖는다고 함.
// 즉, 0보다 크거나 같으면 터치니까 조건문으로 터치인지 아닌지 판별
if (eventData.pointerId >= 0)
{
Debug.Log("Touch!");
touchCount++;
if (touchCount >= 2)
{
// 아이템을 파는 뒷면을 보여주는 로직이 들어갈 것..
touchCount = 0;
}
Debug.Log(touchCount);
} else
{
// 이건 마우스로 일단 확인하기 위함..
Debug.Log("Touch!");
touchCount++;
Debug.Log(touchCount);
if (touchCount >= 2)
{
// 아이템을 파는 뒷면을 보여주는 로직이 들어갈 것..
touchCount = 0;
}
Debug.Log(touchCount);
}
OnItemClicked?.Invoke(this);
}
public void OnDrag(PointerEventData eventData)
{
Debug.Log("움직이는 중~~");
}
}
- UIMouseDragItem
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UIMouseDragItem : MonoBehaviour
{
[SerializeField]
public Canvas canvas;
[SerializeField]
public UIInventoryItem item;
// 캔버스속 Mouse Follower 게임 오브젝트의 활성화가 꺼진채로 시작하면 오류남....
// 켜진채로 시작해야 Awake 함수로 진입해서 레퍼런스 가져오는데 끈채로 시작하면 못가져옴..
// 혹시라도 또 똑같은 이유로 헤맬까봐 적어놓음..
private void Awake()
{
// 일반적인 경우 UI의 root 는 canvas 이므로..
canvas = transform.root.GetComponent<Canvas>();
// 내 밑에 UIInventoryItem 이 있으므로 GetCompoentInChildren 함수를 이용해서 가져옴
item = GetComponentInChildren<UIInventoryItem>();
}
public void SetData(Sprite sprite, int quantity)
{
item.SetData(sprite, quantity);
}
private void Update()
{
// 마우스 포인터의 스크린 좌표(터치도 마우스 포인터로 작동 하니까.. 일단 마우스 포인터의 포지션 이용)
// 를 캔버스 내의 로컬 좌표로 변환한 다음, 변환된 로컬 좌표를 이용하여 특정 UI 요소의 위치 설정
Vector2 position;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
(RectTransform)canvas.transform,
Input.mousePosition,
canvas.worldCamera,
out position
);
// 변환된 로컬 좌표를 월드 좌표로 변환하여 transform.position 에 설정해줌.
transform.position = canvas.transform.TransformPoint(position);
}
public void Toggle(bool val)
{
Debug.Log("Item toggled" + val);
gameObject.SetActive(val);
}
}
- ItemSellPanel
using Inventory.Model;
using JetBrains.Annotations;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ItemSellPanel : MonoBehaviour
{
[SerializeField]
private Button minusButton;
[SerializeField]
private Button plusButton;
[SerializeField]
private Button sellButton;
[SerializeField]
private Text curItemCountText;
[SerializeField]
private Text totalPriceText;
[SerializeField]
private int curItemCount = 1;
[SerializeField]
private int minItemCount = 1;
[SerializeField]
private int maxItemCount;
[SerializeField]
private int totalPrice = 0;
[SerializeField]
int itemPrice = 0; // 현재 아이템의 아이템 가격 저장용 변수..
[SerializeField]
private InventoryItem curItem;
[SerializeField]
// UIInventoryController 의 SellItem 함수 연결..
public event Action<int, int, int> sellButtonClicked; // 아이템 수량이랑 가격, 아이템 타입을 매개변수로 받는 함수를 연결할 것..
private void Awake()
{
// 각 버튼에 함수 연결해주기..
minusButton.onClick.AddListener(MinusItemCount);
plusButton.onClick.AddListener(PlusItemCount);
sellButton.onClick.AddListener(SellItem);
// 판매 수량은 1에서 적어질 수 없으므로 일단 현재 개수가 1인 상태를 가격에 업데이트 하고 시작..
UpdateTotalPrice(curItemCount);
}
public void SetItemInfo(InventoryItem item)
{
curItem = item;
// 플러스 버튼을 눌러서 최대한으로 올라갈 수 있는 한계치를 현재 선택한 아이템의 개수로 설정
maxItemCount = item.quantity;
curItemCount = 1;
// 현재 아이템 가격 결정용..
switch (curItem.item.itemType)
{
case 1:
// 과일
itemPrice = ((FruitItemSO)curItem.item).fruitPrice;
break;
case 2:
// 보석
break;
case 3:
// 케이크
break;
}
UpdateTotalPrice(curItemCount);
}
private void SellItem()
{
// 델리게이트에 연결된 함수 호출..
sellButtonClicked?.Invoke(curItemCount, totalPrice, curItem.item.itemType);
}
private void UpdateTotalPrice(int itemCount)
{
// 총 가격을 현재 아이템 개수만큼 가격이랑 곱해서 구하기..
totalPrice = itemCount * itemPrice;
curItemCountText.text = curItemCount + ""; // 여기서 갯수 텍스토도 변경..
totalPriceText.text = totalPrice + ""; // 총 가격만큼 텍스트도 변경..
}
private void MinusItemCount()
{
// 마이너스 버튼 눌렀을 때 현재 아이템 개수가 최소치보다 작거나 같다면 최대값으로 바꿔주기..
if (curItemCount <= minItemCount)
{
curItemCount = maxItemCount;
UpdateTotalPrice(curItemCount);
return; // 빠져나가기..
}
curItemCount--;
UpdateTotalPrice(curItemCount);
}
private void PlusItemCount()
{
// 플러스 버튼 눌렀을 때 현재 아이템 개수가 최대치보다 크거나 같다면 최소값으로 바꿔주기..
if (curItemCount >= maxItemCount)
{
curItemCount = minItemCount;
UpdateTotalPrice(curItemCount);
return; // 빠져나가기..
}
curItemCount++;
UpdateTotalPrice(curItemCount);
}
}
- ItemSO
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.Antlr3.Runtime;
using UnityEngine;
[CreateAssetMenu]
public class ItemSO : ScriptableObject
{
[field: SerializeField]
public bool IsStackable { get; set; } // 아이템이 인벤토리 칸에 누적 저장될 수 있는지 여부 판단.
// 각각의 스크립터블 오브젝트의 인스턴스는 자신만의 고유 instance id 를 가지고 있음. 즉, 이걸로 같은 아이템인지 아닌지 판단할 것임..
public int ID => GetInstanceID();
[field: SerializeField]
public int MaxStackSize { get; set; } = 1;
[field: SerializeField]
public string Name { get; set; } // 아이템 이름
[field: SerializeField]
[field: TextArea]
public string Description { get; set; } // 아이템 설명
[field: SerializeField]
public Sprite itemImage { get; set; } // 아이템 이미지
[field: SerializeField]
public int itemType; // 아이템 타입(0: 씨앗, 1: 과일, 2: 보석, 3: 케이크, ...)
}
- SeedItemSO
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "Seed Data", menuName = "Scriptable Object/Seed Data", order = int.MaxValue)]
public class SeedItemSO : ItemSO
{
// ItemSO 의 공통적인 속성을 상속받기..
// 이 클래스는 Seed 클래스에서 seedData 라는 이름의 변수로 이용될 것..
[SerializeField]
public int seedPrice; // 씨앗 구매 가격
[SerializeField]
public float growTime; // 성장하는데 걸리는 시간
[SerializeField]
public int seedIdx; // 씨앗 인덱스
}
- FruitItemSO
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "Fruit Data", menuName = "Scriptable Object/Fruit Data", order = int.MaxValue)]
public class FruitItemSO : ItemSO
{
[SerializeField]
public int fruitPrice;
[SerializeField]
public int fruitIdx; // 과일 인덱스
}
- InventorySO
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Inventory.Model
{
[Serializable]
[CreateAssetMenu]
public class InventorySO : ScriptableObject
{
[SerializeField]
private List<InventoryItem> inventoryItems;
[field: SerializeField]
public int Size { get; private set; } = 10; // 인벤토리 사이즈
[field: SerializeField]
public int inventoryType; // 인벤토리 타입 0: 씨앗, 1: 과일, 2: 보석, 3: 케이크...
// 인벤토리가 업데이트 됐는지 여부 확인 후 함수 호출하기 위함(매번 Update 함수에서 확인 안해도됨)..
public event Action<Dictionary<int, InventoryItem>> OnInventoryUpdated; // Dictionary<int, InventoryItem> 타입의 매개변수를 받는 함수를 연결할 수 있음..
public event Action<int> OnInventoryUpdatedInt, OnInventorySizeUpdated; // int 타입의 매개변수를 받는 함수를 연결할 수 있음..
// 맨 처음 게임 시작시 호출하는 함수
public void Initialize()
{
inventoryItems = new List<InventoryItem>();
for (int i = 0; i < Size; i++)
{
inventoryItems.Add(InventoryItem.GetEmptyItem());
}
}
public void AddItem(ItemSO item, int quantity)
{
// 만약 아이템의 수량을 쌓을 수 있으면..
if (item.IsStackable)
{
for (int i=0; i<inventoryItems.Count; i++)
{
// 아이템 칸이 비어있으면 그냥 지나가도록..
if (inventoryItems[i].IsEmpty) continue;
if (inventoryItems[i].item.ID == item.ID)
{
// 남아 있는 여분 공간
int remainingSpace = item.MaxStackSize - inventoryItems[i].quantity;
// 남아 있는 여분 공간이 현재 더하려는 양보다 크거나 같으면
if (remainingSpace >= quantity)
{
inventoryItems[i] = inventoryItems[i].ChangeQuantity(inventoryItems[i].quantity + quantity);
return;
}
// 남아 있는 여분 공간이 현재 더하려는 양보다 작으면
else
{
// 여분 공간을 모두 채워 준 후..
inventoryItems[i] = inventoryItems[i].ChangeQuantity(item.MaxStackSize);
quantity -= remainingSpace; // 남은 양 구하기ㅏ..
}
}
}
}
// 인벤토리를 돌면서
for (int i=0; i<inventoryItems.Count; i++)
{
// 비어있는 인벤토리 칸을 찾기..
if (inventoryItems[i].IsEmpty)
{
// 아이템이 스택 가능하고, 아이템의 양이 아이템의 맥스치보다 크면
if (item.IsStackable && quantity > item.MaxStackSize)
{
// 새로운 아이템 칸을 만들고
inventoryItems[i] = new InventoryItem
{
item = item,
quantity = item.MaxStackSize
};
quantity -= item.MaxStackSize; // 남은 양 구하기..
}
// 아이템의 종류가 스택 불가능 하거나, 아이템의 양이 아이템의 맥스치보다 작으면
else
{
// 새로운 아이템 칸을 만들고
inventoryItems[i] = new InventoryItem
{
item = item,
quantity = quantity // 양 그대로 넣어주기..
};
return;
}
}
}
// 아이템을 저장할 공간이 부족하면..
if (quantity > 0)
{
while (quantity > 0) {
SizeUpInventory(); // 인벤토리를 한칸 늘려주고..
// 아이템이 스택 가능하고, 아이템의 양이 아이템의 맥스치보다 크면
if (item.IsStackable && quantity > item.MaxStackSize)
{
// 새로운 아이템 칸을 만들고
inventoryItems[inventoryItems.Count - 1] = new InventoryItem
{
item = item,
quantity = item.MaxStackSize
};
quantity -= item.MaxStackSize; // 남은 양 구하기..
}
// 아이템의 종류가 스택 불가능 하거나, 아이템의 양이 아이템의 맥스치보다 작으면
else
{
// 새로운 아이템 칸을 만들고
inventoryItems[inventoryItems.Count - 1] = new InventoryItem
{
item = item,
quantity = quantity // 양 그대로 넣어주기..
};
return;
}
}
}
}
public void SizeUpInventory()
{
// 인벤토리 사이즈 늘리고, 새로 만든 칸을 리스트에 넣어주고, 변경사항 알리는 함수 호출!!
Size += 1;
inventoryItems.Add(InventoryItem.GetEmptyItem());
// 델리게이트에 연결되어 있는 함수 호출..
// 인벤토리의 사이즈가 업데이트 될 때 실행되어야 하는 함수 연결되어있음..
OnInventorySizeUpdated?.Invoke(Size); // 매개변수로 현재 인벤토리의 사이즈 보내주기..
}
public void AddItem(InventoryItem item)
{
AddItem(item.item, item.quantity);
InformAboutChange(); // 아이템을 추가하면 인벤토리의 상태가 변경되므로 UI 상태 업데이트 해주기..
}
public void MinusItem(ItemSO item, int quantity)
{
int totalItemCount = 0;
int minIdx = -1;
int min = int.MaxValue;
List<int> itemIdxTmp = new List<int>();
// 스택 가능한 아이템인 경우
if (item.IsStackable)
{
for (int i = 0; i < inventoryItems.Count; i++)
{
if (inventoryItems[i].IsEmpty) continue; // 만약 비어있는 칸이면 그냥 넘기도록..
// 빼려고 하는 아이템의 아이디와 같다면..
if (inventoryItems[i].item.ID == item.ID)
{
// 총 아이템 카운트를 올려주기..
totalItemCount += inventoryItems[i].quantity;
itemIdxTmp.Add(i); // 아이템의 인덱스 넣어주기..
if (min > inventoryItems[i].quantity)
{
min = inventoryItems[i].quantity;
minIdx = i;
}
}
}
// 아이템의 총수량이 현재 빼려고 하는 아이템의 수량보다 크거나 같다면..
if (totalItemCount >= quantity)
{
if (minIdx != -1) // 가장 작은 수를 못 찾은 경우가 아니면..
{
if (inventoryItems[minIdx].quantity > quantity) // 제거해야할 아이템의 수량이, 해당 아이템의 최소 수량 인벤토리 칸의 수량보다 작다면..
{
// 인벤토리 아이템 수량 변경..
inventoryItems[minIdx] = inventoryItems[minIdx].ChangeQuantity(inventoryItems[minIdx].quantity - quantity);
return; // 빠져나가기..
}
else if (inventoryItems[minIdx].quantity == quantity) // 제거해야할 아이템의 수량이, 해당 아이템의 최소 수량 인벤토리 칸의 수량이랑 같다면..
{
// 인벤토리 아이템칸을 비어있는 아이템으로 변경..
inventoryItems[minIdx] = InventoryItem.GetEmptyItem();
return; // 빠져나가기..
}
else if (inventoryItems[minIdx].quantity < quantity) // 제거해야할 아이템의 수량이, 해당 아이템의 최소 수량 인벤토리 칸의 수량보다 크다면..
{
quantity -= inventoryItems[minIdx].quantity;
inventoryItems[minIdx] = InventoryItem.GetEmptyItem();
}
}
// 위 조건문 둘다 만족 안하면..
// 빼야 하는 수량이 0 이랑 같아질때까지 반복문 돌면서(뒤에 있는 아이템부터 없앨 것..)..
int idx = 0;
while (quantity > 0 && idx < itemIdxTmp.Count)
{
Debug.Log(itemIdxTmp.Count);
int temp = itemIdxTmp[itemIdxTmp.Count - 1 - idx]; // 해당 아이템의 인덱스를 모아놓은 리스트에서 요소 가져옴..
idx++;
// 최소 인덱스의 인벤토리 아이템은 이미 없앴으므로 넘기기..
if (temp == minIdx) continue;
// 제거해야할 수량이 인벤토리 아이템의 수량보다 많거나 같을 때..
if (quantity >= inventoryItems[temp].quantity)
{
quantity -= inventoryItems[temp].quantity; // 뺄 수량을 인벤토리 아에팀 수량만큼 감소시킴..
inventoryItems[temp] = InventoryItem.GetEmptyItem(); // 아이템칸 비우기..
}
// 제거해야할 수량이 인벤토리 아이템의 수량보다 적을 때..
else
{
inventoryItems[temp] = inventoryItems[temp].ChangeQuantity(inventoryItems[temp].quantity - quantity);
quantity = 0;
}
}
}
// 아이템의 총수량이 현재 빼려고 하는 아이템의 수량보다 작다면..
else
{
Debug.Log($"{item.Name}이 부족해!!!!!");
return; // 그냥 빠져나오도록..
}
}
// 아이템이 스택가능한 아이템이 아닌 경우에는..
else
{
for (int i = 0; i < inventoryItems.Count; i++)
{
// 빼려고 하는 아이템의 아이디와 같다면..
if (inventoryItems[i].item.ID == item.ID)
{
inventoryItems[i] = InventoryItem.GetEmptyItem(); // 인벤토리 칸을 비워주고..
return; // 빠져나가기..
}
}
// 여기까지 도달했으면 아이템을 못 찾은거니까 빠져나가기..
Debug.Log($"{item.Name}이 없어요!!!!!");
return;
}
}
public void MinusItem(InventoryItem item)
{
MinusItem(item.item, item.quantity);
InformAboutChange();
}
public void MinusItemAt(int itemIdx, int quantity)
{
if (inventoryItems[itemIdx].quantity == quantity) // 해당 인덱스의 아이템의 양과 똑같은 양을 뺀다면 아이템칸이 빈 인벤토리 아이템을 가지도록..
inventoryItems[itemIdx] = InventoryItem.GetEmptyItem();
else // 해당 인덱스의 아이템의 양보다 적은 양을 뺀다면, 그 수만큼 반영해주기(해당 아이템 양보다 더 많은 양을 빼려고 하는 경우가 생기지 않도록 다른 클래스에서 조정할 것..)
inventoryItems[itemIdx] = inventoryItems[itemIdx].ChangeQuantity(inventoryItems[itemIdx].quantity - quantity);
InformAboutChange();
}
// 현재 인벤토리의 상태를 반환하는 함수..
public Dictionary<int, InventoryItem> GetCurrentInventoryState()
{
Dictionary<int, InventoryItem> returnValue = new Dictionary<int, InventoryItem>();
// 현재 인벤토리 사이즈만큼 반복문을 돌면서 인벤토리 아이템이 비어있지 않은 것만 딕셔너리에 넣어줌..
for (int i = 0; i < inventoryItems.Count; i++)
{
if (inventoryItems[i].IsEmpty)
continue;
returnValue[i] = inventoryItems[i];
}
return returnValue;
}
// 특정 인덱스의 아이템을 가져올 것..
public InventoryItem GetItemAt(int itemIndex)
{
return inventoryItems[itemIndex];
}
// 아이템을 서로 바꿈!!
public void SwapItems(int itemIndex1, int itemIndex2)
{
// InventoryItem 이 구조체이므로 값을 복사해서 저장할 수 있음(참조형은 값 복사 x).
InventoryItem item1 = inventoryItems[itemIndex1];
inventoryItems[itemIndex1] = inventoryItems[itemIndex2];
inventoryItems[itemIndex2] = item1; // item1 은 복사된 값이니까 문제없이 원하는 대로 기능함..
InformAboutChange();
}
public void InformAboutChange()
{
// 델리게이트에 연결되어 있는 함수 호출..
// UpdateInventoryUI 함수가 연결되어 있음..
OnInventoryUpdated?.Invoke(GetCurrentInventoryState());
OnInventoryUpdatedInt?.Invoke(inventoryType);
}
}
/*
!!구조체의 장점!!
간단하고 가벼움: 두 개의 필드만 가지고 있어 구조체로서 이상적이다..
값 타입: 인스턴스를 복사하여 전달하므로 원본 데이터를 보호할 수 있다..
불변성 유지: 변경 가능한 메서드(ChangeQuantity)는 새로운 인스턴스를 반환하므로 불변성을 유지한다..
성능: 스택에 할당되어 가비지 컬렉션의 부하를 줄일 수 있다..
*/
// struct 는 a - value 가 아니라 null 값 가질 수 없음
[System.Serializable] // C#에서 클래스, 구조체 또는 열거형을 직렬화할 수 있도록 지정하는 데 사용
public struct InventoryItem
{
public int quantity;
public ItemSO item;
// 이 속성은 item이 null인지 여부를 확인하여 true 또는 false 값을 반환함..
public bool IsEmpty => item == null; // 읽기 전용 속성..
// 아이템의 수량만 바꾼 InventoryItem 을 새로 만들어서 반환함..
// 아이템의 아이디는 계속 같아야 하므로 item 에 this.item 넣어준 것..
public InventoryItem ChangeQuantity(int newQuantity)
{
return new InventoryItem
{
item = this.item,
quantity = newQuantity,
};
}
// 맨 처음 게임 시작할 때, 인벤토리에 빈 칸을 만들어놓기 위함..
public static InventoryItem GetEmptyItem()
=> new InventoryItem
{
item = null,
quantity = 0,
};
}
}
[주요 기능 설명]
1. 인벤토리 UI 만들기
---> 참고자료:
https://www.youtube.com/watch?v=xGNBjHG2Oss&list=PLcRSafycjWFegXSGBBf4fqIKWkHDw_G8D
2. 인벤토리 기능 만들기
1. 인벤토리 UI 상호작용 기능
---> 참고자료:
유니티 이벤트(Events)와 액션(Action)으로 코드 정리하기 — 기밀문서 (tistory.com)
유니티 이벤트(Events)와 액션(Action)으로 코드 정리하기
외국 유튜버의 강좌를 보다가 델리게이트와 유니티 이벤트, 액션을 알게되었다.근데 유니티 이벤트랑 유니티 액션 이전에 이벤트 개념 자체를 한번 살펴보기로 했다.#1 Event(이벤트) 개념 우선
kimyir.tistory.com
유니티 EventTrigger (tistory.com)
유니티 EventTrigger
EventTrigger 컴포넌트란? Event Trigger 컴포넌트는 다양한 UI 이벤트를 감지하고 이에 대응할 수 있게 도와줍니다. Event Trigger 컴포넌트를 사용하면 Pointer Click , Pointer 입력, Pointer 종료, Pointer Down, Pointer
wlsdn629.tistory.com
- UIInventoryItem 클래스는 인벤토리 UI 속 인벤토리 아이템 슬롯을 관리하는 클래스이다.
- UIInventoryItem 타입의 매개변수를 받는 함수를 연결할 수 있는 델리게이트를 선언하였다(OnItemClicked, OnItemDroppedOn, OnItemBeginDrag, OnItemEndDrag).
- 델리게이트에 함수를 연결하는 행위는 UIInventoryPage 클래스에서 CreateInventoryUI 함수가 전담한다.
- UIInventoryItem 클래스에 IBeginDragHandler, IEndDragHandler, IPointerClickHandler, IDropHandler 인터페이스를 추가하여 직접 OnBeginDrag, OnEndDrag, OnPointerClick, OnDrop 함수를 만들어서 관리했다(EventTrigger 컴포넌트를 게임 오브젝트에 직접 부착해서 하면 다른 UI 의 Event 를 인터셉트 할 가능성이 있어서 직접 스크립트 상에서 관리해야 한다고 한다).
- OnBeginDrag, OnEndDrag, OnPointerClick, OnDrop 함수에서 각각의 델리게이트에 함수가 연결되어있는지 여부를 판단한 후 연결된 함수를 호출하도록 했다.
- UIInventoryPage 클래스에는 OnDescriptionRequested, OnItemActionRequested, OnSwapItems 라는 이름의 델리게이트를 선언했고, 이 델리게이트에 함수를 연결하는 로직은 UIInventoryController 클래스 속 PrepareUI 함수에 있다.
- 즉, 인벤토리 UI 상호작용은 델리게이트와 EventTrigger 인터페이스를 이용하여 구현했다.
(UIInventoryPage 클래스에서 가져온 일부 코드)
public event Action<int> OnDescriptionRequested, OnItemActionRequested;
public event Action<int, int> OnSwapItems;
public void CreateInventoryUI()
{
// 인벤토리 데이터에 칸 늘어날 때 이 함수도 호출해야함..
UIInventoryItem item = Instantiate(inventoryItemPrefab, Vector2.zero, Quaternion.identity);
item.Initialize(); // 아이템 초기화..
item.transform.SetParent(inventoryItemParent.transform); // 부모 지정
item.transform.localScale = Vector3.one; // 규모 지정..
// 델리게이트에 함수 연결해주기!!
item.OnItemClicked += HandleItemClicked; // 아이템을 클릭했을 때 로직
item.OnItemDroppedOn += HandleItemDroppedOn; // 아이템을 놓았을 때 로직
item.OnItemBeginDrag += HandleItemBeginDrag; // 아이템 드래그를 시작했을 때 로직
item.OnItemEndDrag += HandleItemEndDrag; // 아이템 드래그를 종료했을 때 로직
inventoryUIItems.Add(item); // 리스트에 아이템 추가..
}
private void HandleItemEndDrag(UIInventoryItem item)
{
mouseDragItem.gameObject.SetActive(false); // 마우스 드래그 아이템 활성화 꺼주기..
}
private void HandleItemBeginDrag(UIInventoryItem item)
{
CloseBorderImage(); // 다른 경계 다 꺼주고 시작..
currentMouseDragIndex = inventoryUIItems.IndexOf(item); // 매개변수로 전달받은 UIInventoryItem 의 인스턴스가 리스트 속 몇 번째 인덱스 요소인지 가져옴..
// 마우스 드래그 아이템의 상태를 현재 드래그 하려고 하는 아이템의 상태로 업데이트..
mouseDragItem.item.itemImage.sprite = inventoryUIItems[currentMouseDragIndex].itemImage.sprite;
mouseDragItem.item.quantityText.text = item.quantityText.text;
//mouseDragItem.item.quantityText.text = "1"; // 일단 임시로 1..
mouseDragItem.gameObject.SetActive(true); // 마우스 드래그 아이템 활성화 켜주기..
HandleItemClicked(item); // 아이템 클릭했을 때 효과를 드래그 시작할 때도 적용되도록..
}
// 이 매개변수로는 아이템을 드롭한 곳의 아이템칸이 들어옴
private void HandleItemDroppedOn(UIInventoryItem item)
{
int index = inventoryUIItems.IndexOf(item);
if (inventoryUIItems[currentMouseDragIndex].empty) return; // 만약 드래그 시작한 아이템이 비어있는 칸이면 그냥 빠져나가도록..
OnSwapItems?.Invoke(currentMouseDragIndex, index); // 아이템 swap 함수 호출
HandleItemClicked(item); // 아이템 클릭했을 때 효과가 드래그 끝난 후에도 적용되도록..
}
// 이 함수로 매개변수가 전달되는 때는, UIInventoryItem 인스턴스가 이 함수가 연결되어 있는 델리게이트를 호출할 때!
private void HandleItemClicked(UIInventoryItem item)
{
CloseBorderImage(); // 다른 경계 다 꺼주고 시작..
if (item.empty)
{
Debug.Log("비어있는디??!!");
// 클릭한 아이템 칸이 비어있으면 경계도 끄고, 설명도 끄고..
inventoryDescription.ResetDescription(); // 설명 초기화..
currentMouseClickIndex = -1; // 현재 아이템칸 클릭 인덱스로 -1로 초기화 해주고..
sellButtonPanel.SetActive(false); // 판매 버튼 활성화 끄기..
return; // 밑에 로직 수행 안하도록 그냥 나가버리기..
}
int index = inventoryUIItems.IndexOf(item); // 매개변수로 전달받은 UIInventoryItem 의 인스턴스가 리스트 속 몇 번 째 인덱스 요소인지 가져옴..
if (index == -1) return;
// 현재 클릭한 아이템칸의 인덱스 저장..
currentMouseClickIndex = index; // 매개변수로 전달받은 UIIventoryItem 의 인스턴스가 리스트 속 몇 번째 인덱스 요소인지 가져옴..
OpenSellButton(); // 판매 버튼 띄우도록..
inventoryUIItems[index].itemBorderImage.enabled = true; // 경계 활성화 키기..
// 아이템이 클릭되었으면 이제 설명 띄워줘야 함.
OnDescriptionRequested?.Invoke(index);
}
(UIInventoryItem 클래스)
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class UIInventoryItem : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IPointerClickHandler, IDropHandler, IDragHandler
{
[SerializeField]
public Image itemImage;
[SerializeField]
public Text quantityText;
[SerializeField]
public Image itemBorderImage;
[SerializeField]
// delegate 선언
// UIInventoryItem 타입의 매개변수를 받는 함수를 연결할 수 있음.
public event Action<UIInventoryItem> OnItemClicked, OnItemDroppedOn, OnItemBeginDrag, OnItemEndDrag;
[SerializeField]
public bool empty = true;
[SerializeField]
public int touchCount = 0; // 두번 클릭하면 아이템 뒷창 뜨도록..
// UI 는 매개변수로 넘겨줄 수 없음.
// 그래서 아이템 이미지는 Sprite 로 넘겨줌
public void SetData(Sprite itemImage, int quantity)
{
this.itemImage.sprite = itemImage; // 해당 아이템 이미지로 설정
this.quantityText.text = quantity + ""; // 해당 아이템의 개수로 설정
this.itemImage.gameObject.SetActive(true); // 이제 아이템 이미지 활성화..
empty = false; // 이제 비어있지 않을테니..
}
public void Select()
{
// 경계 이미지 활성화
itemBorderImage.enabled = true;
}
public void Deselect()
{
// 경계 이미지 활성화 끄기
itemBorderImage.enabled = false;
}
public void ResetData()
{
this.itemImage.gameObject.SetActive(false); // 다시 아이템 이미지 비활성화..
empty = true; // 다시 비어있도록..
// 경계 이미지 활성화 끄기
Deselect();
}
// 맨 처음 초기화시 호출 함수
public void Initialize()
{
itemImage.gameObject.SetActive(false);
quantityText.text = "";
Deselect();
}
public void OnBeginDrag(PointerEventData eventData)
{
Debug.Log("드래그 시작!!");
if (empty) return; // 만약 아이템 칸이 비어있으면 걍 빠져나가도록..
OnItemBeginDrag?.Invoke(this);
}
// Drop 은 드래그가 종료된 위치에 드롭 가능한 오브젝트가 있는지 여부에 따라 호출이 결정됨.
// 드롭 가능한 오브젝트가 있으면 호출됨(레이캐스트가 오브젝트를 제대로 감지해야함).
// 드롭 가능한 오브젝트가 IDropHandler 인터페이스를 구현하고 있어야함.
// 이건 드래그가 종료된 위치의 오브젝트에서 발생하는 이벤트임..
// 즉, 0번 아이템을 드래그 시작해서 3번 아이템에 드래그 종료하면 3번 아이템에서 드롭 이벤트 발생!!
public void OnDrop(PointerEventData eventData)
{
OnItemDroppedOn?.Invoke(this);
}
public void OnEndDrag(PointerEventData eventData)
{
OnItemEndDrag?.Invoke(this);
}
public void OnPointerClick(PointerEventData eventData)
{
OnItemClicked?.Invoke(this);
}
}
(UIInventoryDescription 클래스)
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
public class UIInventoryDescription : MonoBehaviour
{
[SerializeField]
public Image descriptionItemImage;
[SerializeField]
public Text descriptionTitle;
[SerializeField]
public Text description;
private void Awake()
{
ResetDescription(); // 맨 처음 시작할 때 아이템 설명 칸은 다 비운채로 시작함..
}
public void ResetDescription()
{
descriptionItemImage.gameObject.SetActive(false);
descriptionTitle.text = "";
description.text = "";
}
// 아이템을 클릭했을 때, 아이템에 맞는 설명을 띄우기 위한 함수..
public void SetDescription(Sprite sprite, string itemName, string itemDescription)
{
descriptionItemImage.gameObject.SetActive(true);
descriptionItemImage.sprite = sprite;
descriptionTitle.text = itemName;
description.text = itemDescription;
}
}
(UIInventoryController 클래스 일부 코드)
private void PrepareUI()
{
curInventoryData = seedInventoryData; // 일단 처음 시작은 씨앗 인벤토리로..
// 버튼에 함수 연결
seedButton.onClick.AddListener(SetCurInventoryDataSeed); // 씨앗 버튼에 인벤토리 데이터를 씨앗 인벤토리 데이터로 바꿔주는 함수 연결
fruitButton.onClick.AddListener(SetCurInventoryDataFruit); // 과일 버튼에 인벤토리 데이터를 과일 인벤토리 데이터로 바꿔주는 함수 연결
inventoryOpenButton.onClick.AddListener(OpenInventoryUI); // 버튼에 인벤토리창 여는 로직 함수 연결
// 아이템 판매 판넬 클래스의 델리게이트에 SellItem 함수 연결..
itemSellPanel.sellButtonClicked += SellItem;
inventoryUI.InitializeInventoryUI(curInventoryData.Size); // 씨앗 인벤토리 사이즈만큼 UI 초기화해주기
inventoryUI.OnDescriptionRequested += HandleDescriptionRequest;
inventoryUI.OnSwapItems += HandleSwapItems;
}
// 델리게이트 연결 함수
private void HandleDescriptionRequest(int itemIndex)
{
InventoryItem inventoryItem; // InventoryItem 은 구조체라 null 값을 가질 수 없음(r-value 임..)
// 현재 인벤토리 데이터 변수가 가리키는 값이 씨앗 인벤토리 데이터 값이라면..
if (curInventoryData == seedInventoryData)
{
inventoryItem = seedInventoryData.GetItemAt(itemIndex); // 전달받은 아이템의 인덱스로 인벤토리 아이템을 가져옴..
if (inventoryItem.IsEmpty) // 만약 인벤토리 아이템이 비어있으면 디스크립션 초기화하고 빠져나가도록..
{
inventoryUI.ResetDescription();
return;
}
ItemSO item = inventoryItem.item;
inventoryUI.UpdateDescription(itemIndex, item.itemImage, item.Name, item.Description);
}
// 현재 인벤토리 데이터 변수가 가리키는 값이 과일 인벤토리 데이터 값이라면..
else if (curInventoryData == fruitInventoryData)
{
inventoryItem = fruitInventoryData.GetItemAt(itemIndex); // 전달받은 아이템의 인덱스로 인벤토리 아이템을 가져옴..
if (inventoryItem.IsEmpty) // 만약 인벤토리 아이템이 비어있으면 디스크립션 초기화하고 빠져나가도록..
{
inventoryUI.ResetDescription();
return;
}
ItemSO item = inventoryItem.item;
inventoryUI.UpdateDescription(itemIndex, item.itemImage, item.Name, item.Description);
}
}
private void HandleSwapItems(int index1, int index2)
{
// 현재 인벤토리 데이터 변수가 가리키는 값이 씨앗 인벤토리 데이터 값이라면..
if (curInventoryData == seedInventoryData)
{
seedInventoryData.SwapItems(index1, index2); // 씨앗 인벤토리 데이터의 SwapItems 함수를 호출함!
}
// 현재 인벤토리 데이터 변수가 가리키는 값이 과일 인벤토리 데이터 값이라면..
else if (curInventoryData == fruitInventoryData)
{
fruitInventoryData.SwapItems(index1, index2); // 과일 인벤토리 데이터의 SwapItems 함수를 호출함!
}
}
2. 아이템 종류별 인벤토리 창
- 인벤토리 창 UI 를 아이템 종류별로 만들기 보다는 이미 만들어 놓은 UI 를 재사용 하는 식으로 구현하고 싶었다.
- 인벤토리 창 UI 위에 씨앗, 과일 버튼을 놓아서 씨앗 버튼을 누르면 씨앗 인벤토리, 과일 버튼을 누르면 과일 인벤토리 내용만 보이도록 하고 싶었다.
- 씨앗 버튼을 누르면 curInventoryData 변수 값으로 seedInventoryData 가 들어가고, 과일 버튼을 누르면 fruitInventoryData 가 들어가도록 했다(여기서 seedInventoryData 와 fruitInventoryData 는 스크립터블 오브젝트이다).
- inventoryUIItems 배열은 UIInventoryItem 타입의 요소를 저장하고 있다.
- CreateInventoryUI 함수는 현재 인벤토리 창에서 보여주려고 하는 인벤토리의 사이즈가 전에 보여주고 있었던 인벤토리의 사이즈보다 크다면 부족한 만큼의 인벤토리 칸을 새로 생성해주는 역할을 한다.
(UIInventoryController 속 함수)
// 버튼 관련
private void SetCurInventoryDataSeed()
{
// 현재 인벤토리 데이터 값을 Seed 인벤토리 데이터 값으로 설정하는 함수
inventoryUI.sellButtonPanel.gameObject.SetActive(false); // 씨앗은 판매 불가능하니까 씨앗 인벤토리 창으로 전환하면 판매 버튼도 그냥 꺼지도록..
curInventoryData = seedInventoryData;
inventoryUI.SetInventoryUI(curInventoryData.Size); // 인벤토리 UI 를 현재 보려고 선택한 인벤토리 데이터에 맞게 설정..
inventoryUI.ResetDescription(); // 인벤토리 창 변경하면 설명도 꺼지도록..
curInventoryData.InformAboutChange();
}
private void SetCurInventoryDataFruit()
{
// 현재 인벤토리 데이터 값을 Fruit 인벤토리 데이터 값으로 설정하는 함수
curInventoryData = fruitInventoryData;
inventoryUI.SetInventoryUI(curInventoryData.Size); // 인벤토리 UI 를 현재 보려고 선택한 인벤토리 데이터에 맞게 설정..
inventoryUI.ResetDescription(); // 인벤토리 창 변경하면 설명도 꺼지도록..
curInventoryData.InformAboutChange();
}
(UIInventoryPage 속 함수)
public void SetInventoryUI(int inventorySize)
{
int curInventorySize = inventoryUIItems.Count; // 현재 인벤토리 페이지의 아이템칸 사이즈를 가져옴..
if (curInventorySize <= inventorySize) // 새로 설정하려는 인벤토리 사이즈가 현재 인벤토리 사이즈보다 크거나 같다면..
{
for (int i = 0; i < curInventorySize; i++)
{
inventoryUIItems[i].gameObject.SetActive(true); // 새로 설정하려는 사이즈보다 크거나 같을 때, 일단 가지고 있는 인벤토리칸 다 켜기..
}
for (int i = 0; i < inventorySize - curInventorySize; i++) // 부족한 아이템 칸만큼 새로 생성해주기 위한 반복문..
{
CreateInventoryUI(); // 인벤토리 칸 생성
}
}
else if (curInventorySize > inventorySize) //새로 설정하려는 인벤토리 사이즈가 현재 인벤토리 사이즈보다 작다면..
{
for (int i = 0; i < curInventorySize-inventorySize; i++) // 그냥 불필요한 아이템칸 수만큼 활성화 꺼주면 됨..
{
// 뒤에서부터 차례대로 활성화 꺼주기..
inventoryUIItems[curInventorySize - i - 1].gameObject.SetActive(false);
}
}
}
public void CreateInventoryUI()
{
// 인벤토리 데이터에 칸 늘어날 때 이 함수도 호출해야함..
UIInventoryItem item = Instantiate(inventoryItemPrefab, Vector2.zero, Quaternion.identity);
item.Initialize(); // 아이템 초기화..
item.transform.SetParent(inventoryItemParent.transform); // 부모 지정
item.transform.localScale = Vector3.one; // 규모 지정..
// 델리게이트에 함수 연결해주기!!
item.OnItemClicked += HandleItemClicked; // 아이템을 클릭했을 때 로직
item.OnItemDroppedOn += HandleItemDroppedOn; // 아이템을 놓았을 때 로직
item.OnItemBeginDrag += HandleItemBeginDrag; // 아이템 드래그를 시작했을 때 로직
item.OnItemEndDrag += HandleItemEndDrag; // 아이템 드래그를 종료했을 때 로직
inventoryUIItems.Add(item); // 리스트에 아이템 추가..
}
3. 인벤토리 UI 업데이트
- 인벤토리 데이터 상에 변경사항이 생길 때마다 인벤토리 UI 도 그에 맞게 변경해주어야 한다.
- 인벤토리에 아이템을 더하거나 빼고, 아이템의 위치를 변경했을 때 이 변경사항에 대해 반응하기 위해서 InformAboutChange 함수를 이용했다.
- InformAboutChange 함수 속에서 OnInventoryUpdated, OnInventoryUpdatedInt 델리게이트에 연결된 함수를 호출하도록 했다.
- OnInventoryUpdated 는 Dictionary<int, InventoryItem> 타입의 매개변수를 받는 함수를 연결할 수 있도록 했고, OnInventoryUpdatedInt 는 정수 타입의 매개변수를 받는 함수를 연결할 수 있도록 했다.
- 각각의 델리게이트에 함수를 연결하는 로직은 InventoryController 클래스 속 PrepareInventoryData 함수에 있다.
- OnInventoryUpdated 델리게이트에는 UpdateInventoryUI 함수를, OnInventoryUpdatedInt 델리게이트에는 SetInventoryToContainer 함수를 연결해주었다.
- OnInventoryUpdatedInt 델리게이트는 인벤토리 상의 데이터를 seedContainer 와 fruitContainer 의 아이템 개수 저장 배열의 값을 변경사항에 맞게 업데이트 해주기 위해 선언했다.
(InventorySO 클래스 속 함수)
public void InformAboutChange()
{
// 델리게이트에 연결되어 있는 함수 호출..
// UpdateInventoryUI 함수가 연결되어 있음..
OnInventoryUpdated?.Invoke(GetCurrentInventoryState());
OnInventoryUpdatedInt?.Invoke(inventoryType);
}
(InventoryController 클래스 속 함수)
// 인벤토리 준비 함수
private void PrepareInventoryData()
{
// 인벤토리 각각 초기화해주기..
//seedInventoryData.Initialize();
//fruitInventoryData.Initialize();
// 델리게이트에 UpdateInventoryUI 함수를 연결하기..
// 인벤토리 데이터에 변경사항이 생기면 UpdateInventoryUI 함수를 호출할 수 있도록..
seedInventoryData.OnInventoryUpdated += UpdateInventoryUI;
fruitInventoryData.OnInventoryUpdated += UpdateInventoryUI;
// 델리게이트에 SetInvenetoryToContainer 함수를 연결하기..
// 인벤토리 데이터에 변경사항이 생기면 SetInvenetoryToContainer 함수를 호출할 수 있도록..
seedInventoryData.OnInventoryUpdatedInt += SetInventoryToContainer;
fruitInventoryData.OnInventoryUpdatedInt += SetInventoryToContainer;
// 델리게이트에 SetInventoryUI 함수 연결하기..
// 인벤토리 사이즈에 변경사항이 생기면 호출할 수 있도록..
seedInventoryData.OnInventorySizeUpdated += SetInventoryUI;
fruitInventoryData.OnInventorySizeUpdated += SetInventoryUI;
// 델리게이트에 SetItemSellPanel 함수 연결해놓기..
// 판매 버튼 눌렀을 때, 판매 창 정보를 현재 선택한 아이템의 정보로 설정하기 위함..
inventoryUI.OnItemActionRequested += SetItemSellPanel;
// 델리게이트에 SetOpenSellButton 함수 연결해놓기..
// 아이템 눌렀을 때, 아이템이 과일, 보석이면 인벤토리에서 판매버튼 뜰 수 있도록 하기 위함..
inventoryUI.OpenSellButtonPossible += SetOpenSellButton;
}
// 인벤토리 변경 사항 처리 관련 함수
private void UpdateInventoryUI(Dictionary<int, InventoryItem> curInventory)
{
inventoryUI.ResetInventoryItems(); // 한 번 UI 리셋하고 시작..
// 현재 인벤토리 상태를 매개변수로 받은 후, 그 상태에 맞게 UI 새로 업데이트 해주기
foreach (var item in curInventory)
{
int index = item.Key;
InventoryItem temp = item.Value;
inventoryUI.inventoryUIItems[index].SetData(temp.item.itemImage, temp.quantity);
}
}
private void SetInventoryToContainer(int itemType)
{
// 이 함수에서 농장씬의 씨앗, 과일 컨테이너에 인벤토리의 정보를 반영해줄 것임..
// 만약 값이 null 이라면 현재 씬이 농장이 아닌 거니까 인벤토리의 정보를 반영해주는 함수를 호출하면 안됨(에러남)..
// 그러니까 그냥 빠져나오도록..
if (seedContainer == null || fruitContainer == null) return;
// 현재 씬이 농장이면 여기로 도달함..
// 여기서 정보 반영 함수 호출!!
Dictionary<int, InventoryItem> curInventory;
switch (itemType)
{
case 0:
// 씨앗
curInventory = seedInventoryData.GetCurrentInventoryState();
// 컨테이너 한 번 리셋해주기..
seedContainer.ResetContainer();
seedContainer.SetContainer(curInventory);
break;
case 1:
// 과일
curInventory = fruitInventoryData.GetCurrentInventoryState();
// 컨테이너 한 번 리셋해주기..
fruitContainer.ResetContainer();
fruitContainer.SetContainer(curInventory);
break;
case 2:
// 보석
break;
case 3:
// 케이크
break;
}
}
(InventorySO 클래스)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Inventory.Model
{
[Serializable]
[CreateAssetMenu]
public class InventorySO : ScriptableObject
{
[SerializeField]
private List<InventoryItem> inventoryItems;
[field: SerializeField]
public int Size { get; private set; } = 10; // 인벤토리 사이즈
[field: SerializeField]
public int inventoryType; // 인벤토리 타입 0: 씨앗, 1: 과일, 2: 보석, 3: 케이크...
// 인벤토리가 업데이트 됐는지 여부 확인 후 함수 호출하기 위함(매번 Update 함수에서 확인 안해도됨)..
public event Action<Dictionary<int, InventoryItem>> OnInventoryUpdated; // Dictionary<int, InventoryItem> 타입의 매개변수를 받는 함수를 연결할 수 있음..
public event Action<int> OnInventoryUpdatedInt, OnInventorySizeUpdated; // int 타입의 매개변수를 받는 함수를 연결할 수 있음..
// 맨 처음 게임 시작시 호출하는 함수
public void Initialize()
{
inventoryItems = new List<InventoryItem>();
for (int i = 0; i < Size; i++)
{
inventoryItems.Add(InventoryItem.GetEmptyItem());
}
}
public void AddItem(ItemSO item, int quantity)
{
// 만약 아이템의 수량을 쌓을 수 있으면..
if (item.IsStackable)
{
for (int i=0; i<inventoryItems.Count; i++)
{
// 아이템 칸이 비어있으면 그냥 지나가도록..
if (inventoryItems[i].IsEmpty) continue;
if (inventoryItems[i].item.ID == item.ID)
{
// 남아 있는 여분 공간
int remainingSpace = item.MaxStackSize - inventoryItems[i].quantity;
// 남아 있는 여분 공간이 현재 더하려는 양보다 크거나 같으면
if (remainingSpace >= quantity)
{
inventoryItems[i] = inventoryItems[i].ChangeQuantity(inventoryItems[i].quantity + quantity);
return;
}
// 남아 있는 여분 공간이 현재 더하려는 양보다 작으면
else
{
// 여분 공간을 모두 채워 준 후..
inventoryItems[i] = inventoryItems[i].ChangeQuantity(item.MaxStackSize);
quantity -= remainingSpace; // 남은 양 구하기ㅏ..
}
}
}
}
// 인벤토리를 돌면서
for (int i=0; i<inventoryItems.Count; i++)
{
// 비어있는 인벤토리 칸을 찾기..
if (inventoryItems[i].IsEmpty)
{
// 아이템이 스택 가능하고, 아이템의 양이 아이템의 맥스치보다 크면
if (item.IsStackable && quantity > item.MaxStackSize)
{
// 새로운 아이템 칸을 만들고
inventoryItems[i] = new InventoryItem
{
item = item,
quantity = item.MaxStackSize
};
quantity -= item.MaxStackSize; // 남은 양 구하기..
}
// 아이템의 종류가 스택 불가능 하거나, 아이템의 양이 아이템의 맥스치보다 작으면
else
{
// 새로운 아이템 칸을 만들고
inventoryItems[i] = new InventoryItem
{
item = item,
quantity = quantity // 양 그대로 넣어주기..
};
return;
}
}
}
// 아이템을 저장할 공간이 부족하면..
if (quantity > 0)
{
while (quantity > 0) {
SizeUpInventory(); // 인벤토리를 한칸 늘려주고..
// 아이템이 스택 가능하고, 아이템의 양이 아이템의 맥스치보다 크면
if (item.IsStackable && quantity > item.MaxStackSize)
{
// 새로운 아이템 칸을 만들고
inventoryItems[inventoryItems.Count - 1] = new InventoryItem
{
item = item,
quantity = item.MaxStackSize
};
quantity -= item.MaxStackSize; // 남은 양 구하기..
}
// 아이템의 종류가 스택 불가능 하거나, 아이템의 양이 아이템의 맥스치보다 작으면
else
{
// 새로운 아이템 칸을 만들고
inventoryItems[inventoryItems.Count - 1] = new InventoryItem
{
item = item,
quantity = quantity // 양 그대로 넣어주기..
};
return;
}
}
}
}
public void SizeUpInventory()
{
// 인벤토리 사이즈 늘리고, 새로 만든 칸을 리스트에 넣어주고, 변경사항 알리는 함수 호출!!
Size += 1;
inventoryItems.Add(InventoryItem.GetEmptyItem());
// 델리게이트에 연결되어 있는 함수 호출..
// 인벤토리의 사이즈가 업데이트 될 때 실행되어야 하는 함수 연결되어있음..
OnInventorySizeUpdated?.Invoke(Size); // 매개변수로 현재 인벤토리의 사이즈 보내주기..
}
public void AddItem(InventoryItem item)
{
AddItem(item.item, item.quantity);
InformAboutChange(); // 아이템을 추가하면 인벤토리의 상태가 변경되므로 UI 상태 업데이트 해주기..
}
public void MinusItem(ItemSO item, int quantity)
{
int totalItemCount = 0;
int minIdx = -1;
int min = int.MaxValue;
List<int> itemIdxTmp = new List<int>();
// 스택 가능한 아이템인 경우
if (item.IsStackable)
{
for (int i = 0; i < inventoryItems.Count; i++)
{
if (inventoryItems[i].IsEmpty) continue; // 만약 비어있는 칸이면 그냥 넘기도록..
// 빼려고 하는 아이템의 아이디와 같다면..
if (inventoryItems[i].item.ID == item.ID)
{
// 총 아이템 카운트를 올려주기..
totalItemCount += inventoryItems[i].quantity;
itemIdxTmp.Add(i); // 아이템의 인덱스 넣어주기..
if (min > inventoryItems[i].quantity)
{
min = inventoryItems[i].quantity;
minIdx = i;
}
}
}
// 아이템의 총수량이 현재 빼려고 하는 아이템의 수량보다 크거나 같다면..
if (totalItemCount >= quantity)
{
if (minIdx != -1) // 가장 작은 수를 못 찾은 경우가 아니면..
{
if (inventoryItems[minIdx].quantity > quantity) // 제거해야할 아이템의 수량이, 해당 아이템의 최소 수량 인벤토리 칸의 수량보다 작다면..
{
// 인벤토리 아이템 수량 변경..
inventoryItems[minIdx] = inventoryItems[minIdx].ChangeQuantity(inventoryItems[minIdx].quantity - quantity);
return; // 빠져나가기..
}
else if (inventoryItems[minIdx].quantity == quantity) // 제거해야할 아이템의 수량이, 해당 아이템의 최소 수량 인벤토리 칸의 수량이랑 같다면..
{
// 인벤토리 아이템칸을 비어있는 아이템으로 변경..
inventoryItems[minIdx] = InventoryItem.GetEmptyItem();
return; // 빠져나가기..
}
else if (inventoryItems[minIdx].quantity < quantity) // 제거해야할 아이템의 수량이, 해당 아이템의 최소 수량 인벤토리 칸의 수량보다 크다면..
{
quantity -= inventoryItems[minIdx].quantity;
inventoryItems[minIdx] = InventoryItem.GetEmptyItem();
}
}
// 위 조건문 둘다 만족 안하면..
// 빼야 하는 수량이 0 이랑 같아질때까지 반복문 돌면서(뒤에 있는 아이템부터 없앨 것..)..
int idx = 0;
while (quantity > 0 && idx < itemIdxTmp.Count)
{
Debug.Log(itemIdxTmp.Count);
int temp = itemIdxTmp[itemIdxTmp.Count - 1 - idx]; // 해당 아이템의 인덱스를 모아놓은 리스트에서 요소 가져옴..
idx++;
// 최소 인덱스의 인벤토리 아이템은 이미 없앴으므로 넘기기..
if (temp == minIdx) continue;
// 제거해야할 수량이 인벤토리 아이템의 수량보다 많거나 같을 때..
if (quantity >= inventoryItems[temp].quantity)
{
quantity -= inventoryItems[temp].quantity; // 뺄 수량을 인벤토리 아에팀 수량만큼 감소시킴..
inventoryItems[temp] = InventoryItem.GetEmptyItem(); // 아이템칸 비우기..
}
// 제거해야할 수량이 인벤토리 아이템의 수량보다 적을 때..
else
{
inventoryItems[temp] = inventoryItems[temp].ChangeQuantity(inventoryItems[temp].quantity - quantity);
quantity = 0;
}
}
}
// 아이템의 총수량이 현재 빼려고 하는 아이템의 수량보다 작다면..
else
{
Debug.Log($"{item.Name}이 부족해!!!!!");
return; // 그냥 빠져나오도록..
}
}
// 아이템이 스택가능한 아이템이 아닌 경우에는..
else
{
for (int i = 0; i < inventoryItems.Count; i++)
{
// 빼려고 하는 아이템의 아이디와 같다면..
if (inventoryItems[i].item.ID == item.ID)
{
inventoryItems[i] = InventoryItem.GetEmptyItem(); // 인벤토리 칸을 비워주고..
return; // 빠져나가기..
}
}
// 여기까지 도달했으면 아이템을 못 찾은거니까 빠져나가기..
Debug.Log($"{item.Name}이 없어요!!!!!");
return;
}
}
public void MinusItem(InventoryItem item)
{
MinusItem(item.item, item.quantity);
InformAboutChange();
}
public void MinusItemAt(int itemIdx, int quantity)
{
if (inventoryItems[itemIdx].quantity == quantity) // 해당 인덱스의 아이템의 양과 똑같은 양을 뺀다면 아이템칸이 빈 인벤토리 아이템을 가지도록..
inventoryItems[itemIdx] = InventoryItem.GetEmptyItem();
else // 해당 인덱스의 아이템의 양보다 적은 양을 뺀다면, 그 수만큼 반영해주기(해당 아이템 양보다 더 많은 양을 빼려고 하는 경우가 생기지 않도록 다른 클래스에서 조정할 것..)
inventoryItems[itemIdx] = inventoryItems[itemIdx].ChangeQuantity(inventoryItems[itemIdx].quantity - quantity);
InformAboutChange();
}
// 현재 인벤토리의 상태를 반환하는 함수..
public Dictionary<int, InventoryItem> GetCurrentInventoryState()
{
Dictionary<int, InventoryItem> returnValue = new Dictionary<int, InventoryItem>();
// 현재 인벤토리 사이즈만큼 반복문을 돌면서 인벤토리 아이템이 비어있지 않은 것만 딕셔너리에 넣어줌..
for (int i = 0; i < inventoryItems.Count; i++)
{
if (inventoryItems[i].IsEmpty)
continue;
returnValue[i] = inventoryItems[i];
}
return returnValue;
}
// 특정 인덱스의 아이템을 가져올 것..
public InventoryItem GetItemAt(int itemIndex)
{
return inventoryItems[itemIndex];
}
// 아이템을 서로 바꿈!!
public void SwapItems(int itemIndex1, int itemIndex2)
{
// InventoryItem 이 구조체이므로 값을 복사해서 저장할 수 있음(참조형은 값 복사 x).
InventoryItem item1 = inventoryItems[itemIndex1];
inventoryItems[itemIndex1] = inventoryItems[itemIndex2];
inventoryItems[itemIndex2] = item1; // item1 은 복사된 값이니까 문제없이 원하는 대로 기능함..
InformAboutChange();
}
public void InformAboutChange()
{
// 델리게이트에 연결되어 있는 함수 호출..
// UpdateInventoryUI 함수가 연결되어 있음..
OnInventoryUpdated?.Invoke(GetCurrentInventoryState());
OnInventoryUpdatedInt?.Invoke(inventoryType);
}
}
/*
!!구조체의 장점!!
간단하고 가벼움: 두 개의 필드만 가지고 있어 구조체로서 이상적이다..
값 타입: 인스턴스를 복사하여 전달하므로 원본 데이터를 보호할 수 있다..
불변성 유지: 변경 가능한 메서드(ChangeQuantity)는 새로운 인스턴스를 반환하므로 불변성을 유지한다..
성능: 스택에 할당되어 가비지 컬렉션의 부하를 줄일 수 있다..
*/
// struct 는 a - value 가 아니라 null 값 가질 수 없음
[System.Serializable] // C#에서 클래스, 구조체 또는 열거형을 직렬화할 수 있도록 지정하는 데 사용
public struct InventoryItem
{
public int quantity;
public ItemSO item;
// 이 속성은 item이 null인지 여부를 확인하여 true 또는 false 값을 반환함..
public bool IsEmpty => item == null; // 읽기 전용 속성..
// 아이템의 수량만 바꾼 InventoryItem 을 새로 만들어서 반환함..
// 아이템의 아이디는 계속 같아야 하므로 item 에 this.item 넣어준 것..
public InventoryItem ChangeQuantity(int newQuantity)
{
return new InventoryItem
{
item = this.item,
quantity = newQuantity,
};
}
// 맨 처음 게임 시작할 때, 인벤토리에 빈 칸을 만들어놓기 위함..
public static InventoryItem GetEmptyItem()
=> new InventoryItem
{
item = null,
quantity = 0,
};
}
}
4. 인벤토리 사이즈 업그레이드 & 인벤토리 UI 반영
- InventoryController 클래스 속 PrepareInventoryData 함수를 다시보면 다음과 같은 로직이 존재한다.
// 델리게이트에 SetInventoryUI 함수 연결하기..
// 인벤토리 사이즈에 변경사항이 생기면 호출할 수 있도록..
seedInventoryData.OnInventorySizeUpdated += SetInventoryUI;
fruitInventoryData.OnInventorySizeUpdated += SetInventoryUI;
- 인벤토리 사이즈에 변경사항이 생기면 호출할 함수를 델리게이트에 연결해주었다(SetInventoryUI).
private void SetInventoryUI(int inventorySize)
{
inventoryUI.SetInventoryUI(inventorySize);
}
- UIInventoryPage 클래스의 SetInventoryUI 함수 내용은 다음과 같다.
public void SetInventoryUI(int inventorySize)
{
int curInventorySize = inventoryUIItems.Count; // 현재 인벤토리 페이지의 아이템칸 사이즈를 가져옴..
if (curInventorySize <= inventorySize) // 새로 설정하려는 인벤토리 사이즈가 현재 인벤토리 사이즈보다 크거나 같다면..
{
for (int i = 0; i < curInventorySize; i++)
{
inventoryUIItems[i].gameObject.SetActive(true); // 새로 설정하려는 사이즈보다 크거나 같을 때, 일단 가지고 있는 인벤토리칸 다 켜기..
}
for (int i = 0; i < inventorySize - curInventorySize; i++) // 부족한 아이템 칸만큼 새로 생성해주기 위한 반복문..
{
CreateInventoryUI(); // 인벤토리 칸 생성
}
}
else if (curInventorySize > inventorySize) //새로 설정하려는 인벤토리 사이즈가 현재 인벤토리 사이즈보다 작다면..
{
for (int i = 0; i < curInventorySize-inventorySize; i++) // 그냥 불필요한 아이템칸 수만큼 활성화 꺼주면 됨..
{
// 뒤에서부터 차례대로 활성화 꺼주기..
inventoryUIItems[curInventorySize - i - 1].gameObject.SetActive(false);
}
}
}
- OnInventorySizeUpdated 델리게이트가 사용되는 함수는 InventorySO 클래스의 SizeUpInventory 함수이다.
public void SizeUpInventory()
{
// 인벤토리 사이즈 늘리고, 새로 만든 칸을 리스트에 넣어주고, 변경사항 알리는 함수 호출!!
Size += 1;
inventoryItems.Add(InventoryItem.GetEmptyItem());
// 델리게이트에 연결되어 있는 함수 호출..
// 인벤토리의 사이즈가 업데이트 될 때 실행되어야 하는 함수 연결되어있음..
OnInventorySizeUpdated?.Invoke(Size); // 매개변수로 현재 인벤토리의 사이즈 보내주기..
}
- InventorySO 클래스의 AddItem 함수에서 아이템을 추가하다가 공간이 부족하면 비로소 SizeUpInventory 함수가 호출되도록 했다.
public void AddItem(ItemSO item, int quantity)
{
// 만약 아이템의 수량을 쌓을 수 있으면..
if (item.IsStackable)
{
for (int i=0; i<inventoryItems.Count; i++)
{
// 아이템 칸이 비어있으면 그냥 지나가도록..
if (inventoryItems[i].IsEmpty) continue;
if (inventoryItems[i].item.ID == item.ID)
{
// 남아 있는 여분 공간
int remainingSpace = item.MaxStackSize - inventoryItems[i].quantity;
// 남아 있는 여분 공간이 현재 더하려는 양보다 크거나 같으면
if (remainingSpace >= quantity)
{
inventoryItems[i] = inventoryItems[i].ChangeQuantity(inventoryItems[i].quantity + quantity);
return;
}
// 남아 있는 여분 공간이 현재 더하려는 양보다 작으면
else
{
// 여분 공간을 모두 채워 준 후..
inventoryItems[i] = inventoryItems[i].ChangeQuantity(item.MaxStackSize);
quantity -= remainingSpace; // 남은 양 구하기ㅏ..
}
}
}
}
// 인벤토리를 돌면서
for (int i=0; i<inventoryItems.Count; i++)
{
// 비어있는 인벤토리 칸을 찾기..
if (inventoryItems[i].IsEmpty)
{
// 아이템이 스택 가능하고, 아이템의 양이 아이템의 맥스치보다 크면
if (item.IsStackable && quantity > item.MaxStackSize)
{
// 새로운 아이템 칸을 만들고
inventoryItems[i] = new InventoryItem
{
item = item,
quantity = item.MaxStackSize
};
quantity -= item.MaxStackSize; // 남은 양 구하기..
}
// 아이템의 종류가 스택 불가능 하거나, 아이템의 양이 아이템의 맥스치보다 작으면
else
{
// 새로운 아이템 칸을 만들고
inventoryItems[i] = new InventoryItem
{
item = item,
quantity = quantity // 양 그대로 넣어주기..
};
return;
}
}
}
// 아이템을 저장할 공간이 부족하면..
if (quantity > 0)
{
while (quantity > 0) {
SizeUpInventory(); // 인벤토리를 한칸 늘려주고..
// 아이템이 스택 가능하고, 아이템의 양이 아이템의 맥스치보다 크면
if (item.IsStackable && quantity > item.MaxStackSize)
{
// 새로운 아이템 칸을 만들고
inventoryItems[inventoryItems.Count - 1] = new InventoryItem
{
item = item,
quantity = item.MaxStackSize
};
quantity -= item.MaxStackSize; // 남은 양 구하기..
}
// 아이템의 종류가 스택 불가능 하거나, 아이템의 양이 아이템의 맥스치보다 작으면
else
{
// 새로운 아이템 칸을 만들고
inventoryItems[inventoryItems.Count - 1] = new InventoryItem
{
item = item,
quantity = quantity // 양 그대로 넣어주기..
};
return;
}
}
}
}
5. 인벤토리 아이템 판매 기능
- 인벤토리 창에서 아이템을 클릭했을 때, 클릭된 아이템을 판매하도록 하는 기능을 구현했다.
- InventorySO 클래스에 MinusItemAt 함수를 만들다. 마지막에 InformAboutChange 함수를 호출하여 변경사항이 인벤토리 UI 에 반영되도록 했다.
public void MinusItemAt(int itemIdx, int quantity)
{
if (inventoryItems[itemIdx].quantity == quantity) // 해당 인덱스의 아이템의 양과 똑같은 양을 뺀다면 아이템칸이 빈 인벤토리 아이템을 가지도록..
inventoryItems[itemIdx] = InventoryItem.GetEmptyItem();
else // 해당 인덱스의 아이템의 양보다 적은 양을 뺀다면, 그 수만큼 반영해주기(해당 아이템 양보다 더 많은 양을 빼려고 하는 경우가 생기지 않도록 다른 클래스에서 조정할 것..)
inventoryItems[itemIdx] = inventoryItems[itemIdx].ChangeQuantity(inventoryItems[itemIdx].quantity - quantity);
InformAboutChange();
}
- MinusItemAt 함수가 호출되는 곳은 UIInventoryController 클래스 속 SellItem 함수이다.
public void SellItem(int count, int price, int itemType)
{
switch (itemType)
{
case 1:
// 과일
// 현재 마우스로 클릭한 아이템의 인덱스 요소를 판매하려는 아이템의 수만큼 감소시키기..
fruitInventoryData.MinusItemAt(inventoryUI.currentMouseClickIndex, count);
GameManager.instance.money += price; // 판매 가격만큼 돈을 더해줌..
break;
case 2:
// 보석
break;
case 3:
// 케이크
break;
}
itemSellPanel.gameObject.SetActive(false); // 팔고 난 다음에 창 끄기..
inventoryUI.currentMouseClickIndex = -1; // -1 로 다시 바꿔주기..
inventoryUI.ResetDescription(); // 아이템 설명창도 리셋해주기..
inventoryUI.sellButtonPanel.gameObject.SetActive(false); // 판매 버튼 판넬도 꺼주기..
inventoryData.seedInventoryData = seedInventoryData;
inventoryData.fruitInventoryData = fruitInventoryData;
//SaveInventoryData(); // 데이터 저장!
}
- SellItem 함수는 UIInventoryController 클래스 속 PrepareUI 함수에서 sellButtonClicked 델리게이트에 연결해주었다.
private void PrepareUI()
{
curInventoryData = seedInventoryData; // 일단 처음 시작은 씨앗 인벤토리로..
// 버튼에 함수 연결
seedButton.onClick.AddListener(SetCurInventoryDataSeed); // 씨앗 버튼에 인벤토리 데이터를 씨앗 인벤토리 데이터로 바꿔주는 함수 연결
fruitButton.onClick.AddListener(SetCurInventoryDataFruit); // 과일 버튼에 인벤토리 데이터를 과일 인벤토리 데이터로 바꿔주는 함수 연결
inventoryOpenButton.onClick.AddListener(OpenInventoryUI); // 버튼에 인벤토리창 여는 로직 함수 연결
// 아이템 판매 판넬 클래스의 델리게이트에 SellItem 함수 연결..
itemSellPanel.sellButtonClicked += SellItem;
inventoryUI.InitializeInventoryUI(curInventoryData.Size); // 씨앗 인벤토리 사이즈만큼 UI 초기화해주기
inventoryUI.OnDescriptionRequested += HandleDescriptionRequest;
inventoryUI.OnSwapItems += HandleSwapItems;
}
- sellButtonClicked 델리게이트 속 함수를 호출하는 구문은 ItemSellPanel 클래스 속 SellItem 함수에서 담당하고 있다.
private void SellItem()
{
// 델리게이트에 연결된 함수 호출..
sellButtonClicked?.Invoke(curItemCount, totalPrice, curItem.item.itemType);
}
- 이 함수를 ItemSellPanel 클래스의 Awake 함수에서 아이템 판매 버튼에 연결해놓고 사용했다.
private void Awake()
{
// 각 버튼에 함수 연결해주기..
minusButton.onClick.AddListener(MinusItemCount);
plusButton.onClick.AddListener(PlusItemCount);
sellButton.onClick.AddListener(SellItem);
// 판매 수량은 1에서 적어질 수 없으므로 일단 현재 개수가 1인 상태를 가격에 업데이트 하고 시작..
UpdateTotalPrice(curItemCount);
}
- 선택된 아이템을 판매해야 하므로, 인벤토리 창에서 아이템을 누르고 난 뒤 뜨는 판매 버튼을 누르면 정보가 설정되도록 했다. 정보를 설정하는 함수는 ItemSellPanel 클래스의 SetItemInfo 함수이다.
public void SetItemInfo(InventoryItem item)
{
curItem = item;
// 플러스 버튼을 눌러서 최대한으로 올라갈 수 있는 한계치를 현재 선택한 아이템의 개수로 설정
maxItemCount = item.quantity;
curItemCount = 1;
// 현재 아이템 가격 결정용..
switch (curItem.item.itemType)
{
case 1:
// 과일
itemPrice = ((FruitItemSO)curItem.item).fruitPrice;
break;
case 2:
// 보석
break;
case 3:
// 케이크
break;
}
UpdateTotalPrice(curItemCount);
}
- 현재 판매해야 하는 아이템의 정보를 설정하는 함수를 호출하는 함수인 SetItemSellPanel 이 존재한다. 이 함수는 UIInventoryController 클래스에 존재한다. 인벤토리 UI 상의 아이템 인덱스를 받고, 받은 인덱스를 이용하여 현재 인벤토리 데이터에서 아이템을 찾은 후 SetItemInfo 함수로 넘겨주는 역할을 한다.
private void SetItemSellPanel(int itemIndex)
{
InventoryItem item = curInventoryData.GetItemAt(itemIndex);
itemSellPanel.gameObject.SetActive(true);
itemSellPanel.SetItemInfo(item);
}
- UIInventoryController 클래스 속 PrepareInventoryData 함수를 다시 보면 OnItemActionRequested 델리게이트에 함수를 연결하는 모습을 볼 수 있다.
// 델리게이트에 SetItemSellPanel 함수 연결해놓기..
// 판매 버튼 눌렀을 때, 판매 창 정보를 현재 선택한 아이템의 정보로 설정하기 위함..
inventoryUI.OnItemActionRequested += SetItemSellPanel;
- UIInventoryPage 클래스 속 ClickSellButton 함수가 호출될 때, 비로소 OnItemActionRequested 델리게이트에 연결된 함수가 호출되도록 했다.
public void ClickSellButton()
{
// 이 함수를 sellButton 에 연결해줘야함..
if (currentMouseClickIndex == -1) return; // 만약 -1 이면 그냥 빠져나가도록..
// 현재 클릭한 아이템칸의 인덱스를 함수의 매개변수로 보내줌..
// 아이템 판매 판넬의 정보를 이 인덱스의 아이템의 정보로 설정해줄것..
OnItemActionRequested?.Invoke(currentMouseClickIndex);
}
- UIInventoryPage 클래스 속 Awake 함수에서 판매 버튼에 ClickSellButton 함수를 연결해주었다.
public void InitializeInventoryUI(int inventorySize)
{
sellButton.onClick.AddListener(ClickSellButton); // 판매 버튼에 함수 연결..
inventoryUIItems = new List<UIInventoryItem>(initialInventorySize);
for (int i=0; i<inventorySize; i++)
{
CreateInventoryUI(); // 인벤토리 칸 생성
}
}
3. 씬 이동 시 인벤토리 매니저에 참조 변수 값 찾아서 넣어주는 기능
---> 참고자료:
https://coding-of-today.tistory.com/171
[유니티] 코루틴의 사용법 총정리 - Unity Coroutine
코루틴(Coroutine) 1. 어디에 쓰이는가? 우선, 코루틴이 어떤 상황에서 필요한지 알아보자. 유니티에서 특정 코드가 반복적으로 실행되기 위해서는 Update문에 코드를 작성하면 되는데, 간혹 Update가
coding-of-today.tistory.com
[Unity] 유니티 게임 오브젝트를 찾는 방법 - GameObject
이번 포스팅에서는 유니티의 게임 오브젝트를 찾아 레퍼런스를 얻는 방법을 알아보려고 합니다. 이전에 작성한 유니티 컴포넌트 찾는 방법에서 이야기했듯이 유니티 scene 안에 게임 오브젝트는
codeposting.tistory.com
- 씬을 전환하면 이전 씬에 있던 게임 오브젝트가 파괴되면서 InventoryManager 게임오브젝트의 참조 변수들의 값이 다 빠져버리게 되었다.
- 그래서 씬을 전환할 때마다 참조 변수에 해당 타입에 맞는 게임 오브젝트를 다시 넣어주고자 하였다.
- OnSceneLoaded 함수는 유니티에서 제공하는 함수로 씬이 전환될 때 호출되는 함수이다.
- 코루틴을 이용하여 게임 오브젝트들이 생성될 때까지 기다려준 후, 다 생성됐으면 비로소 참조 변수의 값을 설정해주는 함수인 InitializeReferences 를 호출하도록 했다.
- 유니티의 Find 함수는 게임 오브젝트가 활성화 되어있지 않으면 못 찾는다고 한다.
(UIInventoryController 클래스 속 일부 코드)
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 씬이 완전히 로드될 때까지 기다린 후 코루틴 시작..
StartCoroutine(InitializeAfterSceneLoad());
}
private IEnumerator InitializeAfterSceneLoad()
{
// 다음 프레임에 실행 되도록 하는 구문..
yield return null;
Debug.Log("씬 로드됨!!!");
// 씬이 로드될 때 참조 변수 설정
InitializeReferences();
PrepareUI();
PrepareInventoryData();
}
void InitializeReferences()
{
// 씬을 전환하면 이 클래스가 참조하고 있던 게임오브젝트들이 날아감..
// 그래서 현재 씬에서 타입에 맞는 게임오브젝트를 찾아서 연결해줄것..
// 씬에서 필요한 게임 오브젝트 찾기
inventoryUI = FindObjectOfType<UIInventoryPage>();
itemSellPanel = FindObjectOfType<ItemSellPanel>();
// ?. 를 이용해서 null 인지 아닌지 판단함..
// seedContainer 랑 fruitcontainer 는 농장 씬에만 있도록 할거라서 다른 씬에서는 값이 null 로 설정되어 있을 것..
seedButton = GameObject.Find("SeedButton")?.GetComponent<Button>();
fruitButton = GameObject.Find("FruitButton")?.GetComponent<Button>();
seedContainer = GameObject.Find("SeedContainer")?.GetComponent<SeedContainer>();
fruitContainer = GameObject.Find("FruitContainer")?.GetComponent<FruitContainer>();
inventoryOpenButton = GameObject.Find("InventoryOpenButton")?.GetComponent<Button>();
// 일단 다 끈 상태로 시작..
inventoryUI.gameObject.SetActive(false);
seedButton.gameObject.SetActive(false);
fruitButton.gameObject.SetActive(false);
itemSellPanel.gameObject.SetActive(false);
}
4. 농장 데이터 저장
---> 참고자료:
https://wergia.tistory.com/164
[Unity3D] 유니티에서 JSON 사용하기(Unity JSON Utility)
유니티에서 JSON 사용하기(Unity JSON Utility) 작성 기준 버전 :: 2018.3.1f1 JSON은 웹이나 네트워크에서 서버와 클라이언트 사이에서 데이터를 주고 받을 때 사용하는 개방형 표준 포멧으로, 텍스트를 사
wergia.tistory.com
https://dodnet.tistory.com/4539
유니티 C# 딕셔너리 저장? List Dictionary화 JSON Serialize 하는 방법.
유니티 C# 딕셔너리 저장? 유니티에서 Dictionary 를 JSON Serialize 하는 방법? List를 Dictionary로 만드는 방법? 일단 확실히 해두어야 할점은 Dictionary 타입은 유니티의 JSON Serialization 을 지원하지 않는다.
dodnet.tistory.com
(Unity) Dictionary를 Json 데이터로 파싱하기!
Dictionary를 Json 데이터로 파싱하기 놀랍게도 Dictionary형식은 Json 데이터로 파싱이 안된다!! 그렇기에 한번 데이터를 후처리 후 파싱을 시켜야한다. 데이터를 Dictionary로 저장 후 JsonUtility로 ToJson하
velog.io
https://dyunace.tistory.com/30
Unity - 커스텀 Class 내부의 Dictionary 를 Json으로 저장
Json 저장 방식에 추가로 알아보던 중 어느 한 분의 글을 보았습니다. 원문 : https://velog.io/@kohyeonseo1006/Unity-Dictionary를-Json-데이터로-파싱하기 (Unity) Dictionary를 Json 데이터로 파싱하기! Dictionary를 Json
dyunace.tistory.com
- FarmingManager 에서 농장 데이터를 딕셔너리를 이용하여 관리하고 있었다.
- 키는 타일맵의 셀 위치(Vector3Int 타입), 밸류는 농사 데이터(FarmingData 타입) 이다.
- Json 은 딕셔너리, Vector3Int, UI 와 같은 타입의 저장기능을 지원하지 않는다고 한다.
- 즉, 딕셔너리와 Vector3Int 는 따로 클래스를 만들어서 정보를 한번 가공해준 후 사용할 수 있었다.
- 그래서 임의로 SaveFarmingData 클래스(FarmingData 클래스 대신) 를 만들어주었는데, 기존의 FarmingData 에서 UI 변수만 제거한 모습이다.
// 데이터 저장 클래스
[Serializable]
public class SaveFarmingData
{
[SerializeField]
public bool seedOnTile; // 타일 위에 씨앗이 있는지 여부 확인용
[SerializeField]
public bool plowEnableState; // 밭을 갈 수 있는 상태인지 여부 확인용
[SerializeField]
public bool plantEnableState; // 씨앗을 심을 수 있은 상태인지 여부 확인용
[SerializeField]
public bool harvestEnableState; // 작물이 다 자란 상태인지 여부 확인용
[SerializeField]
public string currentState; // 농사 땅 상태..
// 씨앗 데이터
[SerializeField]
public int seedIdx; // 씨앗 인덱스 저장(종류 저장하기 위함)..
[SerializeField]
public float currentTime; // 씨앗을 심은 뒤로 흐른 시간 저장
[SerializeField]
public bool isGrown; // 씨앗이 다 자랐는지 안자랐는지 여부 저장..
public void PrintData()
{
Debug.Log(seedOnTile + " " + plowEnableState + " " + plantEnableState + " " + harvestEnableState + " " + currentState + " " + seedIdx + " " + currentTime + " " + isGrown);
}
}
- Vector3Int 타입도 JSON 이 지원하지 않으니 PosInt 클래스를 따로 만들어 주었다.
[Serializable]
// 아예 따로 클래스 만들어서 값을 저장할 때 Vector3Int 대신 PosInt 써야할 것 같다..
public class PosInt
{
[SerializeField]
public int x;
[SerializeField]
public int y;
[SerializeField]
public int z;
}
- 그리고 딕셔너리 타입도 아예 제네릭 타입으로 새로 만들어주었다.
[Serializable]
// 딕셔너리를 클래스 형식으로 key, value 를 만들어서 구성하가ㅣ..
// 다른 Dictionary 도 고려하여 제네릭 타입으로 만들기..
public class DataDictionary<TKey, TValue>
{
public TKey Key;
public TValue Value;
}
[Serializable]
public class JsonDataArray<TKey, TValue>
{
// 임의로 생성한 딕셔너리 값 저장용 리스트
public List<DataDictionary<TKey, TValue>> data;
}
public static class DictionaryJsonUtility
{
/// <summary>
/// Dictionary를 Json으로 파싱하기
/// </summary>
/// <typeparam name="TKey">Dictionary Key값 형식</typeparam>
/// <typeparam name="TValue">Dictionary Value값 형식</typeparam>
/// <param name="jsonDicData"></param>
/// <returns></returns>
public static string ToJson<TKey, TValue>(Dictionary<TKey, TValue> jsonDicData, bool pretty = false)
{
List<DataDictionary<TKey, TValue>> dataList = new List<DataDictionary<TKey, TValue>>();
DataDictionary<TKey, TValue> dictionaryData;
foreach (TKey key in jsonDicData.Keys)
{
dictionaryData = new DataDictionary<TKey, TValue>();
dictionaryData.Key = key;
dictionaryData.Value = jsonDicData[key];
dataList.Add(dictionaryData);
}
JsonDataArray<TKey, TValue> arrayJson = new JsonDataArray<TKey, TValue>();
arrayJson.data = dataList;
return JsonUtility.ToJson(arrayJson, pretty);
}
/// <summary>
/// Json Data를 다시 Dictionary로 파싱하기
/// </summary>
/// <typeparam name="TKey">Dictionary Key값 형식</typeparam>
/// <typeparam name="TValue">Dictionary Value값 형식</typeparam>
/// <param name="jsonData">파싱되었던 데이터</param>
/// <returns></returns>
public static Dictionary<TKey, TValue> FromJson<TKey, TValue>(string jsonData)
{
JsonDataArray<TKey, TValue> arrayJson = JsonUtility.FromJson<JsonDataArray<TKey, TValue>>(jsonData);
List<DataDictionary<TKey, TValue>> dataList = arrayJson.data;
Dictionary<TKey, TValue> returnDictionary = new Dictionary<TKey, TValue>();
for (int i = 0; i < dataList.Count; i++)
{
DataDictionary<TKey, TValue> dictionaryData = dataList[i];
returnDictionary.Add(dictionaryData.Key, dictionaryData.Value);
//returnDictionary[dictionaryData.Key] = dictionaryData.Value;
}
return returnDictionary;
}
}
- SaveFarmingData 함수를 만들어서 비로소 데이터를 저장할 수 있게되었다.
public void SaveFarmingData()
{
Dictionary<PosInt, SaveFarmingData> tempDic = new Dictionary<PosInt, SaveFarmingData>();
foreach (var item in farmingData)
{
Debug.Log(item.Key + "저장할겁니다!!");
// JSON 저장할 때 Vector3Int 가 직렬화가 안되므로 따로 만든 PosString 이용하가ㅣ..
PosInt pos = new PosInt
{
x = item.Key.x,
y = item.Key.y,
z = item.Key.z
};
SaveFarmingData temp = new SaveFarmingData
{
plowEnableState = farmingData[item.Key].plowEnableState,
plantEnableState = farmingData[item.Key].plantEnableState,
harvestEnableState = farmingData[item.Key].harvestEnableState,
currentState = farmingData[item.Key].currentState
};
// 농사 땅 위에 씨앗이 없을 때 진입..
if (farmingData[item.Key].seed == null)
{
Debug.Log("씨앗 없어여..");
temp.seedOnTile = false;
}
// 농사 땅 위에 씨앗 있을 때 진입..
else
{
Debug.Log("씨앗 있어여..");
temp.seedOnTile = true; // 땅에 씨앗 심어져있는지 여부 판단 정보 저장..
temp.seedIdx = farmingData[item.Key].seed.seedData.seedIdx; // 씨앗 인덱스 저장..
temp.currentTime = farmingData[item.Key].seed.currentTime; // 자라기 까지 남은 시간 저장..
// 만약 씨앗 다 자랐으면..
if (farmingData[item.Key].seed.isGrown)
{
temp.isGrown = true; // 씨앗 다 자란 상태를 변수에 저장..
}
}
tempDic.Add(pos, temp);
tempDic[pos].PrintData();
}
string json = DictionaryJsonUtility.ToJson(tempDic, true);
Debug.Log(json);
Debug.Log("데이터 저장 완료!");
// 외부 폴더에 접근해서 Json 파일 저장하기
// Application.persistentDataPath: 특정 운영체제에서 앱이 사용할 수 있도록 허용한 경로
File.WriteAllText(farmingDataFilePath, json);
}
- LoadFarmingData 함수를 만들어서 FarmingManager 의 Awake 함수에서 호출하도록 했다.
// 씬 로드 된 후에 SetFarmingData 함수 먼저 호출한 후 호출할 함수..
public void LoadFarmingData()
{
// Json 파일 경로 가져오기
string path = Path.Combine(Application.persistentDataPath, "FarmingData.json");
// 지정된 경로에 파일이 있는지 확인한다
if (File.Exists(path))
{
Debug.Log("파일 있어여!!");
// 경로에 파일이 있으면 Json 을 다시 오브젝트로 변환한다.
string json = File.ReadAllText(path);
Debug.Log(json);
Dictionary<PosInt, SaveFarmingData> tempDic = DictionaryJsonUtility.FromJson<PosInt, SaveFarmingData>(json);
Debug.Log(tempDic.Count + "!!!!!!!!!!!!!!!!!!!!1");
foreach (var item in tempDic)
{
tempDic[item.Key].PrintData();
Vector3Int pos = new Vector3Int(item.Key.x, item.Key.y, item.Key.z);
switch (tempDic[item.Key].currentState)
{
// 현재 농사 땅 상태에 맞는 버튼으로 설정해주기..
case "None":
farmingData[pos].stateButton = farmingData[pos].buttons[0];
farmTilemap.SetTile(pos, grassTile); // 타일을 아무것도 안 한 상태로 변경(키 값이 농사땅의 pos 임)
break;
case "plow":
farmingData[pos].stateButton = farmingData[pos].buttons[1];
farmTilemap.SetTile(pos, farmTile); // 타일을 밭 갈린 모습으로 변경..
break;
case "plant":
farmingData[pos].stateButton = farmingData[pos].buttons[2];
farmTilemap.SetTile(pos, plantTile); // 타일을 씨앗 심은 모습으로 변경..
break;
case "harvest":
farmingData[pos].stateButton = farmingData[pos].buttons[2];
farmTilemap.SetTile(pos, harvestTile); // 타일을 다 자란 모습으로 변경..
break;
}
// 저장해놓은 데이터 가져와서 설정해주기..
farmingData[pos].plowEnableState = tempDic[item.Key].plowEnableState;
farmingData[pos].plantEnableState = tempDic[item.Key].plantEnableState;
farmingData[pos].harvestEnableState = tempDic[item.Key].harvestEnableState;
farmingData[pos].currentState = tempDic[item.Key].currentState;
// 저장당시 농사 땅 위에 씨앗 있었으면 씨앗 데이터 설정해주기..
if (tempDic[item.Key].seedOnTile)
{
// 씨앗 데이터 가져와서 데이터에 맞는 씨앗 생성해주기..
farmingData[pos].seed = seedContainer.GetSeed(tempDic[item.Key].seedIdx).GetComponent<Seed>();
// 기존 씨앗 데이터 적용..
farmingData[pos].seed.currentTime = tempDic[item.Key].currentTime;
farmingData[pos].seed.isGrown = tempDic[item.Key].isGrown;
}
}
}
// 지정된 경로에 파일이 없으면
else
{
Debug.Log("파일이 없어요!!");
}
}
- 농장 크기는 정수형 타입으로 굳이 JSON 을 이용하지 않아도 되기 때문에 그냥 PlayerPrefs 를 이용하여 저장하였다.
- 이전의 농장 크기를 게임에 반영하기 위한 함수를 따로 만들었다.
- FarmingManager 클래스의 Awake 함수를 보면 다음과 같다.
private void Awake()
{
// 데이터 저장 경로 설정..
farmingDataFilePath = Path.Combine(Application.persistentDataPath, "FarmingData.json"); // 데이터 경로 설정..
farmingData = new Dictionary<Vector3Int, FarmingData>(); // 딕셔너리 생성
clickPosition = Vector2.zero;
// 농사 가능 구역만 farmingData 에 저장할 것임.
foreach (Vector3Int pos in farmEnableZoneTilemap.cellBounds.allPositionsWithin)
{
if (!farmEnableZoneTilemap.HasTile(pos)) continue;
SetFarmingData(pos); // FarmingData 타입 인스턴스의 정보를 세팅해주는 함수.
}
// 농사 땅 레벨 데이터 불러오기..
farmLevel = PlayerPrefs.GetInt("FarmLevel");
// 농사 땅 레벨 데이터를 불러온 다음에 레벨 데이터에 맞게끔 땅 업그레이드 해주기..
while (farmLevel > 0)
{
SetFarmSize();
farmLevel--;
}
LoadFarmingData(); // 데이터 가져오기..
}
- 농사 땅 레벨 데이터를 불러온 후 사이즈에 맞게 SetFarmSize 함수를 호출해주었다.
public void SetFarmSize()
{
// Awake 함수에서 호출할 함수
// 불러온 농장 레벨에 따라 호출 횟수가 달라짐..
// 땅의 크기를 업그레이드 하는 함수
BoundsInt bounds = farmEnableZoneTilemap.cellBounds; // 농사 가능 구역 타일맵의 현재 크기 가져오기
// 새로 확장할 영역 좌표 계산 로직..
Debug.Log(bounds.xMin);
int minX = bounds.xMin - expansionSize;
int maxX = bounds.xMax + expansionSize;
int minY = bounds.yMin - expansionSize;
int maxY = bounds.yMax + expansionSize;
for (int i = minX; i < maxX; i++)
{
for (int j = minY; j < maxY; j++)
{
// 테투리 부분만 경계타일 까는 로직
// max 값은 1 이 더 더해져있기 때문에 이를 고려해서 조건식 짜야함.
// 그래서 maxX, maxY 일 때는 i, j 에 1 을 더해줌..
if (i == minX || i + 1 == maxX)
farmTilemap.SetTile(new Vector3Int(i, j, 0), grassTile);
if (j == minY || j + 1 == maxY)
farmTilemap.SetTile(new Vector3Int(i, j, 0), grassTile);
Vector3Int pos = new Vector3Int(i, j, 0);
// 농사 가능 구역 타일맵에 타일이 없으면 진입
if (!farmEnableZoneTilemap.HasTile(pos))
{
farmEnableZoneTilemap.SetTile(pos, grassTile);
}
}
}
// 경계 타일맵 깔기 위한 로직
bounds = farmEnableZoneTilemap.cellBounds; // 업데이트된 농사 가능 구역 타일맵의 현재 크기 가져오기
minX = bounds.xMin - 1;
maxX = bounds.xMax + 1;
minY = bounds.yMin - 1;
maxY = bounds.yMax + 1;
Debug.Log("maxX: " + maxX + " maxY: " + maxY);
Debug.Log("minX: " + minX + " minY: " + minY);
for (int i = minX; i < maxX; i++)
{
for (int j = minY; j < maxY; j++)
{
// 테투리 부분만 경계타일 까는 로직
// max 값은 1 이 더 더해져있기 때문에 이를 고려해서 조건식 짜야함.
// 그래서 maxX, maxY 일 때는 i, j 에 1 을 더해줌..
if (i == minX || i + 1 == maxX)
farmTilemap.SetTile(new Vector3Int(i, j, 0), borderTile);
if (j == minY || j + 1 == maxY)
farmTilemap.SetTile(new Vector3Int(i, j, 0), borderTile);
}
}
// 농사 가능 구역 타일맵의 타일들을 모두 돌면서..
foreach (Vector3Int pos in farmEnableZoneTilemap.cellBounds.allPositionsWithin)
{
SetFarmingData(pos); // 새로운 농사 가능 구역의 타일 정보를 딕셔너리에 저장..
}
}
- 농장 크기 반영이 끝나면 LoadFarmingData 함수를 호출하여 비로소 이전 농장의 농사 상태를 반영하도록 하였다.