[BalancingLibra] 유저데이터 구성하기
유저 데이터를 구성하는데 일단 유저의 재화 또는 세이브 데이터, 유저의 게임 내 세팅 데이터가 필요하다고 정리했다.
이 데이터들을 하나의 데이터 파일로만 구성해서 설정하는 것이 아닌, 각 데이터의 특성에 맞게 다르게 구현해보자.
1. IUserData
2. UserGoodsData
3. UserSettingData
4. 후기
1. IUserData
일단 IUserData는 데이터들의 기본 뼈대가 되는 인터페이스로 구성했다.
namespace BalancingLibra.Data
{
public interface IUserData
{
void SetDefaultData();
bool LoadData();
bool SaveData();
}
}
유저 데이터에 대한 인터페이스는 다음과 같이 설정했다. 데이터 가져오기, 데이터 저장하기, 가장 기본 데이터로 설정하기.
여기서 네임스페이스를 도입했다. 그 이유는 다른 플러그인을 사용했을 경우 이름이 충돌하는 것을 방지하기 위해서다. 그리고 코드가 커졌을 때 using 선언 하나만 봐도 스크립트가 어떤 네임스페이스의 코드를 가져다 쓰는지 파악 가능해 가독성이 증가하기 때문이다.
마지막으로 네임스페이스를 선언해야 IDE에서 다양한 클래스가 자동완성 목록에 뜨는 것을 방지할 수 있기 때문이다.
2. UserGoodsData
UserGoods데이터는 유저의 골드와 같은 게임 내 재화들을 관리하는 데이터이다. 코드는 다음과 같다.
using System.IO;
using UnityEngine;
namespace BalancingLibra.Data
{
[System.Serializable]
public class UserGoodsData : IUserData
{
[SerializeField] private long gold;
public long Gold { get => gold; set => gold = value; }
private string SavePath => Path.Combine(Application.persistentDataPath, "UserGoodsData.json");
public void SetDefaultData()
{
Logger.Log($"{nameof(UserGoodsData)} : SetDefaultData");
gold = 0;
}
public bool LoadData()
{
Logger.Log($"{nameof(UserGoodsData)} : LoadData");
if(!File.Exists(SavePath))
{
Logger.Log("Save file not found. Initializing default data.");
SetDefaultData();
return true;
}
try
{
string json = File.ReadAllText(SavePath);
JsonUtility.FromJsonOverwrite(json, this);
Logger.Log($"Gold Loaded : {Gold}");
return true;
}
catch (System.Exception e)
{
Logger.Log($"Load failed ({e.Message})");
return false;
}
}
public bool SaveData()
{
Logger.Log($"{nameof(UserGoodsData)} : SaveData");
try
{
string json = JsonUtility.ToJson(this, true);
File.WriteAllText(SavePath, json);
Logger.Log($"Gold Saved : {Gold}");
return true;
}
catch (System.Exception e)
{
Logger.Log($"Save failed ({e.Message})");
return false;
}
}
public void DeleteData()
{
if(File.Exists(SavePath))
{
File.Delete(SavePath);
Logger.Log($"{nameof(UserGoodsData)} : File Deleted");
}
SetDefaultData();
}
}
}
PlayerPrefs는 레지스트리(window)나 XML(mac)에 평문으로 저장되므로 유저가 임의로 고칠 수 있으므로 JsonUtility를 사용해 데이터를 객체 형태로 관리하게 하여 유저가 접근하기 어렵고, 확장성이 좋도록 처리하였다.
IUserData를 상속받으므로 BalancingLibra.Data로 감싸서 프로젝트의 구조를 명확하게 하였다.
또한 log에서 nameof를 사용했는데, GetType()의 경우 런타임에 타입을 확인해 적지만 성능 비용이 있고, 리플렉션 관련 오버헤드가 있었기 때문이다.
또한, 다른 컴포넌트의 인스펙터에서 디버깅을 편하게 하고 JsonUtility가 데이터를 읽을 수 있게 하기 위해 [System.Serializable]을 추가했다.
또한 SaveData에서 OS에 상관없이 OS의 안전한 폴더에 OS에 맞는 경로 표기법으로 파일을 저장하기 위해 Path.Combine(), Application.persistentDataPath를 추가했다.
3. UserSettingData
유저의 게임 내 설정값을 관리하는 데이터이다.
using UnityEngine;
namespace BalancingLibra.Data
{
public class UserSettingData : IUserData
{
private const string SOUND_KEY = "Setting_Sound";
public bool Sound { get; set; }
public void SetDefaultData()
{
Logger.Log($"{nameof(UserSettingData)} :: SetDefaultData");
Sound = true;
}
public bool LoadData()
{
Logger.Log($"{nameof(UserSettingData)} :: LoadData");
try
{
if (PlayerPrefs.HasKey(SOUND_KEY))
{
Sound = PlayerPrefs.GetInt(SOUND_KEY) == 1;
}
else
{
SetDefaultData();
}
Logger.Log($"Sound : {Sound}");
return true;
}
catch (System.Exception e)
{
Logger.Log($"Load failed ({e.Message})");
return false;
}
}
public bool SaveData()
{
Logger.Log($"{nameof(UserSettingData)} : SaveData");
try
{
PlayerPrefs.SetInt(SOUND_KEY, Sound ? 1 : 0);
// 명시적 저장 호출 (Unity 버전에 따라 자동 저장되나, 확실한 타이밍 제어 필요)
PlayerPrefs.Save();
Logger.Log($"Sound : {Sound}");
return true;
}
catch (System.Exception e)
{
Logger.Log($"Save failed ({e.Message})");
return false;
}
}
public void DeleteData()
{
if(PlayerPrefs.HasKey(SOUND_KEY))
{
PlayerPrefs.DeleteKey(SOUND_KEY);
PlayerPrefs.Save();
Logger.Log($"{nameof(UserSettingData)} : Key Deleted");
}
SetDefaultData();
}
}
}
해당 부분은 보안이 필요하지 않은 데이터이기 때문에 빠름과 효율성을 우선시해 JSON 대신 PlayerPrefs를 사용했다.
게임을 처음으로 킬 경우, 소리가 꺼져 있으면 강제로 키도록 설정한다.
직접 Save 함수를 호출해 디스크 기록을 강제해 확실하게 저장하도록 한다. 설정 변경은 빈번하게 일어나지 않기 때문이다.
설정 데이터는 빠르고 안전하게 하지만, 초기값 오류 없이 하기 위해 이렇게 진행했다.
그리고 위 세 코드를 테스트하기 위해 데이터 프로그래밍을 하고, 테스트 전용 코드를 만들었다.
4. 후기
각 데이터별로 특징이 다르고 필요한 조건이 다르므로 위와 같이 데이터 형태를 다르게 쓰는 방식은 처음이었는데 각 데이터들의 성격에 맞게 적용되어서 만족스럽다. 앞으로도 하나의 기능이어도 목적과 조건에 맞게 더 나은 기술 스택을 사용하는 방향으로 가면 좋을 것 같다.