🤨Nakama?
Nakama는 소셜, 실시간 게임을 위한 서버다.
클라우드 서버가 아니고 오픈 소스로 다 구축된 서버 소스를 제공하고 내가 셀프 호스팅으로 서버를 구축하는 것이다. (밀키트 같은 느낌?)
서버 소스뿐만 아니라 Unity 클라이언트 패키지도 준비돼있다.
사용자 계정 관리, 친구, 그룹(길드 같은), 파티, 매치, 리더보드 등등 다양한 기능들이 구비돼있다.
아니 이런 말도안되는게 있다고?!
라는 생각에 공식 문서를 천천히 읽어보니
"오픈소스! 자유! 만세!" 라는 느낌의 오픈소스는 아니고 약간 마케팅적인 오픈소스이다.
위에 언급한 기능들은 모두 사용 가능한게 맞다 상업적 사용도 물론 가능하다
다만 사용자가 많아지면 분산 서버를 구성해야 할텐데
분산 클러스터 관리 기능은 엔터프라이즈 요금제를 사용해야한다
그래도 기본 기능만으로 연습 프로젝트는 물론 작은 프로젝트도 굴리기는 가능해 보이기에 문제는 전혀 아닌것 같다
일단 호기심이 가득 올라오니 시도해보자
☁️서버 구성
🖥️내 환경
인텔 n100 미니pc
우분투 22.04
16GB 메모리
문서에 따르면 구글 클라우드 기준으로 ` g1-smal`같은 작은 인스턴스에서도 실행 가능하지만
최소 `n1-standard-1`사양을 권장한다고 한다.
구성은 docker-compose로 간단하고 쉽게 가능하다
우선 원하는 위치에 도커 설정파일을 놓을 폴더를 만들고 `docker-compose.yml`파일을 만들자
mkdir nakama
cd nakama/
touch docker-compose.yml
`docker-compose.yml`파일을 채워넣는다 나는 이렇게 작성했다
튜토리얼에서 제공하는 설정에서 약간 수정했다
수정한 부분
- `expose` 부분 주석 처리 ➡️ `port`가 있어서 필요 없다
- `postgres` 호스트 포트 8080을 8081로 변경 ➡️ 나는 8080포트를 이미 다른 컨테이너가 사용중이라서 바꿨다
- `nakama: volumes:` 호스트 부분 작성 ➡️ 원본은 커스텀 하라고 비어있었다 나는 `./nakama/data`로 설정했다
version: '3'
services:
postgres:
container_name: postgres
image: postgres:12.2-alpine
environment:
- POSTGRES_DB=nakama
- POSTGRES_PASSWORD=localdb
volumes:
- data:/var/lib/postgresql/data
# expose:
# - "8080"
# - "5432"
ports:
- "5432:5432"
- "8081:8080"
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres", "-d", "nakama"]
interval: 3s
timeout: 3s
retries: 5
nakama:
container_name: nakama
image: registry.heroiclabs.com/heroiclabs/nakama:3.22.0
entrypoint:
- "/bin/sh"
- "-ecx"
- >
/nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama &&
exec /nakama/nakama --name nakama1 --database.address postgres:localdb@postgres:5432/nakama --logger.level DEBUG --session.token_expiry_sec 7200
restart: always
links:
- "postgres:db"
depends_on:
postgres:
condition: service_healthy
volumes:
- ./nakama/data:/nakama/data
# expose:
# - "7349"
# - "7350"
# - "7351"
ports:
- "7349:7349"
- "7350:7350"
- "7351:7351"
healthcheck:
test: ["CMD", "/nakama/nakama", "healthcheck"]
interval: 10s
timeout: 5s
retries: 5
volumes:
data:
이제 컨테이너를 실행시키자
나는 docker의 플러그인으로 compose를 설치해서 "-"를 안붙이지만 docker-compose를 설치했다면
`docker-compose up -d`로 쳐야한다
docker compose up -d
컨테이너가 올라갔으면 대시보드에 접속해보자
`<ip 주소>:7351`로 접속하면 대시보드에 접속할 수 있다
접속하면 로그인 화면이 반겨준다
기본 ID: `admin`
기본 PW: `password`
로그인을 하고 나면 드디어 대시보드가 나온다
그럼 일단 기본 구성은 끝이다!
🛠️유니티 클라이언트 구성
이제 유니티 클라이언트를 구성해보자
일단 유니티 빈 프로젝트를 만들어놓고 패키지를 임포트하자
🔗에셋스토어에서 받아도 되고, 🔗깃허브 저장소에서 받아도 된다
사실 이게 끝이다 클라이언트쪽은 더이상 해줄게 없다
✨로그인을 해보자
이제 로그인을 시도해보자
회원가입과 로그인이 따로 구분돼있진 않은것 같고 첫 로그인이 회원가입인 방식인듯 하다
우선 나는 원활한 테스트를 위해 다음 2개 패키지를 OpenUPM으로 추가 설치했다
// UniTask
openupm add com.cysharp.unitask
// NaughtyAttributes
openupm add com.dbrizov.naughtyattributes
테스트를 위한 `MonoBehaviour`스크립트를 하나 만들고 다음과 같이 코드를 작성한다
`host`변수의 값만 본인의 서버 ip로 바꿔주면 된다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Nakama;
using NaughtyAttributes;
using Cysharp.Threading.Tasks;
public class TryNakama : MonoBehaviour
{
const string scheme = "http";
const string host = "192.168.0.5"; // 서버ip
const int port = 7350;
const string serverKey = "defaultkey";
private Client activeClient;
private ISession activeSession;
private ISocket activeSocket;
[Button]
public async UniTaskVoid AuthByEmail()
{
activeClient = new Client(scheme, host, port, serverKey);
var email = "test@test.com";
var password = "1234Test";
activeSession = await activeClient.AuthenticateEmailAsync(email, password);
Debug.LogFormat("세션 생성됨: {0}", activeSession.ToString());
}
[Button]
public async UniTaskVoid AuthByDeviceId()
{
activeClient = new Client(scheme, host, port, serverKey);
// 로컬 저장된 디바이스ID가 있다면 가져오고 아니면 시스템에서 받아옴
var deviceId = PlayerPrefs.GetString("deviceId", SystemInfo.deviceUniqueIdentifier);
// 권한문제라던가 디바이스UID를 못받아왔다면 GUID하나 생성
if (deviceId == SystemInfo.unsupportedIdentifier)
{
deviceId = System.Guid.NewGuid().ToString();
}
// 로컬에 저장
PlayerPrefs.SetString("deviceId", deviceId);
// 디바이스 UID로 로그인 -> 사일런트 로그인 구현에 적합
activeSession = await activeClient.AuthenticateDeviceAsync(SystemInfo.deviceUniqueIdentifier);
Debug.LogFormat("세션 생성됨: {0}", activeSession.ToString());
}
[Button]
public async UniTaskVoid ConnectSocket()
{
activeSocket = Socket.From(activeClient);
activeSocket.Connected += () =>
{
Debug.Log("소켓 연결됨 !!");
};
activeSocket.Closed += () =>
{
Debug.Log("소켓 닫힘 !!");
};
activeSocket.ReceivedError += err =>
{
Debug.LogErrorFormat("소켓 에러: {0}", err);
};
await activeSocket.ConnectAsync(activeSession);
}
private void OnApplicationQuit()
{
// 로그아웃
if (activeSocket != null)
activeSocket.CloseAsync();
if (activeSession != null)
activeClient.SessionLogoutAsync(activeSession);
}
}
`AuthByEmail()`함수는 이메일과 pw로 로그인하는 방식이고
`AuthByDeviceId()`함수는 디바이스의 UID로 로그인하는 방식이다
후자는 사일런트 로그인 방식 구현에 유용할 것 같다
그냥 플레이를 종료하면 소켓과 세션이 알아서 끊어지지 않으니 `OnApplicationQuit()`에서 끊는 과정을 추가했다.
이거 말고도 구글, 애플 등 소셜 로그인도 지원하는것 같은데 복잡한 설정이 필요하니 지금은 기본적인 것만 시도해봤다.
이제 에디터에서 `AuthByEmail` or `AuthByDeviceId` ➡️ `ConnectSocket` 순서로 호출해보자
성공했다면 작성한 로그가 이렇게 출력될 것이다
그럼 이제 대시보드에서 확인해보자
먼저 대시보드 메인을 들어가보면 세션이 1로 올라가있을 것이다..! 접속됐다!
그리고 왼쪽에 ACCOUNTS탭에 들어가보면 가입된 유저를 확인할 수 있다
로그에 찍혔던것과 동일한 Username을 클릭해보면 상세 정보를 확인할 수 있다
AUTENTICATION탭에 들어가보면 이렇게 링크된 인증 정보를 확인할 수 있다
위의 테스트 코드에서 작성한 이메일인 `test@test.com`이 보인다
비밀번호는 admin권한으로 변경할 수는 있지만 현재 비밀번호를 확인할 수는 없다
🗨️채팅을 보내보자
로그인도 성공했으니 이제 채팅을 한번 보내보자
아까 작성한 테스트 코드를 이렇게 업데이트 한다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Nakama;
using NaughtyAttributes;
using Cysharp.Threading.Tasks;
using Nakama.TinyJson;
public class TryNakama : MonoBehaviour
{
const string scheme = "http";
const string host = "192.168.0.5"; // 서버ip
const int port = 7350;
const string serverKey = "defaultkey";
public string chatMsg;
private Client activeClient;
private ISession activeSession;
private ISocket activeSocket;
private IChannel activeChannel;
... 생략
[Button]
public async UniTaskVoid ConnectChat()
{
var roomName = "room";
activeChannel = await activeSocket.JoinChatAsync(roomName, ChannelType.Room, persistence: true);
Debug.LogFormat("채팅룸 참여함: {0}", activeChannel.Id);
}
[Button]
public async UniTaskVoid SendChat()
{
string channelId = activeChannel.Id;
var messageContent = new Dictionary<string, string>()
{
{ "message", chatMsg }
};
await activeSocket.WriteChatMessageAsync(activeChannel, JsonWriter.ToJson(messageContent));
Debug.Log("메세지 전송됨");
}
[Button]
public async UniTaskVoid GetChatList()
{
var limit = 100;
var forward = true;
var id = activeChannel.Id;
var result = await activeClient.ListChannelMessagesAsync(activeSession, id, limit, forward, cursor: null);
foreach (var message in result.Messages)
{
Debug.LogFormat("{0}:{1}", message.Username, message.Content);
}
}
private void OnApplicationQuit()
{
// 로그아웃
if (activeSocket != null)
activeSocket.CloseAsync();
if (activeSession != null)
activeClient.SessionLogoutAsync(activeSession);
}
}
채팅의 종류는 룸 채팅, 그룹 채팅, 귓속말을 지원한다
예시 코드는 룸 채팅으로 작성했다
`JoinChatAsync`함수의 `persistence`파라미터는 이 채팅 데이터를 저장할지 여부이다
이게 `false`라면 채팅 이벤트만 룸내 유저들에게 전송되고 채팅이 저장되진 않는것 같다
반면 `true`라면 채팅이 저장되고, 이전 채팅 기록을 받아올 수 있다.
이제 에디터로 돌아가서 함수들을 호출시켜보자
Auth ➡️ 소켓 연결 ➡️ Chat 룸 연결 ➡️ Send Chat 순으로 호출한다
챗을 보냈다면 이제 대시보드에서 또 확인해보자
대시보드 왼쪽에 CHAT MESSAGES 탭에 들어간다
나는 룸이름을 `room`으로 지었으니 해당 이름으로 검색해보면 채팅 메세지가 검색된다
참고로 `persistence`파라미터가 `false`였다면 저장되지 않으니 여기서도 검색되지 않는다.
다시 에디터로 돌아가서 마지막으로 기록된 채팅 리스트를 읽어와보자 `GetChatList`함수를 호출한다.
그러면 보냈던 채팅 메세지들이 로그로 찍힐것이다 여러번 보냈다면 여러번 찍힐것이다.
🔚마치며
게임서버 밀키트같은 이런게 없을까 생각해본적은 있었지만 진짜 있을줄은 몰랐다
다만 직접 사용해보니(찍먹 수준이지만) 1인 개발자이거나 아주 소규모 팀이라면 클라우드 게임 서버 솔루션이 더 나을거 같긴하다 직접 관리함으로써 장점도 있지만 직접 관리 한다는건 단점이기도 하다
그리고 분산 서버 기능을 제공하는 엔터프라이즈 요금제는 얼마인지 궁금한데 공식 웹에 명시는 안해놨다 Contact us로만 돼있어서 엔터프라이즈를 쓴다면 가격적인 메리트가 있는지도 미지수이긴하다
그래도 이런 선택지가 존재한다는건 굉장히 좋은것 같다.
'Unity > Network' 카테고리의 다른 글
[Unity] HTTP 통신 모듈 빌더패턴으로 재구성 (0) | 2023.03.08 |
---|---|
[Unity] HTTP REST api 통신 모듈 구현 (8) | 2023.02.14 |
[Unity] TCP서버 위치 동기화 데드 레커닝 구현 (0) | 2023.01.09 |