Unity Photon Network로 채팅 구현

용준·2023년 10월 11일

Photon

목록 보기
2/2

1. 설치 및 유니티 연동

1-1. 유니티 새 프로젝트를 만들고 Ctrl+9로 Asset Store에 들어가 PUN 2 Free를 설치하고 Import 합니다.

1-2. 포톤 홈페이지 가입을 하고 새 어플리케이션 만들기를 합니다.

1-3. 포톤 종류, 어플리케이션 이름을 설정합니다.

1-4. 만들어진 어플리케이션 ID를 복사합니다.

CCU는 동시 접속자 수(Concurrect Users)를 의미합니다.
20 CCU까지 무료로 지원합니다.

1-5. 유니티에서 Alt + P로 PUN Wizard를 켜고 Locate PhotonServerSettings에 들어갑니다.

1-6. 복사해둔 어플리케이션 ID를 App ID PUN에 붙여넣으면 포톤 클라우드와 유니티가 연동이 됩니다.


2. 프로젝트 설정

2-1. 프로젝트 해상도를 설정합니다. 저는 400x720으로 하였습니다.

2-2. 하이라이키 창에서 캔버스를 생성합니다. 몇 가지 설정을 바꾸어야 합니다.

  • Canvas Scaler → UI Scale Mode → Scale With Screen Size
    화면 크기에 따라 UI의 크기도 커지는 반응형 스케일입니다.
  • Reference Resolution → 해상도와 동일한 값 입력
    현재 화면 해상도가 Reference Resolution보다 크면 확대 또는 작으면 축소되는 기능입니다.
  • Screen Match Mode - Expand
    Expand는 캔버스의 크기가 레퍼런스보다 작아지지 않도록 캔버스 영역을 확장하는 기능입니다.

3. 기본 로직 제작

3-1. GameManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public static GameManager instance; // 싱글톤 패턴

    // 인스턴스에 접근하기 위한 프로퍼티 (대문자 i)
    public static GameManager Instance
    {
        get
        {
            // 인스턴스가 없는 경우에 접근하려 하면 인스턴스 할당
            if (!instance)
            {
                instance = FindObjectOfType(typeof(GameManager)) as GameManager;
                if (instance == null) Debug.Log("싱글톤 오브젝트 없음");
            }
            return instance;
        }
    }

    void Awake()
    {
        if (instance == null) instance = this;
        else if (instance != this) Destroy(gameObject); //새로 생기는 인스턴스 삭제

        DontDestroyOnLoad(gameObject); //씬이 전환되어도 파괴되지 않음
    }

    void Update()
    {
        Screen.SetResolution(400, 720, false); //화면비율 고정 및 전체화면 비활성화
    }
}

핵심 기능

  • 씬이 이동되면서 오브젝트들이 전부 삭제되지만 중요한 데이터를 남겨야 하는 경우엔 오브젝트가 파괴되지 않도록 지정할 필요가 있습니다.
    (Update 함수에서 화면 해상도를 고정시키는 SetResolution을 사용했기 때문입니다.)
  • 나중에 GameManager에 쉽게 접근할 수 있도록 싱글톤 패턴을 사용하였습니다.

3-2. PhotonManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Photon.Pun;
using Photon.Realtime;
using System.Text.RegularExpressions; // https://docko.tistory.com/139

// https://hyokim.tistory.com/3

public class PhotonManager : MonoBehaviourPunCallbacks
{
    public Text connectionStatus;
    public Text idText;
    public Button loginBtn;
    public InputField inputField;

    void Start()
    {
        PhotonNetwork.ConnectUsingSettings();
        loginBtn.interactable = false;
        connectionStatus.text = "마스터 서버 연결 중..";
    }

    public void Connect()
    {
        // 숫자얻기 + 영문자얻기 + 한글얻기 + 특수문자제거 + 공백검출
        if (idText.text != Regex.Replace(idText.text, @"[^0-9a-zA-Z가-힣]", "") || inputField.text.Equals(""))
        {
            return;
        }
        else
        {
            PhotonNetwork.LocalPlayer.NickName = idText.text;
            loginBtn.interactable = false;

            if (PhotonNetwork.IsConnected)
            {
                connectionStatus.text = "방 입장 중..";
                PhotonNetwork.JoinRandomRoom();
            }
            else
            {
                connectionStatus.text = "(오프라인) 연결 실패\n재시도 중..";
                PhotonNetwork.ConnectUsingSettings();
            }
        }
    }

    public override void OnConnectedToMaster()
    {
        loginBtn.interactable = true;
        connectionStatus.text = "(온라인) 마스터 서버에 연결됨";
    }
    public override void OnDisconnected(DisconnectCause cause)
    {
        loginBtn.interactable = false;
        connectionStatus.text = "(오프라인) 연결 실패\n재시도 중..";
        PhotonNetwork.ConnectUsingSettings();
    }
    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        connectionStatus.text = "새 방 생성 중..";
        PhotonNetwork.CreateRoom(null, new RoomOptions { MaxPlayers = 8 });
        // MaxPlayers를 0으로 하면 제한없음
    }

    public override void OnJoinedRoom()
    {
        connectionStatus.text = "참가 성공";
        PhotonNetwork.LoadLevel(1);
    }
}

PhotonManager.cs의 동작 구조

핵심 기능

  • 아이디 생성 시 공백과 특수문자 필터링
    C# 문자 변환은 이 곳을 참조
  • 포톤 네트워크는 Scene 전환 방법이 다릅니다.
    SceneManagement.LoadScene 이 아닌 PhotonNetwork.LoadLevel 을 사용합니다.
  • 포톤은 Callback 함수제공합니다.
    예를 들면 JoinLobby 함수가 실행될 때 OnJoinedLobby 함수가 콜백되어 안에 있는 코드가 작동합니다.

4. 채팅창 제작

4-1. 하이라이키 창에서 UI - Scroll View를 생성하고 자식 오브젝트의 Horizontal은 삭제해줍니다.

Viewport와 Scrollbar는 Image 컴포넌트가 입혀져 있는데요,
Source Image를 불러와 제작자가 원하는 디자인으로 변경할 수 있을 것 같습니다.

4-2. Content의 자식으로 UI - Text 오브젝트를 하나 만들어줍니다. 이름은 Chat Log로 하였습니다.
메세지들이 이 텍스트 오브젝트에 쌓이도록 할 것입니다.

4-3. Chat Log에 Content Size Fitter 컴포넌트를 추가하고 아래 사진과 같이 설정합니다.

  • Horizontal Fit - Unconstrained (조정하지 않음)
  • Vertical Fit - Preferred Size (너비 기반 조정)
    Content Size Fitter 컴포넌트는 Text가 길어져서 초기에 설정해둔 정해놓은 가로 세로를 초과할 경우 글씨가 잘리는 현상이 발생하는데 늘어난 텍스트의 크기를 자동으로 맞춰주는 역할을 합니다.

4-4. Content 오브젝트에는 Content Size Fitter와 Vertical Layout Group 컴포넌트를 추가해줍니다.

  • Child Alignment - Upper Left (텍스트가 좌상단에 밀착하는 효과를 줍니다.)
  • Padding - Top - 10 (텍스트가 화면의 상단에 달라붙는 것을 방지하기 위한 적당한 높이입니다.)

4-5. 채팅을 입력하고 전송할 Input Field와 Button을 만들어줍니다.

4-6. 빈 오브젝트로 Chat Manager를 만들고 Photon View와 Chat Manager 스크립트를 추가합니다.

Photon View는 플레이어를 포톤 네트워크에 동기화해주는 역할을 하기 때문에 반드시 필요합니다.

4-7. ChatManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using Photon.Realtime;
using Photon.Pun;

// https://hyokim.tistory.com/4

public class ChatManager : MonoBehaviourPunCallbacks
{
    public Button sendBtn; //채팅 입력버튼
    public Text chatLog; //채팅 내역
    public InputField inputField; //채팅입력 인풋필드
    public Text playerList; //참가자 목록
    string players; //참가자들
    ScrollRect scroll_rect = null; //채팅이 많이 쌓일 경우 스크롤바의 위치를 아래로 고정하기 위함

    void Start()
    {
        PhotonNetwork.IsMessageQueueRunning = true;
        scroll_rect = GameObject.FindObjectOfType<ScrollRect>();
    }

    /// <summary>
    /// chatterUpdate(); 메소드로 주기적으로 플레이어 리스트를 업데이트하며
    /// input에 포커스가 맞춰져있고 엔터키가 눌려졌을 경우에도 SendButtonOnClicked(); 메소드를 실행.
    /// </summary>
    void Update()
    {
        ChatterUpdate();
        if ((Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter)) && !inputField.isFocused) SendButtonOnClicked();
    }

    /// <summary>
    /// 전송 버튼이 눌리면 실행될 메소드. 메세지 전송을 담당함.
    /// input이 비어있으면 아무것도 전송하지 않고, 비어있지 않다면
    /// "[ID] 메세지"의 형식으로 메세지를 전송함.
    /// 메세지 전송은 photonView.RPC 메소드를 이용해 각 유저들에게 ReceiveMsg 메소드를 실행하게 함.
    /// 자기 자신에게도 메세지를 띄워야 하므로 ReceiveMsg(msg);를 실행함.
    /// input.ActivateInputField();는 메세지 전송 후 바로 메세지를 입력할 수 있게 포커스를 Input Field로 옮김 (편의 기능)
    /// 그 후 input.text를 빈 칸으로 만듦
    /// </summary>
    public void SendButtonOnClicked()
    {
        if (inputField.text.Equals(""))
        {
            Debug.Log("Empty");
            return;
        }
        string msg = string.Format("[{0}] {1}", PhotonNetwork.LocalPlayer.NickName, inputField.text);
        photonView.RPC("ReceiveMsg", RpcTarget.OthersBuffered, msg);
        ReceiveMsg(msg);
        inputField.ActivateInputField(); // 메세지 전송 후 바로 메세지를 입력할 수 있게 포커스를 Input Field로 옮기는 편의 기능
        inputField.text = "";
    }

    /// <summary>
    /// 채팅 참가자 목록을 업데이트 하는 함수.
    /// '참가자 목록' 텍스트 아래에 플레이어들의 ID를 더해주는 식으로 작동하며,
    /// 실시간으로 출입하는 유저들의 ID를 반영함.
    /// </summary>
    void ChatterUpdate()
    {
        players = "참가자 목록\n";
        foreach (Player p in PhotonNetwork.PlayerList)
        {
            players += p.NickName + "\n";
        }
        playerList.text = players;
    }

    public void GameStart()
    {
        if (PhotonNetwork.IsMasterClient)
        {
            photonView.RPC("OnGameRoom", RpcTarget.AllBuffered);
        }
        else
        {
            Debug.Log("마스터 클라이언트가 아님");
        }
    }

    public override void OnPlayerEnteredRoom(Player newPlayer)
    {
        string msg = string.Format("<color=#00ff00>[{0}]님이 입장하셨습니다.</color>", newPlayer.NickName);
        ReceiveMsg(msg);
    }

    public override void OnPlayerLeftRoom(Player otherPlayer)
    {
        string msg = string.Format("<color=#ff0000>[{0}]님이 퇴장하셨습니다.</color>", otherPlayer.NickName);
        ReceiveMsg(msg);
    }

    [PunRPC]
    public void ReceiveMsg(string msg)
    {
        chatLog.text += "\n" + msg;
        StartCoroutine(ScrollUpdate());
    }

    [PunRPC]
    public void OnGameRoom()
    {
        PhotonNetwork.LoadLevel(2);
    }

    IEnumerator ScrollUpdate()
    {
        yield return null;
        scroll_rect.verticalNormalizedPosition = 0.0f;
    }
}

ChatManager.cs의 동작 구조

  • Start, Update 함수
  1. PhotonNetwork.isMessageQueueRunning은 포톤 네트워크 메세지 수신 기능입니다.
  2. scroll_rect는 채팅이 가득찼을 때 스크롤 바를 아래로 내려주는 기능을 만들때 사용하므로 초기화 해줍니다.
  3. chatterUpdate()는 주기적으로 플레이어 리스트를 업데이트하는 함수입니다.
  4. SendButtonOnClicked()는 메세지를 전송하는 함수입니다.
    조건문으로 InputField에 신호가 들어오며 엔터키로도 반응할 수 있게 설정해주었습니다.
  • ChatterUpdate 함수
  1. 채팅 참가자 목록을 업데이트 하는 함수입니다.
  2. '참가자 목록' 텍스트 아래에 플레이어들의 ID를 더해주는 식으로 작동하며 실시간으로 출입하는 유저들의 ID를 반영합니다.
  • SendButtonOnClicked 함수
  1. 전송 버튼이 눌리면 실행되고 메세지 전송을 담당하는 함수입니다.
    inputField가 비어있는 채로 전송 버튼이 눌리는 것을 방지해 문자열 공백체크를 해서 디버그 창에 Empty로 return 시킵니다.
  2. 비어있지 않은 일반적인 상황에선 "[ID] 메세지"의 형식으로 메세지를 전송하고,
    메세지 전송은 photonView.RPC 함수를 이용해 각 유저들에게 ReceiveMsg 메소드를 실행하게 하며 자기 자신에게도 메세지를 띄워야 하므로 ReceiveMsg(msg)를 실행합니다.
  3. input.ActivateInputField()는 메세지 전송 후 바로 메세지를 입력할 수 있게 포커스를 Input Field로 옮기고 그 후 input.text를 빈 칸으로 만들어줍니다.
  • ReceiveMsg 함수, ScrollUpdate 코루틴 함수
  1. 받은 메세지를 Chat Log에 추가하고 한 줄 내려 채팅을 최하단으로 맞춰주는 함수입니다.
    함수 호출 시간이 느린 코루틴을 사용해야 채팅을 입력했을 때 채팅창이 최하단으로 고정되는 효과를 줄 수 있었습니다. (일반 함수로 scroll_rect.verticalNormalizedPosition을 호출할 경우 채팅이 1칸씩 밀렸었습니다.)
  • GameStart / OnGameRoom 함수
  1. 채팅기능과는 무관한 테스트 코드입니다.
    (호스트가 룸에 참가한 게스트와 함께 다음 씬으로 전환되는 함수)
  • OnPlayerEnterRoom / OnPlayerLeftRoom 함수
  1. PlayerEnterRoom과 PlayerLeftRoom이 실행될 때 콜백되는 함수입니다.
    플레이어가 입장 또는 퇴장할 경우 채팅창에 메세지로 안내됩니다.

5. 결과

0개의 댓글