3 분 소요

Intro

기존 프로젝트에서 멀티플레잉이 가능한 모드를 추가로 구현해보려고합니다.

추가할 기능은 다음과 같습니다.

  • 다른 플레이어와 함께 던전을 공략할 수 있는 “매칭시스템” 구현

구조적 문제점과 방향성

기존 프로젝트는 멀티플레잉을 고려하지 않은 구조로 만들어져있습니다.

싱글플레잉을 없애고 멀티플레잉 게임으로 만드는것은 현실적으로 불가능하여

싱글플레잉 + 부분적 멀티요소 구조의 게임으로 바꿔보기로 했습니다.


아래는 기존 씬 구성입니다.

image

  • Persistant Scene
    • 게임내내 유지되는 오브젝트들을 초기화하고, 씬을 언로드하지 않으므로 다른씬에서도 오브젝트들이 유지됩니다.
  • Viliage Scene, Dungeon Scene
    • 포탈을 통해 씬을 옮겨다닐 수 있으며, 일회성 씬입니다.

이 구조의 문제점은

PersistantScene 에서 많은것을 초기화(플레이어 참조, 카메라 참조, 기타 참조 등)를 진행한다는것입니다.

멀티플레이에서 사용되는 여러가지 게임 오브젝트들은 Photon에 의해 생성, 초기화 및 동기화가 이루어져야하므로, 게임 시작전에 미리 할당시켜버리는 기존의 구조로는 멀티플레잉 환경을 구축하기 어려웠습니다.


기존 구조를 유지하면서 멀티플레잉을 구현할 방법이 없을까 고민하다가 내린 결론은

구조는 유지하되, 동적인 방법으로 기존 코드를 손보는것이 좋을것같다는 결론을 내렸습니다.

image

  • 마을에서 던전 매칭 NPC를 통해 다른 플레이어와 던전에 입장할 수 있도록 하였습니다.

  • 기존에 SerializeField 등의 방법으로 참조를 미리 할당하는 방식을 쳐내고, 런타임중에 필요할 때 동적으로 할당하는 방식으로 코드를 변경하였습니다.

  • 플레이어 객체는 싱글플레이와 멀티플레이용 프리팹을 따로만들어 멀티플레이 컨텐츠에 진입할때는 싱글플레이 객체를 파괴하고, 멀티플레이용 객체를 새로 생성한 뒤 파괴직전의 플레이어 정보와 동기화되도록 하였습니다.

    • 반대의 상황도 고려하였습니다.

던전 매칭 구현

다른 플레이어와 던전을 함께 입장할 수 있는 빠른 매칭을 구현하였습니다.

멀티플레이를 위해 다음 작업을 수행하였습니다.

  • 멀티용 던전씬 생성
  • 매칭 NPC 생성

던전씬은 기존의것을 가져왔고, 네트워크를 통해 생성되어야하는 오브젝트들은 제외하였습니다.

매칭 NPC에 매칭버튼이 있는 간단한 UI를 생성하였습니다.

image

매칭하기 버튼을 누르면 매칭중임을 표시하는 UI를 표시합니다.

image


Viliage씬에 Match Manager를 두어 매칭시스템을 관리하도록 하였습니다.

/*
                    MatchManager : 던전 빠른매칭 시스템
            
            - NPC와 상호작용하여 매칭시작
            - 빈방이 없을경우 : 방 생성후 다른플레이어 참가 대기
            - 빈방이 있을경우 : 방 참가후 함께 씬 이동
 */

public class MatchManager : MonoBehaviourPunCallbacks
{
    [SerializeField] private Button matchButton;

    private readonly string gameVersion = "1";

    private void Start()
    {
        PhotonNetwork.GameVersion = gameVersion;
        PhotonNetwork.ConnectUsingSettings();

        matchButton.interactable = false;
    }

    public override void OnConnectedToMaster()
    {
        matchButton.interactable = true;
    }

    public override void OnDisconnected(DisconnectCause cause)
    {
        matchButton.interactable = false;
    }

    public void Match()
    {
        matchButton.interactable = false;
        PhotonNetwork.AutomaticallySyncScene = true;        // 씬 자동 동기화

        if(PhotonNetwork.IsConnected)
        {
            PhotonNetwork.JoinRandomRoom();
        }
        else
        {
            PhotonNetwork.ConnectUsingSettings();
        }
    }

    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        PhotonNetwork.CreateRoom(null, new RoomOptions { MaxPlayers = 2 });
    }

    public override void OnJoinedRoom()
    {

        Debug.Log("방 입장완료. 상대방 대기중...");
    }

    public override void OnPlayerEnteredRoom(Player newPlayer)
    {
        if(PhotonNetwork.CurrentRoom.PlayerCount == 2 && PhotonNetwork.IsMasterClient)
        {
            PhotonNetwork.LoadLevel("MultiDungeon");
        }
    }
}

던전에 입장하면 Multi Dungeon Manager에 의해 플레이어 오브젝트가 생성됩니다.

public class MultiDungeonManager : MonoBehaviourPunCallbacks
{
    [SerializeField] public GameObject playerPrefab;                // 플레이어 프리팹(멀티용)
    [SerializeField] public Transform[] spawnPositions;             // 플레이어 스폰위치

    private static MultiDungeonManager instance;

    public static MultiDungeonManager Instance
    {
        get
        {
            if (Instance == null) instance = FindObjectOfType<MultiDungeonManager>();

            return instance;
        }
    }

    private void Start()
    {
        SpawnPlayer();

        /*
        if(PhotonNetwork.IsMasterClient)
        {
            
        }
        */
    }


    // 플레이어 오브젝트 생성
    private void SpawnPlayer()
    {
        var localPlayerIndex = PhotonNetwork.LocalPlayer.ActorNumber - 1;                    // 플레이어 넘버
        var spawnPosition = spawnPositions[localPlayerIndex];                                // 플레이어 위치 설정

        PhotonNetwork.Instantiate(playerPrefab.name, spawnPosition.position, spawnPosition.rotation);

        // 플레이어 관련 초기화
        GameManager.Instance.FindPlayerObject();
        GameManager.Instance.FindCameraObject();
        DataManager.Instance.LoadPlayerData();
    }
}

클래스 변경점

멀티플레이의 추가에따라 플레이어 및 카메라등의 오브젝트를 참조하고있던 클래스들의 로직을 변경하였습니다.

GameManager


GameManager 클래스에 다음을 추가하였습니다.

  • 멀티플레이 여부 반환 : 현재 플레이어가 멀티플레이 환경인지 여부를 반환합니다.
  • 플레이어 오브젝트 참조 할당 : 멀티/싱글 전환시 새로운 플레이어 오브젝트를 참조합니다.
  • 카메라 오브젝트 참조 할당 : 멀티/싱글 전환시 새로운 카메라 오브젝트를 참조합니다.
public class GameManager : Singleton<GameManager>
{
    public PlayerController player;
    public CinemachineVirtualCamera virtualCamera;
    public bool isMultiPlaying = false;                     // 멀티플레잉 여부

    [SerializeField] public GameObject profileUI;

    protected override void Awake()
    {
        base.Awake();
        FindPlayerObject();
        FindCameraObject();
    }
    
    private void Update()
    {
        // 캐릭터 정보창 활성화
        if (Input.GetKeyDown(KeyCode.P))
        {
            UIManager.Instance.ToggleUI(profileUI);
        }
    }

    // 플레이어 오브젝트 탐색(멀티 <-> 싱글 변경시)후 할당
    public void FindPlayerObject()
    {
        player = FindObjectOfType<PlayerController>();
    }
    
    // 카메라 오브젝트 탐색(멀티 <-> 싱글 변경시)후 할당
    public void FindCameraObject()
    {
        GameObject cam = GameObject.FindWithTag("PlayerCamera");
        if(cam != null)
        {
            virtualCamera = cam.GetComponent<CinemachineVirtualCamera>();
            virtualCamera.Follow = player.transform;
        }
    }
}

PlayerController

PlayerController 클래스에서 멀티플레잉 여부를 지정합니다.

     private void Start()
    {
        GameManager.Instance.isMultiPlaying = PhotonNetwork.InRoom;            // 멀티 여부

        if (GameManager.Instance.isMultiPlaying)
            return;
        // ...
    }

    private void Update()
    {
        playerData = DataManager.Instance.GetPlayerData();

        if (GameManager.Instance.isMultiPlaying && !photonView.IsMine)
            return;

        // ...
    }

WeaponManager

WeaponManager 클래스에서 멀티플레이 입장시 무기를 세팅합니다.

    // 현재 무기 세팅(기본값 Punch)
    public void SetWeapon(string type = "None", string weapon = "Punch")
    {
        if(Enum.TryParse(type, out WeaponType result))
        {
            // 싱글플레이
            if(!GameManager.Instance.isMultiPlaying)
            {
                // ...
            }
            // 멀티플레이 무기프리팹 생성
            else
            {
                // ...
            }
        }
        else
        {
            Debug.Log($"{type} 은(는) 유효한 타입이 아닙니다.");
        }
    }

플레이 테스트 영상

image

댓글남기기