데이터 매니저는 데이터를 관리하고 저장하는 대표적인 시스템입니다. 만약 이것이 매니저가 없다면 재화를 획득했어도 게임을 껐다가 키면 획득한 모든 재화가 날아가게 됩니다. 데이터를 저장하는 것은 매우 중요하고 필수적인 시스템 이기에 설계가 아주 중요합니다. 이번 시간은 데이터 매니저가 무엇인지 알아보고 직접 구현해 보면서, 어디에 사용되는지 알아보겠습니다.


[데이터 매니저란 무엇인가?, 개념]

보통 유니티 또는 언리얼 등 게임 엔진에서 사용되는 언어들이 있기 마련입니다. 이런 언어들로 게임을 만들게 되면 데이터와 코드가 서로 상호교환하면서 플레이하게 됩니다. 데이터는 다른형식으로 저장이 가능합니다. 보통 문자열로 저장하게 됩니다. 또한 저장된 문자열을 불러오거나 서버로 보내주기도 합니다. 이때 불러오거나, 저장하거나, 접근하게 해주는 등 전체적인 관리를 해주는 것을 데이터 매니저라 합니다.

[어떤 요소를 구현 해볼까?]

  • 데이터 저장 및 불러오기
  • 데이터에 접근하여 사용하기

위 두가지를 중점적으로 구현해 보겠습니다. 저장 방식은 json으로 저장을 하고 불러오는 방식을 사용 하겠습니다.

[코드 작성]

[데이터 매니저]

using UnityEngine;
using System.IO;

public class DataManager : MonoBehaviour
{
    private static DataManager instance;

    private string dataPath;

    private PlayerData playerData;
    private SkillData skillData;
    private CurrencyData currencyData;
    private QuestData[] questDatas; // 모든 퀘스트 데이터 배열

    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<DataManager>();
                if (instance == null)
                {
                    GameObject obj = new GameObject("DataManager");
                    instance = obj.AddComponent<DataManager>();
                }
            }
            return instance;
        }
    }

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }

        dataPath = Path.Combine(Application.persistentDataPath, "data.json");

        if (!File.Exists(dataPath))
        {
            InitializeData();
        }
        else
        {
            LoadCachedData();
        }
    }

    private void InitializeData()
    {
        playerData = new PlayerData("Player1", 1);
        SaveData(playerData);

        skillData = new SkillData("Fireball", 1);
        SaveData(skillData);

        currencyData = new CurrencyData("Gold", 0);
        SaveData(currencyData);

        InitializeQuests();
    }

    private void LoadCachedData()
    {
        playerData = LoadData<PlayerData>("PlayerData");
        skillData = LoadData<SkillData>("SkillData");
        currencyData = LoadData<CurrencyData>("CurrencyData");
        LoadQuests();
    }

    private void InitializeQuests()
    {
        questDatas = new QuestData[EnumUtils.GetEnumLength<QuestManager.QuestType>()];
        for (int i = 0; i < questDatas.Length; i++)
        {
            questDatas[i] = new QuestData("Quest" + i.ToString(), false, QuestManager.QuestType.KillMonsters, 0);
            SaveData(questDatas[i], "Quest" + i.ToString());
        }
    }

    private void LoadQuests()
    {
        questDatas = new QuestData[EnumUtils.GetEnumLength<QuestManager.QuestType>()];
        for (int i = 0; i < questDatas.Length; i++)
        {
            questDatas[i] = LoadData<QuestData>("Quest" + i.ToString());
        }
    }

    public void SaveData<T>(T data, string fileName = "")
    {
        string jsonData = JsonUtility.ToJson(data);
        string filePath = string.IsNullOrEmpty(fileName) ? dataPath : Path.Combine(Application.persistentDataPath, fileName + ".json");
        File.WriteAllText(filePath, jsonData);

        // 캐시된 데이터 업데이트
        if (typeof(T) == typeof(PlayerData))
        {
            playerData = data as PlayerData;
        }
        else if (typeof(T) == typeof(SkillData))
        {
            skillData = data as SkillData;
        }
        else if (typeof(T) == typeof(CurrencyData))
        {
            currencyData = data as CurrencyData;
        }
        else if (typeof(T) == typeof(QuestData))
        {
            int questIndex = int.Parse(fileName.Replace("Quest", ""));
            questDatas[questIndex] = data as QuestData;
        }
    }

    public T LoadData<T>(string fileName = "")
    {
        string filePath = string.IsNullOrEmpty(fileName) ? dataPath : Path.Combine(Application.persistentDataPath, fileName + ".json");
        if (File.Exists(filePath))
        {
            string jsonData = File.ReadAllText(filePath);
            return JsonUtility.FromJson<T>(jsonData);
        }
        else
        {
            Debug.LogError("No data found!");
            return default(T);
        }
    }

    // 각 데이터에 대한 접근자 메서드 추가
    public PlayerData GetPlayerData()
    {
        return playerData;
    }

    public SkillData GetSkillData()
    {
        return skillData;
    }

    public CurrencyData GetCurrencyData()
    {
        return currencyData;
    }

    public QuestData GetQuestData(QuestManager.QuestType questType)
    {
        int questIndex = (int)questType;
        return questDatas[questIndex];
    }
}

데이터 매니저를 접근하기 쉽도록 싱글톤으로 만들고, 해당 데이터가 존재하지 않는다면 데이터를 초기화 해줍니다. 만약 데이터가 있다면 json 데이터를 가져와서 각 클래스에 넣어 줍니다. 그리고 SaveData를 작성 해주는데 어떤 데이터든 저장이 쉽도록 제네릭 클래스로 만들어 줬습니다. 마지막으로 데이터를 어디서든 접근할 수 있도록 Get메서드를 만듭니다.

[플레이어 레벨업 사용 예시]

using UnityEngine;

public class PlayerManager : MonoBehaviour
{
    private PlayerData playerData;

    private void Start()
    {
        playerData = DataManager.Instance.GetPlayerData();
        Debug.Log("Player Name: " + playerData.playerName + ", Level: " + playerData.playerLevel);

        // 몬스터 처치 이벤트가 발생하면 플레이어 경험치를 증가시키는 함수를 호출
        MonsterManager.MonsterKilled += IncreasePlayerExperience;
    }

    private void OnDestroy()
    {
        // 이벤트 리스너 제거
        MonsterManager.MonsterKilled -= IncreasePlayerExperience;
    }

    private void IncreasePlayerExperience()
    {
        // 경험치 증가
        playerData.experience += 10;
        Debug.Log("Player experience increased! Current experience: " + playerData.experience);

        // 경험치가 100 이상이면 레벨업
        if (playerData.experience >= 100)
        {
            LevelUpPlayer();
        }
    }

    private void LevelUpPlayer()
    {
        playerData.playerLevel++;
        playerData.experience = 0; // 레벨업 후 경험치 초기화
        Debug.Log("Player leveled up! New Level: " + playerData.playerLevel);

        // 플레이어 데이터 저장
        DataManager.Instance.SaveData(playerData);
    }
}

플레이어 매니저가 몬스터 처치 이벤트를 받아 이벤트가 발생하면 레벨업을 시킨 뒤 저장해 주는 코드 입니다.(이벤트 시스템을 자세히 알고 싶으시다면 링크를 클릭해 주세요.)

using UnityEngine;

public class MonsterManager : MonoBehaviour
{
    // 몬스터 처치 이벤트를 선언
    public delegate void MonsterKillEvent();
    public static event MonsterKillEvent MonsterKilled;

    private void Start()
    {
        // 몬스터가 처치될 때마다 이벤트 발생
        KillMonster();
    }

    private void KillMonster()
    {
        // 몬스터 처치 작업 수행

        // 몬스터가 처치되면 이벤트 발생
        MonsterKilled?.Invoke();
    }
}

몬스터가 처치 될때 마다 몬스터 클래스에서 KillMonster 메서드를 실행시켜 주시면 됩니다.

[스킬 강화 사용 예시]

using UnityEngine;

public class SkillManager : MonoBehaviour
{
    private SkillData skillData;
    private CurrencyData currencyData;

    private void Start()
    {
        // 스킬 데이터 로드
        skillData = DataManager.Instance.GetSkillData();
        Debug.Log("Skill Name: " + skillData.skillName + ", Level: " + skillData.skillLevel);

        // 재화 데이터 로드
        currencyData = DataManager.Instance.GetCurrencyData();
        Debug.Log("Gold: " + currencyData.amount);

        // 스킬 강화 시도
        UpgradeSkill();
    }

    private void UpgradeSkill()
    {
        // 강화에 필요한 골드량
        int upgradeCost = 50;

        // 재화가 충분한지 확인
        if (currencyData.amount >= upgradeCost)
        {
            // 강화 후 재화 감소
            skillData.skillLevel++;
            currencyData.amount -= upgradeCost;

            // 데이터 저장
            DataManager.Instance.SaveData(skillData);
            DataManager.Instance.SaveData(currencyData);

            Debug.Log("Skill upgraded! New Level: " + skillData.skillLevel);
            Debug.Log("Gold spent for upgrade: " + upgradeCost);
            Debug.Log("Remaining Gold: " + currencyData.amount);
        }
        else
        {
            Debug.Log("Not enough gold to upgrade skill!");
        }
    }
}

재화 데이터를 가져와서 재화가 충분한지 확인한 후에 스킬을 강화 시켜주는 메서드 입니다.

[몬스터로 부터 재화를 획득하는 예시 코드]

using UnityEngine;

public class MonsterManager : MonoBehaviour
{
    private CurrencyData currencyData;

    private void Start()
    {
        currencyData = DataManager.Instance.GetCurrencyData();
        Debug.Log("Gold: " + currencyData.amount);

        KillMonster();
    }

    private void KillMonster()
    {
        int goldGained = 50; // 몬스터를 죽여서 획득한 골드
        currencyData.amount += goldGained;
        DataManager.Instance.SaveData(currencyData);
        Debug.Log("Gold gained from monster! New amount: " + currencyData.amount);
    }
}

몬스터를 처치 시 골드를 더해주고, 저장해 주는 코드 입니다.

[몬스터를 처치 퀘스트 완료 예시 코드]

using UnityEngine;

public class QuestManager : MonoBehaviour
{
    private CurrencyData currencyData;

    private void Start()
    {
        // 퀘스트 데이터는 더 이상 직접 로드하지 않음
        // DataManager에서 Get 함수로 가져옴

        // 몬스터 처치 이벤트 등록
        MonsterManager.MonsterKilled += CheckQuestCompletion;
    }

    private void OnDestroy()
    {
        // 이벤트 리스너 제거
        MonsterManager.MonsterKilled -= CheckQuestCompletion;
    }

    private void CheckQuestCompletion()
    {
        // 퀘스트 데이터 가져오기
        QuestData questData = DataManager.Instance.GetQuestData(QuestType.KillMonsters);

        // 퀘스트가 이미 완료된 경우 종료
        if (questData.completed)
        {
            return;
        }

        // 플레이어가 처치한 몬스터 수 증가
        questData.monstersKilled++;

        // 50마리를 처치한 경우 퀘스트 완료
        if (questData.monstersKilled >= 50)
        {
            CompleteQuest(questData);
        }
    }

    private void CompleteQuest(QuestData questData)
    {
        // 퀘스트 완료 처리
        questData.completed = true;

        // 보상 추가
        currencyData = DataManager.Instance.GetCurrencyData();
        currencyData.amount += 100; // 퀘스트 완료 보상

        // 데이터 저장
        DataManager.Instance.SaveData(questData.questType.ToString(), questData);
        DataManager.Instance.SaveData(currencyData);

        Debug.Log("Quest completed! Reward received.");
    }
}

        currencyData = DataManager.Instance.GetCurrencyData();
        currencyData.amount += 100; // 퀘스트 완료 보상

        // 데이터 저장
        DataManager.Instance.SaveData(questData.questType.ToString(), questData);
        DataManager.Instance.SaveData(currencyData);

        Debug.Log("Quest completed! Reward received.");
    }
}

몬스터 처치 이벤트가 발생하면 몬스터 처치관련 퀘스트 데이터를 가져와서 처치 수를 1 증가시키고 완료 됐는지 확인 후 보상을 지급해주는 예시코드 입니다.

[데이터 매니저 장점]

  • 데이터 매니저가 생기면서 데이터를 기반으로한 코드 설계가 가능해 지기에 확장성과 유연성이 증가 합니다.
  • 여러 종류의 데이터를 불러올 수 있기에 테스트용 데이터를 불러오는게 가능해지기에 테스트가 용이해 집니다.
  • 저장하거나 불러오는 로직을 통일하여 사용하기 때문에 버그 발생율이 현저하게 낮아 집니다.

[마무리 글]

데이터 매니저는 게임에서 빠져서는 안될 중요한 요소입니다. 그렇기에 프로젝트 시작 초기에 이를 만들고 데이터를 기반으로 퀘스트나 플레이어, 몬스터, 스킬, 재화 등을 구현해야 합니다. 만약 그 반대로 하게 되면 각 콘텐츠의 구조를 크게 변경해야 하기 때문에 많은 시간이 들어가는 경우가 많습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다