개요
모바일 방치 게임을 개발하면서 파일 저장 기능을 필수적으로 구현해야 했다. 유니티 에디터에서는 분명히 정상적으로 동작했으나, 안드로이드 빌드 파일을 실행했을 때 저장 기능이 제대로 작동하지 않는 문제가 발생했다. 이 문제를 해결하기 위해 일주일 동안 ChatGPT와 구글링을 활용하며 다양한 시도를 해보았다. 덕분에 해결은 했지만 나와 비슷한 상황을 겪은 사람이 있을 수도 있을 것 같아 경험을 정리해본다.
1. 에디터, 안드로이드 차이점
에디터에서는 Json 형식으로 데이터를 저장하고, 불러오는 코드가 동작했지만, 안드로이드에서는 작동하지 않은 이유가 뭘까. 우선 작성한 코드를 살펴보자
private void Awake()
{
filePath = Path.Combine(Application.dataPath, "database.json");
}
// 데이터 불러오기
public void JsonLoad()
{
// 파일 존재 여부 확인
if (!File.Exists(filePath))
{
JsonSave();
return;
}
// 주어진 파일경로의 데이터를 읽어옴
string dataAsJson = File.ReadAllText(filePath);
GameData gameData = new GameData();
gameData = JsonUtility.FromJson<GameData>(dataAsJson);
// 데이터를 정상적으로 불러오지 못 한 경우 return
if (gameData == null)
return;
MainManager.instance.gameInfo = gameData.gameInfo;
}
// 데이터 저장하기
public void JsonSave()
{
GameData gameData = new GameData();
string json = JsonUtility.ToJson(gameData, true);
File.WriteAllText(filePath, json);
}
먼저 Application.dataPath와 저장될 파일의 이름을 합쳐서 filePath에 저장하였다. 게임이 실행될 때 JsonLoad를 호출해서 파일을 불러오도록 했고, 만약 파일이 없다면 새로운 데이터파일을 저장하도록 했다.
문제는 Application.dataPath였다. 모바일 환경에서는 dataPath가 아닌 persistentDataPath로 지정해주어야 한다. dataPath는 Unity 프로젝트 내의 Assets 폴더에서 데이터 파일을 읽을 때 사용하고, persistentDataPath는 앱의 개별 저장 공간에 해당하기때문에 플랫폼 환경에 따라 반드시 구분해서 사용해야한다.
수정된 코드를 아래에서 살펴보자.
2. 동기적 코드 예시
private void Awake()
{
#if UNITY_EDITOR
filePath = Path.Combine(Application.dataPath, "database.json");
#elif UNITY_ANDROID || UNITY_IOS
filePath = Path.Combine(Application.persistentDataPath, "database.json");
}
// 데이터 불러오기
public void JsonLoad()
{
// 파일 존재 여부 확인
if (!File.Exists(filePath))
{
JsonSave();
return;
}
// 주어진 파일경로의 데이터를 읽어옴
string dataAsJson = File.ReadAllText(filePath);
GameData gameData = new GameData();
gameData = JsonUtility.FromJson<GameData>(dataAsJson);
// 데이터를 정상적으로 불러오지 못 한 경우 return
if (gameData == null)
return;
MainManager.instance.gameInfo = gameData.gameInfo;
}
// 데이터 저장하기
public void JsonSave()
{
GameData gameData = new GameData();
string json = JsonUtility.ToJson(gameData, true);
File.WriteAllText(filePath, json);
}
#if 전처리 조건문으로 플랫폼 환경에 따라 파일 경로를 다르게 지정하도록 구현했다.
위의 코드는 동기적 저장 시스템이다. 구현이 간단하고, 코드가 순차적으로 실행돼서 직관적이라는 장점이 있지만, 파일의 용량이 큰 경우 게임이 멈추거나 느려지는 현상이 발생할 수 있다. 이런 문제로 인해 대부분 저장과 관련해서는 비동기적 접근을 활용한다. 저장 시스템을 급하게 구현해야하는 상황이거나, 파일의 용량이 크지 않은 게임의 경우에는 동기적으로 접근하는 것이 좋을수도 있으니(코드의 직관성, 디버깅 용이) 무작정 비동기적으로 구현하는 태도는 지양하기를 바란다.
3. 비동기적 코드 예시
private void Awake()
{
#if UNITY_EDITOR
filePath = Path.Combine(Application.dataPath, "database.json");
#elif UNITY_ANDROID || UNITY_IOS
filePath = Path.Combine(Application.persistentDataPath, "database.json");
}
// 데이터 불러오기
public async Task JsonLoad()
{
// 파일 존재 여부 확인
if (!File.Exists(filePath))
{
await JsonSaveAsync(); // 파일이 없으면 저장
return;
}
long totalBytes = new FileInfo(filePath).Length; // 파일 크기 (바이트 단위)
long bytesRead = 0; // 읽은 바이트 수
string dataAsJson = ""; // 파일 내용을 담을 변수
// 비동기적으로 파일을 읽으면서 진행 상황 표시
using (StreamReader reader = new StreamReader(filePath))
{
char[] buffer = new char[1024]; // 읽을 버퍼
int bytesReadInChunk; // 한 번에 읽은 데이터 크기
while ((bytesReadInChunk = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// 읽은 데이터를 추가
dataAsJson += new string(buffer, 0, bytesReadInChunk);
bytesRead += bytesReadInChunk;
// 진행 상황 계산
float progress = (float)bytesRead / totalBytes;
Debug.Log(progress * 100 + "%");
}
}
GameData gameData = new GameData();
gameData = JsonUtility.FromJson<GameData>(dataAsJson);
// 데이터를 정상적으로 불러오지 못 한 경우 return
if (gameData == null)
return;
MainManager.instance.gameInfo = gameData.gameInfo;
}
// 데이터 저장하기
public async Task JsonSave()
{
GameData gameData = new GameData();
string json = JsonUtility.ToJson(gameData, true);
// 비동기적으로 파일에 저장
await Task.Run(() => File.WriteAllText(filePath, json));
}
딱 봐도 동기적으로 구현했을 때에 비해 코드가 복잡해진 것을 알 수 있다. 모바일 환경에서 게임을 실행하면 로딩창이 뜨며, 이때 로딩바를 통해 현재 몇 %만큼 데이터가 로드되었는지 알 수 있다. 이때 게임화면은 멈추지 않고 재생되는 상태이며, 동시에 로딩도 진행될 수 있는 것은 데이터 로드 과정이 비동기적으로 구현되었기 때문이다.
우선, 로딩바 기능을 구현하려면 데이터를 한 번에 읽어오는 것이 아닌 부분적으로 읽어와야 한다. 이를 위해 1024 크기의 버퍼를 설정한다. 크기를 512로 설정할 경우 데이터 블록을 읽어오는 빈도가 잦아지고, 2048로 설정할 경우 버퍼의 메모리 크기가 커지므로 일반적으로는 1024가 적당하다. 만약 대규모 프로젝트에서 데이터를 불러오는 시간이 오래 걸릴 경우, 2048로 설정하는 것을 고려할 수 있다.
다음으로, ReadAsync를 통해 데이터를 읽어올 버퍼와 시작 범위, 끝 범위를 지정하여 데이터를 읽어올 때까지 반복한다. 이후의 과정은 동기적 구현과 동일하다.
데이터를 저장하는 기능은 동기적 저장과 크게 다르지 않다. 다만, 백그라운드 쓰레드에서 저장이 이루어질 수 있도록 Task.Run() 함수를 활용했다.
느낀 점
지금까지 비동기 기능을 활용해본 경험이 없었지만, 저장과 로드를 구현하면서 얕은 지식이라도 얻을 수 있었다. 이를 통해 비동기와 동기의 차이점과 각각의 용도를 알 수 있었으며, 비동기적 구현을 할 때에는 로그가 굉장히 중요하다는 것을 느꼈다. 말 그대로 비동기이기 때문에 예상치 못한 버그가 발생할 가능성이 크기에 이 점을 꼭 유의해서 코드를 작성해야겠다.
배울수록 끝이 더 멀어지는 것 같은 느낌이지만, 학습의 과정은 언제나 너무 즐겁다. 오늘도 저장과 로드 기능 덕분에 지식이 늘었다. 아직 부족한 점이 많으니 더 열심히 해야겠다!
'유니티' 카테고리의 다른 글
[유니티] TextAsset으로 엑셀파일 불러오기 (2) | 2024.12.22 |
---|