본문 바로가기
개발/Unity 내일배움캠프 TIL

Unity 포톤으로 멀티플레이 구현해보기

by 석시 2023. 10. 17.



이번 글에서는 포톤을 이용하여 멀티플레이를 구현하는 방법을 정리해보도록 하겠다.

베이스라인이 되는 게임은 다음의 간단한 탁구게임을 이용하였다.

Pong 게임 https://github.com/seoksii/Pong

포톤을 프로젝트에 Import하는 등의 설정은 다음의 게시글을 참고하자.

유니티 포톤 (Photon) 셋업하기
포톤 Import 하기어플리케이션 아이디 가져오기 포톤 Import 하기 포톤은 기본적으로 Package Manager에서 제공해주는 것이 아니라 애셋스토어에서 Import를 해야한다. PUN 2 - FREEGet the PUN 2 - FREE package from Photon Engine and speed up your game development process. Find this & other Network options on the Unity Asset Store.https://assetstore.unity.com/packages/tools/network/pun-2-free-119922 먼저 해당 링크에서 포톤을 내 프로젝트에 추가하자. 어플리케이션 아이디 가져오기 포톤을 Import하면 다음과..
https://seoksii.tistory.com/60


로비 씬 만들기

먼저 로비 씬을 만들기 위해 포톤에서 제공해주는 데모 씬을 복사할 것이다.

Project 탭에서 Assets/Photon/PhotonUnityNetworking/Demos/Scenes/DemoAsteroids-LobbyScene을 복제

그 후 이름을 LobbyScene으로 바꾸자.

복제해온 LobbyScene은 기본적으로 방을 만드는 기능, 랜덤한 방에 참가하는 기능, 생성된 방의 리스트를 불러오는 기능이 이미 구현이 되어 있다.

Hierarchy에서 MainPanel을 보면 LobbyMainPanel.cs 스크립트가 부착되어 있는데, 이것을 약간 수정해보자.

  • LobbyMainPanel.cs
    // MaxPlayers를 2로 수정
    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        string roomName = "Room " + Random.Range(1000, 10000);
    
        RoomOptions options = new RoomOptions {MaxPlayers = 2};
    
        PhotonNetwork.CreateRoom(roomName, options, null);
    }
    
    // GameStart 버튼을 눌렀을 때 SampleScene을 로딩하도록 수정
    public void OnStartGameButtonClicked()
    {
        PhotonNetwork.CurrentRoom.IsOpen = false;
        PhotonNetwork.CurrentRoom.IsVisible = false;
    
        PhotonNetwork.LoadLevel("SampleScene");
    }

빌드 세팅에서 LobbyScene과 SampleScene을 추가하는 것도 잊지 말자.


게임 씬 동기화

이제 원래 게임이 구현되어 있던 씬으로 넘어오자.

다음 게임에서 Ball 오브젝트에 PhotonView라고 하는 컴포넌트를 부착할 것이다.

PhotonView에 대한 설명은 다음 게시글에 간단하게 정리되어 있다.

Unity 포톤 (Photon)
포톤 기본 기능PhotonNetwork1) ConnectUsingSettings()2) CreateRoom() & JoinRoom()3) Instatiate()MonoBehaviourPunCallbacks1) OnConnectedToMaster()2) OnJoinedRoom() & OnPlayerEnteredRoom()포톤뷰 (PhotonView) 컴포넌트PhotonViewObserved Component 추가 PhotonView1) PhotonTransformView2) PhotonAnimatorView3) PhotonRigidbodyView 포톤 (Photon) 은 멀티플레이를 구현하는 솔루션 중 가장 유명한 네트워크 엔진 중 하나이다. 여러 엔진에 적용 가능하고, 크로스플랫폼 간 통신도 지원하는 등..
https://seoksii.tistory.com/59#b07f8fc0-dceb-4f79-ab02-dd4199922524

Player1과 Player2의 Paddle.cs는 아예 컴포넌트를 제거해주고 새로운 스크립트를 만들어 부착해줄 것이다.

NetPaddle이라는 이름의 스크립트를 하나 생성해서 다음과 같이 작성해주자.

  • NetPaddle.cs
    using Photon.Pun;
    using UnityEngine;
    
    public class NetPaddle : MonoBehaviourPun
    {
        public float speed = 10f;
    
        void Update()
        {
            if(photonView.IsMine)
            {
                float move = Input.GetAxis("Vertical") * speed * Time.deltaTime;
                transform.Translate(0, move, 0);
            }
        }
    }

그 후 Player1과 Player2에 이 NetPaddle.cs를 추가해주고, PhotonViewPhotonTransformView라는 컴포넌트를 부착해주자.

이 때 PhotonTransformViewSynchronize Options에서 Rotation은 동기화를 꺼주도록 하자.

그 후 Ball과 Player1을 프리팹화 해주자.

Paddle이 Player다.

나머지 스크립트들도 멀티플레이에 맞춰 동기화가 되어야 하기 때문에 다음과 같이 수정해주자.

  • Ball.cs
    using Photon.Pun;
    using UnityEngine;
    
    public class Ball : MonoBehaviourPun, IPunObservable
    {
        public float speed;
        public Rigidbody2D rigidbody;
    
        private void Awake()
        {
            rigidbody = GetComponent<Rigidbody2D>();
        }
    
        void Start()
        {
    
            if(!photonView.AmOwner)
            {
                return;
            }
    
            Launch();
        }
    
        private void Launch()
        {
            if (!photonView.AmOwner)
            {
                return;
            }
    
            float x = Random.Range(0, 2) == 0 ? -1 : 1;
            float y = Random.Range(0, 2) == 0 ? -1 : 1;
    
            rigidbody.velocity = new Vector2(x* speed, y* speed);
        }
    
        public void Reset()
        {
            rigidbody.velocity = Vector2.zero;
            transform.position = Vector2.zero;
            Invoke("Launch", 1);
        }
    
        public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
        {
            if (stream.IsWriting)
            {
                stream.SendNext(rigidbody.position);
                stream.SendNext(rigidbody.velocity);
            }
            else
            {
                rigidbody.position = (Vector2)stream.ReceiveNext();
                rigidbody.velocity = (Vector2)stream.ReceiveNext();
            }
        }
    }

  • Goal.cs
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.name.Contains("Ball"))
        {
            if(isPlayer1Goal)
    '''생략

  • GameManager.cs
    using Photon.Pun;
    using TMPro;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class GameManager : MonoBehaviourPunCallbacks
    {
        [Header("Ball")]
        public Ball ball;
    
        [Header("Player 1")]
        public Paddle player1Paddle;
        public Goal player1Goal;
    
        [Header("Player 2")]
        public Paddle player2Paddle;
        public Goal player2Goal;
    
        [Header("UI")]
        public TextMeshProUGUI player1Text;
        public TextMeshProUGUI player2Text;
    
        private int player1Score;
        private int player2Score;
    
        private void Start()
        {
            SpawnPaddle();
            if(photonView.AmOwner)
                SpawnBall();
        }
    
        private void SpawnPaddle()
        {
            int idx = PhotonNetwork.LocalPlayer.ActorNumber;
            GameObject prefab = Resources.Load<GameObject>("Paddle");
    
            if(idx == 1)
            {
                PhotonNetwork.Instantiate(prefab.name, new Vector3(-12, 0, 0), Quaternion.identity);
            }
            else
            {
                PhotonNetwork.Instantiate(prefab.name, new Vector3(12, 0, 0), Quaternion.identity);
            }
        }
    
        private void SpawnBall()
        {
            GameObject prefab = Resources.Load<GameObject>("Ball");
            GameObject go = PhotonNetwork.Instantiate(prefab.name, Vector3.zero, Quaternion.identity);
            ball = go.GetComponent<Ball>();
    
        }
    
    
        public void Player1Scored()
        {
            if (photonView.AmOwner)
            {
                player1Score++;
                ResetPosition();
                photonView.RPC("UpdateScore", RpcTarget.All, player1Score, player2Score);
            }
        }
    
        public void Player2Scored()
        {
            if (photonView.AmOwner)
            {
                player2Score++;
                ResetPosition();
                photonView.RPC("UpdateScore", RpcTarget.All, player1Score, player2Score);
            }
        }
        
    
        [PunRPC]
        public void UpdateScore(int score1, int score2)
        {
            player1Text.text = score1.ToString();
            player2Text.text = score2.ToString();
    
            if (score1 > 5 || score2 > 5)
                PhotonNetwork.LeaveRoom();
        }
    
        private void ResetPosition()
        {
            ball.Reset();
        }
    
        public override void OnLeftRoom()
        {
            SceneManager.LoadScene("LobbyScene");
        }
    
    
    }

다음과 같이 되면 거의 완성이다.

추가로 게임을 하다가 로비씬으로 돌아오게 되면 이미 Connection이 세팅된 상태이기 때문에 Star버튼이 활성화가 안될 것이다.

해당 문제를 해결하기 위해 LobbyMainPanel.cs에 다음 부분을 추가해주자.

  • LobbyMainPanel.cs
    private void Start()
    {
        if (PhotonNetwork.NetworkClientState == ClientState.Joined)
            this.SetActivePanel(SelectionPanel.name);
    }


빌드해서 테스트해보기

해상도 960x540정도로만 설정해주고 빌드를 해보았다.

다음과 같이 네트워크에 접속하여, 방 만드는 기능까지 완성하였다.

지금까지 포톤으로 멀티플레이를 구현해보았다.

실제 게임에선 Rigidbody와 Animator까지 동기화를 해줘야할 것이기 때문에 좀 더 어려운 부분이 많을 것이다.

해당 예제를 베이스로 멀티플레이를 제대로 구현해보자.


Uploaded by N2T