PUN Classic(PUN1이라고도 불립니다)은 PUN의 첫 버전입니다.         현재는 리팩토링 및 기능 확장에 의해 PUN2로 새롭게 바뀌었습니다.          새 프로젝트에는 PUN2를 이용해 주시고, 기존의 프로젝트도 가능하면 PUN1에서 PUN2로 옮기는 것을 권장합니다.  자세한 내용은: "마이그레이션 노트". PUN Classic은 곧 점검이 시작됩니다.        중요한 버그의 수정과 Unity의 신버전의 지원 등을 예정하고 있습니다. 신기능의 추가는 PUN2에서만 이루어지므로 주의해 주십시오.

파트 1 - 로비

Animator 튜토리얼은 Unity3D 기반 튜토리얼 입니다. 나만의 멀티플레이어가 가능한 어플리케이션을 Photon 클라우드를 이용하여 어떻게 개발하는지 보여 주며 애니메이션을 하기 위해 Animator 를 사용한 캐릭터를 어떻게 이용하는지 보여 줍니다.

Contents

서버 접속, 룸 접근과 생성

우선 이 튜토리얼의 핵심인 Photon 클라우드 서버에 연결에 참가 또는 필요시 룸 생성에 대한 것을 알아 보겠습니다.

  1. 새로운 신을 생성하고 Launcher.unity 로 저장 합니다.
  2. 새로운 C# 스크립트인 Launcher 를 생성 합니다.
  3. Hierarchy 에서 빈 [게임오브젝트]를 생성하고 Launcher 로 이름을 변경 합니다.
  4. [게임오브젝트] LauncherLauncher C# 스크립트를 추가 합니다.
  5. Launcher C# 스크립트를 아래와 같이 편집 합니다.
코딩 팁: Copy & Paste 를 하지 않으면 (이렇게 하는것을 권장 합니다. 모든 것을 타이핑 하면 기억하기 더 좋습니다) 커멘트를 작성하는 것은 매우 쉽습니다. 메소드 또는 프로퍼티 상단 라인에서 /// 를 입력하면 자동으로 구조화된 주석이 생성될 것 입니다. 예를 들면 <summary> 태그가 추가 됩니다.
using UnityEngine;

namespace Com.MyCompany.MyGame
{
    public class Launcher : MonoBehaviour
    {
        #region Public Variables

        #endregion

        #region Private Variables

        /// <summary>
        /// This client's version number. Users are separated from each other by gameversion (which allows you to make breaking changes).
        /// </summary>
        string _gameVersion = "1";

        #endregion

        #region MonoBehaviour CallBacks

        /// <summary>
        /// MonoBehaviour method called on GameObject by Unity during early initialization phase.
        /// </summary>
        void Awake()
        {

            // #NotImportant
            // Force Full LogLevel
            PhotonNetwork.logLevel = PhotonLogLevel.Full;

            // #Critical
            // we don't join the lobby. There is no need to join a lobby to get the list of rooms.
            PhotonNetwork.autoJoinLobby = false;

            // #Critical
            // this makes sure we can use PhotonNetwork.LoadLevel() on the master client and all clients in the same room sync their level automatically
            PhotonNetwork.automaticallySyncScene = true;
        }

        /// <summary>
        /// MonoBehaviour method called on GameObject by Unity during initialization phase.
        /// </summary>
        void Start()
        {
            Connect();
        }

        #endregion


        #region Public Methods

        /// <summary>
        /// Start the connection process. 
        /// - If already connected, we attempt joining a random room
        /// - if not yet connected, Connect this application instance to Photon Cloud Network
        /// </summary>
        public void Connect()
        {

            // we check if we are connected or not, we join if we are , else we initiate the connection to the server.
            if (PhotonNetwork.connected)
            {
                // #Critical we need at this point to attempt joining a Random Room. If it fails, we'll get notified in OnPhotonRandomJoinFailed() and we'll create one.
                PhotonNetwork.JoinRandomRoom();
            }else{
                // #Critical, we must first and foremost connect to Photon Online Server.
                PhotonNetwork.ConnectUsingSettings(_gameVersion);
            }
        }

    #endregion

    }
}

이 스크립트에 대해서 살펴보겠습니다. 우선 Unity 관점에서 살펴보고 이후에 PUN 과 관련한 호출들에 대해 살펴 보겠습니다.

  • 네임스페이스:
    필수는 아니나 스크립트에 적당한 네임스페이스 를 부여하는 것이 다른 에셋과 개발자의 코드와의 충돌을 막을 수 있습니다. 만약 다른 개발자가 Launcher 클래스를 생성하면 어떻게 될까요? Unity 가 이것을 거부 할 것이고 나 또는 다른 개발자가 그 클래스의 이름을 변경 하여 Unity 가 프로젝트를 실행할 수 있도록 해주어야 합니다. 에셋 스토어에서 다운로드 받은 에셋과 충돌 하게 된다면 매우 혼란 스러운 상황일 수 있습니다. 이제 Launcher 클래스는 내부적으로 Com.MyCompany.MyGame.launcher 가 되었으며 이러한 네임스페이스를 갖고 있는 것은 거의 발생 하지 않을 것입니다. 왜냐하면 나만의 도메인을 사용하여 도메인을 역으로 네임스페이스를 만들기 때문에 안전하고 잘 구조화 된 것 이기 때문입니다.
    Com.MyCompany.MyGame 은 나의 도메인과 게임명으로 교체 되는데 convention 을 참고 하여 따라 해 주시기 바랍니다.

  • MonoBehaviour 클래스:
    우리는 클래스를 MonoBehaviour 클래스에서 상속 받고 있으며 GameObject 또는 Prefab을 떨굴 수 있는 Unity Component 로 전환해 줄 수 있는 클래스가 됩니다. MonoBehaviour 클래스를 상속받은 클래스는 중요한 메소드와 프로퍼티들에 접근 할 수 있습니다. 이 경우에 우리는 두개의 콜백인 Awake()Start() 를 사용할 것 입니다.

  • PhotonNetwork.logLevel

    개발하는 동안 PUN 으로 개발하는 것이 처음이면 최대한 Unity 콘솔에 로그를 많이 찍어 어떤 사항이 발생하는지 파악하는 것을 권장 합니다. 예상하는 대로 동작하는 것에 대하여 확신이 서면 로그 레벨을 Informational 으로 변경 하시기 바랍니다.

  • PhotonNetwork.autoJoinLobby

    Awake() 동안에 PhotonNetwork.autoJoinLobby 를 false로 설정 했는데 로비 기능이 필요 하지 않기 때문이며 현재 존재하고 있는 룸에 대한 목록만 얻으면 되기 때문입니다. 동일한 프로젝트의 다른 신이 Lobby에 자동 참가 하고 두 경우 모두 다른 방식 사이에서 방식을 변경하는 것 없이 강제적으로 설정하도록 하는 것은 괜찮은 사상 입니다.

  • PhotonNetwork.ConnectUsingSettings()

    Start() 에서 PhotonNetwork.ConnectUsingSettings() 를 이용하는 public 함수인 connect() 를 호출하여 PUN 클라우드 접속하는 메소드를 호출 했습니다. _gameVersion 변수는 gameversion을 나타냅니다. 이미 출시중인 프로젝트에 대해서 변경이 필요하지 않는 한 "1" 로 유지 하도록 합니다. 기억 해야할 중요한 정보는 PhotonNetwork.ConnectUsingSettings() 이 게임이 네트워크를 통하여 Photon 클라우드로 연결 되는 시작 지점 이라는 것 입니다.

  • PhotonNetwork.automaticallySyncScene

우리 게임은 플레이어 수에 따라 크기가 변경되는 경기장을 갖게 될 것이고 로드된 신은 모든 연결된 플레이어에서 동일 한 것입니다. 우리는 Photon 이 제공하는 매우 편리한 기능을 이용할 것 입니다:PhotonNetwork.automaticallySyncScene 이 값이 true 일 때 MasterClientPhotonNetwork.LoadLevel()을 호출 할 수 있고 모든 연결된 플레이어들이 동일한 레벨을 자동적으로 로드 하게 될 것입니다. 이 시점에서 Launch 신을 저장할 수 있고 플레이를 할 수 있습니다. 유니티 콘솔에서 수십개의 로그들을 볼 수 있어야 합니다. 예를들어 "Connected to masterserver." 로그는 접속되었고 룸에 참여할 준비가 되었다는 것을 알려주는 로그 입니다. 코딩할 때 좋은 습관은 항상 실패에 대한 것을 염두에 두고 있어야 한다는 것 입니다. 여기에서는 컴퓨터가 인터넷에 연결되어 있다는 것을 가정하고 있으나 컴퓨터가 인터넷에 연결이 되어 있지 않으면 어떻게 될까요? 한번 알아 봅시다. 컴퓨터의 인터넷을 끄고 신을 플레이 해봅니다. 유니티 콘솔에서 Error "Connect() to 'ns.exitgames.com' failed: System.Net.Sockets.SocketException: No such host is known" 가 나타나야 합니다.

이상적으로는 스크립트가 이 이슈에 대해서 알고 있어야 하고 이러한 상황에 올바르게 대처해야 하며 , 어떤 상황 또는 문제가 발생 했는지에 상관없이 사용자에게 알려 줘 적절한 대응을 할 수 있도록 해야 합니다.

Launcher 스크립트에서 PUN 서버에 접속되거나 접속되지 않은 상황에 대처하여 사용자에게 알려주는 처리를 해 보도록 하겠습니다. 이것을 통해 PUN 콜백에 대한 것을 충분하게 파악할 수 있을 것 입니다.

Back To Top

PUN 콜백

PUN 의 콜백은 매우 유연하고 3가지의 다른 구현 방식을 제공하고 있습니다. 세가지 방식을 모두 다룰 것이며 상황에 따라 가장 적절한 것을 선택하여 사용할 것 입니다.

Back To Top

"magic" 메소드

일반 MonoBehaviour 를 사용할 때는 단순하게 private 메소드를 생성 할 수 있습니다.

void OnConnectedToMaster()
{

    Debug.Log("DemoAnimator/Launcher: OnConnectedToMaster() was called by PUN");

}

모든 MonoBehaviour 가 이 메소드와 PUN 에서 발생한 메시지를 구현할 수 있다는 것은 정말 마법과 같습니다. 이 방식은 Unity 가 Awake() 또는 Start() 와 같은 것을 MonoBehaviours 로 전송하는 메인 메소드들과 같은 원리를 따릅니다. 하지만 우리는 이 방식을 사용하지는 않습니다. 왜냐하면 'Magic' 메소드 철자가 틀리게 되면 에러에 대해서 아무것도 알려주지 않기 때문입니다. 매우 빠르고 실용적인 구현이긴 하지만 모든 메소드들의 정확한 이름을 알고 디버깅 전문 기술로 이러한 이슈를 빨리 찾을 수 없다면 권장 하지는 않습니다.

Back To Top

IPunCallbacks 과 IPunObservable 인터페이스 사용

PUN 은 두 개의 IPunObservableIPunCallbacks의 C# 인터페이스를 제공하고 있는데 클래스 안에서 구현 할 수 있습니다.

MonoDevelop: Implement Interface Methods
MonoDevelop: Implement Interface Methods

클래스가 모든 인터페이스를 따르고 있는지 확인하는 안정한 방식이지만 개발자가 모든 인터페이스 선언에 대해서 구현해야 한다는 것 입니다. 위에서 MonoDevelop 을 사용할 때 설명한 것 처럼 대부분의 스크립트 에디터는 이것을 매우 쉽게 작업 할 수 있습니다. 하지만 수 많은 메소드는 아무것도 하지 않는 것이 될 것이지만 모든 것을 구현해야만 유니티 컴파일러에서 오류를 발생하지 않습니다. 대부분 또는 모든 PUN 기능을 사용할 때 스크립트가 정말로 무거워지게 됩니다.

우리는 아래에 있는 데이터 직렬화를 위한 튜토리얼에서 IPunObservable 를 사용할 것 입니다.

Back To Top

Photon.PunBehaviour 사용하기

가장 많이 사용하는 마지막 기술이며 가장 손쉬운 방식입니다. MonoBehaviour 에서 상속 받은 클래스를 생성하는 대신에 특정 프로퍼티와 가상 메소드 Photon.PunBehaviour를 사용할 수 있고 편의에 따라 오버라이드 를 제공하는 Photon.PunBehaviour 에서 상속 받을 것 입니다. 오타에 대해서 걱정할 필요도 없으며 모든 메소드를 구현할 필요가 없기 때문에 매우 유용합니다.

MonoDevelop: Override Method
MonoDevelop: Override Method

노트: 오버라이딩 할 때 대부분의 에디터는 기본 호출에 대한 기본 구현 코드를 자동적으로 채워 주지만 우리 경우에 있어서는 Photon.PunBehaviour 의 일반 규칙으로 베이스 메소드를 호출할 필요가 없으므로 자동 구현이 필요가 없습니다.

노트: 오버라이딩할 때의 이점은 메소드 이름을 그대로 사용할 수 있다는 것 입니다.

pun 콜백인 OnConnectedToMaster()OnDisconnectedFromPhoton() 으로 연습 해 봅시다.

  1. c# Launcher 스크립트를 편집합니다.
  2. 기본 클래스를 MonoBehaviour 에서 Photon.PunBehaviour로 변경 합니다.

        public class Launcher : Photon.PunBehaviour {
  3. 클래스의 끝에서 다음 두개의 메소드를 추가 합니다. Photon.PunBehaviour CallBacks region 에 추가 하면 됩니다. 그리고 Launcher 를 저장 합니다.
#region Photon.PunBehaviour CallBacks

public override void OnConnectedToMaster()
{

    Debug.Log("DemoAnimator/Launcher: OnConnectedToMaster() was called by PUN");

}

public override void OnDisconnectedFromPhoton()
{

    Debug.LogWarning("DemoAnimator/Launcher: OnDisconnectedFromPhoton() was called by PUN");        
}

#endregion

인터넷 연결 유무와 관계없이 이 신을 플레이 하면 단계별로 로직을 처리하기 위한 절차를 밟을 수 있습니다. 다음 섹션에서는 UI 구축에 대한 것을 다룰 것 이며 현재는 성공적인 연결에 대해서 다루게 될 것 입니다:

따라서 OnConnectedToMaster() 메소드에 다음의 호출을 추가 합니다:

// #Critical: The first we try to do is to join a potential existing room. If there is, good, else, we'll be called back with OnPhotonRandomJoinFailed()  
PhotonNetwork.JoinRandomRoom();

그리고 주석에서 언급 했듯이 무작위 룸 참여 시도에 실패했을 때 통지를 받게 되며 이 경우에는 룸을 실제로 생성해야 하기 때문에 OnPhotonRandomJoinFailed() 이라는 PUN 콜백을 스크립트에 구현 하였고 PhotonNetwork.CreateRoom() 을 사용하여 룸을 생성하고 이미 짐작 하셨겠지만 성공적으로 룸에 참가했을 때 OnJoinedRoom() 이라는 PUN 콜백으로 알려 주게 됩니다:

public override void OnPhotonRandomJoinFailed (object[] codeAndMsg)
{
    Debug.Log("DemoAnimator/Launcher:OnPhotonRandomJoinFailed() was called by PUN. No random room available, so we create one.\nCalling: PhotonNetwork.CreateRoom(null, new RoomOptions() {maxPlayers = 4}, null);");

    // #Critical: we failed to join a random room, maybe none exists or they are all full. No worries, we create a new room.
    PhotonNetwork.CreateRoom(null, new RoomOptions() { maxPlayers = 4 }, null);
}

public override void OnJoinedRoom()
{
    Debug.Log("DemoAnimator/Launcher: OnJoinedRoom() called by PUN. Now this client is in a room.");
}

신을 실행 시키면 PUN 접속에 성공하여 기존의 룸에 참여를 시도하거나 새로 룸을 생성하고 생성된 그 룸에 참여 하게 될 것 입니다. 이 튜토리얼의 현 시점에서 연결과 룸 참여에 대한 중요한 관점을 다루었으므로 사용이 어려운 것은 소수 이며 나중에 좀더 알아 볼 필요가 있습니다. PUN 학습을 위한 과정은 아니지만 전체적인 중요한 개념을 다루고 있습니다.

Back To Top

Unity 인스펙터에서의 변수 노출

이미 알수도 있고 모를 수도 있으나 MonoBehaviours 는 public 프로퍼티를 유니티 인스펙터에 자동으로 노출 시킵니다. 유니티에서는 매우 중요한 개념으로 우리는 LogLevel 을 public 변수로 만들어 코드를 변경시키지 않고 정의하여 사용합니다.

/// <summary>
/// The PUN loglevel. 
/// </summary>
public PhotonLogLevel Loglevel = PhotonLogLevel.Informational;

그리고 Awake() 는 다음과 같이 변경 합니다:

// #NotImportant
// Force LogLevel
PhotonNetwork.logLevel = Loglevel;

이제 스크립트를 특정 logLevel 로 지정하지 않았습니다. 이제 유니티 인스펙터에서 설정하기만 하면 됩니다. 스크립트를 열고 편집하고 저장 후 유니티가 재 컴파일하는 것을 기다릴 필요 없이 실행만 하면 됩니다. 이러한 방식처럼 생산적이고 유연한 것은 수 없이 많습니다.

룸에 참여할 수 있는 최대 플레이어 수 도 동일한 방식으로 처리 합니다. 코드 내에서 하드코딩하는 것은 바람직하지 않으므로 public 변수로 만들어서 재 컴파일 필요 없이 나중에 적절한 숫자로 결정할 수 있습니다. 클래스 선언부의 시작 지점의 Public Variables 지역에서 추가 합니다:

/// <summary>
/// The maximum number of players per room. When a room is full, it can't be joined by new players, and so new room will be created.
/// </summary>   
[Tooltip("The maximum number of players per room. When a room is full, it can't be joined by new players, and so new room will be created")]
public byte MaxPlayersPerRoom = 4;

그리고 PhotonNetwork.CreateRoom() 호출을 변경하고 이전에 하드 코딩되었던 숫자를 새로운 public 변수로 교체 합니다.

// #Critical: we failed to join a random room, maybe none exists or they are all full. No worries, we create a new room.
PhotonNetwork.CreateRoom(null, new RoomOptions() { maxPlayers = MaxPlayersPerRoom }, null);

Launcher Script Inspector
Launcher 스크립트 인스펙터

다음 파트.
이전 파트.

기술문서 TOP으로 돌아가기