전에 공부용으로 만들었던 코드들을 본격적으로 다시 만들었다. 그래서 현재까지 개발한 사항을 여기에 새로 작성하려고 한다. 전에 코드랑 달라진 부분이 많아서 이제 이 코드를 기준으로 앞으로의 게시물을 작성할 것이다. [주요 기능 설명] 파트는 [현재 프로젝트 클래스 종류] 파트 밑부분에 있다.
[구현한 기능]
- 농사 가능 구역만 농사 가능하도록 하는 기능
- 밭 갈기, 씨앗 심기, 과일 수확하기
- 각 밭(타일) 마다 밭 갈기, 씨앗 심기, 과일 수확하기 버튼 동적 생성
- 버튼 누를 때 뒤에 있는 타일을 안 눌리도록 하는 기능
- 씨앗이 심어진 밭(타일) 클릭하면 다 자라기까지 남은 시간 뜨도록 하는 기능
- 씨앗 구매창(이제 게임 매니저랑 연결해서 로직 더 작성해야함.)
[현재 프로젝트 클래스 종류]
FarmingManager: 농사의 전반적인 시스템을 관리하는 클래스
Seed: 씨앗 클래스
Fruit: 과일 클래스
SeedContainer: 씨앗 프리팹을 배열에 담아놓고, 씨앗을 생성할 때마다 풀에 넣어놓는 클래스
FruitContainer: 과일 프리팹을 배열에 담아놓고, 과일을 생성할 때마다 풀에 넣어놓는 클래스
BuySeedUIManager: 씨앗 구매창 UI 를 관리하는 클래스
SlotManager: 씨앗 구매창의 슬롯을 관리하는 클래스
FarmingData(MonoBehaviour 상속 받지 않는 클래스): 타일이 가지는 농사 데이터 클래스
- FarmingManager
using JetBrains.Annotations;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Tilemaps;
using UnityEngine.UI;
using static UnityEditor.PlayerSettings;
// 타일이 가지는 농사 데이터
class FarmingData
{
public Seed seed; // 타일이 가지는 씨앗 정보
//public bool seedOnTile; // 타일 위에 씨앗이 있는지 여부 확인용(씨앗이 있으면 밭을 갈 수 없음)
public bool plowEnableState; // 밭을 갈 수 있는 상태인지 여부 확인용(밭이 안 갈린 상태)
public bool plantEnableState; // 씨앗을 심을 수 있는 상태인지 여부 확인용
public bool harvestEnableState; // 작물이 다 자란 상태인지 여부 확인용
public Button stateButton; // 타일을 누르면 타일 위에 뜨도록 하는 버튼
public Button[] buttons; // [0]: plow 버튼, [1]: plant 버튼, [2]: harvest 버튼
public string currentState = "None"; // 현재 상태(초기에는 아무것도 안 한 상태니까 None 으로.. -> plow: 밭 갈린 상태, plant: 씨앗 심은 상태, harvest: 다 자란 상태)
}
public class FarmingManager : MonoBehaviour
{
[Header("Game Data")]
public Camera mainCamera; // 마우스 좌표를 게임 월드 좌표로 변환하기 위해 필요한 변수(카메라 오브젝트 할당해줄 것)
public SeedContainer seedContainer; // 현재 가진 씨앗을 가져오기 위해 필요한 변수(씨앗 컨테이너 게임 오브젝트 할당해줄 것)
public FruitContainer fruitContainer; // 수확한 과일을 저장하기 위해 필요한 변수(과일 컨테이너 게임 오브젝트 할당해줄 것)
[Header("Tile")]
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 Canvas buttonParent; // 버튼 생성할 때 부모 지정하기 위한 변수
[Header("Farm interaction Panel")]
public GameObject growTimePanel; // 다 자라기까지 남은 시간 보여주는 판넬
public Text growTimeText; // 다 자라기까지 남은 시간
[Header("Farming Data")]
public Vector2 clickPosition; // 현재 마우스 위치를 게임 월드 위치로 바꿔서 저장
public Vector3Int cellPosition; // 게임 월드 위치를 타일 맵의 타일 셀 위치로 변환
Dictionary<Vector3Int, FarmingData> farmingData;
private void Awake()
{
farmingData = new Dictionary<Vector3Int, FarmingData>();
clickPosition = Vector2.zero;
}
private void Start()
{
// 농사 가능 구역만 farmingData 에 저장할 것임.
foreach (Vector3Int pos in farmEnableZoneTilemap.cellBounds.allPositionsWithin)
{
if (!farmEnableZoneTilemap.HasTile(pos)) continue;
// 유니티에서는 new 를 쓰려면 class 가 MonoBehaviour 를 상속 받으면 안 됨.
farmingData[pos] = new FarmingData();
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));
}
else if (index==2)
{
farmingData[tilePos].buttons[index].onClick.AddListener(() => HarvestTile(tilePos));
}
}
// 맨 처음에는 plow 버튼을 저장하고 있도록
farmingData[pos].stateButton = farmingData[pos].buttons[0];
prevSelectTile = pos;
}
}
void Update()
{
// 버튼 눌렀을 때 뒤에 있는 타일 못 누르도록 하기 위한 구문..
if (IsPointerOverUIObject()) return;
// 땅을 왼쪽 마우스키로 누르면..
if (Input.GetMouseButtonDown(0))
{
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.growTime - farmingData[cellPosition].seed.currentTime);
}
}
// 현재 선택한 타일의 버튼은 활성화 되도록..
//farmingData[cellPosition].stateButton.gameObject.SetActive(true);
// 아래 방법처럼 하면 오류남. 이유는 뭐지??
// --> GameObject 는 컴포넌트가 아니라서 오류가 나는 것이었다... 이번 기회에 알게 됐다.
// --> 그냥 gameObject 로 바로 게임 오브젝트에 접근할 수 있었다.
// --> 기본적인걸 까먹고 있었다.. 이번엔 잘 기억하자..
//farmingData[cellPosition].stateButton.GetComponent<GameObject>().SetActive(true);
}
}
prevSelectTile = cellPosition; // 지금 누른 타일을 이전에 누른 타일 위치를 저장하는 변수에 저장..
}
// 자라는데 남은 시간이 계속 업데이트 되어야 하므로..
if (farmEnableZoneTilemap.HasTile(cellPosition) && farmingData[cellPosition].seed != null)
{
if (!farmingData[cellPosition].seed.isGrown)
growTimeText.text = "남은시간\n" + (int)(farmingData[cellPosition].seed.growTime - farmingData[cellPosition].seed.currentTime);
else
growTimePanel.SetActive(false); // 다 자라면 남은시간 나타내는 판넬 꺼지도록..
}
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 IsPointerOverUIObject()
{
PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current)
{
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 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 PlantTile(Vector3Int pos)
{
// 씨앗을 심는 함수
// 다음에 이 함수의 매개변수로 씨앗 인덱스로 보내줘서 GetSeed 함수의 매개변수로 보낼 것.
farmingData[pos].seed = seedContainer.GetSeed(0).GetComponent<Seed>();
farmTilemap.SetTile(pos, plantTile); // 타일 모습을 씨앗 심은 상태로 바꿔주기
farmingData[pos].plantEnableState = true; // 씨앗을 심을 수 없는 상태를 나타내기 위해 false 로 변경
farmingData[pos].currentState = "plant"; // 씨앗 심은 상태니까 plant 로 바꿔주기
farmingData[pos].stateButton.gameObject.SetActive(false); // 버튼 한 번 눌렀으니까 꺼지도록..
farmingData[pos].stateButton = farmingData[pos].buttons[2]; // harvest 버튼을 가지고 있도록..
}
public void HarvestTile(Vector3Int pos)
{
// 과일을 수확하는 함수
// 생각해보니까 씨앗 인덱스 여기로 안 보내줘도 pos 보내줬으니까, pos 가 가지는 씨앗 인스턴스의 씨앗 인덱스 이용하면 될 듯.
farmingData[pos].plowEnableState = true;
farmingData[pos].currentState = "None"; // 과일을 수확한 상태니까 None 으로 바꿔주기
fruitContainer.GetFruit(farmingData[pos].seed.seedIdx); // 씨앗의 인덱스와 같은 과일 생성
farmingData[pos].stateButton.gameObject.SetActive(false); // 버튼 한 번 눌렀으니까 꺼지도록..
farmingData[pos].stateButton = farmingData[pos].buttons[0]; // plow 버튼을 가지고 있도록..
farmingData[pos].seed = null; // 수확 완료 했으니까 타일의 seed 변수를 다시 null 로 설정해주기..
farmTilemap.SetTile(pos, grassTile); // 타일 모습을 초기 상태의로 바꿔주기
}
}
- Seed
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Seed : MonoBehaviour
{
// 씨앗은 다 프리팹으로 만들어 놓을 것
// 만들어놓은 프리팹은 SeedContainer 에 저장할 것..
public string seedName;
public float seedPrice;
public float growTime; // 성장하는데 걸리는 시간
public float currentTime; // 심은 후부터 현재까지 시간
public bool isGrown = false; // 다 자랐는지 여부 확인용 변수
public int seedIdx; // 씨앗 인덱스
private void OnEnable()
{
isGrown = false;
currentTime = 0;
Debug.Log("씨앗을 심었다!");
}
private void Update()
{
if (currentTime >= growTime) {
isGrown = true;
Debug.Log("다 자랐다!");
transform.gameObject.SetActive(!isGrown);
}
currentTime += Time.deltaTime;
}
}
- Fruit
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Fruit : MonoBehaviour
{
// 과일은 다 프리팹으로 만들어 놓을 것
// 만들어놓은 프리팹은 FruitContainer 에 저장할 것..
public string fruitName;
public float fruitPrice;
public bool isEnabled = false;
public int fruitIdx = 0; // 과일 인덱스
private void OnEnable()
{
Debug.Log("과일을 얻었다!");
}
}
- SeedContainer
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SeedContainer : MonoBehaviour
{
public GameObject[] prefabs;
public List<GameObject>[] pools;
private void Awake()
{
pools = new List<GameObject>[prefabs.Length];
for (int idx = 0; idx < pools.Length; idx++)
{
pools[idx] = new List<GameObject>();
}
}
public GameObject GetSeed(int idx)
{
GameObject select = null;
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;
}
}
- FruitContainer
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FruitContainer : MonoBehaviour
{
public GameObject[] prefabs;
public List<GameObject>[] pools;
public int fruitCount;
private void Awake()
{
fruitCount = 0;
pools = new List<GameObject>[prefabs.Length];
for (int idx = 0; idx < prefabs.Length; idx++)
{
pools[idx] = new List<GameObject>();
}
}
public GameObject GetFruit(int idx)
{
GameObject select = null;
foreach (GameObject obj in pools[idx])
{
if (!obj.activeSelf)
{
select = obj;
obj.SetActive(true);
break;
}
}
if (!select)
{
select = Instantiate(prefabs[idx], transform);
pools[idx].Add(select);
}
fruitCount++;
return select;
}
}
- BuySeedUIManager
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
public class BuySeedUIManager : 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 slotContainer;
public List<Button> BuySeedSlots;
[Header("Current Button")]
public Button selectSlot;
private void Awake()
{
// 현재 씨앗 구매 판넬에 존재하는 슬롯들을 가져와서 저장함.
// 자식만 가져와야 하기 때문에 (자손은 가져오면 안 됨) GetComponentsInChildren 못 씀.
for (int i=0; i< slotContainer.transform.childCount; i++)
{
Transform child = slotContainer.transform.GetChild(i);
BuySeedSlots.Add(child.GetComponent<Button>());
}
// 현재 게임 상 존재하는 씨앗 구매 버튼 정보 설정
for (int i=0; i<BuySeedSlots.Count; i++)
{
SlotManager slot = BuySeedSlots[i].GetComponent<SlotManager>();
Seed slotSeedInfo = seedInfo.prefabs[i].GetComponent<Seed>();
// 각 버튼의 초기값 설정
slot.seedImage.sprite = seedImages[i];
slot.seedName.text = slotSeedInfo.seedName;
slot.totalPrice.text = "가격: " + slotSeedInfo.seedPrice;
slot.seedCountText.text = "1";
}
}
private void Update()
{
// 임시로 W 키 누르면 구매창 켜지도록..
if (Input.GetKeyDown(KeyCode.W))
BuySeedPanel.SetActive(true);
for (int i=0; i<BuySeedSlots.Count; i++)
{
SlotManager slot = BuySeedSlots[i].GetComponent<SlotManager>();
Seed slotSeedInfo = seedInfo.prefabs[i].GetComponent<Seed>();
// BuySlot 이 활성화 되어 있는 슬롯의 정보만 계속해서 변경해줄 것
if (slot.BuySlot.activeSelf)
{
// 선택된 과일 개수랑 총 가격만 계속해서 업데이트 해주면 됨.
slot.seedCountText.text = slot.seedCount + "";
slot.totalPrice.text = "가격: " + (int)(slot.seedCount * slotSeedInfo.seedPrice);
}
}
}
public void CloseBuySlot()
{
for (int i = 0; i < BuySeedSlots.Count; i++)
{
SlotManager slot = BuySeedSlots[i].GetComponent<SlotManager>();
slot.ResetData(); // 슬롯 데이터 한번 리셋해주기(껐다 켜졌는데 상태 그대로면 이상하니까)
slot.BuySlot.SetActive(false);
}
}
public void SlotClick()
{
CloseBuySlot(); // 슬롯 버튼 눌렀을 때, 다른 슬롯의 구매 슬롯이 켜져있으면 다 끄고 시작..
}
public void ExitButton()
{
BuySeedPanel.SetActive(false); // 구매 창 없어지도록..
CloseBuySlot(); // 나가기 버튼 누르면 켜져있던 구매 슬롯 없어지도록..
}
}
- SlotManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class SlotManager : MonoBehaviour
{
public Image seedImage;
public Text seedName;
public GameObject BuySlot;
public Text totalPrice;
public Text seedCountText;
public Button leftButton;
public Button rightButton;
public int seedCount = 1;
public int maxCount = 64;
public int minCount = 1;
public void minusSeedCount()
{
if (seedCount <= minCount) {
// 현재 구매하려고 하는 씨앗 개수가 씨앗 구매 최소 개수보다 작아지는 순간 최댓값으로 넘어가도록
seedCount = maxCount;
return;
}
seedCount--;
}
public void plusSeedCount()
{
// 현재 구매하려고 하는 씨앗 개수가 씨앗 구매 최대 개수보다 커지는 순간 최솟값으로 넘어가도록
if (seedCount >= maxCount) {
seedCount = minCount;
return;
}
seedCount++;
}
private void Update()
{
// UI 가 아닌 부분을 클릭하면 그냥 꺼지도록..
if (IsPointerOverUIObject()) return;
else
{
if (Input.GetMouseButtonDown(0))
{
BuySlot.SetActive(false);
}
}
}
private bool IsPointerOverUIObject()
{
PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current)
{
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 void ResetData()
{
seedCount = 1;
}
}
- FarmingData(FarmingManager 클래스에 있음)
// 타일이 가지는 농사 데이터
class FarmingData
{
public Seed seed; // 타일이 가지는 씨앗 정보
//public bool seedOnTile; // 타일 위에 씨앗이 있는지 여부 확인용(씨앗이 있으면 밭을 갈 수 없음)
public bool plowEnableState; // 밭을 갈 수 있는 상태인지 여부 확인용(밭이 안 갈린 상태)
public bool plantEnableState; // 씨앗을 심을 수 있는 상태인지 여부 확인용
public bool harvestEnableState; // 작물이 다 자란 상태인지 여부 확인용
public Button stateButton; // 타일을 누르면 타일 위에 뜨도록 하는 버튼
public Button[] buttons; // [0]: plow 버튼, [1]: plant 버튼, [2]: harvest 버튼
public string currentState = "None"; // 현재 상태(초기에는 아무것도 안 한 상태니까 None 으로.. -> plow: 밭 갈린 상태, plant: 씨앗 심은 상태, harvest: 다 자란 상태)
}
[주요 기능 설명]
1. 농사 가능 구역만 농사 할 수 있도록 하는 기능
- 게임 하이어라키 창에 농사 가능 구역을 나타내는 타일맵과 진짜 농사가 이루어지는 타일맵 두 개를 만들어 놓았다
- cellBounds.allPositionWithin 은 farmEnableZoneTilemap 타일맵의 모든 셀들의 위치를 가져오는 역할을 한다
- 농사 가능 구역 타일맵의 모든 셀의 위치를 반복문을 통해 돌면서, 해당 위치에 타일이 있으면 farmingData 딕셔너리 변수에 FarmingData 클래스의 인스턴스를 만들어서 저장했다(Key: pos, Value: FarmingData).
- 마우스 왼쪽키를 눌렀을 때, 누른 타일이 농사 가능 구열에 있는 타일이면 농사 관련 로직을 수행할 수 있도록, 딕셔너리에 저장되어 있는 모든 위치를 돌면서 현재 마우스 위치의 타일 위치랑 같은지 판단했다
[Header("Tilemap")]
public Tilemap farmEnableZoneTilemap; // 농사 가능 부지를 나타내는 타일맵
public Tilemap farmTilemap; // 진짜로 현재 타일의 상태에 따라 타일이 변경되는 타일맵
private void Start()
{
// 농사 가능 구역만 farmingData 에 저장할 것임.
foreach (Vector3Int pos in farmEnableZoneTilemap.cellBounds.allPositionsWithin)
{
if (!farmEnableZoneTilemap.HasTile(pos)) continue;
// 유니티에서는 new 를 쓰려면 class 가 MonoBehaviour 를 상속 받으면 안 됨.
farmingData[pos] = new FarmingData();
}
}
void Update()
{
// 땅을 왼쪽 마우스키로 누르면..
if (Input.GetMouseButtonDown(0))
{
// 현재 마우스 위치를 게임 월드 위치로 바꿔서 저장
clickPosition = mainCamera.ScreenToWorldPoint(Input.mousePosition);
// 게임 월드 위치를 타일 맵의 타일 셀 위치로 변환
cellPosition = farmTilemap.WorldToCell(clickPosition);
foreach (Vector3Int pos in farmingData.Keys)
{
if (pos == cellPosition) {
// 농사 관련 로직..
}
}
}
2. FarmingManager 클래스에서 각 타일마다 세 개의 버튼을 가지도록 하는 코드
---> 참고자료: Unity UI Button onClick.AddListener 활용하기 (tistory.com)
- FarmingData 클래스는 멤버 변수 중 하나로 buttons 라는 배열을 가지는데, 이는 각 타일이 버튼을 3개(plow, plant, harvest) 가져야 하므로 선언한 변수이다.
- buttonPrefabs(이 배열에는 미리 세 개의 버튼 프리팹을 넣어놨다) 의 길이만큼 for 문을 돌면서 버튼을 동적으로 생성해준다(클로저 문제를 피하기 위해 값을 변수에 저장해놓고 사용하는 식으로 했다).
- 동적으로 생성된 버튼에 함수를 연결해주었다(동적으로 생성된 버튼에 함수를 연결해주려면 AddListener 라는 함수가 필요하다).
- AddListener 에 버튼과 연결하려는 함수를 매개변수로 넘겨주어야 하는데 밭 갈기, 씨앗 심기, 수확하기 함수는 인자를 필요로 하는 함수이므로 람다식을 이용해서 넘겨주었다.
- 버튼을 다 생성한 다음에, 각 타일이 가지는 현재 버튼(stateButton) 변수에 plow 버튼을 넣어주었다.
private void Start()
{
// 농사 가능 구역만 farmingData 에 저장할 것임.
foreach (Vector3Int pos in farmEnableZoneTilemap.cellBounds.allPositionsWithin)
{
if (!farmEnableZoneTilemap.HasTile(pos)) continue;
// 유니티에서는 new 를 쓰려면 class 가 MonoBehaviour 를 상속 받으면 안 됨.
farmingData[pos] = new FarmingData();
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));
}
else if (index==2)
{
farmingData[tilePos].buttons[index].onClick.AddListener(() => HarvestTile(tilePos));
}
}
// 맨 처음에는 plow 버튼을 저장하고 있도록
farmingData[pos].stateButton = farmingData[pos].buttons[0];
prevSelectTile = pos;
}
}
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;
}
3. 버튼 누를 때 뒤에 있는 타일은 안 눌리도록 하는 기능
[개발일지] 5. 버튼 눌렀을 때 뒤에 있는 타일(게임 오브젝트)은 안 눌리도록 하는 방법 (tistory.com)
4. 밭 갈기, 씨앗 심기, 과일 수확하기 기능
- 밭 갈기, 씨앗 심기, 과일 수확하기 함수를 만들었다.
- 세 함수 모두 Vector3Int 타입의 pos 변수를 매개변수로 받는다(farmingData 딕셔너리 변수의 키가 타일맵의 셀 위치 이므로, 타일맵 셀의 FarmingData 정보를 이용하기 위해서)
- 밭을 갈고, 씨앗을 심고, 과일을 수확하면 타일의 모습이 변해야한다. 즉 타일의 모습을 변경하기 위해 SetTile 함수를 이용했다. SetTile 함수는 Vector3Int 타입의 변수와 TileBase 타입의 변수를 매개변수로 받는다.
- TileBase 타입의 변수를 미리 선언해놓고(grassTile, farmTile, plantTile, harvestTile), 그 변수에 각각의 모습에 맞는 타일 프리팹을 넣어놨다.
- Update 함수에서 각 조건에 맞게 버튼을 띄우고 없애는 등의 전반적인 기능을 적었다(자세한 내용은 밑 코드 참조).
[Header("Tile")]
public TileBase grassTile; // 밭 갈기 전 상태
public TileBase farmTile; // 밭 간 후 상태
public TileBase plantTile; // 씨앗 심은 후 상태
public TileBase harvestTile; // 과일 다 자란 상태
public Vector3Int prevSelectTile; // 이전 클릭된 타일
// 타일이 가지는 농사 데이터
class FarmingData
{
public Seed seed; // 타일이 가지는 씨앗 정보
//public bool seedOnTile; // 타일 위에 씨앗이 있는지 여부 확인용(씨앗이 있으면 밭을 갈 수 없음)
public bool plowEnableState; // 밭을 갈 수 있는 상태인지 여부 확인용(밭이 안 갈린 상태)
public bool plantEnableState; // 씨앗을 심을 수 있는 상태인지 여부 확인용
public bool harvestEnableState; // 작물이 다 자란 상태인지 여부 확인용
public Button stateButton; // 타일을 누르면 타일 위에 뜨도록 하는 버튼
public Button[] buttons; // [0]: plow 버튼, [1]: plant 버튼, [2]: harvest 버튼
public string currentState = "None"; // 현재 상태(초기에는 아무것도 안 한 상태니까 None 으로.. -> plow: 밭 갈린 상태, plant: 씨앗 심은 상태, harvest: 다 자란 상태)
}
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 PlantTile(Vector3Int pos)
{
// 씨앗을 심는 함수
// 다음에 이 함수의 매개변수로 씨앗 인덱스로 보내줘서 GetSeed 함수의 매개변수로 보낼 것.
farmingData[pos].seed = seedContainer.GetSeed(0).GetComponent<Seed>();
farmTilemap.SetTile(pos, plantTile); // 타일 모습을 씨앗 심은 상태로 바꿔주기
farmingData[pos].plantEnableState = true; // 씨앗을 심을 수 없는 상태를 나타내기 위해 false 로 변경
farmingData[pos].currentState = "plant"; // 씨앗 심은 상태니까 plant 로 바꿔주기
farmingData[pos].stateButton.gameObject.SetActive(false); // 버튼 한 번 눌렀으니까 꺼지도록..
farmingData[pos].stateButton = farmingData[pos].buttons[2]; // harvest 버튼을 가지고 있도록..
}
public void HarvestTile(Vector3Int pos)
{
// 과일을 수확하는 함수
// 생각해보니까 씨앗 인덱스 여기로 안 보내줘도 pos 보내줬으니까, pos 가 가지는 씨앗 인스턴스의 씨앗 인덱스 이용하면 될 듯.
farmingData[pos].plowEnableState = true;
farmingData[pos].currentState = "None"; // 과일을 수확한 상태니까 None 으로 바꿔주기
fruitContainer.GetFruit(farmingData[pos].seed.seedIdx); // 씨앗의 인덱스와 같은 과일 생성
farmingData[pos].stateButton.gameObject.SetActive(false); // 버튼 한 번 눌렀으니까 꺼지도록..
farmingData[pos].stateButton = farmingData[pos].buttons[0]; // plow 버튼을 가지고 있도록..
farmingData[pos].seed = null; // 수확 완료 했으니까 타일의 seed 변수를 다시 null 로 설정해주기..
farmTilemap.SetTile(pos, grassTile); // 타일 모습을 초기 상태의로 바꿔주기
}
void Update()
{
// 버튼 눌렀을 때 뒤에 있는 타일 못 누르도록 하기 위한 구문..
if (IsPointerOverUIObject()) return;
// 땅을 왼쪽 마우스키로 누르면..
if (Input.GetMouseButtonDown(0))
{
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);
}
}
}
}
prevSelectTile = cellPosition; // 지금 누른 타일을 이전에 누른 타일 위치를 저장하는 변수에 저장..
}
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";
}
}
}
}
5. 씨앗이 심어진 밭(타일) 클릭하면 다 자라기까지 남은 시간 뜨도록 하는 기능
- Update 문 안에있던 foreach 구문의 내용을 다음과 같이 수정했다.
- else if (!farmingData[cellPosition].seed.isGrown) 로 진입하면 씨앗이 자라는 중임을 의미한다.
- 미리 만들어놓았던 판넬의 위치를 현재 클릭한 타일의 위치로 바꿔주고, 판넬을 활성화 시켜서 화면에 나타나도록 했다.
- 자라는데 남은 시간은 한 번 초기화 됐다고 끝이 아니라 계속해서 값을 업데이트 해줘야 한다.
- 즉 foreach 구문 바로 밑에 두 번째 코드 블록을 추가했다.
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.growTime - farmingData[cellPosition].seed.currentTime);
}
}
}
}
// 자라는데 남은 시간이 계속 업데이트 되어야 하므로..
if (farmEnableZoneTilemap.HasTile(cellPosition) && farmingData[cellPosition].seed != null)
{
if (!farmingData[cellPosition].seed.isGrown)
growTimeText.text = "남은시간\n" + (int)(farmingData[cellPosition].seed.growTime - farmingData[cellPosition].seed.currentTime);
else
growTimePanel.SetActive(false); // 다 자라면 남은시간 나타내는 판넬 꺼지도록..
}
6. 씨앗 구매창(이제 게임 매니저랑 연결해서 로직 더 작성해야함.)
[슬라이드 구매창 만드는데 참고한 영상]
---> 참고자료: 인벤토리 첫번째 영상! UI! (youtube.com)
- 씨앗 구매창을 만들기 위해 필요한 클래스는 SlotManager 와, BuySeedUIManager 이다.
- 슬롯 매니저를 만든 이유는 각 슬롯(씨앗 버튼) 의 정보를 코드상에서 조작할 수 있도록 하기 위함이다.
- 각 슬롯(씨앗 버튼) 의 정보를 하이어라키 창에서 수정하도록 하면 슬롯의 개수가 늘어났을 때(한 100개 정도..) 를 상상해보면 너무 비효율적이기 때문에.. 코드상에서 조작할 수 있도록 하고 싶었다.
- BuySeedUIManager 는 씨앗 구매창 UI 를 관리하는 역할이다.
- BuySeedUIManager 에 각 슬롯(씨앗 버튼) 을 BuySeedSlots 배열에 저장해서 코드상에서 조작할 수 있도록 했다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class SlotManager : MonoBehaviour
{
public Image seedImage;
public Text seedName;
public GameObject BuySlot;
public Text totalPrice;
public Text seedCountText;
public Button leftButton;
public Button rightButton;
public int seedCount = 1;
public int maxCount = 64;
public int minCount = 1;
public void minusSeedCount()
{
if (seedCount <= minCount) {
// 현재 구매하려고 하는 씨앗 개수가 씨앗 구매 최소 개수보다 작아지는 순간 최댓값으로 넘어가도록
seedCount = maxCount;
return;
}
seedCount--;
}
public void plusSeedCount()
{
// 현재 구매하려고 하는 씨앗 개수가 씨앗 구매 최대 개수보다 커지는 순간 최솟값으로 넘어가도록
if (seedCount >= maxCount) {
seedCount = minCount;
return;
}
seedCount++;
}
private void Update()
{
// UI 가 아닌 부분을 클릭하면 그냥 꺼지도록..
if (IsPointerOverUIObject()) return;
else
{
if (Input.GetMouseButtonDown(0))
{
BuySlot.SetActive(false);
}
}
}
private bool IsPointerOverUIObject()
{
PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current)
{
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 void ResetData()
{
seedCount = 1;
}
}
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
public class BuySeedUIManager : 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 slotContainer;
public List<Button> BuySeedSlots;
[Header("Current Button")]
public Button selectSlot;
private void Awake()
{
// 현재 씨앗 구매 판넬에 존재하는 슬롯들을 가져와서 저장함.
// 자식만 가져와야 하기 때문에 (자손은 가져오면 안 됨) GetComponentsInChildren 못 씀.
for (int i=0; i< slotContainer.transform.childCount; i++)
{
Transform child = slotContainer.transform.GetChild(i);
BuySeedSlots.Add(child.GetComponent<Button>());
}
// 현재 게임 상 존재하는 씨앗 구매 버튼 정보 설정
for (int i=0; i<BuySeedSlots.Count; i++)
{
SlotManager slot = BuySeedSlots[i].GetComponent<SlotManager>();
Seed slotSeedInfo = seedInfo.prefabs[i].GetComponent<Seed>();
// 각 버튼의 초기값 설정
slot.seedImage.sprite = seedImages[i];
slot.seedName.text = slotSeedInfo.seedName;
slot.totalPrice.text = "가격: " + slotSeedInfo.seedPrice;
slot.seedCountText.text = "1";
}
}
private void Update()
{
// 임시로 W 키 누르면 구매창 켜지도록..
if (Input.GetKeyDown(KeyCode.W))
BuySeedPanel.SetActive(true);
for (int i=0; i<BuySeedSlots.Count; i++)
{
SlotManager slot = BuySeedSlots[i].GetComponent<SlotManager>();
Seed slotSeedInfo = seedInfo.prefabs[i].GetComponent<Seed>();
// BuySlot 이 활성화 되어 있는 슬롯의 정보만 계속해서 변경해줄 것
if (slot.BuySlot.activeSelf)
{
// 선택된 과일 개수랑 총 가격만 계속해서 업데이트 해주면 됨.
slot.seedCountText.text = slot.seedCount + "";
slot.totalPrice.text = "가격: " + (int)(slot.seedCount * slotSeedInfo.seedPrice);
}
}
}
public void CloseBuySlot()
{
for (int i = 0; i < BuySeedSlots.Count; i++)
{
SlotManager slot = BuySeedSlots[i].GetComponent<SlotManager>();
slot.ResetData(); // 슬롯 데이터 한번 리셋해주기(껐다 켜졌는데 상태 그대로면 이상하니까)
slot.BuySlot.SetActive(false);
}
}
public void SlotClick()
{
CloseBuySlot(); // 슬롯 버튼 눌렀을 때, 다른 슬롯의 구매 슬롯이 켜져있으면 다 끄고 시작..
}
public void ExitButton()
{
BuySeedPanel.SetActive(false); // 구매 창 없어지도록..
CloseBuySlot(); // 나가기 버튼 누르면 켜져있던 구매 슬롯 없어지도록..
}
}