[개발일지] 11. 중간 정리

2024. 8. 8. 18:34·유니티 프로젝트/케이크게임

기능만 만들고 정리를 안 한지 오래 돼서 이번 기회에 중간 정리를 하려고 한다. 주요 기능 설명은 현재 프로젝트 클래스 종류 설명 밑에 있다.

 

[구현한 기능]

  • 인벤토리(아이템 누적, 아이템 설명, 아이템 이동, 아이템 판매, 아이템 종류 별 인벤토리 창, 아이템 칸 부족시 새로 칸 생성)
  • 씬 이동 시 인벤토리 매니저에 참조 변수 값 찾아서 넣어주는 기능
  • 농장 데이터 저장 기능

 

[현재 프로젝트 클래스 종류]

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

https://codeposting.tistory.com/entry/Unity-%EC%9C%A0%EB%8B%88%ED%8B%B0-%EA%B2%8C%EC%9E%84%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-transform-%EB%B0%A9%EB%B2%95-GameObject-find

 

[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

https://velog.io/@kohyeonseo1006/Unity-Dictionary%EB%A5%BC-Json-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C-%ED%8C%8C%EC%8B%B1%ED%95%98%EA%B8%B0

 

(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 함수를 호출하여 비로소 이전 농장의 농사 상태를 반영하도록 하였다.
'유니티 프로젝트/케이크게임' 카테고리의 다른 글
  • [개발일지] 12. 농작물 자라는 로직 변경
  • [개발일지] 10. scriptableobject 를 상속받는 클래스의 장점
  • [개발일지] 9. 농장 크기 업그레이드
  • [개발일지] 8. 씨앗 선택창 심기 버튼 연동 & 씨앗 구매하기 로직 추가
dubu0721
dubu0721
dubu0721 님의 블로그 입니다.
dubu0721 님의 블로그dubu0721 님의 블로그 입니다.
  • dubu0721
    dubu0721 님의 블로그
    dubu0721
  • 전체
    오늘
    어제
    • 분류 전체보기 (343)
      • 프로그래밍언어론 정리 (5)
      • 컴퓨터네트워크 정리 (5)
      • 알고리즘&자료구조 공부 (64)
        • it 취업을 위한 알고리즘 문제풀이 입문 강의 (60)
        • 학교 알고리즘 수업 (3)
        • 실전프로젝트I (0)
      • 백준 문제 (196)
        • 이분탐색 (7)
        • 투포인트 (10)
        • 그래프 (7)
        • 그리디 (24)
        • DP (25)
        • BFS (18)
        • MST (7)
        • KMP (4)
        • Dijkstra (3)
        • Disjoints Set (4)
        • Bellman-Ford (2)
        • 시뮬레이션 (3)
        • 백트래킹 (15)
        • 위상정렬 (5)
        • 자료구조 (25)
        • 기하학 (1)
        • 정렬 (11)
        • 구현 (8)
        • 재귀 (8)
        • 수학 (8)
      • 유니티 공부 (11)
        • 레트로의 유니티 게임 프로그래밍 에센스 (11)
        • 유니티 스터디 자료 (0)
        • C# 공부 (0)
      • 유니티 프로젝트 (48)
        • 케이크게임 (13)
        • 점토게임 (35)
      • 언리얼 공부 (10)
        • 이득우의 언리얼 프로그래밍 (10)
      • 진로 (1)
      • 논문 읽기 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    우선순위큐
    레트로의 유니티 프로그래밍
    스택
    백트래킹
    투포인터
    바킹독
    정렬
    언리얼
    BFS
    백준
    해시
    재귀
    시뮬레이션
    이분탐색
    골드메탈
    이득우
    dp
    유니티
    자료구조
    C#
    이벤트 트리거
    맵
    오블완
    티스토리챌린지
    그리디
    수학
    그래프
    큐
    유니티 프로젝트
    유니티 공부 정리
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
dubu0721
[개발일지] 11. 중간 정리

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.