유니티 초대 기능 심화코드

유니티 초대 기능 심화: 확장 가능하고 견고한 시스템 설계

게임의 규모가 커지고 다양한 플랫폼을 지원하게 될 경우, 단순히 네트워크 함수를 호출하는 방식으로는 한계에 부딪히게 됩니다. 웹상에 흔히 볼 수 있는 코드 스니펫을 넘어, 데이터와 로직, UI를 분리하는 견고한 시스템 설계에 대해 깊이 있게 설명해 드리겠습니다.

이 방식은 초대를 단순한 ‘함수 호출’이 아닌, 상태를 가진 객체로 다루고, 이벤트 기반 통신을 통해 각 모듈의 결합도를 낮추는 데 중점을 둡니다.


1. 핵심 설계 원칙: 책임의 분리와 추상화

초대 기능을 구현하기 위해 세 가지 핵심 모듈을 분리합니다.

  • 초대 서비스 (InvitationService): 초대의 생성, 상태(대기, 수락, 거절) 관리, 네트워크 통신 등 모든 핵심 로직을 담당합니다.
  • 플랫폼 어댑터 (IPlatformInviter): Steam, Photon, 자체 서버 등 실제 통신이 이루어지는 플랫폼별 코드를 추상화합니다. InvitationService는 이 인터페이스만 바라보므로, 플랫폼이 바뀌어도 코드를 수정할 필요가 없습니다.
  • 초대 UI (InvitationUI): InvitationService에서 발생하는 이벤트를 수신하고, 이를 시각적으로 사용자에게 보여주는 역할만 담당합니다. 핵심 로직과 완전히 분리되어 있어 UI 디자인이 변경되어도 로직에 영향을 주지 않습니다.

2. 핵심 컴포넌트 구현: 이벤트와 데이터 기반 아키텍처

2.1. 초대의 상태를 담는 데이터 모델

초대를 단순한 string이 아닌, 상태를 담는 객체로 정의합니다.

C#

public enum InvitationStatus
{
    Pending,
    Accepted,
    Rejected,
    Expired
}

[System.Serializable]
public class Invitation
{
    public string invitationId;
    public string inviterId;
    public string invitedPlayerId;
    public InvitationStatus status;
}

2.2. 초대 로직을 관장하는 핵심 서비스

InvitationServiceMonoBehaviour를 상속받는 싱글톤 패턴으로 구현하여 게임 어디서든 접근 가능하게 만듭니다. 이 클래스는 초대 보내기, 초대 수락하기 등의 핵심 로직을 담당합니다.

C#

using System.Collections.Generic;
using UnityEngine;

public class InvitationService : MonoBehaviour
{
    // 싱글톤 인스턴스
    public static InvitationService Instance { get; private set; }

    // 플랫폼별 초대 기능을 추상화한 인터페이스
    private IPlatformInviter _platformInviter;
    
    // 현재 처리 중인 초대 목록
    private Dictionary<string, Invitation> _activeInvites = new Dictionary<string, Invitation>();

    public delegate void OnInviteReceivedHandler(Invitation invite);
    public event OnInviteReceivedHandler OnInviteReceived;

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }

        // 실제 플랫폼 구현체를 할당 (예시: SteamInviter, PhotonInviter 등)
        _platformInviter = new CustomGameInviter();
    }

    public void SendInvite(string invitedPlayerId)
    {
        // 초대 ID 생성
        string inviteId = System.Guid.NewGuid().ToString();
        string myPlayerId = "LocalPlayerID"; // 로컬 플레이어 ID

        Invitation newInvite = new Invitation
        {
            invitationId = inviteId,
            inviterId = myPlayerId,
            invitedPlayerId = invitedPlayerId,
            status = InvitationStatus.Pending
        };

        _activeInvites[inviteId] = newInvite;

        // 플랫폼 어댑터를 통해 실제 네트워크로 초대 발송
        _platformInviter.SendInvite(newInvite);
    }

    public void AcceptInvite(string inviteId)
    {
        if (_activeInvites.ContainsKey(inviteId))
        {
            _activeInvites[inviteId].status = InvitationStatus.Accepted;
            // 플랫폼 어댑터를 통해 초대 수락 응답 발송
            _platformInviter.AcceptInvite(_activeInvites[inviteId]);
        }
    }

    // 외부에서 초대를 수신했을 때 호출되는 함수
    public void ReceiveInviteFromPlatform(Invitation invite)
    {
        _activeInvites[invite.invitationId] = invite;
        // UI 모듈에 알림을 보내기 위해 이벤트 발생
        OnInviteReceived?.Invoke(invite);
    }
}

2.3. 플랫폼별 통신을 추상화하는 인터페이스

InvitationService는 이 인터페이스만 바라봅니다.

C#

public interface IPlatformInviter
{
    void SendInvite(Invitation invite);
    void AcceptInvite(Invitation invite);
    // ... 기타 플랫폼별 함수
}

// 예시 구현: 커스텀 게임 서버와 통신하는 방식
public class CustomGameInviter : IPlatformInviter
{
    public void SendInvite(Invitation invite)
    {
        Debug.Log($"커스텀 서버로 {invite.invitedPlayerId}에게 초대 발송.");
        // 여기에 실제 커스텀 백엔드 통신 로직 작성
    }

    public void AcceptInvite(Invitation invite)
    {
        Debug.Log($"커스텀 서버로 {invite.inviterId}의 초대 수락.");
        // 여기에 실제 백엔드 통신 로직 작성
    }
}

3. UI와 로직의 분리: 이벤트 기반의 UI 업데이트

InvitationUI 스크립트는 InvitationService의 이벤트를 구독하여 화면을 갱신합니다. 이로써 UI 코드는 InvitationService가 어떻게 작동하는지 전혀 알 필요가 없습니다.

C#

using UnityEngine;

public class InvitationUI : MonoBehaviour
{
    public GameObject invitationPopupPrefab; // 초대 알림 UI 프리팹

    void Start()
    {
        if (InvitationService.Instance != null)
        {
            // 초대 수신 이벤트를 구독
            InvitationService.Instance.OnInviteReceived += OnInviteReceivedHandler;
        }
    }

    private void OnInviteReceivedHandler(Invitation invite)
    {
        // 초대 알림 팝업 생성
        GameObject popup = Instantiate(invitationPopupPrefab, transform);
        popup.GetComponent<InvitationPopup>().Setup(invite); // 팝업 UI 설정
    }
}

이 아키텍처의 장점

이러한 설계 방식은 다음과 같은 장점을 제공합니다.

  • 유지보수 용이성: 새로운 플랫폼(예: Xbox)을 추가하고 싶다면, IPlatformInviter를 구현하는 새로운 클래스만 만들어 InvitationService에 연결하면 됩니다.
  • 높은 확장성: 초대에 ‘파티 인원’, ‘게임 모드’ 같은 추가 정보를 넣고 싶다면 Invitation 클래스만 수정하면 됩니다. 모든 모듈이 유기적으로 연동됩니다.
  • 견고한 코드: 각 모듈이 명확한 책임만 가지므로 버그 발생 시 문제의 원인을 파악하기 쉽고, 테스트 코드를 작성하기 용이합니다.

이러한 접근 방식은 단순히 기능을 구현하는 것을 넘어, 게임 시스템을 설계하는 단계부터 미래를 대비하는 개발자의 자세를 보여줍니다.

댓글 남기기