Group Presence란?
Group Presence란 Meta 플랫폼이 유저의 Destination(장소)·Lobby/Match ID·Joinable 여부를 받아서 친구 목록과 퀘스트 메뉴에 노출하는 소셜 레이어 입니다.
- DestinationApiName: Lobby, Map(동화나라) 같은 장소 키. VRChat의 월드 URL 같은 역할을 합니다.
- LobbySessionID / MatchSessionID: 실제 방 고유값. 세션을 구분하는 키입니다.
- IsJoinable: true면 친구 초대 버튼이 뜹니다.
☕ Tip: Session ID는 64byte 이하. 더 길면 Presence 등록이 실패합니다.
딥링크(Deep-Link)란?
친구가 Group Presence Layer를 통해 "참여"버튼을 수락하면 Meta OS가 앱을 URI와 함께 기동(Cold Start)하거나, 실행 중인 앱에 Launch Intent 이벤트(Warm Start)를 보냅니다. (사실 뭔 말인지 모르겠음)
딥링크 UX 연구에 따르면, 사용자를 원하는 콘텐츠로 바로 연결할 때 전환율이 최대 40%까지 증가한다고 합니다.
활성화 방법
저는 UniTask를 활용하기 때문에 UniTask 포스팅을 보고 와주세요!
유니티 코루틴 대신 unitask
유니티에서 unitask를 사용하기 앞서, 유니티에서 사용하는 코루틴이 무엇인지 알아야 진정한 unitask의 장점을 알 수 있습니다.지피지기 백전불태 그렇기에 유니티에 기본적으로 내장되어 있는 코
wlsdn629.tistory.com
Group Presence와 App Link를 활용하면 위 영상처럼 초대를 받을 수 있고, 바로 친구가 들어가 있는 Room에 접속할 수 있습니다.
먼저, Group Presence와 App Link를 활성화하기 위해서는 Meta Horzion Dashboard에 가셔서, API Platform Service탭을 누른 후 Destinations을 선택(활성화)해줍니다.
App Id도 복사해둡니다.
그 다음 우측 상단 Create 버튼을 눌러줍니다.
방을 만들고, 참여할 수 있게 도와주는 Manager를 하나 생성해줍니다.
//LocalLobbyManager.cs
[Header("Presence")]
[SerializeField] private string destinationApi = "Lobby";
[SerializeField] private string deeplinkInviteText = "Let's play!";
private static uint roomIdCounter;
private CancellationTokenSource presenceCts;
private string lobbyId;
private string matchId;
public bool IsReady { get; private set; }
private bool CanOperate => Runner && Runner.State != NetworkRunner.States.Shutdown;
private void Start() => Connet().Forget();
public async UniTask Connect()
{
await Runner.JoinSessionLobby(SessionLobby.Shared);
IsReady = true;
}
//참여 및 방 생성 후
private async UniTask InitializeAfterJoinOrCreate(bool isHost)
{
if (isHost)
{
await PlatformBootstrap.InitializePlatformAsync();
await RegisterPresenceRetry();
}
}
private async UniTask RegisterPresenceRetry()
{
var opts = new GroupPresenceOptions();
opts.SetDestinationApiName(destinationApi);
opts.SetLobbySessionId(lobbyId);
opts.SetMatchSessionId(matchId);
opts.SetIsJoinable(true); //참여 가능한 형태로 만들기
opts.SetDeeplinkMessageOverride(deeplinkInviteText);
while (true) // 실패 시 재시도
{
var tcs = new UniTaskCompletionSource<bool>();
GroupPresence.Set(opts).OnComplete(m => tcs.TrySetResult(!m.IsError));
if (await tcs.Task)
{
Debug.Log("Presence 등록 완료");
break;
}
Debug.LogWarning("Presence 실패, 1초 후 재시도…");
await UniTask.Delay(1000);
}
}
//Error 추적
private void ShowStartFail(StartGameResult result) =>
Debug.LogError($"[Fusion] StartGame 실패 : {result.ShutdownReason}");
public async UniTask CreateRoom()
{
if (!CanOperate) return;
uint hex = (uint)UnityEngine.Random.Range(int.MinValue, int.MaxValue);
lobbyId = $"Lobby-{hex:X8}";
matchId = SelectedMapNum == 0 ? lobbyId : $"Map{SelectedMapNum}{lobbyId}";
var start = await Runner.StartGame(new StartGameArgs
{
SessionName = matchId,
GameMode = GameMode.Shared,
});
if (!start.Ok) { ShowStartFail(start); return; }
InitializeAfterJoinOrCreate(isHost: true).Forget();
}
public async UniTask JoinRoom(string sessionName)
{
var start = await Runner.StartGame(new StartGameArgs
{
SessionName = sessionName,
GameMode = GameMode.Shared,
}
if (!start.Ok) { ShowStartFail(start); return; }
InitializeAfterJoinOrCreate(isHost: Runner.IsSharedModeMasterClient).Forget();
}
핵심은 아래 함수에 있습니다.
private async UniTask InitializeAfterJoinOrCreate(bool isHost)
{
if (isHost)
{
await PlatformBootstrap.InitializePlatformAsync();
await RegisterPresenceRetry();
}
}
단계 | 무슨 작업? | 왜 필요한가? |
PlatformBootstrap.InitializePlatformAsync() | Meta SDK 초기화 | 호스트가 방을 만들었을 때만 Meta 서버에 인증을 완료해야 합니다. |
RegisterPresenceRetry() | Group Presence 정보를 Meta 서버에 등록 | 호스트가 만든 로비를 "초대 가능" 상태로 게시합니다. |
using System;
using Cysharp.Threading.Tasks;
using Oculus.Platform;
using UnityEngine;
public static class PlatformBootstrap
{
private static async UniTask<bool> WaitEntitlementAsync()
{
var tcs = new UniTaskCompletionSource<bool>();
Entitlements.IsUserEntitledToApplication().OnComplete(msg => tcs.TrySetResult(!msg.IsError));
var (winner, isEntitled, _) = await UniTask.WhenAny(tcs.Task, UniTask.Delay(TimeSpan.FromSeconds(5)).ContinueWith(() => false));
return winner == 0 && isEntitled;
}
public static async UniTask InitializePlatformAsync()
{
if (!Core.IsInitialized()) Core.Initialize("App Id");
bool ok = await WaitEntitlementAsync();
if (!ok)
{
Debug.LogError($"[Entitlement]Error - InitializePlatformAsync");
}
}
}
using Cysharp.Threading.Tasks;
using UnityEngine;
using Oculus.Platform;
using Oculus.Platform.Models;
public class FriendsInviteBridge : MonoBehaviour
{
[SerializeField] private LobbyManager lobbyMgr;
private bool joinIntentProcessed;
private async void Awake()
{
// ① 플랫폼 초기화
await PlatformBootstrap.InitializePlatformAsync();
// ② 앱이 실행 중일 때 - 초대 수신
GroupPresence.SetJoinIntentReceivedNotificationCallback(OnJoinIntent);
// ③ 앱이 꺼진 상태 - 초대 받은 경우
await UniTask.DelayFrame(3);
ApplicationLifecycle.SetLaunchIntentChangedNotificationCallback(OnLaunchIntent);
}
private void OnLaunchIntent(Message<string> msg)
{
HandleLaunch(ApplicationLifecycle.GetLaunchDetails());
}
private void HandleLaunch(LaunchDetails details)
{
if (joinIntentProcessed || details == null) return;
if (string.IsNullOrEmpty(details.LobbySessionID)) return;
joinIntentProcessed = true;
string room = string.IsNullOrEmpty(details.MatchSessionID) ? details.LobbySessionID : details.MatchSessionID;
WaitForLobbyAndJoin(room).Forget();
}
private async UniTaskVoid WaitForLobbyAndJoin(string room)
{
await UniTask.WaitUntil(() => lobbyMgr && lobbyMgr.IsReady);
lobbyMgr.JoinRoom(room).Forget();
}
private void OnJoinIntent(Message<GroupPresenceJoinIntent> msg)
{
if (joinIntentProcessed) return; // 이미 처리한 경우 무시
if (msg.IsError)
{
Debug.LogError($"[OnJoinIntent] Error : {msg}");
return;
}
joinIntentProcessed = true;
string room = string.IsNullOrEmpty(msg.Data.MatchSessionId) ? msg.Data.LobbySessionId : msg.Data.MatchSessionId;
WaitForLobbyAndJoin(room).Forget();
}
}
핵심 동작 | 왜 필요한가? | |
Awake | ① Meta SDK 초기화 ② 앱 실행 중일 때 초대 수신 콜백 등록 ③ 콜드 스타트(앱 꺼짐) 대비 |
초대를 받을 준비를 해야 앱 강제종료를 피함. |
OnLaunchIntent | OS가 Launch Intent(문자열) 전달 | 앱이 켜져 있는 경우 모두 여기로 수신. |
HandleLaunch | 1. 중복 처리 방지 2. LobbySessionID 존재 여부 확인(딥링크 아님이면 무시) 3. 세션 ID 추출(MatchSessionID 우선, 없으면 LobbySessionID) 4. WaitForLobbyAndJoin() 호출 |
방 정보가 없을 때 오류 로깅만 하고 끝. |
WaitForLobbyAndJoin | LobbyManager.JoinRoom(room) 호출 | Runner가 완전히 준비될 때까지 폴링. |
OnJoinIntent | 앱 실행 중 초대 알림 수신 경로. | Warm-Start 전용; LaunchIntent와 동일 로직을 재사용. |
UI를 통해 앱 링크를 보낼 수 있는 Group Presence Layer를 생성하는 코드입니다.
public void LaunchInviteButton() => LaunchInvitePanel().Forget();
private async UniTask LaunchInvitePanel()
{
try
{
var msg = await InvitePanelUtil.LaunchInvitePanelAsync();
if (!msg.IsError && msg.Data.InvitesSent) Debug.Log("초대 전송 완료!");
}
catch (Exception e)
{
Debug.LogError($"[LaunchInvitePanel]Error : {e.Message}");
}
}
using Cysharp.Threading.Tasks;
using Oculus.Platform;
using Oculus.Platform.Models;
public static class InvitePanelUtil
{
public static UniTask<Message<InvitePanelResultInfo>> LaunchInvitePanelAsync(InviteOptions opt = null)
{
var tcs = new UniTaskCompletionSource<Message<InvitePanelResultInfo>>();
GroupPresence.LaunchInvitePanel(opt ?? new InviteOptions()).OnComplete(m => tcs.TrySetResult(m));
return tcs.Task;
}
}
이로써 친구 초대하기 기능을 모두 알아보았습니다! 국내 자료도 없고, 해외에도 해당 기능을 알려주는 글을 본적이 거의 없었던거 같습니다. 이 글을 보는 여러분은 행운아입니다 ㅎㅎ
열심히 스터디해서 시행착오 끝에 알아낸 기술이오니, 좋아요..댓글...