[Unity] 쉽게 배우는 Photon

WestCoast·2022년 8월 22일
1

Unity

목록 보기
3/5

개요


목표


멀티플레이 게임 까짓거 한 번 만들어봅시다!

포톤이란?


  • 포톤은 네트워크 게임 엔진
  • 유니티 에셋스토어에서 에셋처럼 임포트하여 사용할 수 있음
  • 포톤이 제공해주는 서버와 API 들을 통해 네트워크에 대해 자세히 알지 못해도 멀티플레이 게임을 손쉽게 제작할 수 있음
  • 무료 버전은 10~20명 정도의 동시 접속이 가능
  • 유료 버전 가격은 링크 참고
  • 서비스 종류: PUN, BOLT, QUANTUM 등이 있음
    • 다양한 서비스 중 PUN2가 가장 유니티 & 초보자에게 친화적!

PUN 은 또 무엇인가

  • PUN: Photon Unity Networking 의 약자
  • 대놓고 '유니티 네트워킹'이라고 자칭하고 있음
  • 네트워크 동기화 및 RPC를 유니티에서 쉽게 사용할 수 있도록 도와줌

PUN 특징 정리

  • PUN vs Bolt

    • 클라이언트-서버 구조
    • Master Client : 룸을 생성한 클라이언트
      • 마스터 클라이언트는 호스트도 아니고 서버도 아님!
      • 특별한 권한을 가지고 있는 특별한 '클라이언트'임
      • 마스터 클라이언트가 접속 종료한 경우 다른 클라이언트가 새로운 마스터 클라이언트가 된다.


Photon 시작하기


Photon PUN2 다운로드

PUN Setup


PUN2 동기화 방법 3가지


간단 정리

???: 그래서 언제 어떤 걸 쓰면 되는건데요...?

갱신이 빈번한 경우: PhotonView

갱신이 드문 경우: RPC

갱신이 아주 드문 경우: Custom Properties

캐릭터의 경우 아주 빠른 이동이나 회전이 아닌 이상 PhotonView, PhotonTransformView를 사용하면 커버가 가능하다.

RPC와 Custom Properties의 구분은 명확하게 가르기 힘들다. 상황에 맞추어 사용할 수 있지만, 필자의 경우 RPC를 선호하는 편이며 대부분은 RPC로 구현이 가능하다.

PhotonView

PhotonView: Photon이 제공하는 가장 간편한 네트워크 동기화를 적용시켜주는 컴포넌트

위 사진과 같이 PhotonView, Photon Transform View를 추가하면 오브젝트의 Position, Rotation, Scale과 같은 값들을 동기화 할 수 있다.

// PhotonView를 사용하여 플레이어를 생성하는 간단한 로직
private void OnJoinPlayer()
{
	int index = UnityEngine.Random.Range(0, spawnPoints.Length);
	PhotonNetwork.Instantiate("Player", spawnPoints[index].position, spawnPoints[index].rotation);
}

PhotonNetwork.Instantiate() : PhotonView를 사용하여 동기화되는 객체를 생성하기 위해서는 유니티 내장 Instantiate()가 아닌 해당 메소드를 사용해야 한다.

PhotonNetwork.Destroy() : 삭제할 때는 꼭 이 메소드를 사용하여 삭제하자.

    // 플레이어 이동, 회전의 간단한 예제
    private void Update()
    {
        if (!photonView.IsMine)
            return;

        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        tr.Translate(Vector3.forward * v * Time.deltaTime * speed);
        tr.Rotate(Vector3.up * h * Time.deltaTime * rotSpeed);
	}

photonView.IsMine : 해당 프로퍼티를 통해 오브젝트가 실제 내(로컬)가 생성한 객체(오리지널)인지 아니면 다른 플레이어 객체인지 판단할 수 있다.
위 코드에서 photonView.IsMine 구문을 제거하면 복수의 플레이어를 모두 제어하는 재미있는(?) 광경을 볼 수 있다.

알아둘 점: 실제로 움직이는 것은 오리지널이며, 레플리카(원격 클라이언트의 복제된 나)는 오리지널의 '위치, 회전' 등을 동기화를 통해 전달받고 따라할 뿐이라는 점을 알아두자.

RPC

RPC(Remote Procedure call)란?

원격 프로시저 호출 - 위키피디아
원격 프로시저 호출(영어: remote procedure call, 리모트 프로시저 콜, RPC)은 별도의 원격 제어를 위한 코딩 없이 다른 주소 공간에서 함수나 프로시저를 실행할 수 있게하는 프로세스 간 통신 기술이다. 다시 말해, 원격 프로시저 호출을 이용하면 프로그래머는 함수가 실행 프로그램에 로컬 위치에 있든 원격 위치에 있든 동일한 코드를 이용할 수 있다.

쉽게 말하자면, 로컬과 원격에 동일한 코드가 있을 때 원격의 코드를 실행한다는 것이다.
그런데 photonView를 놔두고 이런 어려운 RPC가 필요할까?

	// 총알을 생성하고 쏘는 함수
    public void Fire(Vector3 position, Quaternion rotation)
    {
        GameObject bullet;
        
        // 불릿 생성
        bullet = Instantiate(BulletPrefab, gunOffset.position, Quaternion.identity) as GameObject;
    }

위와 같은 간단한 총알 생성 로직이 있다고 해보자. 이 총알은 PhotonView가 붙어있지 않다. 즉, PhotonView가 위치 동기화를 해주지 않는다.

왜 안붙였을까? 총알이 '매우' 빠를 수 있기 때문이다.

PhotonView는 꽤 많은 전송을 요구한다. 그도 그럴 것이 position을 동기화하기 위해 position을 분명히 원격의 클라이언트로 계속 전송해줄텐데 요즘은 60프레임 이하는 게임으로 취급해주지 않으니 초당 60번을 전송해야 '끊김없는 움직임'이 가능할 것이다.(물론 데드레커닝 등의 동기화 기법을 적용하여 부드러운 움직임이 가능하도록 할 순 있지만 일단 넘어가자.)

또한 총알은 게임 내에서 수백~수천개가 생성될텐데 PhotonView를 통해 계속 동기화 대상을 늘리는 것은 전혀 좋은 방법은 아닌 것 같다.

    private void Update()
    {
        if (!photonView.IsMine)
            return;

        bool fire = Input.GetMouseButton(0);
        if (fire)
        {
            // RpcTarget.AllViaServer = RPC 호출을 서버에게 요청하고 서버가 '나'를 포함한 모든 클라이언트에게 '순서'대로 쏴준다
            photonView.RPC("Fire", RpcTarget.AllViaServer, rigidbody.position, rigidbody.rotation);
        }
    }

    [PunRPC]
    public void Fire(Vector3 position, Quaternion rotation, PhotonMessageInfo info)
    {
        float lag = (float)(PhotonNetwork.Time - info.SentServerTime);
        GameObject bullet;
        
        // 불릿 생성
        bullet = Instantiate(BulletPrefab, gunOffset.position, Quaternion.identity) as GameObject;
        // 불릿 초기화 로직
        bullet.GetComponent<BulletCtrl>().InitializeBullet(photonView.Owner, (rotation * Vector3.forward), Mathf.Abs(lag));
    }

photonView.RPC() : RPC 함수를 호출하기 위해서는 반드시 이 함수를 사용한다.

[PunRPC] : 해당 속성필드를 붙여주어야 RPC 함수로 사용 가능하다.

위 로직은 '로컬 플레이어'가 Fire 함수를 사용하여 bullet 오브젝트를 생성함과 동시에 해당 Fire 함수 호출을 '네트워크 상의 복제되어진 플레이어'가 실행하도록 하는 코드이다.

이렇게 하면 '총을 쏜다'라는 사실만이 동기화되고 총알의 생성과 이동은 '각 클라이언트'에게 맡겨진다.
즉, 총알의 위치 동기화를 계속해주지 않아도 부드러운 총알의 움직임을 구현할 수 있다.

커스텀 프로퍼티

커스텀 프로퍼티는 키-값 형태의 해시테이블로 구성되어 있다.
간단히 말하자면 변수(프로퍼티)를 동기화 시켜주는 녀석이라고 할 수 있다.

using Hashtable = ExitGames.Client.Photon.Hashtable;

중략...

Hashtable initialProps = new Hashtable() { { "isReady", false } };
PhotonNetwork.LocalPlayer.SetCustomProperties(initialProps);

로비에서 플레이어들의 레디 상태를 체크하기 위해 커스텀 프로퍼티를 사용할 수 있다.

SetCustomProperties() 메소드를 통하여 네트워크 상의 다른 플레이어들에게 전달된다.

위와 같이 해시테이블을 생성하고 "isReady" 라는 키를 생성하여 초기 값을 세팅한다.

    public override void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
    {
        GameObject entry;
        if (_playerRowDict.TryGetValue(targetPlayer.ActorNumber, out entry))
        {
            object isPlayerReady;
            if (changedProps.TryGetValue("isReady", out isPlayerReady))
            {
                entry.GetComponent<LobbyPlayerRow>().SetReadyUI((bool)isPlayerReady);
            }
        }

        bool isAllReady = CheckAllPlayerReady();
        OnSetGameStartButton.Invoke(isAllReady);
    }

OnPlayerPropertiesUpdate() : 네트워크 상의 클라이언트 중 하나의 커스텀 프로퍼티가 업데이트 되었다면 해당 콜백이 실행된다.

예를 들어 레디 버튼을 눌러 레디를 한 플레이어가 있다면 커스텀 프로퍼티가 업데이트되어 위 콜백이 실행될 것이다.


더보기


북마크 강추

Photon 공식 사이트

ETC


"본 글은 엔픽셀 인턴쉽 중 작성한 문서로서 회사의 허락을 받아 공개합니다."

profile
게임... 만들지 않겠는가..

0개의 댓글