[Unity] Network - PUN2

Lingtea_luv·2025년 7월 11일
0

Unity

목록 보기
29/30
post-thumbnail

Network


Unity Setting for Network

네트워크 게임을 만들기 위해서는 기본적인 세팅을 해주어야한다.

  1. Asset Store에서 Photon PUN2 추가 - App ID 입력
  2. Package Manager에서 https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync 입력 후 Parrel Sync Import - 동기화 테스트 진행하기 위한 에셋
  3. 상단 ParrelSync - Clones Manager - Add new clone - Open in New Editor로 연동시킬 에디터 실행

Photon

Photon PUN2는 멀티 플레이어 게임용 유니티 패키지로, 직관적이고 유용한 기능을 다수 제공하고 있어 현재까지도 많이 사용되는 매치메이킹 기반 실시간 멀티 플레이어 게임용 유니티 패키지이다.

MatchMaking

Photon 서버에 접속한 여러 클라이언트들이 서버에 존재하는 여러 게임룸을 확인하며 참가하는 방식으로 아래의 기능들을 지원한다.

  • 서버에게 매칭되는 룸을 선정 받는 빠른 참가 기능
  • 룸의 목록을 얻어 사용자가 선택한 룸에 참가하는 기능
  • 친구를 따라 룸에 입장하는 기능

Room

네트워크 게임에서는 다른 클라이언트들에게 자신의 로컬 상태를 최신 상태로 동기화 시켜주는 것이 사실 상 전부이며, 같은 룸에 참가한 클라이언트들은 동기화를 통해 실시간 네트워크 게임을 구성할 수 있다. Photon은 이를 기반으로 아래의 기능들을 지원한다.

  • 데이터 스트림으로 변수 데이터 전달을 통한 객체 동기화
  • 모든 클라이언트가 동일한 함수를 호출할 수 있도록 전달하는 원격 함수 호출(RPC - Remote Procedure Call)
  • 룸과 플레이어의 정보를 캐싱해두고 동기화하는 커스텀 프로퍼티

Photon 구현 컨셉

photon은 네트워크 멀티플레이어 게임 패키지로 클라이언트와 서버의 통신을 통해서 요청과 반응을 구현하도록 되어있다.

PhotonNetwork

클라이언트 측에서 서버쪽으로 요청이 필요한 경우, 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 room = PhotonNetwork.CurrentRoom;               	// 현재 방 정보 확인
  • Player : 닉네임, 아이디, 방장 여부 등의 정보들을 포함하는 클래스
  • Room : 이름, 최대 참여 인원, 현재 참여 인원, 공개 여부 등의 정보들을 포함하는 클래스

MonoBehaviourPunCallbacks

반대로 서버측에서 클라이언트 쪽으로 반응을 전달하는 경우, 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) { }  // 방 목록 변경시 호출됨
}

Custom Property

Photon에서 제공하는 정보만으로는 본격적인 네트워크 게임 구현에 한계가 있다. 게임에 따라서는 플레이어나 방에 추가 정보가 필요하기 때문인데, 이는 커스텀 프로퍼티를 통해 추가로 구현하는 것이 가능하다.

추가하고자 하는 정보의 이름과 값을 설정하여 같은 게임을 플레이하는 구성원들에게 정보를 공유하고 동기화하는 것이 가능한데, ExitGames.Client.Photon.HashTable 은 Photon에서 구현한 자료구조로 Dictionary와 사용 방법이 동일하지만 직렬화 처리가 추가되어있다.

이를 활용하여 네트워크 동기화를 하고 플레이어나 방의 정보를 변경하거나 확인할 수 있다.

Master Client

멀티 플레이 게임에서 모든 참가자가 게임의 모든 권한을 갖고 있지 않다. 이는 게임의 진행에 방해가 없도록 하기 위함으로 보통 1명의 플레이어가 모든 권한을 갖게 되는데, 마찬가지로 Photon에는 방장이라는 Master Client 개념이 있다.

public class NetworkManager : MonoBehaviourPunCallbacks
{
   public void GameStart()
   {
      // 자신이 방장이 아닌 경우 반환하여 아래의 코드가 실행되지 않도록 함
      if (PhotonNetwork.LocalPlayer.IsMasterClient == false)
           return;
    
      // 방장만이 실행할 수 있는 소스코드
      PhotonNetwork.AutomaticallySyncScene = true; // 모든 방구성원이 같은 씬으로 이동하도록 동기화
      PhotonNetwork.LoadLevel("GameScene");        // 네트워크를 통해 씬을 이동하도록 신청
    }
}

Photon 실습

간단하게 멀티 게임에 존재하는 로비와 방을 구현한 것이다.

NetworkManager

로비까지의 기능과 MonoBehaviourPunCallbacks을 상속 받아 NetworkManager에 콜백함수를 overriding하여 구현했다.

public class NetworkManager : MonoBehaviourPunCallbacks
{
    [Header("로딩 창")]
    [SerializeField] private GameObject _loadingPanel;
    [SerializeField] private TextMeshProUGUI _stateText;
    
    [Header("닉네임")]
    [SerializeField] private GameObject _nicknamePanel;
    [SerializeField] private TMP_InputField _nicknameField;
    [SerializeField] private Button _nicknameAdmitBtn;
    
    [Header("로비(룸)")]
    [SerializeField] private GameObject _lobbyPanel;
    [SerializeField] private TMP_InputField _roomNameField;
    [SerializeField] private Button _roomCreateBtn;
    [SerializeField] private Transform _roomListContent;
    
    // 방을 삭제할 때 찾아서 없애야하기 때문에 이 녀석 또한 자료구조로 저장해야한다.
    [SerializeField] private GameObject _roomListItemPrefabs;

    [SerializeField] private RoomManager _roomManager;
    
    private Dictionary<string, GameObject> _roomItemDic = new Dictionary<string, GameObject>();

    private void Start()
    {
        // 네트워크와 연결하는 메서드 + 네트워크에 문제가 없으면 마스터 서버까지 연결 해준다.
        PhotonNetwork.ConnectUsingSettings();
        
        // 닉네임 입력 버튼, 방 생성 버튼 등록
        _nicknameAdmitBtn.onClick.AddListener(NicknameAdmit);
        _roomCreateBtn.onClick.AddListener(CreateRoom);
        
        // 연결을 끊어버리는 메서드
        // PhotonNetwork.Disconnect();
    }

	// 현재 클라이언트의 상태를 바로 확인할 수 있도록 화면 상단에 노출시켰다.
    private void Update()
    {
        _stateText.text = $"Current State : {PhotonNetwork.NetworkClientState}";
    }
    
    // 단순히 네트워크와 연결되었을 때 호출(콜백)되는 메서드
    public override void OnConnected()
    {
        base.OnConnected();
    }
    
    // 마스터 서버와 연결되었을 때 호출(콜백)되는 메서드
    // 마스터 서버와 연결되어야 방 관련 설정이 가능하기 때문에
    // OnConnected 콜백보다 OnConnectedToMaster 콜백을 더 많이 사용한다.
    public override void OnConnectedToMaster()
    {
        // 처음에 연결되었을 때 => 로비로 바로 입장하지 않는다.
        if (_loadingPanel.activeSelf)
        {
            base.OnConnectedToMaster();
            
            // 입장하면 로딩 창 끄기
            _loadingPanel.SetActive(false);
        }
        // LeaveRoom으로 방에서 나와 마스터까지 나갔던 경우 => 바로 로비로 입장한다.
        else
        {
            PhotonNetwork.JoinLobby();
        }
    }

    // 연결이 되지 않았을 때 호출(콜백)되는 메서드
    public override void OnDisconnected(DisconnectCause cause)
    {
        base.OnDisconnected(cause);
        
        // 접속에 실패하면 서버 재접속 시도
        PhotonNetwork.ConnectUsingSettings();
    }

	// 닉네임 설정 버튼에 연동되는 private 메서드
    private void NicknameAdmit()
    {
        if (string.IsNullOrWhiteSpace(_nicknameField.text))
        {
            Debug.LogError("닉네임 입력 값 없음");
            return;
        }
        PhotonNetwork.NickName = _nicknameField.text;
        // 로비로 들어가는 메서드
        PhotonNetwork.JoinLobby();
    }

    // 로비에 접속했을 때 호출(콜백)되는 메서드
    public override void OnJoinedLobby()
    {
        base.OnJoinedLobby();
        _nicknamePanel.SetActive(false);
        _lobbyPanel.SetActive(true);
        Debug.Log("로비 접속");
    }

	// 방 생성 버튼에 연동되는 private 메서드
    private void CreateRoom()
    {
    	// 입력값이 비어있거나 공백인 경우 반환
        if (string.IsNullOrWhiteSpace(_roomNameField.text))
        {
            Debug.LogError("방 이름 입력 값 없음");
            return;
        }

		// 제대로 입력되면 방 생성 버튼 잠그기
        _roomCreateBtn.interactable = false;
        
        // 룸의 상세 프로퍼티(세부 사항)를 설정하는 RoomOptions
        RoomOptions options = new RoomOptions{MaxPlayers = 8};
        
        // 매개변수로 이름, RoomOptions를 넣어준다.
        PhotonNetwork.CreateRoom(_roomNameField.text, options);

        // 방을 만들었다면 생성할 때 입력한 이름은 지워주자.
        _roomNameField.text = null;
    }
    
    // 룸이 만들어졌을 때 호출(콜백)되는 메서드
    // 룸을 생성하게 되면 자동으로 참가되어 아래의 OnJoinedRoom 메서드까지 호출(콜백)된다.
    public override void OnCreatedRoom()
    {
        _roomCreateBtn.interactable = true;
    }
    
    // 룸에 입장했을 때 호출(콜백)되는 메서드
    // 아래의 OnPlayerEnteredRoom와 다른 점은 방을 생성했을 때도 호출이 된다.
    public override void OnJoinedRoom()
    {
    	// 방 참가하면 로비 화면 끄기
        _lobbyPanel.SetActive(false);
        // 플레이어 패널을 생성하는 메서드
        _roomManager.PlayerPanelSpawn();
    }
    
    
    // 새로운 플레이어가 방에 입장했을 때 호출되는 메서드
    // 내가 방에 입장했을 때도 호출된다.
    // Player를 받을 수 있기 때문에 RoomManager의 메서드를 여기서 호출한다.
    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);
        }
    }
    
    방의 목록이 새롭게 업데이트 되었을 때 호출(콜백)되는 메서드
    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        foreach (var info in roomList)
        {
            // 이미 로비에서 삭제된 경우
            if (info.RemovedFromList)
            {
                // UI에서 제거하고, 찾아서 Dictionary에서도 삭제해준다.
                if (_roomItemDic.TryGetValue(info.Name, out GameObject obj))
                {
                    Destroy(obj);
                    _roomItemDic.Remove(info.Name);
                }
            }
            // 중복으로 생성된 경우에는 스킵
            if (_roomItemDic.ContainsKey(info.Name))
            {
                // 방이 있는 상황에서 새로 로비에 입장했을 때. 한번 업데이트(참가자 수)를 한다.
                _roomItemDic[info.Name].GetComponent<RoomListItem>().Init(info);
            }
            else
            {
                GameObject roomListItem = Instantiate(_roomListItemPrefabs,_roomListContent);
                roomListItem.GetComponent<RoomListItem>().Init(info);
                _roomItemDic.Add(info.Name, roomListItem);
            }
        }
    }
}

RoomManager

방 내부 기능 전반을 담당하는 클래스로 기능 분리를 위해 새롭게 구현했다.

public class RoomManager : MonoBehaviour
{
    [SerializeField] private Button _startButton;
    [SerializeField] private Button _leaveButton;
    
    [SerializeField] private GameObject _playerPanelPrefab;
    [SerializeField] private Transform _playerPanelContent;

    private void Start()
    {
        _leaveButton.onClick.AddListener(LeaveRoom);
    }
    
    // Player의 ID를 넣어서 해보자.
    private Dictionary<int, PlayerPanelItem> _playerDic = new Dictionary<int, PlayerPanelItem>();
        
    
    // Player : 플레이어에 대한 정보를 가지고 있다.
    public void PlayerPanelSpawn(Player player)
    {
        // 내가 생성한 방에 다른 플레이어가 입장했을 때 호출
        GameObject obj = Instantiate(_playerPanelPrefab, _playerPanelContent);
        PlayerPanelItem item = obj.GetComponent<PlayerPanelItem>();
        item.Init(player);
        _playerDic.Add(player.ActorNumber,item);
    }

    public void PlayerPanelSpawn()
    {
        // 내가 방장이 아니면
        if (!PhotonNetwork.IsMasterClient)
        {
            // 시작버튼은 누를 수 없게 만든다.
            _startButton.interactable = false;
        }
        // 생성된 방에 내가 입장했을 때 기존에 참가하고 있는 플레이어들을 표시해야한다.
        foreach (Player player in PhotonNetwork.PlayerList)
        {
            GameObject obj = Instantiate(_playerPanelPrefab, _playerPanelContent);
            PlayerPanelItem item = obj.GetComponent<PlayerPanelItem>();
            item.Init(player);
            _playerDic.Add(player.ActorNumber,item);
        }
    }
    // 플레이어가 나갈 때 호출할 메서드
    public void PlayerPanelDestroy(Player player)
    {
        if (_playerDic.TryGetValue(player.ActorNumber, out PlayerPanelItem panel))
        {
            Destroy(panel.gameObject);
            _playerDic.Remove(player.ActorNumber);
        }
    }
    
    // LeaveBtn에 연결시킬 메서드
    // 플레이어가 방을 떠날 때의 처리
    private void LeaveRoom()
    {
        // 방을 떠나면 해당 플레이어에게는 방이 사라져야한다.
        // 따라서 참가하고 있는 플레이어의 패널 오브젝트를 모두 삭제시키고
        foreach (Player player in PhotonNetwork.PlayerList)
        {
            Destroy(_playerDic[player.ActorNumber].gameObject);
        }
        // 참가하고 있던 방의 정보를 담고 있던 Dictionary를 비운다.(초기화)
        _playerDic.Clear();
        PhotonNetwork.LeaveRoom();
    }
}

PlayerPanel & RoomList Prefab

플레이어의 참가 화면과 방의 목록 단위를 나타내는 오브젝트들로 각각 고유한 기능과 초기화 메서드를 담당한다.

public class PlayerPanelItem : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI _nickNameText;
    [SerializeField] private TextMeshProUGUI _readyText;
    [SerializeField] private Image _hostImage;
    [SerializeField] private Image _readyBtnImage;
    [SerializeField] private Button _readyBtn;

    public void Init(Player player)
    {
        _nickNameText.text = player.NickName;
        // 방장일 때만 호스트 이미지가 뜨드록
        _hostImage.enabled = player.IsMasterClient;
        // 자기 자신의 준비 버튼만 활성화, 남의 꺼는 비활성화
        _readyBtn.interactable = player.IsLocal;
    }
}

public class RoomListItem : MonoBehaviour
{
    [Header("Drag&Drop")] 
    // 방이름
    [SerializeField] private TextMeshProUGUI _roomNameText;
    // 입장 인원
    [SerializeField] private TextMeshProUGUI _playerCountText;
    // 버튼을 통한 방 참가
    [SerializeField] private Button _joinBtn;

    private string _roomName;

	// 초기화 메서드
    public void Init(RoomInfo info)
    {
        _roomName = info.Name;
        _roomNameText.text = $"Room Name : {_roomName}";
        _playerCountText.text = $"{info.PlayerCount} / {info.MaxPlayers}";
        
        _joinBtn.onClick.AddListener(JoinRoom);
    }

    private void JoinRoom()
    {
    	// 진입중이 아닌 경우에만 호출하고록 예외처리
        if (PhotonNetwork.InLobby)
        {
            PhotonNetwork.JoinRoom(_roomName);
            _joinBtn.onClick.RemoveListener(JoinRoom);
        }
    }
}
profile
뚠뚠뚠뚠

0개의 댓글