[원하는 기능]
- 갈린 밭에 씨앗 심기
- 다 자란 과일 수확하기
- 수확하기 버튼으로 수확하기
[관련클래스]
- Fruit: 과일 클래스. 멤버 변수로 과일 이름, 과일 가격, 게임 상에서 활성화 기준을 결정하는 flag 변수(isEnabled)
- Seed: 씨앗 클래스. 멤버 변수로 씨앗 이름, 성장시간, 시작시간(씨앗이 밭에 심어진 후부터 증가), 씨앗 가격, 게임 상에서 활성화 기준을 결정하는 flag 변수(isEnabled)
- FruitContainer: 과일 게임 오브젝트를 생성하고 저장해놓는 클래스
- SeedContainer: 씨앗 게임오브젝트를 생성하고 저장해놓는 클래스
- FarmController: 전반적인 농장 기능을 수행하도록 하는 클래스
[코드]
- Fruit
- 과일은 다 자란 작물의 수확하기 버튼을 눌러서 얻을 수 있다.
- FarmController 클래스에서 FruitContainer 의 GetFruit 함수를 호출해서 새로운 과일 인스턴스를 생성하고 저장한다.
- 과일은 재활용 될 수 있게 할 것이므로 미리 isEnabled 변수를 선언해놓았다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Fruit : MonoBehaviour
{
public string fruitName;
public float fruitPrice;
public bool isEnabled = false;
public int fruitIdx = 0;
private void Awake()
{
fruitName = "과일";
fruitPrice = 1000;
}
private void OnEnable()
{
Debug.Log("과일을 수확했어여!!!");
}
}
- Seed
- FarmController 클래스에서 씨앗 인스턴스를 생성한 후부터 startTime 의 값이 오르기 시작한다.
- startTime 이 growTime 의 값과 같아지면 과일이 다 자란 것 이므로 isEnabled 값을 false 로 바꿔준다.
- 씨앗은 한 번 만들면 재사용 할 것이므로 새로 Enable 될 때 isEnabled 변수를 true 로 startTime 을 0으로 설정해준다(처음 만든 상태와 동일하도록).
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class seed : MonoBehaviour
{
public string seedName;
public float growTime;
public float startTime;
public float seedPrice;
public bool isEnabled = false;
public int seedIdx = 0;
private void Awake()
{
seedName = "씨앗";
growTime = 10f;
seedPrice = 100;
}
private void OnEnable()
{
isEnabled = true;
startTime = 0;
Debug.Log("씨앗을 심었어여!!!");
}
void Update()
{
if (startTime >= growTime)
{
isEnabled = false;
Debug.Log("다 자랐어여!!!!!");
transform.gameObject.SetActive(isEnabled);
}
startTime += Time.deltaTime;
}
}
- FruitContainer
- 미리 과일 프리팹을 만들어 놓은 후 배열에 넣어놓는다.
- GetFruit 함수를 통해 새로운 과일 인스턴스를 생성하는데, 만약 게임 상에서 놀고있는 인스턴스가 미리 생성되어 있다면 이를 재사용하는 식으로 코드를 작성했다.
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;
}
}
- SeedContainer
- 미리 과일 프리팹을 만들어 놓은 후 배열에 넣어놓는다.
- GetSeed 함수를 통해 새로운 씨앗 인스턴스를 생성하는데, 만약 게임 상에서 놀고있는 인스턴스가 미리 생성되어 있다면 이를 재사용하는 식으로 코드를 작성했다.
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;
}
}
- FarmController
- 타일의 상태를 저장하기 위한 tileData 클래스를 따로 만들었다.
- seedOnTile 변수는 현재 타일에 씨앗이 심어져 있는지를 확인하기 위한 변수이다.
- isSeedEnabled 변수는 현재 타일 상태가 씨앗을 심을 수 있는 상태인지를 확인하기 위한 변수이다.
- seed 변수는 생성한 씨앗 인스턴스를 저장하기 위한 변수이다.
- button 변수는 생성한 버튼 인스턴스를 저장하기 위한 변수이다(자란 작물 위에 버튼을 띄우기 위해 동적으로 버튼을 생성하도록 해야했다. 그래서 따로 버튼 변수를 만든 것).
- Dictionary<Vector3Int, tileData> 타입의 tileData 변수를 만들어서, 각 타일 위치에서의 타일 상태를 저장하도록 했다.
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Unity.VisualScripting;
using UnityEditor;
using UnityEditor.U2D.Aseprite;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Tilemaps;
using UnityEngine.UI;
public class tileData
{
public bool seedOnTile = false;
public bool isSeedEnabled = false;
public seed seed;
public Button button;
}
public class farmColtroller : MonoBehaviour
{
public Camera mainCamera;
[Header("sprite")]
public Sprite newSprite;
[Header("tile")]
public Tilemap borderTilemap;
public Tilemap tilemap;
public TileBase grassTile;
public TileBase farmTile;
public TileBase selectTile;
public Dictionary<Vector3Int, tileData> tileData;
[Header("GameData")]
public seedContainer seedContainer;
public FruitContainer fruitContainer;
[Header("UI")]
public GameObject canvas;
public GameObject getFruitButtonPrefab;
public void Awake()
{
mainCamera = GetComponent<Camera>();
tileData = new Dictionary<Vector3Int, tileData>();
}
private void Start()
{
foreach (Vector3Int pos in borderTilemap.cellBounds.allPositionsWithin)
{
if (!borderTilemap.HasTile(pos)) continue;
TileBase tile = tilemap.GetTile<TileBase>(pos);
tileData[pos] = new tileData();
}
}
public void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector3Int gridPos = tilemap.WorldToCell(mousePos);
foreach (Vector3Int pos in tileData.Keys)
{
if (pos == gridPos)
{
if (!tileData[gridPos].isSeedEnabled)
{
tileData[gridPos].isSeedEnabled = true;
tilemap.SetTile(gridPos, farmTile);
}
else if (tileData[gridPos].isSeedEnabled && !tileData[gridPos].seedOnTile)
{
tileData[gridPos].seedOnTile = true;
tileData[gridPos].seed = seedContainer.GetSeed(0).GetComponent<seed>();
}
}
}
}
foreach(Vector3Int pos in tileData.Keys)
{
if (tileData[pos].seed != null)
{
if (!tileData[pos].seed.isEnabled && tileData[pos].seedOnTile)
{
tileData[pos].seed.isEnabled = true;
CreateGetFruitButton(pos);
}
}
if (tileData[pos].button != null)
{
Vector2 worldPos = tilemap.CellToWorld(pos);
Vector3 screenPos = mainCamera.WorldToScreenPoint(worldPos);
tileData[pos].button.transform.position = (screenPos) + new Vector3(0, 2, 0);
}
}
}
public void CreateGetFruitButton(Vector3Int pos)
{
if (tileData[pos].button == null)
{
GameObject buttonObj = Instantiate(getFruitButtonPrefab, canvas.transform);
Button button = buttonObj.GetComponent<Button>();
tileData[pos].button = button;
button.onClick.AddListener(() => GetFruit(pos));
}
}
public void GetFruit(Vector3Int pos)
{
fruitContainer.GetFruit(0);
Debug.Log("과일을 수확합니다!!!: " + pos);
tileData[pos].seed.isEnabled = true;
tileData[pos].isSeedEnabled = false;
tileData[pos].seedOnTile = false;
tilemap.SetTile(pos, grassTile);
if (tileData[pos].button != null)
{
Destroy(tileData[pos].button.gameObject);
tileData[pos].button = null;
}
tileData[pos].seed = null;
}
}
[기능 구현]
- 갈린 밭에 씨앗 심기
- 마우스 왼쪽 키를 눌렀을 때, 그 위치의 타일의 isSeedEnabled 값이 false 이면 밭이 안 갈린 상태이다.
- 이 경우에는 조건문에 진입해서 isSeedEnabled 값을 true 로 변경해주고, 타일을 갈린 모양의 타일로 바꿔준다.
- 만약 밭이 갈린 상태이고, 그 타일 위에 씨앗이 없다면 tileData[gridPos].seedOnTile 의 값을 true 로 바꿔주고, tileData[gridPos].seed = seedContainer.GetSeed(0).GetComponent<seed>(); 구문으로 씨앗을 생성한 후 씨앗 인스턴스를 seed 변수에 넣어준다.
if (Input.GetMouseButtonDown(0))
{
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector3Int gridPos = tilemap.WorldToCell(mousePos);
foreach (Vector3Int pos in tileData.Keys)
{
if (pos == gridPos)
{
if (!tileData[gridPos].isSeedEnabled)
{
tileData[gridPos].isSeedEnabled = true;
tilemap.SetTile(gridPos, farmTile);
}
else if (tileData[gridPos].isSeedEnabled && !tileData[gridPos].seedOnTile)
{
tileData[gridPos].seedOnTile = true;
tileData[gridPos].seed = seedContainer.GetSeed(0).GetComponent<seed>();
}
}
}
}
- 다 자란 과일 수확하기
- tileData 에 담겨있는 타일들을 돌면서, 어떤 타일의 seed 변수가 null 이 아니면(씨앗을 심은 상태면) 조건문에 진입한다. 이 조건문 내부에서는 또 다른 조건문이 존재하는데, 타일의 seed 변수의 isEnabled 값이 false 인지 그리고 타일의 seedOnTile 변수가 true 인지 판단한다.
- seed 변수의 isEnabled 값이 false 이면 씨앗이 다 자란 상태임을 의미한다. seedOnTile 변수가 true 라는 것은 타일 위에 씨앗이 있다는 것을 의미한다.
- 즉, 두 조건을 모두 만족했다는 것은 씨앗이 다 자랐다는 것을 의미하므로 수확하기 버튼을 새로 만들어준다.
- 수확하기 버튼을 GetFruit 함수와 연결시켜준다.
foreach(Vector3Int pos in tileData.Keys)
{
if (tileData[pos].seed != null)
{
if (!tileData[pos].seed.isEnabled && tileData[pos].seedOnTile)
{
tileData[pos].seed.isEnabled = true;
CreateGetFruitButton(pos);
}
}
if (tileData[pos].button != null)
{
Vector2 worldPos = tilemap.CellToWorld(pos);
Vector3 screenPos = mainCamera.WorldToScreenPoint(worldPos);
tileData[pos].button.transform.position = (screenPos) + new Vector3(0, 2, 0);
}
}
public void CreateGetFruitButton(Vector3Int pos)
{
if (tileData[pos].button == null)
{
GameObject buttonObj = Instantiate(getFruitButtonPrefab, canvas.transform);
Button button = buttonObj.GetComponent<Button>();
tileData[pos].button = button;
button.onClick.AddListener(() => GetFruit(pos));
}
}
public void GetFruit(Vector3Int pos)
{
fruitContainer.GetFruit(0);
Debug.Log("과일을 수확합니다!!!: " + pos);
tileData[pos].seed.isEnabled = true;
tileData[pos].isSeedEnabled = false;
tileData[pos].seedOnTile = false;
tilemap.SetTile(pos, grassTile);
if (tileData[pos].button != null)
{
Destroy(tileData[pos].button.gameObject);
tileData[pos].button = null;
}
tileData[pos].seed = null;
}
[참고자료]
[유니티 C#][자료구조] Dictionary (딕셔너리) :: IT's me (tistory.com)
[유니티 C#][자료구조] Dictionary (딕셔너리)
유니티 C# 자료구조 Dictionary (딕셔너리) C#에는 많은 자료 구조가 있지만, 상황별로 가장적절한 자료구조를 쓰는 것이 중요하다.오늘은 게임 개발에서 유용하게 쓰일 수 있는C# 자료구조 Dictionary
grayt.tistory.com
Unity Tilemap의 타일에 데이터 저장하기
타일형 게임을 구현할 때 유니티에서 제공하는 타일맵을 사용하면 팔레트를 이용해 쉽게 타일을 배치 할 수 있다는 장점이 있다. 단순히 타일을 배치하는 것만으로 충분할 수도 있지만, 구현하
upbo.tistory.com
https://mentum.tistory.com/379
Unity UI Button onClick.AddListener 활용하기
#. 버튼에 이벤트 할당하기 유니티에서 UI에 동작을 할당 할 때는 간편하게 인스펙터에서 버튼 컴포넌트에 노출되어있는 UnityEvent에 할당하는 방법이 있다. 쉽고 간편해서 많은 유니티 튜토리얼
mentum.tistory.com