MVP 패턴에 대해서 모르신다면 여기를 참고하세요.
경험치 서비스를 설명하기에 앞서 유저 데이터 시스템의 다이어그램을 먼저 살펴보자.
유저 데이터 시스템은 MVP 구조로 설계했으며, 서비스와 UI를 분리하기로 한다.

RPG 장르를 포함한 거의 모든 게임에서는 레벨이라는 요소가 필수적으로 사용된다. 그리고 이 레벨가 뗄레야 뗄 수 없는 요소가 바로 경험치다. 경험치가 있어야 레벨이 있으니
스테이터스 UI를 완성하기 위해서는 먼저 필요한 요소들을 정리해야 한다. 게임마다 프로그래머마다 다르겠지만 나는 레벨, 경험치, 체력, 마나 상태만을 포함하기로 결정했다.
체력과 마나를 제외하여 말하면 스테이터스 UI에 나타날 레벨과 경험치를 경험치 서비스에서 제공한다.
비교의 대상이 되는 총 경험치는 게임 도중에 변경되는 일이 없어야 한다. → 읽기 전용
총 경험치와 현재 경험치를 같이 관리하기 보단 따로 관리하는 편이 좋다. → 책임 분리
우선 확장하기 쉽도록 총 경험치에 대한 서비스의 인터페이스를 정의한다.
namespace EXPService
{
public interface IEXPService
{
public int GetEXP(int current_level);
public void Load();
}
}
| 메서드 | 설명 |
|---|---|
GetEXP(int current_level) | 현재 레벨을 입력하면 그 다음 레벨이 되기 위한 총 경험치를 반환한다. |
Load() | 전체 경험치 데이터를 모두 로드한다. |
그리고 이 인터페이스를 토대로 로컬에서 동작하는 경험치 서비스를 제작해야 한다.
경험치를 로컬에 저장하는 방법은 다양하지만, 나는 Json을 StreamingAssets 폴더 하위에 저장하고 이를 로드하는 방식을 선택했다.
단순히 기본 자료형이기 때문에 Playerprefs를 사용해도 무방하지만, 추후에 소개될 다른 서비스에서는 Playerprefs로 저장할 수 없는 자료형들이 포함되기 때문에 일관성을 유지하기 위해서 모든 로컬 데이터는 Json으로 관리하도록 통일했다.
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace EXPService
{
// 직렬화를 위한 데이터의 구조
#region Serialization
[System.Serializable]
public struct EXPData
{
public int Level; // 레벨과
public int EXP; // 레벨에 해당하는 총 경험치를 묶는다.
}
[System.Serializable]
public class DataWrapper
{
public EXPData[] List; // Json은 배열을 읽고 쓰는 데 어려움이 있으므로
} // 한번 더 데이터를 래핑하는 과정을 거친다.
#endregion Serialization
public class LocalEXPService : IEXPService
{
private Dictionary<int, int> m_exp_dict = new();
public LocalEXPService()
{
Load();
}
// StreamingAssets 폴더 하위에서 EXPData.json을 읽어 파싱하여 딕셔너리에 저장한다.
public void Load()
{
var local_data_path = Path.Combine(Application.streamingAssetsPath, "EXPData.json");
if (File.Exists(local_data_path))
{
var json_data = File.ReadAllText(local_data_path);
var wrapped_data = JsonUtility.FromJson<DataWrapper>(json_data);
foreach (var exp_data in wrapped_data.List)
{
m_exp_dict.TryAdd(exp_data.Level, exp_data.EXP);
}
#if UNITY_EDITOR
Debug.Log("<color=cyan>성공적으로 EXP 데이터를 로드하였습니다.</color>");
#endif
}
else
{
// 존재하지 않는다면, 정상적인 게임이 불가능하므로 강제 종료한다.
#if UNITY_EDITOR
Debug.LogError($"{local_data_path}가 존재하지 않습니다.");
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
}
// 딕셔너리를 활용하여 O(1)의 속도로 다음 레벨이 되기 위한 총 경험치를 반환한다.
public int GetEXP(int current_level)
{
return m_exp_dict.TryGetValue(current_level + 1, out var exp) ? exp : 0;
}
}
}
서비스 등록의 시점은 다를 수도 있다. 나는 Title Bootstrapper에 이 책임을 물었다.
로그인 씬에서 이 데이터를 로드하는 것이 자연스럽다고 생각했기 때문이다.
// ...
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());
// 이전 글과 동일
}
public static void Register<T>(T service)
{
// 이전 글과 동일
}
public static T Get<T>()
{
// 이전 글과 동일
}
}
이제 이 경험치 서비스는 Title Bootstrapper를 통해서 자동으로 로드되기 때문에 경험치 서비스에 접근하여 GetEXP()를 통해 총 경험치 데이터를 받을 수 있다.
다음 글에서는 이 총 경험치와 현재 경험치를 비교하여 스테이터스 UI에서 경험치를 표시하도록 기반을 다져볼 것이다.
글이 기엽고 글쓴이는 더 기엽네요 !