로비에 입장하면 포톤 클라우드 서버는 현재 생성된 모든 룸 목록을 전달해준다. 로비에 접속하지 않고 포톤 클라우드 서버에만 접속한 경우에는 룸 목록을 받을 수 없다. PhtonManager 스크립트에 OnRoomListUpdate콜백 함수를 추가해 룸 목록을 전달 받는지를 확인해보자.

 

public  override void OnJoinedRoom() 
 //...중략
public override void OnRoomListUpdate(List<RoomInfo> roomList) {
    foreach (var roomInfo in roomList) {
        // 룸이 삭제된 경우
        Debug.Log($"Room={roomInfo.Name} ({roomInfo.PlayerCount}/{roomInfo.MaxPlayers})");
    }
}
//...중략

OnRoomListUpdate함수는 RoomInfo타입의 데이터를 리스트 자료형으로 넘겨준다. 다음 그림은 새로 빌드한 실행 파일로 세개의 룸을 생성하고 3번째 유저는 ROOM_034 룸에 입장했다. 마지막으로 유니티를 실행해 로비까지만 입장한 상태에서 콘솔뷰를 살펴보자 룸의 목록이 표시되고 ROOM_034 룸의PlayerCount와MaxPlayer가 2/20으로 표시된 것을 알 수 있다.

 

룸목록은 룸정보의 변화가 발생할 때마다 콜백 함수가 호출된다. 다만 삭제된 룸에 대한 정보도 넘어온다.

룸의 삭제여부는 RemovedFromList속성으로 알수 있다. 따라서 룸목록을 Dictionary타입의 자료형으로 관리한다. 앞서 작성했던 OnRoomListUpdate함수를 다음과 같이 수정한다.

 

룸 목록은 룸 이름과 해당 룸을 ScrollView하위에 생성할 RoomItem프리팹을 쌍으로 저장해야 관리하기 편리하기 때문에 dictionary자료형을 사용한다. c#에서 Dictionary를 사용하기 위해서 System.Collections.Generic 네임스페이스를 선언한다.

using System.Collections.Generic;

클래스 선언부에 룸목록에 대한 데이터를 저장할 rooms를 선언한다.

private Dictionary<string, GameObject> rooms = new Dictionary<string, GameObject>();

RoomItem프리팹을 로드할 변수를 Awake()에서 할당한다.

// RoomItem 프리팹 로드
roomItemPrefab = Resources.Load<GameObject>("RoomItem");

룸 목록이 갱신되면 호출되는 OnRoomListUpdate함수의 로직을 간단히 설명하면

 public override void OnRoomListUpdate(List<RoomInfo> roomList) {
        // 삭제된 RoomItem 프리팹을 저장할 임시변수
        GameObject tempRoom = null;

        foreach (var roomInfo in roomList) {
            if (roomInfo.RemovedFromList == true) {  // 룸이 삭제된 경우
                // 딕셔너리에서 룸 이름으로 검색해 저장된 RoomItem 프리팹를 추출
                rooms.TryGetValue(roomInfo.Name, out tempRoom);     
                Destroy(tempRoom); // RoomItem 프리팹 삭제        
                rooms.Remove(roomInfo.Name); // 딕셔너리에서 해당 룸 이름의 데이터를 삭제
            } else // 룸 정보가 변경된 경우
              {
                // 룸 이름이 딕셔너리에 없는 경우 새로 추가
                if (rooms.ContainsKey(roomInfo.Name) == false) {
                    // RoomInfo 프리팹을 scrollContent 하위에 생성
                    GameObject roomPrefab = Instantiate(roomItemPrefab, scrollContent);
                    // 룸 정보를 표시하기 위해 RoomInfo 정보 전달
                    roomPrefab.GetComponent<RoomData>().RoomInfo = roomInfo;

                    // 딕셔너리 자료형에 데이터 추가
                    rooms.Add(roomInfo.Name, roomPrefab);
                } else // 룸 이름이 딕셔너리에 없는 경우에 룸 정보를 갱신
                  {
                    rooms.TryGetValue(roomInfo.Name, out tempRoom); 룸검색해 RoomItem을 tempRoom에 저장
                    tempRoom.GetComponent<RoomData>().RoomInfo = roomInfo;  //룸 정보 전달
                }
            }
            Debug.Log($"Room={roomInfo.Name} ({roomInfo.PlayerCount}/{roomInfo.MaxPlayers})");
        }
    }

버튼 이벤트 동적 연결

OnRoomListUpdate() 함수에서 룸에 대한 정보에 따라서  RoomItem프리팹을 동적으로 생성하거나 이미 만들어진 RoomItem 프리팹에 룸 정보를 저장하고 클릭했을 때 버튼 이벤트에서 룸에 접속하기 위해 새로운 스크립트를 생성하고 이름을 RoomData로 지정한다. 스크립트는 다음과 같이 작성하고 프로젝트 뷰의 Resource 폴더에 있는 RoomItem 프리팹에 추가한다.

using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using TMPro;

public class RoomData : MonoBehaviour {
    private RoomInfo _roomInfo;
    private TMP_Text roomInfoText;   // 하위에 있는 TMP_Text를 저장할 변수
    private PhotonManager photonManager; // PhotonManager 접근 변수

    // 프로퍼티 정의
    public RoomInfo RoomInfo {
        get {
            return _roomInfo;
        }
        set {
            _roomInfo = value;
            // 룸 정보 표시
            roomInfoText.text = $"{_roomInfo.Name} ({_roomInfo.PlayerCount}/{_roomInfo.MaxPlayers})";
            // 버튼 클릭 이벤트에 함수 연결
            GetComponent<UnityEngine.UI.Button>().onClick.AddListener(() => OnEnterRoom(_roomInfo.Name));
        }
    }

    void Awake() {
        roomInfoText = GetComponentInChildren<TMP_Text>();
        photonManager = GameObject.Find("PhotonManager").GetComponent<PhotonManager>();
    }

    void OnEnterRoom(string roomName) {
        // 유저명 설정
        photonManager.SetUserId();
        //룸의 속성 정의
        RoomOptions ro =  new RoomOptions();
        ro.MaxPlayers = 20;  //룸에 입장할 수 있는 최대 접속자수
        ro.IsOpen = true;  // 룸의 오픈 여부
        ro.IsVisible = true; //로비에서 룸 목록에 노출시킬지 여부
        // 룸 접속
        PhotonNetwork.JoinRoom(roomName);
    }
}

RoomData 스크립트는 OnRoomListUpdate에서 룸정보가 갱신될때 마다 접근해 roomInfo 데이터를 넘겨받아서 내부적으로 저장하고하위에 있는 텍스트 UI에 룸 이름과 접속자 정보를 표시한다. 또한 버튼을 클릭했을 때 룸에 접속하는 함수를 람다식으로 연결한다.

 

//룸정보 표시
roomInfoText.text = $"{_roomInfo.Name} ({_roomInfo.PlayerCount}/{_roomInfo.MaxPlayers})";
// 버튼 클릭 이벤트에 함수 연결
GetComponent<UnityEngine.UI.Button>().onClick.AddListener(() => OnEnterRoom(_roomInfo.Name));

예제 게임을 다시 빌드하고 룸을 생성하고 입장한 후 유니티 에디터에서 로비에 입장만 한 상태에서 룸 목록이 표시 되는지 확인해보자. 다음은 룸을 2개 생성하고 2명의 유저는 같은 방에 들어가고 한명의 유저는 혼자 룸에 입장한 시나리오일 때의 룸 목록이다. 룸목록에 표시된 버튼을 클릭하며 해당 룸으로 입장하는지 확인해보자
   

두번 째로 로그인한 유저의 룸 접속 방식은 랜덤 매치메이킹을 통해 자동입장하는것인데 이제 직접 룸을 만든후 다른 유저는 룸의 목록을 조회하고 선택후 입장할 수 있게 해보자

 

Scroll Rect 컴포넌트

먼저 하이라키뷰의 Canvas 하위에 있는 Panel-Login을 선택하고 새로운 PanelUI항목을 추가한다. Panel-RoomList로 변경하고 앵커프리셋을 가로 세로 모두 Stretch로 설정한다. 상하좌우 여백은 다음과 같이 적적히 생성한다.

 Panel-RoomList을 선택하고 UI->ScrollView를 추가한다. 앵커프리셋을 가로세로 Stretch로 설정한다.

ScrollView오브젝트의  ScrollRect컴포넌트의 Horizontal은 언체크한다.

 Scroll View->Viewport 하위의 것들은 실제 스크롤링될 객체이며 룸 목록객체를 정렬하는 역할을 한다.  Content 속성에는 이미 Content 게임오브젝트가 연결됐다.

 

스크롤 객체 생성

로비에 접속했을 때 포톤 클라우드 서버에서 보내주는 룸 목록을 수신받아 동적으로 구성해야 하기 때문에 UI를 디자인하는 시점에서 생성될 룸의 개수가 몇 개인지 알 수 없으며, 가변적일 수 밖에 없다.  따라서 룸 정보를 표시하는 UI를 프리팹으로 미리 만들어두고 로비에 접속했을때 수신된 룸 개수 만큼 반복문을 반복하면서 동적으로 룸정보를 표시하는 프리팹을 Scroll View-ViewPort-Contents하위에 생성하는 방식으로 룸 목록을 구현한다.

하이라키 뷰에서 Contets를 선택하고 컨텍스트 메뉴에서 UI-Image를 선택해 생성한다. 이름을 RoomItem으로 변경하고 Source Image속성은 백그라운드로 설정한다. 백그라운다 이미지는 속성오른쪽 Image Browser를 열어 검색해서 선택한다.

RommItem차일드로 Text Mesh Pro를 추가하고 이름을 Text-RoomInfo로 변경한다

RommItem은 상하 방향으로 스크롤되는 객체이지만 동시에 클릭할 수 있어야한다. 따라서 Button컴포넌트를 추가해야 하지만 동적으로 생성되기 때문에 스크립트에서 처리한다.

 

Grid Layout Group컴포넌트

지금까지 만든 RoomItem을 실행시점에서 동적으로 생성하기 위해 프로젝트뷰의 Resources폴더로 옮겨 프리팹으로 만든다.

실행후 로비에 접속시 이미 5개의 룸이 생성되어 있다면 위 아래로 나란히 정렬되게 해야한다. Layout계열의 컴포넌트를 이용하면 손쉽게 정렬 기능을 구현할 수 있다.

RoomItem을 5개쯤 카피한다. 위치가 겹쳐져있다. 

Content를 선택후 인스펙터에서 AddComponent-[Component][Layout][Grid Layout Group]을 선택해 Grid Layout Group 컴포넌트를 추가하면 자동정렬된다.

Grid Layout Group 속성을 다음과 같이 설정한다.

예제 게임을 실행해 룸 목록을 스크롤해보면 상하로 스크롤 되는걸 확인할 수 있다.

룸에 입장한후 로드된 BattleField씬에서는 아무런 동작도 없이 단순히 씬만 전환된것 뿐이다 .Lobby씬에서 유저명을 입력받아 네트워크상 식별자를 부여한 후 무작위 추출된 룸에 입장하고 BattleField씬으로 전환된것 이다. 따라서 BattleField씬으로 넘어왔다는 것은 이미 룸에 입장한 상태이므로 주인공 캐릭터를 생성하는 로직을 구현해야 한다.

배틀필드 씬을 열어 하이라키뷰에 빈 게임오브젝트를 하나 만들고 이름을 GameManager로 지정한다. 주인공을 생성하는 로직을 처리할 GameManager스크립트를 만들고 추가한다.

using UnityEngine;
using Photon.Pun;

public class GameManager : MonoBehaviourPunCallbacks {

    void Awake() {
        CreatePlayer();
    }

    void CreatePlayer() {
        // 출현 위치 정보를 배열에 저장
        Transform[] points = GameObject.Find("SpawnPointGroup").GetComponentsInChildren<Transform>();
        int idx = Random.Range(1, points.Length);

        // 네트워크상에 캐릭터 생성
        PhotonNetwork.Instantiate("Player",
                                  points[idx].position,
                                  points[idx].rotation,
                                  0);
    }
}

메뉴 [File][Build settings..]를 선택해 Lobby와 BattleField씬을 끌어다 놓는다. 위쪽의 씬이 먼저 실행된다

빌드후 유니티는 로비씬에서 실행한다.

2개의 게임은 로그인 상태이고 로그인 버튼을 먼저 누른 우저가 룸을 생성해 입장한다.

 

하이라키뷰의 Player(Clone)를 선택해 인스펙터뷰에서 PhotonView컴포넌트를 보면 owner속성에 User ID값이 설정된 것을 확인할 수 있다. 

네트워크 게임상 각 플레이어를 식별하기 위해 유저명을 입력받아야 한다. 먼저 UI를 구성하자. 로비 UI구현에 사용할 텍스트는 TextMeshPro를 사용해보자. 메뉴에서 [Window][TextMeshPro][Import TMP Essential Resources]를 선택해 초기화한다.

Canvas를 생성한 후 다음과 같이 로비 UI에서 유저명과 생성할 룸의 이름을 입력받을 Text 항목과 Button을 각각 추가한다. Canvas Scaler의 UI Scale Mode 속성은 Scale With Screen Size로 설정하고 진행한다. UI가 너무 커서 Resolution을 조정후

하이라키뷰의 Canvas를 더블클릭하고 씬뷰를 2D로 만든후 보기 좋게 하고 다음과 같이 배치한다.

로그인 UI구성을 완료한 후 유저명을 입력받아 설정하고 룸을 생성하는 로직을 구현해 본다. PhotoneManager스크립트를 다음과 같이 수정한다.

using TMpro 스페이스를 추가한다.

using System.Collections.Generic;
using UnityEngine;
using TMPro;  //TextMeshPro UI를 위한 네임스페이스
using Photon.Pun;  //포톤 API를 위한 네임스페이스
using Photon.Realtime; //포톤 API를 위한 네임스페이스

 UI 필드를 참조할 변수를 설정한다.

public TMP_InputField userIF; // 유저명을 입력할 TextMeshPro Input Field
public TMP_InputField roomNameIF; // 룸 이름을 입력할 TextMeshPro Input Field

Awake()내의 NickName은 참조처리한다.

//PhotonNetwork.NickName = userId;  //접속 유저의 닉네임 설정

처음 실행했을때 유저명을 자동으로 InputField에 표시하기 위해 PlayerPrefab을 이용 유저명을 로드하고 표시한다. 저장된 유저명이 없다면 랜덤으로 지정한다.

void Start()
{
    // 저장된 유저명을 로드
    userId = PlayerPrefs.GetString("USER_ID", $"USER_{Random.Range(1,21):00}");
    userIF.text = userId;
    // 접속 유저의 닉네임 등록
    PhotonNetwork.NickName = userId;  
}

로그인 UI의 [Login]버튼 또는 [Make Room]버튼을 클릭했을때, 유저명의 변경 사항을 최종확인하고 PlayerPrefs를 사용해 유저명을 저장한후PhotoneNetwork.NickName을 설정한다.

// 유저명을 설정하는 로직
public void SetUserId()
{
    if (string.IsNullOrEmpty(userIF.text))
    {
        userId = $"USER_{Random.Range(1,21):00}";
    }
    else
    {
        userId = userIF.text;
    }

    // 유저명 저장
    PlayerPrefs.SetString("USER_ID", userId);
    // 접속 유저의 닉네임 등록
    PhotonNetwork.NickName = userId;
}

게임 실행시 자동 입장을 방지하기 위해 PhotonNetwork.JoinRandomRoom() 주석처리한다

// 로비에 접속 후 호출되는 콜백 함수
public override void OnJoinedLobby() {
    Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
    //PhotonNetwork.JoinRandomRoom();     수동입장을 위해 주석처리
}

무작위 룸에 입장을 시도했다가 실패했을때 호출되는 OnJoinedRandomFailed 콜백 함수도 룸을 생성하는 로직을 주석 처리하고 OnMakeRoomClick 함수를 호출하도록 수정한다.

// 랜덤한 룸 입장이 실패했을 경우 호출되는 콜백 함수
public override void OnJoinRandomFailed(short returnCode, string message) {
    Debug.Log($"JoinRandom Filed {returnCode}:{message}");
    OnMakeRoomClick();
      // 룸의 속성 정의
    //RoomOptions ro = new RoomOptions();
    //ro.MaxPlayers = 20;     // 룸에 입장할 수 있는 최대 접속자 수
    //ro.IsOpen = true;       // 룸의 오픈 여부
    //ro.IsVisible = true;    // 로비에서 룸 목록에 노출시킬 여부

    // 룸 생성
    //PhotonNetwork.CreateRoom("My Room", ro);
}

룸에 입장한후 호출되는 OnJoinedRoom 콜백함수에서 캐릭터를 생성하는 로직은 주석처리하고 BattleField씬을 호출한다. Phtoneview가 추가된 주인공 캐릭터는 BattleField씬에서 생성한다. 씬을 로딩하는 함수는 유니티가 제공하는 SceneManagement.SceneManager.LoadScene함수대신 PhotoneNetwork.LoadLevel함수를 이용한다.  이 함수는 씬을 로딩전 데이터 송수신을 잠시 멈추고 재개하는 로직이 포함되어 있다. 만일 수동으로 처리한다면 PhotoneNetwork.IsMessageQueueRunning 속성을 false로 지정한후 로딩후 true로 변경해야한다.

씬의 로딩은 마스터 클라이언트만 호출해야 한다. 룸에 입장한 다른 네트워크유저는 PhotonNetwork.AutomaticallySyncScene을 true로 설정했기 때문에 마스터 클라이언트가 다른 씬을 로딩하면 자동으로 씬이 로딩된다.

// 룸에 입장한 후 호출되는 콜백 함수
public override void OnJoinedRoom() {
    Debug.Log($"PhotonNetwork.InRoom = {PhotonNetwork.InRoom}");
    Debug.Log($"Player Count = {PhotonNetwork.CurrentRoom.PlayerCount}");
    foreach (var player in PhotonNetwork.CurrentRoom.Players) {
        Debug.Log($"{player.Value.NickName} , {player.Value.ActorNumber}");
    }
    //출연위치를 배열에 저장
    //Transform[] points = GameObject.Find("SpawnPointGroup").GetComponentsInChildren<Transform>();
    //int idx = Random.Range(1, points.Length);
    //네트워크에 캐릭터 생성
    //PhotonNetwork.Instantiate("Player", points[idx].position, points[idx].rotation, 0);
    // 마스터 클라이언트인 경우에 룸에 입장한 후 전투 씬을 로딩한다.
    if (PhotonNetwork.IsMasterClient) {
        PhotonNetwork.LoadLevel("BattleField");
    }
}

OnLoginClick()함수는 UI의[Login]버튼의 클릭 이벤트에 연결할 함수로 유저명을 설정하고 무작위로 선택한 룸에 입장한다.

public void OnLoginClick() {
    // 유저명 저장
    SetUserId();

    // 무작위로 추출한 룸으로 입장
    PhotonNetwork.JoinRandomRoom();
}

OnMakeRoomClick() 함수 역시 로그인 UI [Maker Room] 버튼에 연결할 함수 이며 입력한 룸 이름으로 룸을 생성한다.

public void OnMakeRoomClick() {
    // 유저명 저장
    SetUserId();

    // 룸의 속성 정의
    RoomOptions ro = new RoomOptions();
    ro.MaxPlayers = 20;     // 룸에 입장할 수 있는 최대 접속자 수
    ro.IsOpen = true;       // 룸의 오픈 여부
    ro.IsVisible = true;    // 로비에서 룸 목록에 노출시킬 여부

    // 룸 생성
    PhotonNetwork.CreateRoom(SetRoomName(), ro);
}

PhotonManager.cs
0.00MB

전체코드

using System.Collections.Generic;
using UnityEngine;
using TMPro;  //TextMeshPro UI를 위한 네임스페이스
using Photon.Pun;  //포톤 API를 위한 네임스페이스
using Photon.Realtime; //포톤 API를 위한 네임스페이스


public class PhotonManager : MonoBehaviourPunCallbacks {
    private readonly string version = "1.0"; // 게임의 버전
    private string userId = "Zack"; // 유저의 닉네임  
                                    
    public TMP_InputField userIF; // 유저명을 입력할 TextMeshPro Input Field
    public TMP_InputField roomNameIF; // 룸 이름을 입력할 TextMeshPro Input Field

    void Awake() {      
        PhotonNetwork.AutomaticallySyncScene = true;  // 마스터 클라이언트의 씬 자동 동기화 옵션
        PhotonNetwork.GameVersion = version; // 게임 버전 설정
        //PhotonNetwork.NickName = userId;  //접속 유저의 닉네임 설정
        Debug.Log(PhotonNetwork.SendRate);// 포톤 서버와의 데이터의 초당 전송 횟수
        PhotonNetwork.ConnectUsingSettings(); // 포톤 서버 접속
    }
    void Start() {
        // 저장된 유저명을 로드
        userId = PlayerPrefs.GetString("USER_ID", $"USER_{Random.Range(1, 21):00}");
        userIF.text = userId;
        // 접속 유저의 닉네임 등록
        PhotonNetwork.NickName = userId;
    }

    // 유저명을 설정하는 로직
    public void SetUserId() {
        if (string.IsNullOrEmpty(userIF.text)) {
            userId = $"USER_{Random.Range(1, 21):00}";
        } else {
            userId = userIF.text;
        }

        // 유저명 저장
        PlayerPrefs.SetString("USER_ID", userId);
        // 접속 유저의 닉네임 등록
        PhotonNetwork.NickName = userId;
    }
    // 룸 명의 입력여부를 확인하는 로직
    string SetRoomName() {
        if (string.IsNullOrEmpty(roomNameIF.text)) {
            roomNameIF.text = $"ROOM_{Random.Range(1, 101):000}";
        }

        return roomNameIF.text;
    }
    public override void OnConnectedToMaster() {
        Debug.Log("Connected to Master!");
        Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
        PhotonNetwork.JoinLobby();
    }

    // 로비에 접속 후 호출되는 콜백 함수
    public override void OnJoinedLobby() {
        Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
        //PhotonNetwork.JoinRandomRoom();     수동입장을 위해 주석처리
    }

    // 랜덤한 룸 입장이 실패했을 경우 호출되는 콜백 함수
    public override void OnJoinRandomFailed(short returnCode, string message) {
        Debug.Log($"JoinRandom Filed {returnCode}:{message}");
        OnMakeRoomClick();
          // 룸의 속성 정의
        //RoomOptions ro = new RoomOptions();
        //ro.MaxPlayers = 20;     // 룸에 입장할 수 있는 최대 접속자 수
        //ro.IsOpen = true;       // 룸의 오픈 여부
        //ro.IsVisible = true;    // 로비에서 룸 목록에 노출시킬 여부

        // 룸 생성
        //PhotonNetwork.CreateRoom("My Room", ro);
    }

    // 룸 생성이 완료된 후 호출되는 콜백 함수
    public override void OnCreatedRoom() {
        Debug.Log("Created Room");
        Debug.Log($"Room Name = {PhotonNetwork.CurrentRoom.Name}");
    }

    // 룸에 입장한 후 호출되는 콜백 함수
    public override void OnJoinedRoom() {
        Debug.Log($"PhotonNetwork.InRoom = {PhotonNetwork.InRoom}");
        Debug.Log($"Player Count = {PhotonNetwork.CurrentRoom.PlayerCount}");
        foreach (var player in PhotonNetwork.CurrentRoom.Players) {
            Debug.Log($"{player.Value.NickName} , {player.Value.ActorNumber}");
        }
        //출연위치를 배열에 저장
        //Transform[] points = GameObject.Find("SpawnPointGroup").GetComponentsInChildren<Transform>();
        //int idx = Random.Range(1, points.Length);
        //네트워크에 캐릭터 생성
        //PhotonNetwork.Instantiate("Player", points[idx].position, points[idx].rotation, 0);
        // 마스터 클라이언트인 경우에 룸에 입장한 후 전투 씬을 로딩한다.
        if (PhotonNetwork.IsMasterClient) {
            PhotonNetwork.LoadLevel("BattleField");
        }
    }
    #region UI_BUTTON_EVENT

    public void OnLoginClick() {
        // 유저명 저장
        SetUserId();

        // 무작위로 추출한 룸으로 입장
        PhotonNetwork.JoinRandomRoom();
    }

    public void OnMakeRoomClick() {
        // 유저명 저장
        SetUserId();

        // 룸의 속성 정의
        RoomOptions ro = new RoomOptions();
        ro.MaxPlayers = 20;     // 룸에 입장할 수 있는 최대 접속자 수
        ro.IsOpen = true;       // 룸의 오픈 여부
        ro.IsVisible = true;    // 로비에서 룸 목록에 노출시킬 여부

        // 룸 생성
        PhotonNetwork.CreateRoom(SetRoomName(), ro);
    }

    #endregion
}

스크립트를 작성 저장후  로그인 UI의 InputField(TMP)-UserID와 InputField(TMP)-Room을 PhtonManager의 User IF, Room Name IF에 각각 드래그해 연결한다.

하이라키뷰의 [Canvas][Button-Login]을 선택하고 인스펙터의 OnClick에서 +를 눌러 이벤트를 하나 만들어 주고 하이라키뷰에서 PhotonManager를 끌어다 놔주면 활성화되는데 

No Fuction을 선택해 OnLoginClick()을 지정해준다.

같은 방법으로 Room Button도 OnMakerRoomClick을 지정해준다.

게임룸 입장

이제 실행해보면 씬이 로드되지 않았다는 에러가 나서

File-BuildSetting에서 우리가 만든 2개의 씬을 추가해주었다

이제 실행해보면 User ID 입력필드에 무작위 이름이 표시된다.

여러분의 이름을 직접입력하고 [Login]버튼을 누르면 입장되고 콘솔뷰에 유저명과 룸에 접속한 접속자 수가 표시된다.

또한 씬이 BattleField로 변경된것을 확인할 수 있다.

룸에 입장한후 로드된 BattleField씬에서는 아무런 동작도 없이 단순히 씬만 전환된것 뿐이다 .Lobby씬에서 유저명을 입력받아 네트워크상 식별자를 부여한 후 무작위 추출된 룸에 입장하고 BattleField씬으로 전환된것 이다. 따라서 BattleField씬으로 넘어왔다는 것은 이미 룸에 입장한 상태이므로 주인공 캐릭터를 생성하는 로직을 구현해야 한다.

로비는 네트워크에 접속한 모든 플레이어가 대기하며 방을 생성하거나 다른방에 입장할 수 있는 기능을 제공해야 한다. 포톤 클라우드에서 로비에 접속해야만 현재 생성된 룸의 정보를 서버로부터 받아올수 있다. 예제 게임에서는 이미 로비에 입장했기 때문에 룸 정보를 받을 준비가 끝났다.

 로비씬 제작

로비를 구성하기 위해 현재 열려있는 SampleScene을 저장후 프로젝트뷰 Scenes폴더에서 이름을 BattleField로 변경한후 복제해 로비씬으로 사용한다.  씬을 선택하고  Ctrl-D으로 씬을 복제하낟. 복제한 씬의 이름을 Lobby로 변경한다.

이제부터 포톤 클라우드에 접속하는 과정은 Lobby씬에서 처리한다. 따라서 BattleField씬에 있는 PhotonManager 게임오브젝트는 삭제한다.

이제 로비씬을 선택해 수정한다.

로비 씬에서는 주인공을 추적하는 기능이 필요없기 때문에 CM vcam1 게임오브젝트와 Main Camera의 cinemachine Brain 컴포넌트도 삭제한다.

프로젝트뷰의 AngryBotResource/Prefabs폴더의 Player를 하이라키뷰에 추가하고 적절한 위치에 배치한다. 또한 게임 뷰를 보면서 Main Camera 위치와 각도를 다음과 같이 설정한다.

*  씬뷰에서 적당한 화면 구도를 잡는다. 하이라뷰의 Main Camera를 선택후 Ctrl-Shit-F를 누르면 Align with view가 실행된다.

로비씬에는 시각적 효과를 위해 피사계심도와 색보정 효과를 추가해보겠다.

로비씬은 BattleField씬을 복사했기 때문에 Global Volume은 같은 Profile을 공유하고 있다.  따라서 New옆의 Clone을 클릭해 Lobby씬의 Post Processing 효과를 위한 새로운 Profile을 만들어야 한다.

Add Override로 Depth of Field, Color Adjustment후처리를 추가한다 

효과는 씬뷰에서는 확인이 안되고 게임뷰에서 확인할 수 있다

 

같은 룸에 입장한 네트워크 유저간의 RPC를 이용해 총알이 발사되는 로직을 완성했다. 이제 상대편 네트워크 유저가 발사한 총알에 데미지를 입고 사망하는 로직을 구현하자.  그 반대도 같은 로직이다. 또 사망한 후 일정시간이 지난 후 리스폰(재생,부활) 되게 한다.  이 처리는 새로운 스크립트에서 처리한다. 스크립트를 새로 만들고 이름을 Damage라고 하자 프로젝트뷰의 Resoures폴더에 있는 Player프리팹에 추가한 후 다음과 같이 작성한다.

핵심은 총알을 맞아서 생명수치 hp값이 0이하일 때 캐릭터의 Die 애니메이션을 실행한 후 화면상에 보이지 않게 모든 Renderer 컴포넌트를 비활성화 하는 것이다. 리스폰 시간이 지난 후 불규칙한 위치로 이동시키고 다시 Renderer 컴포넌트를 활성화해 화면에 표시한다.

using System.Collections;
using UnityEngine;


public class Damage : MonoBehaviour { // MonoBehaviourPunCallbacks {
    private Renderer[] renderers;  // 사망 후 투명 처리를 위한 MeshRenderer 컴포넌트의 배열
    private int initHp = 100;  // 캐릭터의 초기 생명치
    public int currHp = 100;  // 캐릭터의 현재 생명치
     private Animator anim;
    private CharacterController cc;

    // 애니메이터 뷰에 생성한 파라미터의 헤시값 추출
    private readonly int hashDie = Animator.StringToHash("Die");
    private readonly int hashRespawn = Animator.StringToHash("Respawn");



    void Awake() {
        // 캐릭터 모델의 모든 Renderer 컴포넌트를 추출한 후 배열에 할당
        renderers = GetComponentsInChildren<Renderer>();
        anim = GetComponent<Animator>();
        cc = GetComponent<CharacterController>(); 
        currHp = initHp;  //현재 생명치를 초기 생명치로 초깃값 설정

    }

    void OnCollisionEnter(Collision coll) {
        // 생명 수치가 0보다 크고 충돌체의 태그가 BULLET인 경우에 생명 수치를 차감
        if (currHp > 0 && coll.collider.CompareTag("BULLET")) {
            currHp -= 20;
            if (currHp <= 0) {
                // 자신의 PhotonView 일 때만 메시지를 출력

                StartCoroutine(PlayerDie());
            }
        }
    }

    IEnumerator PlayerDie() {
        cc.enabled = false; // CharacterController 컴포넌트 비활성화
        anim.SetBool(hashRespawn, false);  // 리스폰 비활성화
        anim.SetTrigger(hashDie); // 캐릭터 사망 애니메이션 실행
        yield return new WaitForSeconds(3.0f);   
        anim.SetBool(hashRespawn, true); // 리스폰 활성화     
        SetPlayerVisible(false); // 캐릭터 투명 처리
        yield return new WaitForSeconds(1.5f);

        currHp = 100;    // 리스폰 시 생명 초깃값 설정       
        SetPlayerVisible(true); // 캐릭터를 다시 보이게 처리
        cc.enabled = true;  // CharacterController 컴포넌트 활성화
    }

    //Renderer 컴포넌트를 활성/비활성화하는 함수
    void SetPlayerVisible(bool isVisible) {
        for (int i = 0; i < renderers.Length; i++) {
            renderers[i].enabled = isVisible;
        }
    }
}

소스코드와 프로젝트를 모두 저장후 빌드후 캐릭터가 피격후 사망후 리스폰되는지 확인해보자.

블러거는 실행후 BULLET Tag가 없다고 해서 Resources/Prefab/Bullet 에서 BULLET Tag를 만든후 적용해 주었다

 

 

원격 네트워크 유저에게 총알을 발사하는 로직을 만들어 보자. 앞서 이동 로직에서 구현했듯이 총알 프리팹에 PhotonView컴포넌트를 추가해 생상하면 아주 간단하게 만들수 있겠지만 이는 잘못된 방법이다.

PhotonView 컴포넌트는 초당 20회 데이터를 전송하기 때문에 스테이지에 많은 총알을 생성하면 생성된 모든 총알에서 트래픽이 발생하기 때문에 이런 이벤트성 동작을 네트워크 유저와 공유할 때는 RPC(Remote Procedure Calls)를 통해 구현하는 것이 일방적인 방식이다.

RPC : 원격 프로시저 호출은 물리적으로 떨어져 있는 다른 디바이스의 함수를 호출하는 기능으로 RPC함수를 호출하면 네트워크를 통해 다른 사용자의 스크립트에서 해당 함수가 호출된다. 비슷한 개념으로  RMI(Remote Method Invocation)가 있다.

총알로 사용할 Bullet은 미리 만들어진 프리팹을 사용한다. 임포트한 AngryBotResorces/Prefabs 하위의 Bullet 프리팹을 씬뷰로 드래그해 확인해보자. Bullet프리팹은 Capsulre Collider와 Rigidbody 컴포넌트로 구성된다.  BULLET태그룰 생성하고 지정한다.

Bullet Prefab원본에 Bullet 스크립트를 연결한다. 

Start()함수에서 Force로 발사  되게 만들고 충돌시 충돌지점에 스파크이펙트를 생성하는 스크립트이다.

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

public class Bullet : MonoBehaviour {
    public GameObject effect;
    // 총알을 발사한 플레이어의 고유번호
    public int actorNumber;

    void Start() {
        GetComponent<Rigidbody>().AddRelativeForce(Vector3.forward * 1000.0f);
        // 일정시간이 지난 후 총알을 삭제
        Destroy(this.gameObject, 3.0f);
    }

    void OnCollisionEnter(Collision coll) {
        // 충돌 지점 추출
        var contact = coll.GetContact(0);
        // 충돌 지점에 스파크 이펙트 생성
        var obj = Instantiate(effect,
                              contact.point,
                              Quaternion.LookRotation(-contact.normal));
        Destroy(obj, 2.0f);
        Destroy(this.gameObject);
    }
}

public GameObject effect는 AngryBotResources/Particles/Prefab하위의 Bullet_Impact_Wall프리팹을 연결하고 저장한다.

 

유니티를 실행시키면 총알이 발사된다. 그런 하이라키뷰의 총알은 지운다. 이제 총알을 발사할 Fire 스크립트를 생성한다.

void Update() {
    // 로컬 유저여부와 마우스 왼쪽 버튼을 클릭했을 때 총알을 발사
    if (pv.IsMine && isMouseClick) {
        FireBullet(pv.Owner.ActorNumber);
        //RPC로 원격지에 있는 함수를 호출
        pv.RPC("FireBullet", RpcTarget.Others, pv.Owner.ActorNumber);
    }
}

포톤서버에서 일반적인 RPC호출은 PhtoneView.RPC(호출함수명, 호출대상, 전달할 데이터) 함수를 사용한다. 원격으로 호출할 함수명 인자는 string 타입으로 전달하고 호출 대상은 특정 플레이어를 지정하거나 RpcTarget옵션으로 전달 대상의 범위를 지정할 수 있다.

  • All : 자기자신을 포함한 모든 유저에게 함수를 뿌림
  • Others : 자신을 제외한 모든 유저에게 함수를 뿌림
  • MasterClient : 방장에게 함수를 뿌림
  • AllBuffered : 자신을 포함한 모든유저에게 함수를 뿌림, 또한 나중에 입장한 유저는 버퍼에 저장된 RPC를 전달받는다.
  • OtherBuffered : 자신을 제외한 모든유저에게 함수를 뿌림, 나중에 입장한 유저는 버퍼에 저장된 RPC를 전달받는다.
  • AllViaServer : 모든 네트워크 유저에게 거의 동일한 시간에 RPC를 전송하기위해 서버의 모든클라이언트에게 RPC를 동시전송
  • AllBufferedViaServer : AllViaServer와 동일하며, 버퍼에 저장된 RPC를 나중에 입장한 유저에게 전달

총알 발사 로직에서 로컬 FireBullet함수를 호출하지 않고 다음과 같이 RpcTarget.All 옵션을 사용해도 동일한 결과를 볼 수 있다. RpcTarget.All은 Rpc함수를 룸에 입장한 모든 네트워크 유저에 대해 호출하고 로컬 유저는 해당 함수를 즉시 호출한다.

pv.RPC("FireBullet",RpcTarget.All, null);

RPC로 호출할때는 반드시 [PunRPC] 어트리뷰트를 함수앞에 명시해야한다.

[PunRPC]
void FireBullet(int actorNo) {
    // 총구화염 효과가 실행 중이 아닌 경우에 총구 화염효과 실행
    if (!muzzleFlash.isPlaying) muzzleFlash.Play(true);

    GameObject bullet = Instantiate(bulletPrefab,
                                    firePos.position,
                                    firePos.rotation);
    bullet.GetComponent<Bullet>().actorNumber = actorNo;
}

 

Fire.cs 전체코드

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

public class Fire : MonoBehaviour
{
    public Transform firePos;
    public GameObject bulletPrefab;
    private ParticleSystem muzzleFlash;

    private PhotonView pv;
    // 왼쪽 마우스 버튼 클릭 이벤트 저장
    private bool isMouseClick => Input.GetMouseButtonDown(0);

    void Start()
    {
        // 포톤뷰 컴포넌트 연결
        pv = GetComponent<PhotonView>();  
        // FirePos 하위에 있는 총구 화염 효과 연결  
        muzzleFlash = firePos.Find("MuzzleFlash").GetComponent<ParticleSystem>();
    }

    void Update()
    {
        // 로컬 유저여부와 마우스 왼쪽 버튼을 클릭했을 때 총알을 발사
        if (pv.IsMine && isMouseClick)
        {
            FireBullet(pv.Owner.ActorNumber);
            //RPC로 원격지에 있는 함수를 호출
            pv.RPC("FireBullet", RpcTarget.Others, pv.Owner.ActorNumber);
        }        
    }

    [PunRPC]
    void FireBullet(int actorNo)
    {
        // 총구화염 효과가 실행 중이 아닌 경우에 총구 화염효과 실행
        if (!muzzleFlash.isPlaying) muzzleFlash.Play(true);

        GameObject bullet = Instantiate(bulletPrefab,
                                        firePos.position,
                                        firePos.rotation);
        bullet.GetComponent<Bullet>().actorNumber = actorNo;
    }
}

작성된 스크립트를 Resources폴더에 있는 Player프리팹에 추가한다. Resources폴더에 있는 Player 프리팹을 선택한 다음 인스펙터 뷰에서 [Open Prefab] 버튼을 클릭한다. 프리팹 에디터 뷰로 진입하면 하이라키뷰에 있는 Gun/Fire Pos속성에 연결한다. Bullet Prefab 속성에는 프로젝트뷰의 AngryBotResources/Prefabs하위에 있는 Bullet프리팹을 연결한다.

* RPC호출 목적으로만 사용하려면 PhotonView컴포넌트의 Synchronization 속성을 Off로 설정해야 한다.

총알의 발사는 자신의 캐릭터에서만 동작해야 하기 때문에 PhotonView.IsMine 속성을 체크해야 한다. 자신의 경우 로컬 FireBullet함수를 호출하고 원격유저의 캐릭터는 RPC함수를 사용해 원격 FireBullet 함수를 호출한다.

게임을 빌드후 실행해 서로 총알을 발사해 확인해보자.

 

 

지금까지 포톤 서버에 접속하고, 룸을 생성한 다음 입장하는 기능을 구현했다. 이제 주인공 캐릭터를 네트워크 통신이 가능하게 수정해보자. 하이라키뷰에서 Player를 선택하고 메뉴에서 [Component]->[Photon Networking]->[Photon View]를 선택해 PhotonView 컴포넌트를 추가한다.

PhotonView 컴포넌트

플레이어간 데이터를 송수신하는 통신 모듈이다. 동일한 룸에 입장한 다른 플레이어에게 자신의 위치와 회전 정보를 동기화시키고 특정 데이터를 송수신하려면 이 컴포넌트가 필요하다.

Synchronization

  • None : 동기화 처리한지 않는다.
  • Reliable Dellta Compress : 마지막 데이터가 변경되지 않았을 때 데이터를 송신하지 않는다
  • Unreliable : 송신한 패킷의 수신 여부를 확인하지 않는다.
  • Unreliable On Change : Unreliable과 동일한데 변경사항이 발생했을 때만 송신한다.

Observed Components : Photon View 컴포넌트가 관찰해 데이터를 송수신할 대상을 등록하는 속성이다.

주인공 캐릭터는 같은 룸에 입장한 다른 유저에게 자신의 위치와 회전값, 애니메이션에 대한 정보를 동기화해야한다.

  • Photon Transform View, Photon Animation View 컴포넌트를 사용하는 방식
  • OnPhotonSerializeView 콜백 함수를 사용하는 방식

첫번째 방식은 가장 쉽게 네트워크를 동기화 할 수 있지만. 세밀한 조정을 할 수 없으며 레이턴시 발생시 수동으로 보정할 수 없다.  2번째 방식은 Photon의 컴포넌트를 사용하지 않고 수동으로 관리하는 방식이다.

 

Photon Transform View, Photon Animator View

먼저 첫번째 방식으로 컴포넌트를 이용해 구현해보자. 하이라키뷰의 Player를 선택하고 [Component]-[Photon Networking] 하위 Photon Transform View와 Photon Animator View 컴포넌트를 추가해 동기화할 데이터및 속도를 지정할 수 있다.

Photon Animatior View에서 동기화 속도를 설정해야한다. 모드 Discrete로 지정한다.

네트워크 환경에서 생성하기 위한 준비

프로젝트뷰에서 Resouces 폴더를 생성한 후 하이라키 뷰의 Player를 Resources 폴더로 드래그해 프리팹으로 생성한후 하이라키뷰의 Palyer는 삭제한다. 포톤에서 네트워크로 동기화할 대상은 PhotonNetwork.Instantiate 함수를 사용하며 모두 Resources 폴더에 위치해야 한다.

 

다수의 네트워크 유저가 접속하기 때문에 미리 스폰 포인트를 설정하고 그중에서 랜덤한 위치에 생성되게 한다. 8장의 SpawnPointGroup을 생성했던 내용을 참고해 동일하게 생성한다.

접속한 유저가 출현할 불규칙한 위치 정보는 게임 매니저가 알고 있어야 한다. 먼저 빈 게임오브젝트를 생성하고 SpawnPointGroup으로 지정한다. Position은 (0,0,0)이다 SpawnPointGroup를 선택하고 하위에 빈게임오브젝트를 생성하고 Point라고 이름을 바꾼다.

시각적으로 표현하기 위해 MyGizmo.cs를 작성해서 연결한다.

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

public class MyGizmo : MonoBehaviour
{
    public Color _color = Color.yellow;
    public float _radius = 0.1f;
    private void OnDrawGizmos() {
        Gizmos.color = _color;
        Gizmos.DrawSphere(transform.position, _radius);
    }
}

이제 유저가 접속했을때 여러 Spawn Point중에서 랜덤으로 발생시킬것이다. . 

포인트를 Ctr-D로 복사해서 여러군대 배치한다. 이름을 Point_1 ~ Point_9로 변경한다.

PhotonManager 스크립트에서 룸에 입장한 후 주인공 캐릭터를 네트워크상에서 동기화되게 생성한다. OnJoinedRoom  함수를 다음과 같이 수정한다. 

 

// 룸에 입장한 후 호출되는 콜백 함수
public override void OnJoinedRoom() {
    Debug.Log($"PhotonNetwork.InRoom = {PhotonNetwork.InRoom}");
    Debug.Log($"Player Count = {PhotonNetwork.CurrentRoom.PlayerCount}");
    foreach (var player in PhotonNetwork.CurrentRoom.Players) {
        Debug.Log($"{player.Value.NickName} , {player.Value.ActorNumber}");
    }
    //출연위치를 배열에 저장
    Transform[] points = GameObject.Find("SpawnPointGroup").GetComponentsInChildren<Transform>();
    int idx = Random.Range(1, points.Length);
    //네트워크에 캐릭터 생성
    PhotonNetwork.Instantiate("Player", points[idx].position, points[idx].rotation, 0);
}

유니티를 실행후 Player(Clone)이 생성되었는지 확인한다.

런타임(동적)으로 생성되었기 때문에 시네머신 카메라의 Follow와 LookAt속성이 모두 연결이 끊어져 주인공을 따라가지는 않는다. 다음과 같이 Movement.cs를 수정해서 따라가도록 한다. 

using Photon.Realtime 네임스페이스를 추가한다.

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

우선 .Movement Class의 MonoBehaviour속성을 MonoBehaviourPunCallbacks로 변경한다.

public class Movement : MonoBehaviourPunCallbacks{

네트워크에 의해 생성한 캐릭터 참조 pv와 시네머신 가상 카메라를 저장할 변수를 전역변수로 지정한다.

// PhotonView 컴포넌트 캐시처리를 위한 변수
private PhotonView pv;
// 시네머신 가상 카메라를 저장할 변수
private CinemachineVirtualCamera virtualCamera;

자신이 생성한 캐릭터와 네트워크 유저는 Photonview.IsMine속성으로 구별할 수 있다. true일 경우 자신이다.  

Start()에서 연결한 virtualCamera를 pv.IsMine==true일 경우 연결한다.

void Start() {
    controller = GetComponent<CharacterController>();
    transform = GetComponent<Transform>();
    animator = GetComponent<Animator>();
    camera = Camera.main;

    pv = GetComponent<PhotonView>();
    virtualCamera = GameObject.FindObjectOfType<CinemachineVirtualCamera>();
    //PhotonView가 자신의 것일 경우 시네머신 가상카메라를 연결
    if (pv.IsMine) {
        virtualCamera.Follow = transform;
        virtualCamera.LookAt = transform;
    }
    // 가상의 바닥을 주인공의 위치를 기준으로 생성
    plane = new Plane(transform.up, transform.position);
}

네트워크 유저 캐릭터는 PhotonView에 의해서 관리되므로 자신이 생성한 캐릭터만 관리한다

    void Update() {
        // 자신이 생성한 네트워크 객체만 컨트롤
        if (pv.IsMine) {
            Move();
            Turn();
        }
    }

실행후 룸에 접속했을때 주인공 캐릭터가 불규칙한 위치에 생성되고 카메라가 주인공을 따라가는 것을 볼 수 있다. 하이라키뷰에서 Player(Clone)을 선택하고 인스펙터뷰에서 PhotonView의 IsMine속성과 Controller, Owner, Creator에 대한 정보를 확인할 수 있다.

 

PhotonManager.cs
0.00MB
Movement.cs
0.00MB
MyGizmo.cs
0.00MB

포톤은 무작위 입장 함수인 JoinRandomRoom()와 아무방도 없다면 OnJoinRandomFailed() 콜백함수를 제공한다. PhotonManager 스크립트를 다음과 같이 수정한다.

// 로비에 접속 후 호출되는 콜백 함수
public override void OnJoinedLobby() {
    Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
    PhotonNetwork.JoinRandomRoom();        
}

// 랜덤한 룸 입장이 실패했을 경우 호출되는 콜백 함수
public override void OnJoinRandomFailed(short returnCode, string message) {
    Debug.Log($"JoinRandom Filed {returnCode}:{message}");
}

실행해보면 방이 없기 때문에 당연히 OnJoinRandomFailed() 콜백함수가 실행되면 입장할수 없다는 메시지가 나올것이다

 

CreateRoom함수

입장할 수 없다는 메시지 아래 방을 만드는 코드를 추가해보자. 만들 룸의 옵션을 설정후 CreateRoom()을 이용해 만든다.

// 랜덤한 룸 입장이 실패했을 경우 호출되는 콜백 함수
public override void OnJoinRandomFailed(short returnCode, string message) {
    Debug.Log($"JoinRandom Filed {returnCode}:{message}");
      // 룸의 속성 정의
    RoomOptions ro = new RoomOptions();
    ro.MaxPlayers = 20;     // 룸에 입장할 수 있는 최대 접속자 수
    ro.IsOpen = true;       // 룸의 오픈 여부
    ro.IsVisible = true;    // 로비에서 룸 목록에 노출시킬 여부

    // 룸 생성
    PhotonNetwork.CreateRoom("My Room", ro);
}

생성이 완료되는 OnCreatedRoom 콜백함수가 호출되며 실패하면 OnCreateRoomFailed()가 호출된다

// 룸 생성이 완료된 후 호출되는 콜백 함수
public override void OnCreatedRoom() {
    Debug.Log("Created Room");
    Debug.Log($"Room Name = {PhotonNetwork.CurrentRoom.Name}");
}

룸을 만든 유저는 자동으로 방장이되어 방에 입장하면서 OnJoinedRoom콜백 함수가 호출된다. PlayerCount는 1번이다.

// 룸에 입장한 후 호출되는 콜백 함수
public override void OnJoinedRoom() {
    Debug.Log($"PhotonNetwork.InRoom = {PhotonNetwork.InRoom}");
    Debug.Log($"Player Count = {PhotonNetwork.CurrentRoom.PlayerCount}");
}

OnJoinedRoom()함수를 다음과 같이 변경해 룸에 접속한 사용자 정보를 얻어오자 NickName은 동일할수 있고 ActorNumber는 고유하다.

// 룸에 입장한 후 호출되는 콜백 함수
public override void OnJoinedRoom() {
    Debug.Log($"PhotonNetwork.InRoom = {PhotonNetwork.InRoom}");
    Debug.Log($"Player Count = {PhotonNetwork.CurrentRoom.PlayerCount}");
    foreach (var player in PhotonNetwork.CurrentRoom.Players) {
        Debug.Log($"{player.Value.NickName} , {player.Value.ActorNumber}");
    }
}

실행해보면 방을 잘 만들어서 입장하는 걸 볼 수 있다.

PhotonManager스크립트의 전체코드

using UnityEngine;
using TMPro;
using Photon.Pun;  //포톤 API를 위한 네임스페이스
using Photon.Realtime; //포톤 API를 위한 네임스페이스
using System.Collections.Generic;

public class PhotonManager : MonoBehaviourPunCallbacks {
    private readonly string version = "1.0"; // 게임의 버전
    private string userId = "Zack"; // 유저의 닉네임  

    void Awake() {      
        PhotonNetwork.AutomaticallySyncScene = true;  // 마스터 클라이언트의 씬 자동 동기화 옵션
        PhotonNetwork.GameVersion = version; // 게임 버전 설정
        PhotonNetwork.NickName = userId;  //접속 유저의 닉네임 설정
        Debug.Log(PhotonNetwork.SendRate);// 포톤 서버와의 데이터의 초당 전송 횟수
        PhotonNetwork.ConnectUsingSettings(); // 포톤 서버 접속
    }

    public override void OnConnectedToMaster() {
        Debug.Log("Connected to Master!");
        Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
        PhotonNetwork.JoinLobby();
    }

    // 로비에 접속 후 호출되는 콜백 함수
    public override void OnJoinedLobby() {
        Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
        PhotonNetwork.JoinRandomRoom();        
    }

    // 랜덤한 룸 입장이 실패했을 경우 호출되는 콜백 함수
    public override void OnJoinRandomFailed(short returnCode, string message) {
        Debug.Log($"JoinRandom Filed {returnCode}:{message}");
          // 룸의 속성 정의
        RoomOptions ro = new RoomOptions();
        ro.MaxPlayers = 20;     // 룸에 입장할 수 있는 최대 접속자 수
        ro.IsOpen = true;       // 룸의 오픈 여부
        ro.IsVisible = true;    // 로비에서 룸 목록에 노출시킬 여부

        // 룸 생성
        PhotonNetwork.CreateRoom("My Room", ro);
    }

    // 룸 생성이 완료된 후 호출되는 콜백 함수
    public override void OnCreatedRoom() {
        Debug.Log("Created Room");
        Debug.Log($"Room Name = {PhotonNetwork.CurrentRoom.Name}");
    }

    // 룸에 입장한 후 호출되는 콜백 함수
    public override void OnJoinedRoom() {
        Debug.Log($"PhotonNetwork.InRoom = {PhotonNetwork.InRoom}");
        Debug.Log($"Player Count = {PhotonNetwork.CurrentRoom.PlayerCount}");
        foreach (var player in PhotonNetwork.CurrentRoom.Players) {
            Debug.Log($"{player.Value.NickName} , {player.Value.ActorNumber}");
        }
    }
}

 

네트워크 게임에 참여하려면 맨 먼저 포톤 서버에 접속해야한다. 포톤서버는 로비와 룸의 개념이 존재한다.

함수를 통해 로비에 입장하면 룸들의 리스트를 얻을수 있고 특정 룸을 선택해 입장해서 그 방에 있는 유저들과 게임을 함께 할 수 있다.

 

포톤 서버에 접속해 룸을 생성하는 절차를 처리할 스크립트를 만들어 보자.  하이라키뷰에 빈게임오브젝트를 만들고 이름을 PhotonManager라고 한다. 동일 이름으로 스크립트를 만들고 연결한다.

포톤 API를 사용하기 위한 네임스페이스를 선언해야한다

using Photon.Pun;  //포톤 API를 위한 네임스페이스
using Photon.Realtime; //포톤 API를 위한 네임스페이스

PhotonManager 스크립트는 포톤의 다양한 기능을 사용하기 위해 MonoBehaviourPunCallbacks Class를 베이스 클래스로 변경한다.

public class PhotonManager : MonoBehaviourPunCallbacks {
}

Awake() 함수에서 포톤의 기본적 설정을 한후 접속하는 코드이다.

룸을 생성한 유저는 자동으로 입장하고 방장이 된다. 

PhotonNetwork.AutomaticallySyncScene = true;  // 마스터 클라이언트의 씬 자동 동기화 옵션

동일 버전의 유저끼리만 접속을 허용해준다.

PhotonNetwork.GameVersion = version; // 게임 버전 설정
PhotonNetwork.NickName = userId;  //접속 유저의 닉네임 설정

포톤서버와의 초당 통신회수를 정한다 디폴트는 30회이다

Debug.Log(PhotonNetwork.SendRate);// 포톤 서버와의 데이터의 초당 전송 횟수

설정을 마치면 서버에 접속한다.

PhotonNetwork.ConnectUsingSettings(); // 포톤 서버 접속

서버에 접속되면 제일 먼저 OnConnectedToMaster() 콜백함수가 호출된다. 여기서 로비에 입장했는지 체크해보자

Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");

포톤은 로비에 자동으로 입장할수 없기 때문에 False가 출력된다. JoinLobby로 입장해보자

PhotonNetwork.JoinLobby();

제대로 입장되었다면 OnJoinedLobby()콜백이 호출된다.

public override void OnJoinedLobby() {
    Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
    // 수동으로 접속하기 위해 자동 입장은 주석처리
    // PhotonNetwork.JoinRandomRoom();        
}
using UnityEngine;
using TMPro;
using Photon.Pun;  //포톤 API를 위한 네임스페이스
using Photon.Realtime; //포톤 API를 위한 네임스페이스
using System.Collections.Generic;

public class PhotonManager : MonoBehaviourPunCallbacks {
    private readonly string version = "1.0"; // 게임의 버전
    private string userId = "Zack"; // 유저의 닉네임  

    void Awake() {      
        PhotonNetwork.AutomaticallySyncScene = true;  // 마스터 클라이언트의 씬 자동 동기화 옵션
        PhotonNetwork.GameVersion = version; // 게임 버전 설정
        PhotonNetwork.NickName = userId;  //접속 유저의 닉네임 설정
        // 포톤 서버와의 데이터의 초당 전송 횟수
        Debug.Log(PhotonNetwork.SendRate);
        PhotonNetwork.ConnectUsingSettings(); // 포톤 서버 접속
    }

    public override void OnConnectedToMaster() {
        Debug.Log("Connected to Master!");
        Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
        PhotonNetwork.JoinLobby();
    }

    // 로비에 접속 후 호출되는 콜백 함수
    public override void OnJoinedLobby() {
        Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
        // 수동으로 접속하기 위해 자동 입장은 주석처리
        // PhotonNetwork.JoinRandomRoom();        
    }
}

실행해보면 한국 서버인 kr로 접속되고 Master에 연결된후 InLobby된다.

+ Recent posts