0. 들어가기 전에
이번에는 야생을 클리어 해서 잡아온 점토를 메인 씬에 소환하도록 했다. 만약 이미 점토 집이 꽉 찬 상태라면 소환하는 대신에 돈으로 받도록 했다.
그리고 이제는 야생 판넬의 버튼에 기능을 연결해서 난이도를 설정할 수 있도록 했다. 이제 쉬움 야생 버튼을 누르면 알아서 야생 씬이 쉬움 단계로 세팅된다.
1. 프리팹
이번에 새로 만든 프리팹은 normal, hard 야생에서 얻을 수 있는 점토들이다.
1. 인스펙터 창
normal, hard 점토 게임 오브젝트의 인스펙터 창의 모습은 easy 점토 게임 오브젝트의 모습과 다르지 않다.

Clay 스크립트의 Clay Data 요소의 값만 easy, normal, hard 점토가 다른 값을 가지도록 했다.

2. 게임 오브젝트
기존 게임 오브젝트를 살짝 수정했다.
2.1 PoolManager
1. 인스펙터 창
PoolManager 의 ClayPrefabs 요소로 새로 만든 점토 프리팹들을 추가로 할당해주었다.

2.2 GameManager
1. 인스펙터 창
UnlockedClays 와 Catched Clays 의 크기를 15로 바꿔주었다. 현재 점토 프리팹의 개수가 총 15개라 15로 설정했다.

3. 스크립트
이번에 새로 만든 스크립트는 없다. 대신 많은 스크립트를 수정했다.
3.1 GameManager 스크립트
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using static UnityEditor.Experimental.GraphView.Port;
public class GameManager : MonoBehaviour
{
[Header("Game Data")]
public float love; // 애정
public float gold; // 골드
public bool[] unLockedClays; // 점토들의 해금 여부
public bool[] catchedClays; // 야생에서 잡아왔는지 확인용
public int clayHouseLevel = 1; // 점토 아파트 레벨
public int clayClickLevel = 1; // 점토 클릭 레벨
public int[] clayHouseLoveList; // 업그레이드 비용
public int[] clayClickLoveList; // 업그레이드 비용
public int curPossibleClayNum = 1; // 최대로 키울 수 있는 점토의 개수(1레벨은 1마리, 2레벨은 2마리...)
[Header("Game Manager")]
public static GameManager instance; // 싱글톤 이용하기 위함
public string curScene;
public bool isInitialized = false; // 데이터 초기화 완료 여부
private Coroutine dataSetCoroutine; // 데이터 초기화 코루틴 저장
[Header("GameDataUI")]
public GameDataUIController gameDataUI;
public delegate void SetInfoPanelHandler(string text); // 델리게이트 선언
public event SetInfoPanelHandler OnSetInfoPanel;
[Header("Pool Manager")]
public PoolManager poolManager;
[Header("Coroutine")]
public Coroutine updateTextUICoroutine;
[Header("Game Exit")]
public Button gameExitButton;
[Header("Effect")]
// 0: 점토 레벨업, 1: 점토 판매, 2: 점토 해금, 3: 업그레이드
public ParticleSystem[] effectsPrefabs; // 프리팹 넣어놓기
public ParticleSystem[] effects; // 관리용 변수
public string[] effectGameObjectNames;
[Header("Toy Control")]
public int curToyIdx = -1; // 현재 선택된 장난감
public RuntimeAnimatorController[] clayToyAnimators; // 가구랑 상호작용하는 애니메이터
public string[] toyInfo; // 가구를 클릭하면 안내 판넬에 띄울 내용
public delegate void SetClayHouseLevel(int houseLevel, int clickLevel);
public SetClayHouseLevel OnSetClayHouseInfo;
// Light & UI Control
public delegate void SetLightHandler(bool flag);
public event SetLightHandler OnSetLightHandler; // 여기에 빛 관리하는 메서드 연결해놓을 것(점토의 드래그 시작되면 이 델리게이트에 연결된 메서드를 호출하도록..)
// 야생 콘텐츠 관련
[Header("World Manager")]
// 게임 시작 종료 여부
public bool isGameStart = false;
public bool isGameEnd = false;
public bool isGameClear = false;
public int worldLevel = 0; // 월드 게임 난이도(0: 쉬움, 1: 보통, 2: 어려움)
public int getClayIdx; // 얻은 점토 인덱스(이 값으로 새로운 점토를 소환할 것!) 씬 로드할 때 하면 될 듯?
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 여기서 데이터 세팅하기
// DataManager 준비될 때까지 기다려!
if (dataSetCoroutine != null)
StopCoroutine(dataSetCoroutine); // 이미 시작한 코루틴 있으면 끝내고 다시 시작
dataSetCoroutine = StartCoroutine(OnSceneLoadedSetting()); // 정보 세팅
}
private void Awake()
{
// 싱글톤 이용
if (instance != null && instance != this)
{
// 만약 이미 존재하면 그냥 없애
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임
}
private void OnEnable()
{
// 씬이 로드될 때마다 알아서 호출될 수 있도록..
SceneManager.sceneLoaded += OnSceneLoaded; // 이벤트에 메서드 연결
}
private void OnDisable()
{
// 게임 오브젝트가 비활성화 될 때 이벤트에 연결해놓은 메서드 없애기
SceneManager.sceneLoaded -= OnSceneLoaded;
}
// 씬 로드될 때 수행되어야 하는 로직 모음집..
private IEnumerator OnSceneLoadedSetting()
{
// 데이터 매니저 준비될 때까지 기다려!!!
// DataManager 초기화 완료 기다리기
while (!DataManager.instance.isInitialized)
{
yield return null;
}
Debug.Log("음 이제 DataManager 이용할 수 있어용~");
curScene = SceneManager.GetActiveScene().name; // 씬 이름 가져오기
// 씬에 따라 정보 세팅 다르게..
if (curScene == "ClayHouse")
{
// 현재 씬이 클레이 하우스인 경우에만 호출 되도록..
poolManager = GameObject.Find("PoolManager").GetComponent<PoolManager>(); // 풀매니저 찾아서 할당
gameDataUI = GameObject.Find("GameDataUIController").GetComponent<GameDataUIController>(); // 게임 데이터 UI 찾아서 할당
for (int i = 0; i < effectsPrefabs.Length; i++)
{
// 이펙트 생성해서 넣어놓기
effects[i] = Instantiate(effectsPrefabs[i], GameObject.Find(effectGameObjectNames[i]).transform);
effects[i].gameObject.SetActive(false); // 비활성화
}
gameExitButton = GameObject.Find("OptionPanelParent").transform.Find("Option Panel").transform.Find("Image").transform.Find("Exit Button").GetComponent<Button>();
gameExitButton.onClick.AddListener(DataManager.instance.SaveGameData); // 게임 데이터 저장 메서드 연결
gameExitButton.onClick.AddListener(GameExit); // 게임 종료 메서드 연결
// 메서드 연결하기
DataManager.instance.OnSave -= SetSaveData; // 중복 방지하기 위해 먼저 빼줌
DataManager.instance.OnSave += SetSaveData;
LoadDataSet(); // 데이터 반영
}
else if (curScene == "World")
{
// 현재 씬이 World 일 때
}
isInitialized = true; // 데이터 반영 완료
}
private void LoadDataSet()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 저장된 게임 데이터가 있는 경우 데이터 가져와서 반영
if (DataManager.instance.data.unlockClays != null)
{
// 저장된 데이터 반영해서 가져오기
for (int i = 0; i < unLockedClays.Length; i++)
{
unLockedClays[i] = DataManager.instance.data.unlockClays[i];
catchedClays[i] = DataManager.instance.data.catchClays[i];
}
}
// 저장된 게임 수치 데이터가 있는 경우 데이터 가져와서 반영
if (DataManager.instance.data.valueDatas != null)
{
// 저장된 데이터 반영해서 가져오기
gold = DataManager.instance.data.valueDatas.gold;
love = DataManager.instance.data.valueDatas.love;
clayHouseLevel = DataManager.instance.data.valueDatas.clayHouseLevel;
clayClickLevel = DataManager.instance.data.valueDatas.clayClickLevel;
curPossibleClayNum = DataManager.instance.data.valueDatas.curPossibleClayNum;
SetGoldLove(); // 로드한 데이터 반영해서 데이터 UI 업데이트..
SetUpgradePanel(); // 델리게이트 호출
}
}
public void SetUpgradePanel()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
OnSetClayHouseInfo?.Invoke(clayHouseLevel, clayClickLevel); // UpgradePanel 클래스의 SetUpgardePanel() 메서드 호출
}
public void SetGoldLove()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 로드한 데이터에 맞게 데이터 UI 변경할 수 있도록..
// 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함.
if (updateTextUICoroutine != null)
{
StopCoroutine(updateTextUICoroutine);
}
updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("gold", gold, gold));
updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("love", love, love));
}
// 재화 얻는 함수
public void GetGold(float capacity)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함.
if (updateTextUICoroutine != null)
{
StopCoroutine(updateTextUICoroutine);
}
updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("gold", gold + capacity, gold));
gold += capacity;
//PlayerPrefs.SetFloat("Gold", gold); // 데이터 저장
}
public void GetLove(float capacity)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함.
if (updateTextUICoroutine != null)
{
StopCoroutine(updateTextUICoroutine);
}
updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("love", love + capacity, love));
love += capacity;
//PlayerPrefs.SetFloat("Love", love); // 데이터 저장
}
public void SetSaveData()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
int size = unLockedClays.Length;
DataManager.instance.data.unlockClays = new List<bool>();
DataManager.instance.data.catchClays = new List<bool>();
for (int i = 0; i < size; i++)
{
DataManager.instance.data.unlockClays.Add(unLockedClays[i]); // 해금 여부 저장
DataManager.instance.data.catchClays.Add(catchedClays[i]); // 포획 여부 저장
}
// 수치 데이터 저장
DataManager.instance.data.valueDatas = new ValueDatas();
DataManager.instance.data.valueDatas.gold = gold;
DataManager.instance.data.valueDatas.love = love;
DataManager.instance.data.valueDatas.clayHouseLevel = clayHouseLevel;
DataManager.instance.data.valueDatas.clayClickLevel = clayClickLevel;
DataManager.instance.data.valueDatas.curPossibleClayNum = curPossibleClayNum;
}
public void GameExit()
{
// 게임 종료
Application.Quit();
}
public void StartInfoPanel(string text)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 연결된 메서드 실행시키기
OnSetInfoPanel?.Invoke(text);
}
public void SetLightAndUI(bool flag)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
OnSetLightHandler?.Invoke(flag); // flag 값을 전달해서 델리게이트에 연결된 메서드 호출
}
public void MoveScene()
{
// 게임 매니저와 데이터 매니저의 isInitailized 값을 다시 false 로 바꿔주기
isInitialized = false; // 다시 false 로..
DataManager.instance.isInitialized = false;
if (curScene == "ClayHouse")
{
DataManager.instance.SaveGameData(); // 씬 전환 하기 전 데이터 저장!
ResetWorldGame(); // 월드 게임 정보 초기화
curScene = "World"; // 씬 이름 바꿔주깅
// 현재가 점토 집인 경우에는 World 씬으로 이동
SceneManager.LoadScene("World");
}
else if (curScene == "World")
{
// 야생일 때는 딱히 저장할 데이터 없음
curScene = "ClayHouse";
// 현재 World 씬인 경우 점토 집 씬으로 이동
SceneManager.LoadScene("ClayHouse");
}
}
public void ResetWorldGame()
{
// 야생으로 넘어갈 때 호출되는 메서드
isGameStart = false;
isGameEnd = false;
isGameClear = false;
}
public void WorldGameClear()
{
// 야생에 성공했을 때 호출되는 메서드
isGameStart = false;
isGameEnd = true;
isGameClear = true;
}
public void WorldGameFail()
{
// 야생에 성공했을 때 호출되는 메서드
isGameStart = false;
isGameEnd = true;
isGameClear = false;
}
}
3.2 GameManager 스크립트 변경 사항 설명
1. 변수
다음과 같은 변수를 추가했다.
worldLevel 값으로 world 씬에서 야생 난이도를 설정해주도록 했다. 그리고 getClayIdx 를 통해서 야생을 성공하고 돌아왔을 때 해당 점토를 소환하도록 했다.
public int worldLevel = 0; // 월드 게임 난이도(0: 쉬움, 1: 보통, 2: 어려움)
public int getClayIdx; // 얻은 점토 인덱스(이 값으로 새로운 점토를 소환할 것!) 씬 로드할 때 하면 될 듯?
2. MoveScene()
내용을 살짝 수정했다. 현재 씬이 ClayHouse(메인씬) 일 때 다른 씬으로 넘어가려고 하면 월드 게임 정보를 초기화 하는 로직을 수행하도록 수정했다.
public void MoveScene()
{
// 게임 매니저와 데이터 매니저의 isInitailized 값을 다시 false 로 바꿔주기
isInitialized = false; // 다시 false 로..
DataManager.instance.isInitialized = false;
if (curScene == "ClayHouse")
{
DataManager.instance.SaveGameData(); // 씬 전환 하기 전 데이터 저장!
ResetWorldGame(); // 월드 게임 정보 초기화
curScene = "World"; // 씬 이름 바꿔주깅
// 현재가 점토 집인 경우에는 World 씬으로 이동
SceneManager.LoadScene("World");
}
else if (curScene == "World")
{
// 야생일 때는 딱히 저장할 데이터 없음
curScene = "ClayHouse";
// 현재 World 씬인 경우 점토 집 씬으로 이동
SceneManager.LoadScene("ClayHouse");
}
}
3. ResetWorldGame()
월드 게임의 정보를 초기화하는 메서드이다. 이 변수들은 게임 매니저의 변수이고, 월드 씬에서 게임 매니저에 접근해서 이용한다.
public void ResetWorldGame()
{
// 야생으로 넘어갈 때 호출되는 메서드
isGameStart = false;
isGameEnd = false;
isGameClear = false;
}
3.3 PoolManager 스크립트
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PoolManager : MonoBehaviour
{
[Header("Clay Info")]
public GameObject[] clayPrefabs;
[Header("Clay Pool")]
public List<GameObject>[] pools;
public int curClayNum; // 현재 점토 개수
[Header("Clay Spawn")]
public GameObject spawnPoint; // 중앙에서 스폰될 것..
[Header("Clay Sell")]
public Clay curClay; // 현재 클릭된 대상 클레이
public bool isSellZone; // 판매 버톤에 들어가있는지 확인용
public bool isPossibleSell; // 이 값이 true 면 팔아!
public delegate void SellClayHandler();
public event SellClayHandler OnSellClay;
private void Awake()
{
pools = new List<GameObject>[clayPrefabs.Length];
for (int index = 0; index < pools.Length; index++)
pools[index] = new List<GameObject>();
}
private IEnumerator Start()
{
// DataManager 초기화 완료 기다리기
while (!DataManager.instance.isInitialized)
{
yield return null;
}
// GameManager 초기화 완료 기다리기
while (!GameManager.instance.isInitialized)
{
yield return null;
}
// 중복 연결 막기 위해서 미리 뺐다가 다시 더하기
// - 는 연결되어 있지 않아도 에러 발생시키지 않음.
DataManager.instance.OnSave -= SetSaveClayData;
DataManager.instance.OnSave += SetSaveClayData;
LoadDataSet(); // 불러온 데이터 반영하기
// 점토 잡아왔으면 반영
if (GameManager.instance.isGameClear)
GetWorldClay();
}
private void OnDisable()
{
// 게임 오브젝트 파괴될 때 델리게이트에 연결했던 메서드 빼주기
DataManager.instance.OnSave -= SetSaveClayData;
}
private void GetWorldClay()
{
// 흠.. 점토 구매 버튼에 빛 효과 줄까??
if (GameManager.instance.catchedClays[GameManager.instance.getClayIdx] == false)
{
GameManager.instance.catchedClays[GameManager.instance.getClayIdx] = true;
}
// 야생에서 얻어온 점토 소환
// 만약 공간이 부족하면 걍 돈으로 얻어
if (curClayNum >= GameManager.instance.curPossibleClayNum)
{
GameManager.instance.StartInfoPanel("집이 좁아서 점토는 판매됩니다ㅠ_ㅠ");
GameManager.instance.GetGold(clayPrefabs[GameManager.instance.getClayIdx].GetComponent<Clay>().buyPrice); // 상점 구매가로 설정..
}
else
{
GameManager.instance.StartInfoPanel("점토를 잡아왔다!");
GetGameObject(GameManager.instance.getClayIdx); // 잡아온 점토 소환!
curClayNum++; // 반영해주세여~
}
}
private void LoadDataSet()
{
curClayNum = DataManager.instance.data.clayInfos.Count; // 저장된 점토 개수 가져와용~
for (int index = 0; index < curClayNum; index++)
{
ClayDatas tmpData = DataManager.instance.data.clayInfos[index];
int idx = tmpData.clayIdx;
int level = tmpData.clayLevel;
int cnt = tmpData.curTouchCnt;
GetGameObject(idx).GetComponent<Clay>().ResetInfo(level, cnt); // 점토 정보 설정
}
}
public void TrySellClay()
{
if (isPossibleSell)
{
isPossibleSell = false; // 상태 초기화
OnSellClay?.Invoke(); // 이벤트 호출
}
}
public GameObject GetGameObject(int index)
{
GameObject select = null;
foreach (GameObject gameObj in pools[index])
{
// 풀을 돌면서 놀고 있는 게임 오브젝트 찾기
if (gameObj.activeSelf == false)
{
// 찾으면 반환
select = gameObj;
ActivateClay(select);
break;
}
}
// 놀고 있는 게임 오브젝트가 없다면..
if (!select)
{
// 새로 생성해서 반환..
// 새로 생성한 게임 오브젝트는 풀 매니저 하위에 놓을 것.. 그래서 부모에 transform 넣어줌..
select = Instantiate(clayPrefabs[index], transform);
pools[index].Add(select);
ActivateClay(select);
}
return select;
}
public void ActivateClay(GameObject clay)
{
clay.SetActive(true);
clay.transform.position = spawnPoint.transform.position; // 점토의 위치를 중앙으로..
clay.GetComponent<Clay>().ResetInfo(); // 점토 상태 초기화시키기..
}
public void ReturnToPool(GameObject clay)
{
clay.SetActive(false);
}
// 점토 데이터를 저장할 것(활성돠 된 점토만..)
public void SetSaveClayData()
{
// 프리팹 사이즈만큼 점토 정보 저장할 리스트 만들기
DataManager.instance.data.clayInfos = new List<ClayDatas>();
for (int index = 0; index < pools.Length; index++)
{
foreach (GameObject gameObj in pools[index])
{
// 풀을 돌면서 활성화 되어 있는 애들만 저장
if (gameObject.activeSelf == true)
{
// 저장할 내용 만들기
ClayDatas clayData = new ClayDatas();
Clay tmpClay = gameObj.GetComponent<Clay>();
clayData.clayIdx = tmpClay.clayIdx;
clayData.clayLevel = tmpClay.clayLevel;
clayData.curTouchCnt = tmpClay.curTouchCnt;
DataManager.instance.data.clayInfos.Add(clayData); // 리스트에 추가하기
}
}
}
}
}
3.4 PoolManager 스크립트 변경 사항 설명
1. Start()
야생을 클리어 했을 때 얻어온 점토를 소환하는 로직이 맨 마지막 줄에 추가되었다.
private IEnumerator Start()
{
// DataManager 초기화 완료 기다리기
while (!DataManager.instance.isInitialized)
{
yield return null;
}
// GameManager 초기화 완료 기다리기
while (!GameManager.instance.isInitialized)
{
yield return null;
}
// 중복 연결 막기 위해서 미리 뺐다가 다시 더하기
// - 는 연결되어 있지 않아도 에러 발생시키지 않음.
DataManager.instance.OnSave -= SetSaveClayData;
DataManager.instance.OnSave += SetSaveClayData;
LoadDataSet(); // 불러온 데이터 반영하기
// 점토 잡아왔으면 반영
if (GameManager.instance.isGameClear)
GetWorldClay();
}
2. GetWorldClay()
야생에서 잡아온 점토를 소환하는 메서드이다.
만약 처음 잡아온 점토라면 catchClays 요소의 값을 true 로 설정해줘서 이제 점토 구매창에서 해금이 가능하도록 했다.
근데 만약 공간이 부족하다면 자동으로 잡아온 점토를 판매하도록 했다. 판매 가격은 점토 구매 창에서 점토를 구매하는 가격과 동일하도록 일단 설정해놨다.
private void GetWorldClay()
{
// 흠.. 점토 구매 버튼에 빛 효과 줄까??
if (GameManager.instance.catchedClays[GameManager.instance.getClayIdx] == false)
{
GameManager.instance.catchedClays[GameManager.instance.getClayIdx] = true;
}
// 야생에서 얻어온 점토 소환
// 만약 공간이 부족하면 걍 돈으로 얻어
if (curClayNum >= GameManager.instance.curPossibleClayNum)
{
GameManager.instance.StartInfoPanel("집이 좁아서 점토는 판매됩니다ㅠ_ㅠ");
GameManager.instance.GetGold(clayPrefabs[GameManager.instance.getClayIdx].GetComponent<Clay>().buyPrice); // 상점 구매가로 설정..
}
else
{
GameManager.instance.StartInfoPanel("점토를 잡아왔다!");
GetGameObject(GameManager.instance.getClayIdx); // 잡아온 점토 소환!
curClayNum++; // 반영해주세여~
}
}
3.5 WorldPanel 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
public class WorldPanel : MonoBehaviour
{
[Header("World Panel UI")]
public Button[] worldButtons;
public Text[] worldTexts;
[Header("Prices")]
public int[] worldPrices;
private void Start()
{
SetWorldPanel(); // 버튼 설정
}
public void SetWorldPanel()
{
for (int i=0; i<worldButtons.Length; i++)
{
/*
* Closure Problem
람다식은 실제 실행되기 전에는 참조형태로 가지고있는데,
for문을 돌리면서 같은 변수인 i를 계속 줬기 때문에 마지막 값으로 통일된 것.
이를 closure problem이라고 부른다.
*/
// 람다식과 for 문을 같이 쓰면 클로저 문제를 만나게 됨..;;
int idx = i; // 클로저 문제를 피하기 위해서 idx 변수 새로 만들어서 i 값 저장
int price = worldPrices[i];
// 각 버튼에 메서드 연결
worldButtons[i].onClick.AddListener(() => SetWorldButton(idx));
// 텍스트에 가격 표시..
worldTexts[i].text = worldPrices[i] + "";
}
}
public void ClosePanel()
{
gameObject.SetActive(false); // 활성화 끄기..
}
public void SetWorldButton(int level)
{
Debug.Log("야생 레벨: " + level);
// 야생 버튼 누를 때마다 수행되도록..
// 정보 설정 한 후 GameManager 의 MoveScene 메서드 호출하도록..
// 레벨에 맞는 점토를 얻을 수 있도록..
int tmpGetClayIdx = 0;
int tmpCnt = GameManager.instance.catchedClays.Length / 3; // 총 점토 개수 / 3
float needGold = worldPrices[level];
// 게임 매니저의 월드 레벨 설정
GameManager.instance.worldLevel = level;
// 돈이 부족하면 그냥 빠져나가
if (needGold > GameManager.instance.gold)
{
GameManager.instance.StartInfoPanel("돈이 부족해요 ㅠ_ㅠ");
return;
}
if (level == 0)
{
// easy 단계
tmpGetClayIdx = Random.Range(0, tmpCnt); // 0 ~ 4 번 점토 중 한 개
}
else if (level == 1)
{
// normal 단계
tmpGetClayIdx = Random.Range(tmpCnt, 2*tmpCnt); // 5 ~ 9 번 점토 중 한 개
}
else if (level == 2)
{
// hard 단계
tmpGetClayIdx = Random.Range(2 * tmpCnt, 3 * tmpCnt); // 10 ~ 14 번 점토 중 한 개
}
// 돈 차감
GameManager.instance.GetGold(-needGold);
// 잡아 올 점토 설정
GameManager.instance.getClayIdx = tmpGetClayIdx;
// 씬 이동
GameManager.instance.MoveScene();
}
}
3.6 WorldPanel 스크립트 변경 사항 설명
1. SetWorldPanel()
야생 버튼의 정보를 설정하는 메서드 내용을 수정했다.
public void SetWorldPanel()
{
for (int i=0; i<worldButtons.Length; i++)
{
/*
* Closure Problem
람다식은 실제 실행되기 전에는 참조형태로 가지고있는데,
for문을 돌리면서 같은 변수인 i를 계속 줬기 때문에 마지막 값으로 통일된 것.
이를 closure problem이라고 부른다.
*/
// 람다식과 for 문을 같이 쓰면 클로저 문제를 만나게 됨..;;
int idx = i; // 클로저 문제를 피하기 위해서 idx 변수 새로 만들어서 i 값 저장
int price = worldPrices[i];
// 각 버튼에 메서드 연결
worldButtons[i].onClick.AddListener(() => SetWorldButton(idx));
// 텍스트에 가격 표시..
worldTexts[i].text = worldPrices[i] + "";
}
}
2. SetWorldButton(int level)
야생 버튼의 정보를 설정하는 메서드이다.
public void SetWorldButton(int level)
{
Debug.Log("야생 레벨: " + level);
// 야생 버튼 누를 때마다 수행되도록..
// 정보 설정 한 후 GameManager 의 MoveScene 메서드 호출하도록..
// 레벨에 맞는 점토를 얻을 수 있도록..
int tmpGetClayIdx = 0;
int tmpCnt = GameManager.instance.catchedClays.Length / 3; // 총 점토 개수 / 3
float needGold = worldPrices[level];
// 게임 매니저의 월드 레벨 설정
GameManager.instance.worldLevel = level;
// 돈이 부족하면 그냥 빠져나가
if (needGold > GameManager.instance.gold)
{
GameManager.instance.StartInfoPanel("돈이 부족해요 ㅠ_ㅠ");
return;
}
if (level == 0)
{
// easy 단계
tmpGetClayIdx = Random.Range(0, tmpCnt); // 0 ~ 4 번 점토 중 한 개
}
else if (level == 1)
{
// normal 단계
tmpGetClayIdx = Random.Range(tmpCnt, 2*tmpCnt); // 5 ~ 9 번 점토 중 한 개
}
else if (level == 2)
{
// hard 단계
tmpGetClayIdx = Random.Range(2 * tmpCnt, 3 * tmpCnt); // 10 ~ 14 번 점토 중 한 개
}
// 돈 차감
GameManager.instance.GetGold(-needGold);
// 잡아 올 점토 설정
GameManager.instance.getClayIdx = tmpGetClayIdx;
// 씬 이동
GameManager.instance.MoveScene();
}
3.7 MapSpawner 스크립트
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Unity.VisualScripting;
using UnityEngine;
public class MapSpawner : MonoBehaviour
{
[Header("Map Contoller")]
public GameObject[] mapPrefabs; // 현재 맵 프리팹
public GameObject[] easyMapPrefabs;
public GameObject[] normalMapPrefabs;
public GameObject[] hardMapPrefabs;
public List<GameObject>[] pool;
public GameObject easyBackground; // 쉬움 단계 배경
public GameObject normalBackground; // 보통 단계 배경
public GameObject hardBackground; // 어려움 단계 배경
public float curTime = 0;
public float targetTime = 0.5f; // targetTime 마다 맵 생성
public int mapCount;
public int mapLevel; // 0: 쉬움, 1: 보통, 2: 어려움
private Coroutine mapCoroutine;
private void Awake()
{
mapLevel = GameManager.instance.worldLevel; // 게임 매니저의 맵 레벨로 설정해주기
// 맵 난이도에 맞게 mapCount 설정..
if (mapLevel == 0)
{
easyBackground.SetActive(true); // 배경 활성화
mapPrefabs = easyMapPrefabs; // 현재 팹 프리팹을 이지맵으로 설정
}
else if (mapLevel == 1)
{
normalBackground.SetActive(true); // 배경 활성화
mapPrefabs = normalMapPrefabs; // 현재 팹 프리팹을 노멀맵으로 설정
}
else if (mapLevel == 2)
{
hardBackground.SetActive(true); // 배경 활성화
mapPrefabs = hardMapPrefabs; // 현재 팹 프리팹을 하드맵으로 설정
}
mapCount = mapPrefabs.Length; // 크기 설정
pool = new List<GameObject>[mapCount]; // 배열 만들기
for (int i=0; i<mapCount; i++)
{
pool[i] = new List<GameObject>(); // 리스트 새로 만들기
}
}
private IEnumerator Start()
{
// 무한 루프..
// 야생 게임이 시작될 때까지 기다리기..
while (!GameManager.instance.isGameStart)
{
yield return null;
}
mapCoroutine = StartCoroutine(SpawnMap()); // 맵 생성 코루틴 시작
StartCoroutine(GameEnd()); // 게임 종료 코루틴 시작
}
private IEnumerator SpawnMap()
{
while (true)
{
int mapIdx = Random.Range(0, mapCount);
GameObject select = null;
foreach (GameObject map in pool[mapIdx])
{
// 만약 놀고 있는 맵 게임 오브젝트를 발견하면 그거 활성화
if (map.activeSelf == false)
{
select = map;
map.SetActive(true); // 맵 활성화
break;
}
}
// 발견 못 하면 새로 생성
if (select == null)
{
select = Instantiate(mapPrefabs[mapIdx], transform);
pool[mapIdx].Add(select); // 새로 생성한 게임 오브젝트를 풀에 넣기
}
yield return new WaitForSeconds(targetTime); // targetTime 만큼 기다리기
}
}
private IEnumerator GameEnd()
{
while (!GameManager.instance.isGameEnd)
yield return null;
StopCoroutine(mapCoroutine); // 게임 끝났으니까 종료!
}
}
3.8 MapSpawner 스크립트 변경 사항 설명
1. Awake()
맨 처음에 mapLevel 의 값을 GameManager 의 worldLevel 값으로 설정해주는 로직이 추가되었다. 이렇게 해주면 이제 알아서 맵을 세팅한다.
private void Awake()
{
mapLevel = GameManager.instance.worldLevel; // 게임 매니저의 맵 레벨로 설정해주기
// 맵 난이도에 맞게 mapCount 설정..
if (mapLevel == 0)
{
easyBackground.SetActive(true); // 배경 활성화
mapPrefabs = easyMapPrefabs; // 현재 팹 프리팹을 이지맵으로 설정
}
else if (mapLevel == 1)
{
normalBackground.SetActive(true); // 배경 활성화
mapPrefabs = normalMapPrefabs; // 현재 팹 프리팹을 노멀맵으로 설정
}
else if (mapLevel == 2)
{
hardBackground.SetActive(true); // 배경 활성화
mapPrefabs = hardMapPrefabs; // 현재 팹 프리팹을 하드맵으로 설정
}
mapCount = mapPrefabs.Length; // 크기 설정
pool = new List<GameObject>[mapCount]; // 배열 만들기
for (int i=0; i<mapCount; i++)
{
pool[i] = new List<GameObject>(); // 리스트 새로 만들기
}
}
4. 결과물
야생에서 잡아온 점토가 소환되는 모습을 확인할 수 있다. 만약 공간이 부족하다면 점토를 자동으로 파는 로직도 정상적으로 동작한다.
5. 참고자료
이번에 공부한 것들.
5.1 Closure Problem
[Unity] Unity Event 1 - Button Onclick Listener Scripting + Lambda(유니티 이벤트 1 - 버튼 스크립트로 매핑하기 (
※ Practical Environment Unity Editor Version - 2021.3.11f1 ※ Intro 버튼에 이벤트 할당하기 유니티에서 UI에 동작을 할당 할 때는 간편하게 인스펙터에서 버튼 컴포넌트에 노출되어있는 UnityEvent에 할당하는
sugar0810.tistory.com