XR플밍 - (주말작업) 네트워크 프로젝트 Round5 9.2일차 (8/3)

이형원·2025년 8월 3일
0

XR플밍

목록 보기
154/215

1. 금일 한 업무 정리

작업을 하긴 했는데 PR은 못올렸다. 오늘 한 작업 내용은 짧게 말할 수 있으면서도 매우 어려운 작업이었다.

  • 인게임 맵 생성 동기화 인게임 맵 생성 동기화

아직 네트워크에 연동된 게임매니저가 없다 보니 여러모로 버그가 많이 터지는 것을 확인할 수 있다. 다음 라운드로 넘어갈 때 마스터가 아닌 클라이언트는 이전 맵이 비활성화가 안 된다든가, 체인이 깨지는 현상 및 UI 연동이 안되는 문제가 있다.
이 부분은 게임매니저도 구성하고 좀 더 고민을 한 다음에 구성할 예정이다.

2. 문제의 발생과 해결 과정

2.1 PhotonNetwork.Instantiate 에 대한 이해

이전에는 맵 프리팹 자체에다 PhotonView와 Photon Transform View를 붙이고 PhotonNetwork.Instantiate로 생성해 보고선 잘 생성되네 하고 넘어갔었다. 하지만 이젠 맵 생성 로직도 변경되었고, 맵을 한 번에 9개를 지정된 위치에다 생성하다 보니 문제가 생겼다.

이와 같이 PhotonNetwork.Instantiate로 생성하니, 마스터 클라이언트는 해당 오브젝트를 저기 안에 제대로 생성했는데 마스터가 아닌 클라이언트는 바깥에다가 생성한 것을 확인할 수 있다.
이는 마스터에게는 해당 위치에 생성하도록 내용이 전달되었지만 다른 클라이언트한테는 생성 위치가 전달되지 않음으로서 발생한 일이다.
생성 위치가 중요한가에 대한 부분에서는, 애초에 생성 위치가 맞지 않으면 맵이 움직이는 로직 또한 작동하지 않는다. 따라서 해당 시점에서는 맵이 동기화가 되지 않는 현상이 발견되었다. 이를 해결하기 위해선 어떻게 해야할까? 여러가지 방법을 시도해봤지만 제대로 된 해결책이 나오지 않았다.

여러 가지 시도해본 결과 나온 해결책은 RPC를 사용하는 것이었다.

  • 맵은 마스터 플레이어만 생성하고 랜덤 뽑기를 한다.
    Instantiate되는 맵은 PhotonView 가 붙어있으므로, 해당 오브젝트에 붙어 있는 스크립트에서 생성 위치 동기화 작업을 한다.
using DG.Tweening;
using Photon.Pun;
using UnityEngine;

/// <summary>
/// 게임 종료 후 맵을 이동시킬 때, 그 움직임을 표현하는 스크립트
/// </summary>
public class MapDynamicMovement : MonoBehaviourPun, IPunObservable
{
    private MapController mapController;
    private RandomMapPresetCreator randomMapPresetCreator;

    [SerializeField] GameObject[] mapComponents;

    // 첫 번째 플랫폼이 움직이기 시작하는 시점(딜레이)
    [SerializeField] float moveDelay = 1f;
    // 각 플랫폼이 이동하기 시작하는 간격
    [SerializeField] float moveDurationOffset = 0.2f;

    private Vector3 networkPos;
    private Quaternion networkRot;
    private bool networkActiveSelf;

    private void Start()
    {
        mapController = GetComponentInParent<MapController>();
        randomMapPresetCreator = GetComponentInParent<RandomMapPresetCreator>();
    }

    private void Update()
    {
        if (!PhotonNetwork.IsMasterClient)
        {
            transform.position = Vector3.Lerp(transform.position, networkPos, Time.deltaTime * 10f);
        }
    }

    public void DynamicMove()
    {
        Debug.Log("ddd");
        photonView.RPC("RPC_DynamicMove", RpcTarget.All);
    }

    [PunRPC]
    public void RPC_DynamicMove()
    {
        for (int i = 0; i < mapComponents.Length; i++)
        {
            float duration = moveDelay + (i * moveDurationOffset);
            mapComponents[i].transform.DOMove(mapComponents[i].transform.position + new Vector3(-randomMapPresetCreator.MapTransformOffset, 0, 0), duration)
                .SetDelay(mapController.MapChangeDelay).SetEase(Ease.InOutCirc);
        }
    }

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting && PhotonNetwork.IsMasterClient)
        {
            stream.SendNext(transform.position);
        }
        else if (stream.IsReading)
        {
            networkPos = (Vector3)stream.ReceiveNext();
        }
    }

    [PunRPC]
    public void SetParentToRound(int round)
    {
        Transform roundParent = FindObjectOfType<RandomMapPresetCreator>().GetRoundTransform(round);
        transform.SetParent(roundParent);
    }
}
using Cinemachine;
using Photon.Pun;
using UnityEngine;

public class RandomMapPresetCreator : MonoBehaviour
{
    // 맵을 Resources로 저장할 거면 해당 방식으로 변경 필요
    [SerializeField] GameObject[] mapResources;

    // 맵 위치 오프셋
    [SerializeField] private float mapTransformOffset = 35;
    public float MapTransformOffset { get { return mapTransformOffset; } }

    // 단일 라운드 수
    [SerializeField] int gameCycleNum = 3;

    [SerializeField] Transform[] mapListTransform;

    private WeightedRandom<GameObject> mapWeightedRandom = new WeightedRandom<GameObject>();

    private void OnEnable()
    {
        if (PhotonNetwork.IsMasterClient)
        {
            for (int i = 0; i < mapListTransform.Length; i++)
            {
                RandomInit();
                RandomMapSelect(i);
            }
        }
    }

    /// <summary>
    /// 랜덤 확률 초기 세팅
    /// </summary>
    private void RandomInit()
    {
        for (int i = 0; i < mapResources.Length; i++)
        {
            mapWeightedRandom.Add(mapResources[i], 1);
        }
    }

    /// <summary>
    /// 랜덤 맵 선택 - 한 번 선택한 맵은 랜덤 확률에서 아예 제외되므로 맵이 중복되지 않게 됨
    /// </summary>
    private void RandomMapSelect(int round)
    {
        for (int i = 0; i < gameCycleNum; i++)
        {
            GameObject selectedMap = mapWeightedRandom.GetRandomItemBySub();
            Vector3 selectedMapPosition = new Vector3((i + 1) * mapTransformOffset, 0, 5);
            GameObject map = PhotonNetwork.Instantiate(selectedMap.name, selectedMapPosition, Quaternion.identity);
            //GameObject map = Instantiate(selectedMap, selectedMapPosition, Quaternion.identity);
            map.transform.SetParent(mapListTransform[round]);

            PhotonView mapView = map.GetComponent<PhotonView>();
            mapView.RPC("SetParentToRound", RpcTarget.OthersBuffered, round);
        }
    }

    public Transform GetRoundTransform(int round)
    {
        return mapListTransform[round];
    }

    public void MapUpdate(int round)
    {
        if (TestIngameManager.Instance.IsGameOver) return;
        mapListTransform[round - 1].gameObject.SetActive(false);
        mapListTransform[round].gameObject.SetActive(true);
    }
}

RPC의 선언은 그 오브젝트가 붙어 있는 스크립트에서 선언, 그 다음 오브젝트를 마스터 플레이어가 랜덤으로 뽑고 생성한 다음 해당 신호를 RPC로 전달해준다. 그러면 참여한 플레이어가 해당 내용과 동일하게 프리팹을 정해진 위치에 생성한다.
여기서 중요한 점은 RPC가 PhotonView를 가진 오브젝트에서 발동된다는 점이다. 실제로 처음에는 외부에서 그냥 조작해보려고 시도했지만 작동되지 않는 것을 확인했다.

2.2 네트워크와 DOTween

이번에 깨달은 점 두 번째는 DOTween에 대한 점과 네트워크 동기화에 필요한 점이었다.

일단 이것 하나를 먼저 알아두자

DOTween으로 이동 같은 것을 했을 때, 그것은 실제 인스펙터 상으로 반영되는 것이 아니다.

이걸 몰랐기 때문에 많이 헤맸다.

따라서 이를 해결하기 위한 방법은 결국 RPC를 활용하는 것이다.

using DG.Tweening;
using Photon.Pun;
using UnityEngine;

/// <summary>
/// 게임 종료 후 맵을 이동시킬 때, 그 움직임을 표현하는 스크립트
/// </summary>
public class MapDynamicMovement : MonoBehaviourPun, IPunObservable
{
    private MapController mapController;
    private RandomMapPresetCreator randomMapPresetCreator;

    [SerializeField] GameObject[] mapComponents;

    // 첫 번째 플랫폼이 움직이기 시작하는 시점(딜레이)
    [SerializeField] float moveDelay = 1f;
    // 각 플랫폼이 이동하기 시작하는 간격
    [SerializeField] float moveDurationOffset = 0.2f;

    private Vector3 networkPos;
    private Quaternion networkRot;
    private bool networkActiveSelf;

    private void Start()
    {
        mapController = GetComponentInParent<MapController>();
        randomMapPresetCreator = GetComponentInParent<RandomMapPresetCreator>();
    }

    private void Update()
    {
        if (!PhotonNetwork.IsMasterClient)
        {
            transform.position = Vector3.Lerp(transform.position, networkPos, Time.deltaTime * 10f);
        }
    }

    public void DynamicMove()
    {
        Debug.Log("ddd");
        photonView.RPC("RPC_DynamicMove", RpcTarget.All);
    }

    [PunRPC]
    public void RPC_DynamicMove()
    {
        for (int i = 0; i < mapComponents.Length; i++)
        {
            float duration = moveDelay + (i * moveDurationOffset);
            mapComponents[i].transform.DOMove(mapComponents[i].transform.position + new Vector3(-randomMapPresetCreator.MapTransformOffset, 0, 0), duration)
                .SetDelay(mapController.MapChangeDelay).SetEase(Ease.InOutCirc);
        }
    }

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting && PhotonNetwork.IsMasterClient)
        {
            stream.SendNext(transform.position);
        }
        else if (stream.IsReading)
        {
            networkPos = (Vector3)stream.ReceiveNext();
        }
    }

    [PunRPC]
    public void SetParentToRound(int round)
    {
        Transform roundParent = FindObjectOfType<RandomMapPresetCreator>().GetRoundTransform(round);
        transform.SetParent(roundParent);
    }
}
using DG.Tweening;
using Photon.Pun;
using System.Collections;
using UnityEngine;

/// <summary>
/// 생성된 랜덤 3개의 맵이 한 게임 종료 후 이동하게 하는 스크립트
/// </summary>
public class MapController : MonoBehaviour
{
    [Header("Offset")]
    [Tooltip("맵 전환 시작 딜레이")]
    [SerializeField] private float mapChangeDelay = 0.8f;

    [SerializeField] private GameObject[] rounds;
    public float MapChangeDelay { get { return mapChangeDelay; } }

    private Coroutine moveCoroutine;

    private void OnEnable()
    {
        TestIngameManager.OnRoundOver += GoToNextStage;
        TestIngameManager.onCardSelectEnd += MapMove;
    }

    private void OnDisable()
    {
        TestIngameManager.OnRoundOver -= GoToNextStage;
        TestIngameManager.onCardSelectEnd -= MapMove;
    }

    public void GoToNextStage()
    {
        MapShake();
        MapMove();
    }

    /// <summary>
    /// 게임 종료 후 맵이 한 번 흔들림
    /// </summary>
    private void MapShake()
    {
        gameObject.transform.DOShakePosition(0.5f, 1, 10, 90);
    }

    /// <summary>
    /// 딜레이 시간 이후 맵의 움직임이 시작됨
    /// </summary>
    private void MapMove()
    {
        if (!PhotonNetwork.IsMasterClient) return;

        if (!TestIngameManager.Instance.IsCardSelectTime)
        {
            moveCoroutine = StartCoroutine(MovementCoroutine());
        }
    }

    IEnumerator MovementCoroutine()
    {
        WaitForSeconds delay = new WaitForSeconds(mapChangeDelay);

        MapDynamicMovement[] movements = rounds[TestIngameManager.Instance.CurrentGameRound].GetComponentsInChildren<MapDynamicMovement>();

        for (int i = 0; i < movements.Length; i++)
        {
            if (movements[i] != null)
            {

                movements[i].DynamicMove();
                yield return delay;
            }
        }

        moveCoroutine = null;
    }
}

이와 같이 움직임을 선언하는 과정 자체를 RPC로 선언해야지 동기화가 되며, 또한 이를 위해서는 해당 움직이는 오브젝트 대상자에게도 PhotonView 및 Photon Transform View를 붙여줘야 한다.

이와 같이 해야지 겨우 맵 이동의 네트워크 동기화를 할 수 있었다.

3. 개선점 및 과제

3.1 UI 동기화 및 버그 픽스

3.2 맵 중복 버그 픽스

3.3 인게임 매니저 및 연결작업

3.4 (도전) 맵 추가

profile
게임 만들러 코딩 공부중

0개의 댓글