학기 중 진행했던 게임 해커톤에 Firebase RealtimeDB를 사용해서 랭킹 시스템을 구현해보았다.
사실 해커톤 중에 시간이 남아서 구현을 했었고 완성을 했는데... 빌드 환경을 고려하지 못해서 실패하였다.
Unity 2021.3.45f1
플랫폼: PC(WebGL)
개발 관련 포스팅 할 때 항상 환경부터 써놓고 시작하는데, 이번에도 이게 가장 문제였다.
WebGL 빌드!!!!!
요즘 사람들은 다운로드를 잘 안한다. (특히 PC게임은...)
해커톤에서 만든 게임을 그래도 유저에게 노출시키려면 WebGL 빌드가 최선인 것 같다.
해커톤 당시, 피곤함과 촉박함 때문에 문서를 제대로 읽지 못한 것이 화를 불러왔다.
iOS, tvOS, Android...
어디에도 web을 지원한다는 말이 없다.
당시 WebGL 환경에서 Firebase를 통합한 아주 자세한 블로그 글을 발견해서, 아무 생각 없이 그대로 진행한 것이다.
구현을 마치고 빌드를 하니 당연히 실패가 떴다. (애초에 Firebase SDK는 웹 빌드가 안되니 당연한 것...)
다시 제대로 읽어보니 참고한 블로그에서도 js코드를 직접 써서 우회 작업을 해주고 있었다.
결국 해커톤은 랭킹 없이 끝나버렸다
해커톤이 끝나고 다시 생각해봤는데, 애시당초 SDK를 꼭 쓸 필요가 없다.
우리에겐 REST API가 있으니까...!
Unity에서 web request를 보내려면 여러가지 방법이 있는데, 나는 UnityWebRequest 패키지를 사용했다.
기본적으로 포함되어 있기 때문에 따로 설치할 필요는 없다.
기본적으로 코루틴 방식으로 되어 있는데, 우선 데이터를 받을 때도 그렇고 async/await 방식이 컨트롤이 쉬울 것 같아서 Async 방식으로 감싸주는 클래스를 새로 만들었다. (Thanks to GPT...)
using UnityEngine.Networking;
using System.Threading.Tasks;
public static class UnityWebRequestAsync
{
public static Task<UnityWebRequest> SendWebRequestAsync(UnityWebRequest request)
{
var tcs = new TaskCompletionSource<UnityWebRequest>();
request.SendWebRequest().completed += operation =>
{
if (request.result == UnityWebRequest.Result.Success)
{
tcs.SetResult(request);
}
else
{
tcs.SetException(new UnityWebRequestException(request));
}
};
return tcs.Task;
}
public class UnityWebRequestException : System.Exception
{
public UnityWebRequest Request { get; }
public UnityWebRequestException(UnityWebRequest request)
: base($"UnityWebRequest Error: {request.error}")
{
Request = request;
}
}
}
사용은 아래와 같이 하면 된다.
GET
UnityWebRequest request = UnityWebRequest.Get(url);
UnityWebRequest response = await UnityWebRequestAsync.SendWebRequestAsync(request);
POST
UnityWebRequest request = new UnityWebRequest(url, "POST");
byte[] bodyRaw = Encoding.UTF8.GetBytes(data.ToJson());
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
UnityWebRequest response = await UnityWebRequestAsync.SendWebRequestAsync(request);
POST는 GET보다 조금 복잡하게 짰는데, 이유는 application/json
타입을 보내기 위해서이다.
패키지 내부를 까보면 아는데, UnityWebRequest.Post(url, json)
방식을 사용하면 contentType이 application/x-www-form-urlencoded
로 되어 있다. 그래서 그냥 직접 작성해줬다.
여기서도 은근히 시간을 많이 잡아먹었다.
Firebase RealtimeDB는 Key-Value 방식인데, 유니티에서 기본으로 사용되는 JsonUtility가 Dictionary를 처리하지 못하기 때문이다.
다음은 랭킹을 시간순으로 size만큼 가져오는 쿼리이다.
UnityWebRequest request = UnityWebRequest.Get($"{GetURI()}&orderBy=\"time\"&limitToFirst={size}");
UnityWebRequest response = await UnityWebRequestAsync.SendWebRequestAsync(request);
응답을 response.downloadHandler.text
에서 받아올 수 있는데, JsonUtility로는 아무리 용을 써도 안된다.
직접 파싱하는 로직을 구현해도 되지만, JsonConvertor
를 사용하면 간단히 해결된다.
우선 샘플 데이터는 아래와 같다.
{
"ranking":{
"-OEt6lu9MkTkc35bEi6b":{
"name": "XXX",
"time": 3000,
}
}
}
이 랭킹에 해당하는 클래스를 짜주어야 한다.
[System.Serializable]
public class Ranking
{
public string name;
public int time;
public Ranking() { }
public string ToJson() // POST 요청에서 Json으로 만들 때 사용
{
return JsonUtility.ToJson(this);
}
}
이후에 파싱은 간단하다.
var result = JsonConvert.DeserializeObject<Dictionary<string, Ranking>>(response.downloadHandler.text);
Json을 dictionary로 받은 것이다. 이후에는 result.Values로 접근하면 랭킹 목록을 얻을 수 있다.
이 때, result 내부에서는 정렬된 결과를 받지 못하므로 한번 더 정렬을 거쳐서 내보내야 한다.
만약 제대로 된 응답이 오지 않고 400 Bad Request가 뜬다면, 쿼리가 맞는지 점검해보자.
Firebase 공식문서 - Filtering Data
우선, OrderBy는 다음 다섯개 중 하나와 반드시 같이 결합해서 사용해야 한다.
limitToFirst
limitToLast
startAt
endAt
equalTo
다 설정했는데도 제대로 된 응답이 오지 않는다면, 인덱스를 설정했는지 봐야 한다.
rule에서 해당 데이터에 ".indexOn"을 설정해주었는지 확인하자!
이게 중요한데, 요청이 다 끝나고 나면, 요청 객체를 dispose() 해주어야 한다. 안하면 Memory leak 경고가 뜸...!
request.dispose()
사실 이게 전부이다.
랭킹이라고 해봤자 랭킹을 올리고(POST), 현재 목록을 받아오기(GET)이 전부이지 않은가...
그러나, 이대로 두면 우선 Firebase에서 위험한 규칙이 있다고 하루에 한번씩 메일이 온다.
DB 주소만 알면 누구나 읽고 쓰기가 가능하기 때문이다. 그러니 약간의 인증을 추가해보자.
Firebase Authentication에서 지원하는 기능인데, 실제로 유저에게 로그인을 요구하진 않지만, 유저를 일시적으로 구분할 수 있는 익명 로그인이다.
콘솔에서 Authentication 추가해주고, Native provider > Anonymous 선택해주면 설정은 끝이다.
로그인도 간단하다.
string AUTH_URL = "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=";
UnityWebRequest request = new UnityWebRequest($"{AUTH_URL}{API_KEY}", "POST");
var body = new { returnSecureToken = true };
byte[] bodyRaw = Encoding.UTF8.GetBytes(JsonUtility.ToJson(body));
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
UnityWebRequest response = await UnityWebRequestAsync.SendWebRequestAsync(request);
userToken = JsonConvert.DeserializeObject<Dictionary<string, string>>(response.downloadHandler.text)["idToken"];
request.Dispose();
이렇게 idToken을 발급 받아서, 이후 요청 Url에 넣어주면 된다.
var url = "{DB_URL}?auth={userToken}"
이제 인증 관련해서 rule을 설정해주면 된다.
로그인 한 사용자는 전부 허용하려면, auth != null
을 추가해주면 된다.
{
"rules": {
"DB": {
"$variable":{
"ranking":{
".indexOn":["time", "name"],
".read": "auth != null",
".write": "auth != null",
}
}
}
}
}
$variable
은 임의값을 의미한다. 사실상 *
과 같은 뜻이라고 보면 된다.
즉, 위 규칙은 DB > * > ranking
에 대한 조건을 세팅해준 것이다.
게임 버전이나 밸런스 패치 등 랭킹 판을 바꿔줘야 할 일이 있을 것 같아 이와 같이 세팅했다.
위 규칙은 완벽히 안전한 것이 아니고, 최소한의 조치이니 적당히 참고해서 사용하면 될 듯하다!
참고로, 전역에다가 auth != null
을 설정해도 규칙이 안전하지 않다는 메일이 온다.
모든 유저가 읽기 쓰기 권한이 있다는 뜻이니 당연할지도...
{
"rules":{
".read": "auth != null",
".write": "auth != null",
}
}
유니티 + 파이어베이스
Unity firebase SDK 공식 문서
Firebase 데이터베이스 REST API
Firebase REST 요청 인증