해당 글은 유튜버 골드메탈님의 강의를 기반으로 작성되었습니다.
이번 강의에서는 젤리를 구매하는 기능과 그에 따른 젤리 생성, 관리, 저장, 그리고 자동 재화 획득 기능을 추가하는 것에 대해 설명하였습니다.
우선 사용자의 젤리 구매에 따른 젤리 생성을 위해 젤리 프리펩을 만들어주겠습니다.
젤리에 따른 그림자의 위치 다르게 하기 위해서 Jelly 스크립트의 Awake() 함수에 다음의 코드를 추가해줍니다.
GameObject shadow;
float shadow_pos_y;
void Awake()
{
...
shadow = transform.Find("Shadow").gameObject;
switch (id) {
case 0: shadow_pos_y = -0.05f; break;
case 6: shadow_pos_y = -0.12f; break;
case 3: shadow_pos_y = -0.14f; break;
case 10: shadow_pos_y = -0.16f; break;
case 11: shadow_pos_y = -0.16f; break;
default: shadow_pos_y = -0.05f; break;
}
shadow.transform.localPosition = new Vector3(0, shadow_pos_y, 0);
}
위 코드는 젤리 오브젝트의 자식 오브젝트로 있는 Shadow 오브젝트를 Find 함수로 찾은 뒤 이것을 이용해 id 값에 따라 위치를 조정하는 코드입니다. Shadow의 경우 젤리 오브젝트 안에 있기 때문에 localPosition으로 위치를 조정해주었습니다.
이제 유니티 프로그램으로 돌아가 'Prefabs'라는 이름으로 폴더를 만들고 젤리 오브젝트를 폴더 안으로 드래그 해줍니다.
이후 게임 상에 놓여 있던 젤리 오브젝트는 삭제해줍니다.
다음으로 젤리를 구매하는 기능을 구현해보겠습니다.
GameManager 스크립트에 새로운 변수와 함께 아래의 함수를 추가해줍니다.
public GameObject prefab;
public void BuyJelly()
{
if (gold < jelly_goldlist[page]) return;
Instantiate(prefab, new Vector3(0, 0, 0), Quaternion.identity);
gold -= jelly_goldlist[page];
}
이후 유니티 프로그램으로 돌아가 변수에 젤리 프리펩을 할당해줍니다.
젤리 오브젝트가 프리펩으로 변경되었기 때문에 기존에 public을 사용하여 유니티 프로그램으로 직접 오브젝트 객체를 받아왔던 GameManager와 LeftTop, RightBottom 오브젝트를 다른 방식으로 지정해주어야 합니다.
따라서 Jelly 스크립트에 다음의 코드를 추가해줍니다.
public GameObject game_manager_obj;
void Awake()
{
left_top = GameObject.Find("LeftTop").gameObject;
right_bottom = GameObject.Find("RightBottom").gameObject;
game_manager_obj = GameObject.Find("GameManager").gameObject;
game_manager = game_manager_obj.GetComponent<GameManager>();
...
}
이후 게임을 실행하여 젤리를 해금한 뒤, 구매 버튼을 누르면 젤리가 화면 중앙에서 생성되는 것을 확인할 수 있습니다.
다음으로 젤리의 Sprite와 id 값을 변경하여 구매하는 젤리에 따라 해당 젤리의 모습으로 생성되도록 구현해보겠습니다.
GameManager 스크립트의 BuyJelly() 함수를 다음과 같이 수정해줍니다.
public void BuyJelly()
{
if (gold < jelly_goldlist[page]) return;
GameObject obj = Instantiate(prefab, new Vector3(0, 0, 0), Quaternion.identity);
Jelly jelly = obj.GetComponent<Jelly>();
obj.name = "Jelly " + page;
jelly.id = page;
jelly.sprite_renderer.sprite = jelly_spritelist[page];
gold -= jelly_goldlist[page];
}
위 코드를 통해 생성된 젤리 오브젝트의 id 값과 Sprite를 임의로 설정할 수 있습니다.
Jelly 스크립트의 sprite_renderer의 경우 GameManager에서 이를 활용하기 위해 private에서 public으로 변경해주었습니다.
이제 젤리를 사고 팜에 따라 현재 생성되어 있는 젤리를 저장하기 위한 리스트를 만들어 젤리를 관리하는 기능을 구현해보겠습니다.
이를 위해 GameManager 스크립트 내에 List 변수를 선언한 뒤 초기화하고 BuyJelly() 함수와 GetGold() 함수 내에서 해당 리스트에 객체를 추가 또는 삭제시켜주록 하였습니다.
List<GameObject> jelly_list;
void Awake()
{
...
jelly_list = new List<GameObject>();
}
public void GetGold(int id, int level, GameObject obj)
{
...
jelly_list.Remove(obj);
}
public void BuyJelly()
{
...
jelly_list.Add(obj);
}
List 변수를 public으로 설정한 뒤, 게임을 실행하여 젤리를 구매하거나 팔면 리스트에서 객체가 추가/삭제 되는 것을 확인할 수 있습니다.
이제 위에서 저장한 젤리 리스트를 통해 세이브 기능을 추가해주겠습니다.
먼저 저 또한 지금까지 세이브 기능을 유니티에서 제공하는 PlayerPrefs 클래스를 이용하여 정말 간단한 데이터를 저장하는 정도로만 구현해왔기 때문에 이번에 작성하는 Json을 이용한 세이브 기능은 작동은 잘 되나 효율적인 코드가 아닐 수 있다는 점을 알려드립니다.
또한 이틀정도 구글링하며 구르다보니 스크립트의 코드들이 전체적으로 수정되었습니다. 때문에 글 마지막 부분에 최종 코드를 올려놓겠습니다.
먼저 저장할 데이터는 GameManager 스크립트에 저장되어 있는 Jelly 리스트와 jelatin, gold 그리고 젤리 해금 리스트 정도입니다. 또한 Jelly 스크립트에 저장되어 있는 각 젤리의 id, level, exp, 위치 또한 저장해주어야 합니다. 때문에 우선 Data 스크립트를 새로 만들어 Data 클래스를 정의한 뒤, GameManager 스크립트에서 이를 저장하는 형식으로 구현하였습니다.
Data.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Data
{
public int id;
public int level;
public float exp;
public Vector3 pos;
public Data(Vector3 pos, int id, int level, float exp)
{
this.pos = pos;
this.id = id;
this.level = level;
this.exp = exp;
}
}
GameManager.cs
public static GameManager instance;
public int jelatin;
public int gold;
public List<Jelly> jelly_list = new List<Jelly>();
public List<Data> jelly_data_list = new List<Data>();
public bool[] jelly_unlock_list;
void Awake()
{
instance = this;
...
}
이제 실제로 데이터를 저장하는 기능을 하는 DataManager 스크립트를 생성하여 다음과 같이 작성해줍니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
[System.Serializable]
public class SaveData
{
public int jelatin;
public int gold;
public bool[] jelly_unlock_list = new bool[12];
public List<Data> jelly_list = new List<Data>();
}
public class DataManager : MonoBehaviour
{
string path;
void Start()
{
path = Path.Combine(Application.dataPath, "database.json");
JsonLoad();
}
public void JsonLoad()
{
SaveData save_data = new SaveData();
if (!File.Exists(path)) {
GameManager.instance.jelatin = 0;
GameManager.instance.gold = 0;
JsonSave();
}
else {
string load_json = File.ReadAllText(path);
save_data = JsonUtility.FromJson<SaveData>(load_json);
if(save_data != null) {
for (int i = 0; i < save_data.jelly_list.Count; ++i)
GameManager.instance.jelly_data_list.Add(save_data.jelly_list[i]);
for (int i = 0; i < save_data.jelly_unlock_list.Length; ++i)
GameManager.instance.jelly_unlock_list[i] = save_data.jelly_unlock_list[i];
GameManager.instance.jelatin = save_data.jelatin;
GameManager.instance.gold = save_data.gold;
}
}
}
public void JsonSave()
{
SaveData save_data = new SaveData();
for (int i = 0; i < GameManager.instance.jelly_list.Count; ++i) {
Jelly jelly = GameManager.instance.jelly_list[i];
save_data.jelly_list.Add(new Data(jelly.gameObject.transform.position, jelly.id, jelly.level, jelly.exp));
}
for(int i = 0; i < GameManager.instance.jelly_unlock_list.Length; ++i)
save_data.jelly_unlock_list[i] = GameManager.instance.jelly_unlock_list[i];
save_data.jelatin = GameManager.instance.jelatin;
save_data.gold = GameManager.instance.gold;
string json = JsonUtility.ToJson(save_data, true);
File.WriteAllText(path, json);
}
}
위 코드는 유니티에서 기본으로 제공하는 JsonUility를 이용해 Json 형식으로 데이터를 저장하고 불러오는 기본적인 방법입니다. 보시는 바와 같이 GameManager에 저장되어 있는 데이터를 새로운 객체에 저장하거나 이전에 저장했던 데이터를 GameManager로 저장하는 식으로 구현되어 있습니다.
다음으로 GameManager에 다음의 코드를 추가로 작성해주었습니다.
void Awake()
{
...
data_manager = data_manager_obj.GetComponent<DataManager>();
}
void Start()
{
Invoke("LoadData", 0.1f);
}
void LoadData()
{
lock_group.gameObject.SetActive(!jelly_unlock_list[page]);
for (int i = 0; i < jelly_data_list.Count; ++i)
{
GameObject obj = Instantiate(prefab, jelly_data_list[i].pos, Quaternion.identity);
Jelly jelly = obj.GetComponent<Jelly>();
jelly.id = jelly_data_list[i].id;
jelly.level = jelly_data_list[i].level;
jelly.exp = jelly_data_list[i].exp;
jelly.sprite_renderer.sprite = jelly_spritelist[jelly.id];
jelly.anim.runtimeAnimatorController = level_ac[jelly.level - 1];
obj.name = "Jelly " + jelly.id;
jelly_list.Add(jelly);
}
}
void OnApplicationQuit()
{
data_manager.JsonSave();
}
Start() 함수에서 Invoke() 함수를 사용한 이유는 DataManager에 의해 데이터가 로드되기 전에 GameManager가 활성화 되어 빈 데이터를 참조하는 현상이 생기기 때문입니다.
제가 구현한 세이브 기능에 대한 설명은 여기까지이며 저 또한 구현하면서 예기치 않게 수정한 부분도 굉장히 많고 이 글을 작성하면서 빠뜨린 부분도 많기 때문에 그대로 따라하여도 제대로 동작하지 않을 수 있습니다.
다음으로 자동 재화 획득 기능을 구현해보겠습니다.
Jelly 스크립트 내에 코루틴 함수를 추가해줍니다.
int jelatin_delay;
bool isGetting;
void Awake()
{
...
isGetting = false;
}
IEnumerator GetJelatin()
{
jelatin_delay = 3;
isGetting = true;
game_manager.GetJelatin(id, level);
yield return new WaitForSeconds(jelatin_delay);
isGetting = false;
}
이후 게임을 실행하여 모든 기능이 정상적으로 구현되는지 확인합니다.
전체 코드
Jelly.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Jelly : MonoBehaviour
{
public int id;
public int level;
public float exp;
public float required_exp;
public float max_exp;
public GameObject game_manager_obj;
public GameManager game_manager;
public GameObject left_top;
public GameObject right_bottom;
public SpriteRenderer sprite_renderer;
public Animator anim;
float pick_time;
int move_delay;
int move_time;
float speed_x;
float speed_y;
bool isWandering;
bool isWalking;
GameObject shadow;
float shadow_pos_y;
int jelatin_delay;
bool isGetting;
void Awake()
{
left_top = GameObject.Find("LeftTop").gameObject;
right_bottom = GameObject.Find("RightBottom").gameObject;
game_manager_obj = GameObject.Find("GameManager").gameObject;
game_manager = game_manager_obj.GetComponent<GameManager>();
sprite_renderer = GetComponent<SpriteRenderer>();
anim = GetComponent<Animator>();
isWandering = false;
isWalking = false;
isGetting = false;
shadow = transform.Find("Shadow").gameObject;
switch (id) {
case 0: shadow_pos_y = -0.05f; break;
case 6: shadow_pos_y = -0.12f; break;
case 3: shadow_pos_y = -0.14f; break;
case 10: shadow_pos_y = -0.16f; break;
case 11: shadow_pos_y = -0.16f; break;
default: shadow_pos_y = -0.05f; break;
}
shadow.transform.localPosition = new Vector3(0, shadow_pos_y, 0);
}
void Update()
{
if(exp < max_exp)
exp += Time.deltaTime;
if (exp > required_exp * level && level < 3)
game_manager.ChangeAc(anim, ++level);
if (!isGetting)
StartCoroutine(GetJelatin());
}
void FixedUpdate()
{
if (!isWandering)
StartCoroutine(Wander());
if (isWalking)
Move();
float pos_x = transform.position.x;
float pos_y = transform.position.y;
if (pos_x < left_top.transform.position.x || pos_x > right_bottom.transform.position.x)
speed_x = -speed_x;
if (pos_y > left_top.transform.position.y || pos_y < right_bottom.transform.position.y)
speed_y = -speed_y;
}
void OnMouseDown()
{
if (!game_manager.isLive) return;
isWalking = false;
anim.SetBool("isWalk", false);
anim.SetTrigger("doTouch");
if(exp < max_exp) ++exp;
game_manager.GetJelatin(id, level);
}
void OnMouseDrag()
{
if (!game_manager.isLive) return;
pick_time += Time.deltaTime;
if (pick_time < 0.1f) return;
isWalking = false;
anim.SetBool("isWalk", false);
anim.SetTrigger("doTouch");
Vector3 mouse_pos = Input.mousePosition;
Vector3 point = Camera.main.ScreenToWorldPoint(new Vector3(mouse_pos.x, mouse_pos.y, mouse_pos.y));
transform.position = point;
}
void OnMouseUp()
{
if (!game_manager.isLive) return;
pick_time = 0;
if (game_manager.isSell) {
game_manager.GetGold(id, level, this);
Destroy(gameObject);
}
float pos_x = transform.position.x;
float pos_y = transform.position.y;
if (pos_x < left_top.transform.position.x || pos_x > right_bottom.transform.position.x ||
pos_y > left_top.transform.position.y || pos_y < right_bottom.transform.position.y)
transform.position = new Vector3(0, -1, 0);
}
void Move()
{
if (speed_x != 0)
sprite_renderer.flipX = speed_x < 0;
transform.Translate(speed_x, speed_y, speed_y);
}
IEnumerator Wander()
{
move_delay = Random.Range(3, 6);
move_time = Random.Range(3, 6);
speed_x = Random.Range(-0.8f, 0.8f) * Time.deltaTime;
speed_y = Random.Range(-0.8f, 0.8f) * Time.deltaTime;
isWandering = true;
yield return new WaitForSeconds(move_delay);
isWalking = true;
anim.SetBool("isWalk", true);
yield return new WaitForSeconds(move_time);
isWalking = false;
anim.SetBool("isWalk", false);
isWandering = false;
}
IEnumerator GetJelatin()
{
jelatin_delay = 3;
isGetting = true;
game_manager.GetJelatin(id, level);
yield return new WaitForSeconds(jelatin_delay);
isGetting = false;
}
}
GameManger.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public static GameManager instance;
public int jelatin;
public int gold;
public List<Jelly> jelly_list = new List<Jelly>();
public List<Data> jelly_data_list = new List<Data>();
public bool[] jelly_unlock_list;
public int max_jelatin;
public int max_gold;
public bool isSell;
public bool isLive;
public Sprite[] jelly_spritelist;
public string[] jelly_namelist;
public int[] jelly_jelatinlist;
public int[] jelly_goldlist;
public Text page_text;
public Image unlock_group_jelly_img;
public Text unlock_group_gold_text;
public Text unlock_group_name_text;
public GameObject lock_group;
public Image lock_group_jelly_img;
public Text lock_group_jelatin_text;
public RuntimeAnimatorController[] level_ac;
public Text jelatin_text;
public Text gold_text;
public Image jelly_panel;
public Image plant_panel;
public Image option_panel;
public GameObject prefab;
public GameObject data_manager_obj;
DataManager data_manager;
Animator jelly_anim;
Animator plant_anim;
bool isJellyClick;
bool isPlantClick;
bool isOption;
int page;
void Awake()
{
instance = this;
jelly_anim = jelly_panel.GetComponent<Animator>();
plant_anim = plant_panel.GetComponent<Animator>();
isLive = true;
jelatin_text.text = jelatin.ToString();
gold_text.text = gold.ToString();
unlock_group_gold_text.text = jelly_goldlist[0].ToString();
lock_group_jelatin_text.text = jelly_jelatinlist[0].ToString();
data_manager = data_manager_obj.GetComponent<DataManager>();
page = 0;
jelly_unlock_list = new bool[12];
}
void Start()
{
Invoke("LoadData", 0.1f);
}
void Update()
{
if (Input.GetButtonDown("Cancel")) {
if (isJellyClick) ClickJellyBtn();
else if (isPlantClick) ClickPlantBtn();
else Option();
}
}
void LateUpdate()
{
jelatin_text.text = string.Format("{0:n0}", (int)Mathf.SmoothStep(float.Parse(jelatin_text.text), jelatin, 0.5f));
gold_text.text = string.Format("{0:n0}", (int)Mathf.SmoothStep(float.Parse(gold_text.text), gold, 0.5f));
}
public void ChangeAc(Animator anim, int level)
{
anim.runtimeAnimatorController = level_ac[level - 1];
}
public void GetJelatin(int id, int level)
{
jelatin += (id + 1) * level;
if (jelatin > max_jelatin)
jelatin = max_jelatin;
}
public void GetGold(int id, int level, Jelly jelly)
{
gold += jelly_goldlist[id] * level;
if (gold > max_gold)
gold = max_gold;
jelly_list.Remove(jelly);
}
public void CheckSell()
{
isSell = isSell == false;
}
public void ClickJellyBtn()
{
if (isPlantClick) {
plant_anim.SetTrigger("doHide");
isPlantClick = false;
isLive = true;
}
if (isJellyClick)
jelly_anim.SetTrigger("doHide");
else
jelly_anim.SetTrigger("doShow");
isJellyClick = !isJellyClick;
isLive = !isLive;
}
public void ClickPlantBtn()
{
if (isJellyClick) {
jelly_anim.SetTrigger("doHide");
isJellyClick = false;
isLive = true;
}
if (isPlantClick)
plant_anim.SetTrigger("doHide");
else
plant_anim.SetTrigger("doShow");
isPlantClick = !isPlantClick;
isLive = !isLive;
}
void Option()
{
isOption = !isOption;
isLive = !isLive;
option_panel.gameObject.SetActive(isOption);
Time.timeScale = isOption == true ? 0 : 1;
}
public void PageUp()
{
if (page >= 11) return;
++page;
ChangePage();
}
public void PageDown()
{
if (page <= 0) return;
--page;
ChangePage();
}
void ChangePage()
{
lock_group.gameObject.SetActive(!jelly_unlock_list[page]);
page_text.text = string.Format("#{0:00}", (page + 1));
if (lock_group.activeSelf) {
lock_group_jelly_img.sprite = jelly_spritelist[page];
lock_group_jelatin_text.text = string.Format("{0:n0}", jelly_jelatinlist[page]);
lock_group_jelly_img.SetNativeSize();
}
else {
unlock_group_jelly_img.sprite = jelly_spritelist[page];
unlock_group_name_text.text = jelly_namelist[page];
unlock_group_gold_text.text = string.Format("{0:n0}", jelly_goldlist[page]);
unlock_group_jelly_img.SetNativeSize();
}
}
public void Unlock()
{
if (jelatin < jelly_jelatinlist[page]) return;
jelly_unlock_list[page] = true;
ChangePage();
jelatin -= jelly_jelatinlist[page];
}
public void BuyJelly()
{
if (gold < jelly_goldlist[page]) return;
gold -= jelly_goldlist[page];
GameObject obj = Instantiate(prefab, new Vector3(0, 0, 0), Quaternion.identity);
Jelly jelly = obj.GetComponent<Jelly>();
obj.name = "Jelly " + page;
jelly.id = page;
jelly.sprite_renderer.sprite = jelly_spritelist[page];
jelly_list.Add(jelly);
}
void LoadData()
{
lock_group.gameObject.SetActive(!jelly_unlock_list[page]);
for (int i = 0; i < jelly_data_list.Count; ++i)
{
GameObject obj = Instantiate(prefab, jelly_data_list[i].pos, Quaternion.identity);
Jelly jelly = obj.GetComponent<Jelly>();
jelly.id = jelly_data_list[i].id;
jelly.level = jelly_data_list[i].level;
jelly.exp = jelly_data_list[i].exp;
jelly.sprite_renderer.sprite = jelly_spritelist[jelly.id];
jelly.anim.runtimeAnimatorController = level_ac[jelly.level - 1];
obj.name = "Jelly " + jelly.id;
jelly_list.Add(jelly);
}
}
void OnApplicationQuit()
{
data_manager.JsonSave();
}
}
DataManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
[System.Serializable]
public class SaveData
{
public int jelatin;
public int gold;
public bool[] jelly_unlock_list = new bool[12];
public List<Data> jelly_list = new List<Data>();
}
public class DataManager : MonoBehaviour
{
string path;
void Start()
{
path = Path.Combine(Application.dataPath, "database.json");
JsonLoad();
}
public void JsonLoad()
{
SaveData save_data = new SaveData();
if (!File.Exists(path)) {
GameManager.instance.jelatin = 0;
GameManager.instance.gold = 0;
JsonSave();
}
else {
string load_json = File.ReadAllText(path);
save_data = JsonUtility.FromJson<SaveData>(load_json);
if(save_data != null) {
for (int i = 0; i < save_data.jelly_list.Count; ++i)
GameManager.instance.jelly_data_list.Add(save_data.jelly_list[i]);
for (int i = 0; i < save_data.jelly_unlock_list.Length; ++i)
GameManager.instance.jelly_unlock_list[i] = save_data.jelly_unlock_list[i];
GameManager.instance.jelatin = save_data.jelatin;
GameManager.instance.gold = save_data.gold;
}
}
}
public void JsonSave()
{
SaveData save_data = new SaveData();
for (int i = 0; i < GameManager.instance.jelly_list.Count; ++i) {
Jelly jelly = GameManager.instance.jelly_list[i];
save_data.jelly_list.Add(new Data(jelly.gameObject.transform.position, jelly.id, jelly.level, jelly.exp));
}
for(int i = 0; i < GameManager.instance.jelly_unlock_list.Length; ++i)
save_data.jelly_unlock_list[i] = GameManager.instance.jelly_unlock_list[i];
save_data.jelatin = GameManager.instance.jelatin;
save_data.gold = GameManager.instance.gold;
string json = JsonUtility.ToJson(save_data, true);
File.WriteAllText(path, json);
}
}
Data.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Data
{
public int id;
public int level;
public float exp;
public Vector3 pos;
public Data(Vector3 pos, int id, int level, float exp)
{
this.pos = pos;
this.id = id;
this.level = level;
this.exp = exp;
}
}
'📌 etc > Unity 2D Raising Jelly Game' 카테고리의 다른 글
[Unity / C#] 2D 젤리 키우기 게임 - 9. 사운드와 옵션 시스템 (1) | 2022.03.28 |
---|---|
[Unity / C#] 2D 젤리 키우기 게임 - 8. 업그레이드 시스템 구현 (2) | 2022.03.27 |
[Unity / C#] 2D 젤리 키우기 게임 - 6. 해금 시스템 만들기 (0) | 2022.03.26 |
[Unity / C#] 2D 젤리 키우기 게임 - 5. UI 창 구축하기 (0) | 2022.03.23 |
[Unity / C#] 2D 젤리 키우기 게임 - 4. 젤리 판매 기능 구현 (0) | 2022.03.22 |