0. 들어가기 전에
이번엔 게임 오브젝트로 생성된 점토를 관리하고, 이를 저장하는 기능을 만들었다.
게임을 종료하고 다시 돌아왔을 때 이전 상태 그대로 반영되어야 하기 때문에 저장&불러오기 기능이 필요하다.
1. 게임 오브젝트
풀 매니저 오브젝트와 데이터 매니저 오브젝트를 새로 만들었다. 풀 매니저 매니저는 PoolManager 스크립트를, 데이터 매니저는 DataManager 스크립트를 컴포넌트로 갖는다.
2. 스크립트
이번엔 PoolManager, DataManager, Datas, ClayDatas 스크립트를 새로 만들었다. 그리고 기존에 만들었던 Clay 스크립트에 기능을 추가했다.
2.1 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;
[Header("Clay Spawn")]
public GameObject spawnPoint; // 중앙에서 스폰될 것..
private void Awake()
{
pools = new List<GameObject>[clayPrefabs.Length];
for (int index = 0; index < pools.Length; index++)
pools[index] = new List<GameObject>();
GetGameObject(4);
}
private void Start()
{
DataManager.instance.LoadGameData();
for (int index=0; index<DataManager.instance.data.clayInfos.Count; index++)
{
ClayDatas tmpData = DataManager.instance.data.clayInfos[index];
int idx = tmpData.clayIdx;
int level = tmpData.clayLevel;
int cnt = tmpData.curTouchCnt;
GetGameObject(index).GetComponent<Clay>().ResetInfo(level, cnt); // 점토 정보 설정
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.S))
SaveClayData();
}
public GameObject GetGameObject(int index)
{
GameObject select = null;
foreach (GameObject gameObj in pools[index])
{
// 풀을 돌면서 놀고 있는 게임 오브젝트 찾기
if (gameObj.activeSelf == false)
{
// 찾으면 반환
select = gameObj;
select.SetActive(true);
select.transform.position = spawnPoint.transform.position; // 점토의 위치를 중앙으로..
select.GetComponent<Clay>().ResetInfo(); // 점토 상태 초기화시키기..
break;
}
}
// 놀고 있는 게임 오브젝트가 없다면..
if (!select)
{
// 새로 생성해서 반환..
// 새로 생성한 게임 오브젝트는 풀 매니저 하위에 놓을 것.. 그래서 부모에 transform 넣어줌..
select = Instantiate(clayPrefabs[index], transform);
pools[index].Add(select);
select.transform.position = spawnPoint.transform.position; // 점토의 위치를 중앙으로..
}
return select;
}
// 점토 데이터를 저장할 것(활성돠 된 점토만..)
public void SaveClayData()
{
// 프리팹 사이즈만큼 점토 정보 저장할 리스트 만들기
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;
Debug.Log(clayData.clayIdx + " " + clayData.clayLevel + " " + clayData.curTouchCnt);
DataManager.instance.data.clayInfos.Add(clayData); // 리스트에 추가하기
Debug.Log("추가했어영");
}
}
}
DataManager.instance.SaveGameData(); // 데이터 저장하기
}
}
2.2 PoolManager 스크립트 설명
PoolManager 스크립트에 대한 설명은 다음과 같다.
1. 변수
clayPrefabs 배열을 선언해서 미리 만들어둔 게임 오브젝트 프리팹들을 할당했다. 점토를 구매할 때마다 해당 점토에 맞는 게임 오브젝트를 생성하기 위함이다.
pools 는 현재 게임 상에 존재하는 게임 오브젝트들을 관리하기 위해 선언했다. 인덱스로 게임 오브젝트 프리팹을 구분하도록 했다. pools[index] 이렇게 하면 index 에 맞는 점토들만 들어있는 List 에 접근하게 된다.
spawmPoint 는 점토가 화면 중앙에서 스폰될 수 있도록 하기 위한 변수이다.
[Header("Clay Info")]
public GameObject[] clayPrefabs;
[Header("Clay Pool")]
public List<GameObject>[] pools;
[Header("Clay Spawn")]
public GameObject spawnPoint; // 중앙에서 스폰될 것..
2. Awake()
pools 에 새로운 List 배열을 만들어서 할당해준다. 그리고 각 요소마다 new List<GameObject>() 를 할당해준다. 점토 게임 오브젝트가 생성될 때마다 pools[index] 에 점토를 Add 해줘야 하는데 할당해주지 않으면 List 가 존재하지 않아서 Add 를 할 수 없다.
GetGameObject(int index) 는 점토를 생성하는 메서드인데 일단 제대로 작동하는지 확인하기 위해 임의로 넣어놓은 구문이다. 다음에 없앨 것.
private void Awake()
{
pools = new List<GameObject>[clayPrefabs.Length];
for (int index = 0; index < pools.Length; index++)
pools[index] = new List<GameObject>();
GetGameObject(4);
}
3. Start()
Start 에서는 저장한 데이터를 가져오고, 가져온 데이터를 반영하도록 했다.
private void Start()
{
DataManager.instance.LoadGameData();
for (int index=0; index<DataManager.instance.data.clayInfos.Count; index++)
{
ClayDatas tmpData = DataManager.instance.data.clayInfos[index];
int idx = tmpData.clayIdx;
int level = tmpData.clayLevel;
int cnt = tmpData.curTouchCnt;
GetGameObject(index).GetComponent<Clay>().ResetInfo(level, cnt); // 점토 정보 설정
}
}
4. Update()
구매 기능을 확인하기 위해서 임시로 추가했다. 나중에는 구매 기능을 UI 를 통해 구현할 것.
private void Update()
{
if (Input.GetKeyDown(KeyCode.S))
SaveClayData();
}
5. GetGameObject(int index)
점토를 구매할 때 호출할 메서드이다.
일단 select 변수를 선언하고 null 을 넣어놓는다. index 에 맞는 점토 중에 노는 점토(비활성화 된채로 게임 상에 존재하는 게임 오브젝트) 가 있다면 걔를 select 에 넣어주고, 정보를 초기화해주도록 했다.
만약 놀고 있는 게임 오브젝트가 없다면 그제서야 새로운 게임 오브젝트를 만들도록 했다.
public GameObject GetGameObject(int index)
{
GameObject select = null;
foreach (GameObject gameObj in pools[index])
{
// 풀을 돌면서 놀고 있는 게임 오브젝트 찾기
if (gameObj.activeSelf == false)
{
// 찾으면 반환
select = gameObj;
select.SetActive(true);
select.transform.position = spawnPoint.transform.position; // 점토의 위치를 중앙으로..
select.GetComponent<Clay>().ResetInfo(); // 점토 상태 초기화시키기..
break;
}
}
// 놀고 있는 게임 오브젝트가 없다면..
if (!select)
{
// 새로 생성해서 반환..
// 새로 생성한 게임 오브젝트는 풀 매니저 하위에 놓을 것.. 그래서 부모에 transform 넣어줌..
select = Instantiate(clayPrefabs[index], transform);
pools[index].Add(select);
select.transform.position = spawnPoint.transform.position; // 점토의 위치를 중앙으로..
}
return select;
}
6. SaveClayData()
점토 데이터를 저장할 때 호출할 메서드이다.
일단 게임 상에 존재하는 점토 오브젝트를 모두 저장하는게 아니라 활성화 되어 있는 것만 저장하도록 했다.
clayData 타입의 변수를 만들고 할당해준다음 그 값을 저장할 값으로 변경한다. 그리고 이 점토의 정보를 점토들의 정보를 저장하는 clayInfo 에 추가했다.
// 점토 데이터를 저장할 것(활성돠 된 점토만..)
public void SaveClayData()
{
// 프리팹 사이즈만큼 점토 정보 저장할 리스트 만들기
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;
Debug.Log(clayData.clayIdx + " " + clayData.clayLevel + " " + clayData.curTouchCnt);
DataManager.instance.data.clayInfos.Add(clayData); // 리스트에 추가하기
Debug.Log("추가했어영");
}
}
}
DataManager.instance.SaveGameData(); // 데이터 저장하기
}
2.3 DataManager 스크립트
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class DataManager : MonoBehaviour
{
[Header("Data Manager")]
public static DataManager instance;
[Header("Save Datas")]
string GameDataFileName = "GameData.json";
public Datas data = new Datas(); // 저장용 클래스 변수
private void Awake()
{
// 싱글톤 이용
if (instance != null && instance != this)
{
// 만약 이미 존재하면 그냥 없애
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임
}
// 불러오기
public void LoadGameData()
{
string filePath = Application.persistentDataPath + "/" + GameDataFileName;
// 저장된 게임이 있다면
if (File.Exists(filePath))
{
// 저장된 파일 읽어오고 Json 을 클래스 형식으로 전환해서 할당
string FromJsonData = File.ReadAllText(filePath);
data = JsonUtility.FromJson<Datas>(FromJsonData);
print("불러오기 완료");
}
}
// 저장하기
public void SaveGameData()
{
// 클래스를 Json 형식으로 변환(true: 가독성 좋게 작성)
string ToJsonData = JsonUtility.ToJson(data, true);
Debug.Log(ToJsonData + "저장할건디용");
string filePath = Application.persistentDataPath + "/" + GameDataFileName;
// 이미 저장된 파일이 있다면 덮어쓰고, 없다면 새로 만들어서 저장
File.WriteAllText(filePath, ToJsonData);
// 올바르게 저장됐는지 확인
print("저장완료");
}
}
2.4 DataManager 스크립트 설명
DataManager 스크립트에 대한 설명은 다음과 같다.
1. 변수
DataManager 를 싱글톤으로 이용할 것이기 때문에 DataManager 타입의 instance 변수를 선언해주었다.
그리고 데이터를 저장하는 것과 관련있는 변수들도 선언했다. GameDataFileName 은 저장되는 데이터 파일의 이름이다. data 변수는 Datas 타입인데 저장 하는데 이용하기 위해 선언했다.
[Header("Data Manager")]
public static DataManager instance;
[Header("Save Datas")]
string GameDataFileName = "GameData.json";
public Datas data = new Datas(); // 저장용 클래스 변수
2. Awake()
만약 이미 존재하는 거면 파괴하도록 했고 그게 아니라면 instance 에 자기 자신을 할당하고 DontDestroyLoad 메서드를 이용해서 다른 씬으로 전환되어도 안 없어지도록 했다.
private void Awake()
{
// 싱글톤 이용
if (instance != null && instance != this)
{
// 만약 이미 존재하면 그냥 없애
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임
}
3. LoadGameData()
저장한 데이터를 불러올 때 사용하는 메서드이다. filePath 에 적절한 값을 넣어주고, 만약 이 filePath 에 이미 저장된 게임이 있다면 그 값을 읽어오도록 한다. Json 을 클래스 형식으로 전환해서 data 에 다시 넣어주었다.
// 불러오기
public void LoadGameData()
{
string filePath = Application.persistentDataPath + "/" + GameDataFileName;
// 저장된 게임이 있다면
if (File.Exists(filePath))
{
// 저장된 파일 읽어오고 Json 을 클래스 형식으로 전환해서 할당
string FromJsonData = File.ReadAllText(filePath);
data = JsonUtility.FromJson<Datas>(FromJsonData);
print("불러오기 완료");
}
}
4. SaveGameData()
데이터를 저장할 때 이용할 메서드이다. Datas 타입의 data 를 저장하기 위해 ToJson 메서드를 이용했다.
filePath 에 적절한 값을 넣어주고 WriteAllText 메서드를 이용해서 데이터를 저장했다.
// 저장하기
public void SaveGameData()
{
// 클래스를 Json 형식으로 변환(true: 가독성 좋게 작성)
string ToJsonData = JsonUtility.ToJson(data, true);
Debug.Log(ToJsonData + "저장할건디용");
string filePath = Application.persistentDataPath + "/" + GameDataFileName;
// 이미 저장된 파일이 있다면 덮어쓰고, 없다면 새로 만들어서 저장
File.WriteAllText(filePath, ToJsonData);
// 올바르게 저장됐는지 확인
print("저장완료");
}
2.5 Datas 스크립트
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable] // 직렬화
public class Datas
{
[Header("Clay")]
public List<ClayDatas> clayInfos;
}
2.6 Datas 스크립트 설명
Datas 스크립트에 대한 설명은 다음과 같다.
일단 Datas 는 MonoBehaviour 를 상속 받지 않도록 한다.
1. 변수
일단은 점토의 정보만 저장할 것이기 때문에 변수로 clayInfos 만 선언했다.
[Header("Clay")]
public List<ClayDatas> clayInfos;
2.7 ClayDatas 스크립트
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable] // 직렬화
public class ClayDatas
{
[Header("Clay Info")]
public int clayIdx;
public int clayLevel;
public int curTouchCnt;
}
2.8 ClayDatas 스크립트 설명
ClayDatas 스크립트에 대한 설명은 다음과 같다.
1. 변수
저장할 점토의 정보는 clayIdx, clayLevel, curTouchCnt 이다.
clayIdx 는 저장된 데이터를 토대로 게임 오브젝트를 새로 생성할 때 어떤 점토를 생성할 것인지 지정하기 때문이다.
clayLeve 은 새로 생성된 게임 오브젝트의 레벨에 반영해주어야 하기 때문이다. curTouchCnt 도 같은 맥락으로 이해하면 된다.
[Header("Clay Info")]
public int clayIdx;
public int clayLevel;
public int curTouchCnt;
2.9 Clay 스크립트
using System.Collections;
using System.Collections.Generic;
using TreeEditor;
using UnityEngine;
public class Clay : MonoBehaviour
{
[Header("Clay Data")]
public float[] loves; // 점토를 클릭했을 때 얻는 애정 수치(1~5 레벨)
public int[] touchCnts; // 해당 요소만큼 클릭되면 레벨업
public int clayLevel = 1; // 1레벨에서 시작(5레벨까지 있음)
public int curTouchCnt = 0;
public int clayIdx; // 점토 소환할 때 필요한 인덱스
[Header("Animation")]
Animator anim; // 점토가 터치될 때 애니메이션 실행하기 위함
public RuntimeAnimatorController[] animators;
[Header("Sprites")]
public Sprite clay; // 점토 모습
public Sprite animal; // 다 자란 동물 모습
[Header("Drag Clay")]
public float targetTime = 1; // 드래그 시작할 수 있는 시간
public float curTime; // 현재 시간
public Vector3 prevPos; // 점토를 드래그 하기 전 위치
private void Awake()
{
anim = GetComponent<Animator>();
}
// 점토가 터치되면 저절로 호출됨
private void OnMouseDown()
{
prevPos = transform.position; // 드래그 하기 전 위치 저장
Debug.Log("터치됨!");
anim.SetTrigger("doTouch");
GameManager.instance.GetLove(loves[clayLevel - 1]); // 레벨에 맞는 수치를 함수로 넘겨주기
curTouchCnt++;
// 이미 점토의 레벨이 최고 레벨에 도달했으면 밑으로 진입 안 하도록..
if (clayLevel != 5)
{
if (curTouchCnt == touchCnts[clayLevel - 1])
{
clayLevel++; // 레벨 1 증가
curTouchCnt = 0; // 초기화
anim.runtimeAnimatorController = animators[clayLevel - 1]; // 레벨에 맞는 애니메이터로 바꿔주기
if (clayLevel == 5)
gameObject.GetComponent<SpriteRenderer>().sprite = animal;
}
}
}
// 점토 드래그할 때 호출되는 함수
private void OnMouseDrag()
{
curTime += Time.deltaTime;
// 만약 현재 시간이 targetTime 보다 크거나 같다면 점토가 마우스를 따라오도록 하기..
if (curTime >= targetTime)
{
// 마우스 위치를 가져온 후 Z 축을 카메라와의 거리로 설정한다
Vector3 mousePosition = Input.mousePosition;
mousePosition.z = Camera.main.WorldToScreenPoint(transform.position).z;
// 스크린 좌표를 월드 좌표로 변환
Vector3 worldPosition = Camera.main.ScreenToWorldPoint(mousePosition);
// 오브젝트의 위치를 마우스의 월드 좌표로 이동
transform.position = worldPosition;
// 이 경우에는 UI 보다도 앞에 갈 수 있도록..
gameObject.GetComponent<SpriteRenderer>().sortingOrder = 10;
}
}
// 점토 내려놓을 때 호출되는 함수
private void OnMouseUp()
{
curTime = 0;
if (transform.position.x < -6.5 || transform.position.x > 6.5 || transform.position.y < -3 || transform.position.y > 0.6)
transform.position = prevPos;
// 다시 UI 보다 아래로 가도록..
gameObject.GetComponent<SpriteRenderer>().sortingOrder = 1;
}
public void ResetInfo(int level, int cnt)
{
if (level == 5)
gameObject.GetComponent<SpriteRenderer>().sprite = animal; // 동물 모습으로 바꿔주기..
// 점토 정보 설정
clayLevel = level;
curTouchCnt = cnt;
anim.runtimeAnimatorController = animators[clayLevel - 1]; // 레벨에 맞는 애니메이터로 바꿔주기..
}
public void ResetInfo()
{
// 점토 정보 초기화
clayLevel = 1;
curTouchCnt = 0;
gameObject.GetComponent<SpriteRenderer>().sprite = clay; // 점토 모습으로 바꿔주기..
anim.runtimeAnimatorController = animators[clayLevel - 1]; // 애니메이터도 맨 처음걸로 바꿔주기..
}
}
2.10 Clay 스크립트 변경 사항 설명
Clay 스크립트에 대한 설명은 다음과 같다.
1. 변수
점토 데이터를 저장할 때 점토의 인덱스도 필요하기 때문에 새로 추가해주었다.
public int clayIdx; // 점토 소환할 때 필요한 인덱스
2. ResetInfo(), ResetInfo(int level, int cnt)
점토를 새로 생성할 때 비활성화 되어 있던 게임 오브젝트를 다시 활성화 상태로 설정하는 경우가 있다. 이 경우에 점토는 새로 생성된 것으로 취급해야 하므로 정보도 다시 새걸로 바꿔주어야 한다. 이때 이용하기 위해 ResetInfo() 메서드를 만들었다.
ResetInfo(int level, int cnt) 는 엄밀히 말하면 저장된 데이터를 가져와서 반영할 때 이용하는 메서드이다. 그냥 매개변수로 받은 값으로 level 이랑 cnt 값만 새로 설정해주고 애니메이터도 레벨에 맞도록 변경해주면 된다.
public void ResetInfo(int level, int cnt)
{
if (level == 5)
gameObject.GetComponent<SpriteRenderer>().sprite = animal; // 동물 모습으로 바꿔주기..
// 점토 정보 설정
clayLevel = level;
curTouchCnt = cnt;
anim.runtimeAnimatorController = animators[clayLevel - 1]; // 레벨에 맞는 애니메이터로 바꿔주기..
}
public void ResetInfo()
{
// 점토 정보 초기화
clayLevel = 1;
curTouchCnt = 0;
gameObject.GetComponent<SpriteRenderer>().sprite = clay; // 점토 모습으로 바꿔주기..
anim.runtimeAnimatorController = animators[clayLevel - 1]; // 애니메이터도 맨 처음걸로 바꿔주기..
}
3. 결과물
이전에 저장한 데이터를 반영해서 점토가 생성되는 모습을 볼 수 있다.
4. 참고자료
데이터 저장은 내 취향이 아닌 것 같다. UI 와 동급으로 재미없다고 느껴지는게 데이터 저장이다.. 사담은 뒤로 하고 JSON 에 대해 잘 정리해놓은 글이 있길래 가져왔다.
4.1 Json
[Unity/C#] 데이터 저장/불러오기 가장 쉬운 방법 (Json)
설명은 이 블로그가 제일 쉽게 잘해놓은 것 같다.
4.2 Newtonsoft Json
[Unity3D] 유니티에서 JSON 사용하기(Newtonsoft JSON) :: 베르의 프로그래밍 노트
[unity/json]Newtonsoft Json 라이브러리 손쉽게 추가하는법(package manager 이용)