저번 글에서는 스테이터스 UI에 필요한 요소인 총 경험치를 로드하고 제공하는 경험치 서비스를 구현했었다.
이번 글에서는 스테이터스 UI에 표시하기 위한 나머지 데이터들을 관리하고 제공하는 유저 서비스를 구현해볼 예정이다. 시작해보자.
우선 서비스에서 제공할 데이터를 먼저 생각해야 한다. 유저 데이터가 포함할 수 있는 정보에는 엄청나게 많은 정보가 있다. 예를 들면 닉네임, 레벨, 경험치, 체력, 마나, 인벤토리, 장비, 스킬 등등이다.
하지만 이를 모두 유저 서비스에서 관리하면 어디까지가 유저 서비스의 책임인지가 불분명해진다.
그래서 과감히 유저 서비스는 레벨, 경험치, 체력, 마나, 플레이 시간과 유저의 위치, 카메라의 위치까지만을 관리하기로 결정했다.
카메라의 위치는 일반적이진 않지만 내가 만든 게임에서는 카메라의 위치가 고정되는 형태라 데이터를 로드할 때 반드시 필요한 요소다. 하지만 유저 서비스와는 전혀 무관한 데이터다.

아무튼 이에 맞춰서 StatusData를 먼저 정의한다. StatusData는 플레이어의 레벨, 경험치, 체력, 마나를 보관하는 클래스다.
[System.Serializable]
public class StatusData
{
public int Level;
public int EXP;
public float HP;
public float MP;
public StatusData(int level = 1, int exp = 0, float hp = 500f, float mp = 300f)
{
Level = level;
EXP = exp;
HP = hp;
MP = mp;
}
}
그리고 StatusData를 멤버로 가지는 UserData를 정의한다. UserData는 StatusData를 포함하여 모든 플레이어와 관련된 정보를 보관하는 클래스다.
[System.Serializable]
public class UserData
{
public Vector3 Position;
public Vector3 Camera;
public float PlayTime;
public StatusData Status;
public UserData()
{
// 플레이어의 시작 위치와 카메라의 시작 위치를 하드 코딩했다. (무시해도 된다.)
Position = new Vector3(23f, -27f, 0f);
Camera = new Vector3(5.5f, -19.5f, -10f);
PlayTime = 0f;
Status = new StatusData();
}
public UserData(Vector3 position, Vector3 camera, float playtime, StatusData status)
{
Position = position;
Camera = camera;
PlayTime = playtime;
Status = status;
}
}
이제 위에서 정의한 UserData를 관리할 유저 서비스를 구현할 차례다. 모든 서비스와 마찬가지로 유저 서비스도 확장과 변경에 용이하도록 인터페이스를 구현한다.
using System;
using UnityEngine;
namespace UserService
{
public interface IUserService
{
Vector3 Position { get; set; }
Vector3 Camera { get; set; }
float PlayTime { get; set; }
StatusData Status { get; set; }
// 경험치 획득에 따른 이벤트 연결을 위한 델리게이트다.
event Action<int, int> OnUpdatedLevel;
void InitializeLevel();
void UpdateLevel(int exp);
}
}
| 속성 | 설명 |
|---|---|
Position | 플레이어의 위치를 저장하고 반환한다. |
Camera | 카메라의 위치를 저장하고 반환한다. |
PlayTime | 현재까지의 플레이타임을 저장하고 반환한다. |
Status | 플레이어의 기본 정보를 저장하고 반환한다. |
| 메서드 | 설명 |
|---|---|
InitializeLevel() | 레벨을 초기화한다. |
UpdateLevel(int exp) | 경험치를 exp만큼 획득한다. |
유저 서비스는 유저의 현재 정보를 저장하고 불러오는 Load()와 Save()도 가능해야 한다.
하지만 넓은 구조로 바라봤을 때 유저 서비스는 단순히 유저의 정보를 제공하는 서비스이지 Load()와 Save()를 하는 서비스가 아니다.
따라서 ISaveable이라는 인터페이스를 새롭게 정의하고 이를 유저 서비스가 구현하도록 한다.
// offset은 게임의 특성 상 필요하다. 반드시 필요한 요소가 절대 아니다.
public interface ISaveable
{
bool Load(int offset); // 데이터를 불러오는 데 실패한 경우, 예외 처리를 위해 bool을 반환한다.
void Save(int offset);
}
offset은 반드시 필요한 요소가 아니지만, 나는 로드 세이브를 RPG Maker 식으로 제작하고 싶었기 때문에 슬롯을 구별하기 위해 offset을 두었다.

using System;
using System.IO;
using UnityEngine;
using UserService;
public class LocalUserService : ISaveable, IUserService
{
private Vector3 m_position;
private Vector3 m_camera;
private float m_playtime;
private StatusData m_status;
public event Action<int, int> OnUpdatedLevel;
public Vector3 Position
{
get => m_position;
set => m_position = value;
}
public Vector3 Camera
{
get => m_camera;
set => m_camera = value;
}
public float PlayTime
{
get => m_playtime;
set => m_playtime = value;
}
public StatusData Status
{
get => m_status;
set => m_status = value;
}
// 처음부터 슬롯이 할당되지 않기 때문에 기본 정보로 세팅한다.
public LocalUserService()
{
var user_data = new UserData();
m_position = user_data.Position;
m_camera = user_data.Camera;
m_playtime = user_data.PlayTime;
m_status = user_data.Status;
CreateDirectory();
}
// 디렉터리 경로가 없는 경우에는 디렉터리 경로를 생성한다.
private void CreateDirectory()
{
var local_directory = Path.Combine(Application.persistentDataPath, "User");
if (!Directory.Exists(local_directory))
{
Directory.CreateDirectory(local_directory);
}
}
// 처음에는 경험치 획득이 없기 때문에 강제로 이벤트를 발생시킨다.
public void InitializeLevel()
{
OnUpdatedLevel?.Invoke(m_status.Level, m_status.EXP);
}
// 경험치 획득에 사용한다.
public void UpdateLevel(int exp)
{
m_status.EXP += exp;
OnUpdatedLevel?.Invoke(m_status.Level, m_status.EXP);
}
public bool Load(int offset)
{
var local_data_path = Path.Combine(Application.persistentDataPath, "User", $"UserData{offset}.json");
if (File.Exists(local_data_path))
{
var json_data = File.ReadAllText(local_data_path);
var user_data = JsonUtility.FromJson<UserData>(json_data);
m_position = user_data.Position;
m_camera = user_data.Camera;
m_playtime = user_data.PlayTime;
m_status = user_data.Status;
return true;
}
return false;
}
public void Save(int offset)
{
var local_data_path = Path.Combine(Application.persistentDataPath, "User", $"UserData{offset}.json");
var user_data = new UserData(m_position, m_camera, m_playtime, m_status);
var json_data = JsonUtility.ToJson(user_data, true);
File.WriteAllText(local_data_path, json_data);
}
}
이렇게 로컬 유저 서비스를 모두 구현했다면 이를 서비스 로케이터에 등록하면 된다.
// 이전과 동일
public static class ServiceLocator
{
private static Dictionary<Type, object> m_services = new();
public static IDictionary<Type, object> Services => m_services;
public static void Initialize()
{
Register<IEXPService>(new LocalEXPService());
Register<IUserService>(new LocalUserService());
// 이전과 동일
}
public static void Register<T>(T service)
{
// 이전과 동일
}
public static T Get<T>()
{
// 이전과 동일
}
}
아직까진 눈에 보이는 작업이 없어서 흥미가 없을 수도 있다고 생각한다. 다음 글부터는 UI를 제작하고 연결하는 작업까지니 끝까지 해보자.