}

유니티 Deep-Link로 친구를 초대해보자! #Group Presence, Deep-Link #Meta

Group Presence란?

https://developers.meta.com/horizon/documentation/unity/ps-group-presence-overview/

Group Presence란 Meta 플랫폼이 유저의 Destination(장소)·Lobby/Match ID·Joinable 여부를 받아서 친구 목록과 퀘스트 메뉴에 노출하는 소셜 레이어 입니다.

  • DestinationApiName: Lobby, Map(동화나라) 같은 장소 키. VRChat의 월드 URL 같은 역할을 합니다.
  • LobbySessionID / MatchSessionID: 실제 방 고유값. 세션을 구분하는 키입니다.
  • IsJoinable: true면 친구 초대 버튼이 뜹니다.

☕ Tip: Session ID는 64byte 이하. 더 길면 Presence 등록이 실패합니다.


딥링크(Deep-Link)란?

딥링크(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에 접속할 수 있습니다.

API 활성화 / App ID 얻어오기

먼저, 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를 생성하는 코드입니다.

https://developers.meta.com/horizon/documentation/unity/ps-invite-overview

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;
    }
}

 

이로써 친구 초대하기 기능을 모두 알아보았습니다! 국내 자료도 없고, 해외에도 해당 기능을 알려주는 글을 본적이 거의 없었던거 같습니다. 이 글을 보는 여러분은 행운아입니다 ㅎㅎ

 

열심히 스터디해서 시행착오 끝에 알아낸 기술이오니, 좋아요..댓글...