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-1. 프로젝트 해상도를 설정합니다. 저는 400x720으로 하였습니다.
2-2. 하이라이키 창에서 캔버스를 생성합니다. 몇 가지 설정을 바꾸어야 합니다.
![]()
- Canvas Scaler → UI Scale Mode → Scale With Screen Size
화면 크기에 따라 UI의 크기도 커지는 반응형 스케일입니다.- Reference Resolution → 해상도와 동일한 값 입력
현재 화면 해상도가 Reference Resolution보다 크면 확대 또는 작으면 축소되는 기능입니다.- Screen Match Mode - Expand
Expand는 캔버스의 크기가 레퍼런스보다 작아지지 않도록 캔버스 영역을 확장하는 기능입니다.
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에 쉽게 접근할 수 있도록 싱글톤 패턴을 사용하였습니다.
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-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는 플레이어를 포톤 네트워크에 동기화해주는 역할을 하기 때문에 반드시 필요합니다.
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 함수
- PhotonNetwork.isMessageQueueRunning은 포톤 네트워크 메세지 수신 기능입니다.
- scroll_rect는 채팅이 가득찼을 때 스크롤 바를 아래로 내려주는 기능을 만들때 사용하므로 초기화 해줍니다.
- chatterUpdate()는 주기적으로 플레이어 리스트를 업데이트하는 함수입니다.
- SendButtonOnClicked()는 메세지를 전송하는 함수입니다.
조건문으로 InputField에 신호가 들어오며 엔터키로도 반응할 수 있게 설정해주었습니다.
- ChatterUpdate 함수
![]()
- 채팅 참가자 목록을 업데이트 하는 함수입니다.
- '참가자 목록' 텍스트 아래에 플레이어들의 ID를 더해주는 식으로 작동하며 실시간으로 출입하는 유저들의 ID를 반영합니다.
- SendButtonOnClicked 함수
- 전송 버튼이 눌리면 실행되고 메세지 전송을 담당하는 함수입니다.
inputField가 비어있는 채로 전송 버튼이 눌리는 것을 방지해 문자열 공백체크를 해서 디버그 창에 Empty로 return 시킵니다.- 비어있지 않은 일반적인 상황에선 "[ID] 메세지"의 형식으로 메세지를 전송하고,
메세지 전송은 photonView.RPC 함수를 이용해 각 유저들에게 ReceiveMsg 메소드를 실행하게 하며 자기 자신에게도 메세지를 띄워야 하므로 ReceiveMsg(msg)를 실행합니다.- input.ActivateInputField()는 메세지 전송 후 바로 메세지를 입력할 수 있게 포커스를 Input Field로 옮기고 그 후 input.text를 빈 칸으로 만들어줍니다.
- ReceiveMsg 함수, ScrollUpdate 코루틴 함수
- 받은 메세지를 Chat Log에 추가하고 한 줄 내려 채팅을 최하단으로 맞춰주는 함수입니다.
함수 호출 시간이 느린 코루틴을 사용해야 채팅을 입력했을 때 채팅창이 최하단으로 고정되는 효과를 줄 수 있었습니다. (일반 함수로 scroll_rect.verticalNormalizedPosition을 호출할 경우 채팅이 1칸씩 밀렸었습니다.)
- GameStart / OnGameRoom 함수
![]()
- 채팅기능과는 무관한 테스트 코드입니다.
(호스트가 룸에 참가한 게스트와 함께 다음 씬으로 전환되는 함수)
- OnPlayerEnterRoom / OnPlayerLeftRoom 함수
- PlayerEnterRoom과 PlayerLeftRoom이 실행될 때 콜백되는 함수입니다.
플레이어가 입장 또는 퇴장할 경우 채팅창에 메세지로 안내됩니다.
