
유니티에서 제공하는 네트워크 기능인 PUN2를 통해 네트워크 게임을 구현해보자

PUN)photon 기능들을 유니티에서 사용할 수 있게 만든 SDK이다.
룸 : 같은 룸에 참가한 클라이언트들은 동기화를 통해 실시간 네트워크 구성이 가능하다.
실시간 동기화작업이 중요하다
객체 동기화 : 데이터 스트림으로 변수 데이터 전달을 통해 이루어진다.RPC : Remote Procedure Call. 모든 클라이언트가 동일한 함수를 호출 할 수 있도록 전달하는 원격 함수 호출 커스텀 프로퍼티 : 룸과 플레이어의 정보를 캐시해두고 동기화포톤 서버를 이용하려면 앱을 먼저 생성해야 한다.

먼저 로그인 한다. 아이디가 없으면 만들고 접속하자

어플리케이션을 만들어보자

만들고자 하는 용도에 맞게 작성한다

이러면 서버 어플리케이션이 생성된다. App ID를 복사하자

에셋 스토어에서 PUN2를 추가해줘야 한다. 추가한 이후, 유니티에서 패키지매니저로 추가할 수 있다.

패키지를 설치하고 나면 위와같은 화면이 나온다. 여기에 아까 만든 서버의 App ID를 집어넣는다.
기능이 제대로 되는지 테스트를 진행해본다
아래와 같이 스크립트를 작성한다
using UnityEngine;
using Photon.Pun;
public class NetworkManager : MonoBehaviourPunCallbacks
{
void Start()
{
PhotonNetwork.ConnectUsingSettings(); // 접속 시도
}
public override void OnConnected() // 접속됐을 경우 Callback
{
base.OnConnected();
Debug.Log($"연결");
}
public override void OnConnectedToMaster() // 마스터 서버 접속 시 Callback
{
base.OnConnectedToMaster();
Debug.Log("마스터연결");
}
}
작성한 스크립트를 씬의 빈 오브젝트에 컴포넌트로 추가하고 실행해보자.

인터넷 연결, 서버 접속을 확인하는 순으로 진행하는 것을 로그를 통해 알 수 있다.

여기서 Locate PhotonServerSettings를 클릭

해당 스크립터블 오브젝트를 통해 서버 세팅이 가능하다
포톤은 매치메이킹 기능으로 룸과 로비를 구성하는 기능을 제공한다.

로비 : 다른 플레이어들이 생성한 방 목록을 확인하고, 방을 만들거나 참가할 수 있다.

룸 : 방에 참여한 플레이어들을 확인하고, 상태를 최신화 할 수 있음. 이후 게임에서 방의 플레이어들만 참여하게 할 수 있다. 이러한 과정들을 준비하는 공간.
클라이언트(유저들)와 서버 간의 통신을 통해 요청, 반응을 구현한다.
PhotonNetwork 클래스 사용. 해당 클래스의 함수들을 사용해서, 클라이언트가 서버에게 요청을 한다.
PhotonNetwork.ConnectUsingSettings(); // 접속 시도 요청
PhotonNetwork.Disconnect(); // 접속 해제 요청
PhotonNetwork.CreateRoom("RoomName"); // 방 생성 요청
PhotonNetwork.JoinRoom("RoomName"); // 방 입장 요청
PhotonNetwork.LeaveRoom(); // 방 퇴장 요청
PhotonNetwork.JoinLobby(); // 로비 입장 요청
PhotonNetwork.LeaveLobby(); // 로비 퇴장 요청
PhotonNetwork.LoadLevel("SceneName"); // 씬 전환 요청
bool isConnected = PhotonNetwork.IsConnected; // 접속 여부 확인
bool isInRoom = PhotonNetwork.InRoom; // 방 입장 여부 확인
bool isLobby = PhotonNetwork.InLobby; // 로비 입장 여부 확인
ClientState state = PhotonNetwork.NetworkClientState; // 클라이언트 상태 확인
Player player = PhotonNetwork.LocalPlayer; // 포톤 플레이어 정보 확인
Room players = PhotonNetwork.CurrentRoom; // 현재 방 정보 확인
MonoBehaviourPunCallbacks 클래스 사용. 해당 클래스를 상속받아 구현하고, 해당 클래스의 함수들을 통해 클라이언트에게서 받은 요청에 대한 서버의 반응을 다시 클라이언트에게 전달한다.
public class NetworkManager : MonoBehaviourPunCallbacks
{
public override void OnConnected() { } // 포톤 접속시 호출됨
public override void OnConnectedToMaster() { } // 마스터 서버 접속시 호출됨
public override void OnDisconnected(DisconnectCause cause) { } // 접속 해제시 호출됨
public override void OnCreatedRoom() { } // 방 생성시 호출됨
public override void OnJoinedRoom() { } // 방 입장시 호출됨
public override void OnLeftRoom() { } // 방 퇴장시 호출됨
public override void OnPlayerEnteredRoom(Player newPlayer) { } // 새로운 플레이어가 방 입장시 호출됨
public override void OnPlayerLeftRoom(Player otherPlayer) { } // 다른 플레이어가 방 퇴장시 호출됨
public override void OnCreateRoomFailed(short returnCode, string message) { } // 방 생성 실패시 호출됨
public override void OnJoinRoomFailed(short returnCode, string message) { } // 방 입장 실패시 호출됨
public override void OnJoinedLobby() { } // 로비 입장시 호출됨
public override void OnLeftLobby() { } // 로비 퇴장시 호출됨
public override void OnRoomListUpdate(List<RoomInfo> roomList) { } // 방 목록 변경시 호출됨
}
멀티 플레이 환경에서 필요한 정보들이 있다. 룸, 플레이어의 경우 각각 아래와 같다.
룸 : 이름, 최대 참여가능 인원, 현재 참여인원, 공개/비공개, 비밀번호, 맵, 개인전 / 팀전 여부 등
플레이어 : 닉네임, 아이디, 방장여부, 레디 여부, 캐릭터, 팀 등
위와 같은 정보들은 포톤에서 기본적으로 구현되어 있지 않다. 이러한 기능들을 사용하기 위해서 커스텀 프로퍼티가 제공된다. 이를 통해 추가하고자 하는 정보의 이름과 값(딕셔너리)을 설정하여 같은 게임을 플레이하는 클라이언트들과 정보공유를 통해 동기화가 가능하다.
포톤에서 제공하는 HashTable자료구조. C#의 HashTable인 Dictionary와 사용 방법이 같다. 네트워크 전송을 위해 직렬화 처리가 추가되었다.
Room room = PhotonNetwork.CurrentRoom; // 현재 참가한 룸을 확인
// 룸 커스텀 프로퍼티 설정
ExitGames.Client.Photon.Hashtable roomProperty = new ExitGames.Client.Photon.Hashtabl> ();
roomProperty["Map"] = "Select Map";
room.SetCustomProperties(roomProperty);
// 룸 커스텀 프로퍼티 확인
string curMap = (string)room.CustomProperties["Map"];
Player player = PhotonNetwork.LocalPlayer; // 자신 플레이어를 확인
// 플레이어 커스텀 프로퍼티 설정
ExitGames.Client.Photon.Hashtable playerProperty = new ExitGames.Client.Photon> Hashtable();
playerProperty["Ready"] = true;
player.SetCustomProperties(playerProperty);
// 플레이어 커스텀 프로퍼티 확인
bool ready = (bool)player.CustomProperties["Ready"];
아래 함수들은 커스텀 프로퍼티의 콜백함수들이다. 프로퍼티의 변동사항을 추적해서 제어할 수 있다.
public class NetworkManager : MonoBehaviourPunCallbacks
{
public override void OnRoomPropertiesUpdate(ExitGames.Client.Photon.Hashtable propertiesThatChanged)
{
// 현재 참여한 방의 프로퍼티가 업데이트시 호출됨
}
public override void OnPlayerPropertiesUpdate(Player targetPlayer, ExitGames.Client.Photon.Hashtable changedProps)
{
// 같은 방의 플레이어의 프로퍼티가 업데이트시 호출됨
}
}
네트워크 서비스를 테스트하기 위해서는 매번 빌드를 해서 테스트 해야한다. 이러한 불편함을 해소하기 위해서 ParrelSync를 이용한다.
추가적으로, 한 대의 컴퓨터로 다른 클라이언트의 역할을 할 수 있게 프로젝트를 클론해주는 기능도 있다. 이를 통해 여러 대의 클라이언트를 테스트하는 효과를 볼 수 있다.

링크한 Github에서 릴리즈를 다운 받으면 유니티 패키지가 있다. 이 유니티 패키지를 추가해주거나, 해당 깃허브 링크를 패키지 매니저에서 추가해주면 된다.

여기서 Github 주소를 추가해주면 된다.

설치하면 위와같이 탭에 ParrelSync가 생기게 된다. Clones Manager를 눌러보자.

여기서 Add new clone을 누르면 된다.
Open in New Editor를 누르면 클론의 에디터가 실행된다. 원본 에디터에서 플레이를 누르고, 클론 에디터도 플레이를 누르면 둘 다 서버에 접속해서 개별적인 클라이언트로 작동하게 된다. ParrelSync덕에 빌드없이 테스트가 가능하다.
온라인 게임에서 하나의 룸에서 게임에 대한 권한을 가진 구성원을 방장이라고 할 수 있다. 이러한 권한 설정을 포톤에서는 Master Client로 구현한다.
public class NetworkManager : MonoBehaviourPunCallbacks
{
public void GameStart()
{
// 자신 플레이어가 방장이 아닌 경우 반환하여 아래의 코드가 실행되지 않도록 함
if (PhotonNetwork.LocalPlayer.IsMasterClient == false)
return;
// 방장만이 실행할 수 있는 소스코드
PhotonNetwork.AutomaticallySyncScene = true; // 모든 방구성원이 같은 씬으로 > 이동하도록 동기화함
PhotonNetwork.LoadLevel("GameScene"); // 네트워크를 통해 씬을 이동하도록 > 요청함
}
}
이제 직접 프로젝트를 생성하고 구현해보자.
구현하기에 앞서 결과물이 어떤지 확인하고 시작하자.

로딩 창에서 네트워크 접속을 확인한다. 마스터 서버까지 접속이 완료될 경우, 닉네임 입력 창으로 넘어간다. Current State는 현재 상황을 실시간으로 보여준다.

게임에서 사용할 닉네임을 설정하는 창이다.
닉네임 입력을 완료하면, 로비 창으로 넘어간다.

로비 창에서는 방 생성, 방목록 확인 및 참가가 가능하다.

방을 생성하면, 방을 생성한 클라이언트는 방으로 들어간 후 방장이 된다. 동시에 다른 클라이언트에서는 생성된 방을 목록에서 확인 가능하다.
맵 설정 : 게임에 사용될 맵을 설정할 수 있다.
레디 : 레디 상태를 변경할 수 있다.
스타트 : 방 구성원들과 동기화 후, 게임화면으로 넘어감. 방장만 누를 수 있고, 모든 구성원에 레디 상태여야 한다.
떠나기 : 방을 떠날 수 있다. 구성원이 2명 이상이고, 방장이 떠날 경우 남은 사람이 방장이 된다. 아무도 남지 않을 경우, 방
은 삭제된다.
채팅 : 채팅을 칠 수 있다. 채팅 내용은 방 구성원 모두에게 공유된다.
플레이어 패널 : 현재 방에 접속 중인 구성원 수 만큼 패널이 늘어난다. 방장일 경우 별도로 표시하는 UI를 활성화 한다.
네트워크 매니저로 현재 네트워크 연결 상태를 관리한다. 로딩 창, 닉네임 입력 창, 로비 창 입장까지의 단계가 표현된 상태다.
using UnityEngine;
using UnityEngine.UI;
using Photon.Pun; // 네트워크 기능을 쓰기 위한 네임스페이스
using Photon.Realtime; // 동기화에 쓰이는 네임스페이스
using TMPro;
public class NetworkManager : MonoBehaviourPunCallbacks
{
[SerializeField] private GameObject loadingPanel;
[SerializeField] private GameObject nicknamePanel;
[SerializeField] private TextMeshProUGUI stateText;
[SerializeField] private TMP_InputField nicknameText;
[SerializeField] private Button nicknameAdmitBtn;
void Start()
{
PhotonNetwork.ConnectUsingSettings();// 인터넷을 통해 서버에 접속을 요청
nicknameAdmitBtn.onClick.AddListener(NicknameAdmit);
}
private void Update()
{
// 여러 필드들을 확인할 수 있음.
stateText.text = $"Current State : {PhotonNetwork.NetworkClientState}";
}
public void NicknameAdmit() // 아이디 작성 완료 후, 버튼을 누르면 수행. 로비로 전환을 서버에 요청한다
{
if (!string.IsNullOrWhiteSpace(nicknameText.text))
{
PhotonNetwork.NickName = nicknameText.text; // 플레이어의 닉네임 설정 가능
PhotonNetwork.JoinLobby(); // 서버에 로비 접속 요청
}
else
{
Debug.Log("이름을 입력해주세요");
}
}
public override void OnConnected() // 서버 접속 요청에 대한 응답이 와서 접속한 경우
{
base.OnConnected();
Debug.Log($"연결");
}
public override void OnConnectedToMaster() // ConnectUsingSettings를 사용하면, 마스터 서버까지 접속을 요청한다.
{
base.OnConnectedToMaster();
Debug.Log("마스터연결");
loadingPanel.SetActive(false); // 접속에 성공했으니, 로딩창을 끄고 닉네임 입력 창으로 전환
}
public override void OnJoinedLobby() // 로비로 이동요청 후, 서버의 응답이 와서 로비에 접속할 경우
{
base.OnJoinedLobby();
nicknamePanel.SetActive(false);
Debug.Log($"로비 참가");
}
public override void OnDisconnected(DisconnectCause cause) // 서버 접속에 실패했을 때 수행
{
base.OnDisconnected(cause);
PhotonNetwork.ConnectUsingSettings(); // 재연결 시도
}
}

방을 생성함과 동시에 해당 방에 참가하는 기능을 구현해본다. 쓸 수 있는 프로퍼티와 함수들은 API Reference를 참고하자.

위의 하이라키 창과 같이 UI 구성을 한다. 그리고 아래와 같이 NetworkManager 스크립트를 수정한다.
[SerializeField] private GameObject lobbyPanel;
[SerializeField] private TMP_InputField roomNameText;
[SerializeField] private Button roomNameAdmitBtn;
void Start()
{
roomNameAdmitBtn.onClick.AddListener(CreateRoom); // 버튼에 방 생성 이벤트 연결
}
public void CreateRoom() // 방 생성
{
if (string.IsNullOrEmpty(roomNameText.text))
{
Debug.LogWarning("방 이름 입력이 없음");
return;
}
roomNameAdmitBtn.interactable = false; // 더블클릭 같은 문제를 미연에 방지함
RoomOptions roomOptions = new RoomOptions{MaxPlayers = 8}; // 룸 옵션에는 여러 사용할 수 있는 옵션들이 있다.
PhotonNetwork.CreateRoom(roomNameText.text, roomOptions); // 최대 8명까지 접속 가능한 방으로 설정
roomNameText.text = null; // 방 생성 후 인풋 필드 초기화
Debug.Log("방을 생성 시도");
}
public override void OnCreatedRoom()
{
base.OnCreatedRoom();
lobbyPanel.SetActive(false);
Debug.Log("방을 만들기 완료. 방에 참가 시도");
}
public override void OnJoinedRoom()
{
base.OnJoinedRoom();
Debug.Log("방에 참가 완료");
}

중복된 이름의 방 생성 차단, 방에서 로비로 나올 경우의 초기화 작업은 나중에 다시 하도록 한다.

로비창에서 방 목록에 방이 생길 때마다 추가해보자.

Content에 Vertical Layout Group을 추가해준다. 이를 통해, 룸 리스트 아이템이 추가될 때마다 자동으로 정렬되서 아래에 추가된다.
private Dictionary<string,GameObject> roomListItemDict = new Dictionary<string, GameObject>();
public override void OnRoomListUpdate(List<RoomInfo> roomList) // 방 목록 변경시 호출됨. RoomInfo에는 방의 정보들이 담김
{
foreach (RoomInfo info in roomList) // 방 정보를 순회
{
if (info.RemovedFromList) // 방이 꽉 찼거나, 닫혔거나, 숨겨졌을 경우
{
if (roomListItemDict.TryGetValue(info.Name, out GameObject obj)) // 방 생성 시마다 추가했던 딕셔너리에 있을 경우 딕셔너리에서도 삭제
{
Destroy(obj); // 오브젝트 삭제
roomListItemDict.Remove(info.Name); // 딕셔너리에서 삭제
}
continue; // 다음방 정보로 넘어감
}
if (roomListItemDict.ContainsKey(info.Name)) // 딕셔너리에 방이 있음
{
roomListItemDict[info.Name].GetComponent<RoomListItem>().Init(info); //플레이어의 수가 변경되는 경우 등
}
else // 딕셔너리에 방이 없음. 로비에 새로 입장, 방이 새로 생성
{
GameObject roomListItem = Instantiate(roomListItemPrefab);// 룸리스트 오브젝트 생성
roomListItem.transform.SetParent(roomListItemParent); //스크롤뷰의 컨텐트에 넣어줌
roomListItemDict.Add(info.Name, roomListItem); // 딕셔너리에 추가함
roomListItem.GetComponent<RoomListItem>().Init(info); // 초기화
}
}
}
로비에서 보이는 방 목록에 들어갈 UI 프리팹이다. 해당 스크립트를 부착해서 RoomInfo를 넘기면 초기화 할 수 있게 한다.
public class RoomListItem : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI roomNameText;
[SerializeField] private TextMeshProUGUI playerCountText;
[SerializeField] private Button joinButton;
private string roomName;
public void Init(RoomInfo info)
{
roomName = info.Name;
roomNameText.text = $"Room Name : {roomName} ";
playerCountText.text = $"{info.PlayerCount} / {info.MaxPlayers}";
joinButton.onClick.AddListener(JoinRoom);
}
public void JoinRoom()
{
joinButton.onClick.RemoveAllListeners();
PhotonNetwork.JoinRoom(roomName);
}
}
플레이해서 방을 생성하는 단계까지 진행 후, ParrelSync로 만든 클론 에디터로 방이 생성되었는지 확인한다.

멀티플레이로 방을 생성해서 들어왔으니, 내부를 꾸며보자



플레이어 슬롯이 추가되는 패널에는 Grid Layout Group을 추가한다.
왼쪽 아래의 인풋 필드는 추후에 채팅 창으로 구현한다.
public override void OnJoinedRoom() // 방 입장 시 호출
{
base.OnJoinedRoom();
lobbyPanel.SetActive(false);
roomManager.PlayerPanelSpawn();
Debug.Log("방에 참가 완료");
}
public override void OnPlayerEnteredRoom(Player newPlayer) // 새로운 플레이어가 방 입장시 호출됨. 본인은 호출안됨
{
if (newPlayer != PhotonNetwork.LocalPlayer) //본인이 아닐 경우에만 수행. 이미 처리가 되어있지만 안전장치 역할을 함
{
roomManager.PlayerPanelSpawn(newPlayer);
}
}
public override void OnPlayerLeftRoom(Player otherPlayer) // 본인이 아닌, 다른 플레이어가 방 퇴장시 호출됨
{
if(otherPlayer != PhotonNetwork.LocalPlayer) // 이미 시스템 상 막혀있지만 혹시모를 경우를 대비한 예외처리
roomManager.PlayerPanelDestroy(otherPlayer);
}
using Hashtable = ExitGames.Client.Photon.Hashtable;
public class RoomManager : MonoBehaviour
{
public Dictionary<int,PlayerPanelItem> playerDict = new Dictionary<int, PlayerPanelItem>();
private void Start()
{
startBtn.onClick.AddListener(GameStart);
leaveBtn.onClick.AddListener(LeaveRoom);
mapLeftBtn.onClick.AddListener(ClickLeftMapButton);
mapRightBtn.onClick.AddListener(ClickRightMapButton);
}
public void PlayerPanelSpawn()// 내가 새로 입장했을 때 호출. OnJoinedRoom()에서 수행됨.
{
foreach (Player player in PhotonNetwork.PlayerList) // 현재 방에 접속한 모든 플레이어(PlayerList)
{
PhotonNetwork.AutomaticallySyncScene = true; // 마스터 클라이언트를 따라 동시에 같은 레벨을 로드함
if (!PhotonNetwork.IsMasterClient) // 본인이 호스트가 아닐 경우, 권한 뺏음
{
startBtn.interactable = false;
mapLeftBtn.interactable = false;
mapRightBtn.interactable = false;
}
// 플레이어 슬롯을 생성해서 패널에 추가함. 초기화 과정
GameObject obj = Instantiate(playerPanelItemPrefabs);
obj.transform.SetParent(playerPanelContoent);
obj.GetComponent<PlayerPanelItem>().Init(player);
PlayerPanelItem item = obj.GetComponent<PlayerPanelItem>(); // 초기화
playerDict.Add(player.ActorNumber,item); // 방에 들어온 순서대로 ActorNumber가 올라감. 재입장해도 새로 배정 받음
}
}
public void PlayerPanelSpawn(Player player)
// Player : 플레이어와 관련한 정보들이 담겨있음. 현재 방에 새로운 플레이어 입장시 호출
{
if (playerDict.TryGetValue(player.ActorNumber, out PlayerPanelItem playerPanelItem))
{
startBtn.interactable = false;
mapLeftBtn.interactable = false;
mapRightBtn.interactable = false;
playerPanelItem.Init(player);
}
GameObject obj = Instantiate(playerPanelItemPrefabs);
obj.transform.SetParent(playerPanelContoent);
obj.GetComponent<PlayerPanelItem>().Init(player);
PlayerPanelItem item = obj.GetComponent<PlayerPanelItem>(); // 초기화
playerDict.Add(player.ActorNumber,item); // 방에 들어온 순서대로 ActorNumber가 올라감.
}
public void PlayerPanelDestroy(Player player) // 플레이어가 떠날 시 호출
{
if (playerDict.TryGetValue(player.ActorNumber, out PlayerPanelItem item))
{
Destroy(item.gameObject); // 플레이어가 떠났으니, 슬롯 삭제
playerDict.Remove(player.ActorNumber); // 딕셔너리에서도 제거
}
else
{
Debug.LogWarning($"플레이어가 딕셔너리에 없음: {player.ActorNumber}");
}
}
}

방장이 방을 생성 후, 클론 클라이언트가 해당 방에 입장했을 때, 방장 클라이언트의 룸 패널 상황

클론 클라이언트가 게임 종료 시, 방장의 룸 패널 상황
방을 떠날 때 해야 하는 작업들이다.
플레이어가 입장/퇴장 시 추가/삭제되는 프리팹의 스크립트
public class RoomListItem : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI roomNameText;
[SerializeField] private TextMeshProUGUI playerCountText;
[SerializeField] private Button joinButton;
private string roomName;
public void Init(RoomInfo info)
{
roomName = info.Name;
roomNameText.text = $"Room Name : {roomName} ";
playerCountText.text = $"{info.PlayerCount} / {info.MaxPlayers}";
joinButton.onClick.AddListener(JoinRoom);
}
public void JoinRoom()
{
joinButton.onClick.RemoveAllListeners();
PhotonNetwork.JoinRoom(roomName);
}
}
방을 떠날 때 작업을 추가해주자. 해당 방의 정보들을 지운다.
public void LeaveRoom() // 본인이 방을 떠날 때 호출
{
// 현재 있었던 방의 정보를 지움
foreach (Player player in PhotonNetwork.PlayerList)
{
Destroy(playerDict[player.ActorNumber].gameObject);
}
playerDict.Clear();
PhotonNetwork.LeaveRoom();
}

만들어진 방에 접속 후, 떠난다. 다시 방에 접속하려고 하면 해당 에러가 발생한다. 분명 OnJoinedLobby() 콜백함수 이후에 한 것인데도 불구하고, 에러가 발생한다.

현재 상태를 보면 Joining이라고 나온다.
분명 방에 접속 전에는 상태가 JoinedLobby였는데, 왜 에러는 로비에 접속을 못했다고 하는 것인지 모르겠다. Joining이 로비에 대한 접속인 것 같다.

일시정지를 풀면, 정상적으로 방에 접속은 된다.
알아보니, 콜백으로 로비에 참가했어도, 실제로는 로비에 참가 이후 후처리가 남아있다고 한다. 이 후처리들이 끝나기도 전에 다시 방에 참가하려고 하니까 문제가 생긴 것. 아래와 같이 방에 참가하려는 것에 조건을 걸어주자.
클라이언트의 상태가 JoinedLobby로 바뀌는 것은 후처리까지 끝난 이후다.
public void JoinRoom()
{
if (PhotonNetwork.NetworkClientState == ClientState.JoinedLobby)
{
PhotonNetwork.JoinRoom(roomName);
}
joinButton.onClick.RemoveAllListeners();
}