멈추지 않고 끈질기게

[Unity][포트폴리오] 멀티플레이 관련 이슈 본문

포트폴리오

[Unity][포트폴리오] 멀티플레이 관련 이슈

sam0308 2023. 6. 11. 16:01

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

※ 해당 포스팅은 Unity 2021.3.15f1 버전을 기준으로 작성되었습니다.

 개발 중인 유니티 3D 포트폴리오의 소스 코드를 포함하고 있습니다.

 

 

 

1. 골인 시 처리

 멀티 플레이에서 골인 시 처리는 다음과 같은 로직으로 작성했습니다.

public void Goal()
{
    switch (curMode) 
    {
        case GameMode.SingleGame:
             StartCoroutine(Goal_SingleGame());
             break;
        // 승자는 Goal_MultiGame 직접 호출(isWinner = true)
        // 나머지 인원(패자)들은 RPC로 호출(isWinner = false)
        case GameMode.MultiGame:
             StartCoroutine(Goal_MultiGame(true));
             PV.RPC(nameof(Goal_Others), RpcTarget.Others); 
             break;
    }
}

 골인 시 처리 코루틴은 bool 매개변수를 받아 승자와 패자를 구분하도록 작성했습니다. 해당 코루틴을 골인한 플레이어는 직접 true로 호출하여 승자 처리를 하고, 해당 코루틴을 RPC로 타겟은 Others, 매개변수는 false로 호출하여 나머지 인원들은 패자에 대한 처리를 실행하도록 했습니다.

 

 다만 위와 같이 작성한 직후에 테스트해보니 골인한 플레이어도 패자 처리를 호출하는 이슈가 발생했습니다. 영상 1을 보면 골인 UI 직후 바로 패배 UI가 노출되는 것을 확인할 수 있습니다.

영상 1. 골인 시 처리 이슈

 원인은 플레이어의 충돌 판정을 내 클라이언트일 경우만 실행하도록 체크하지 않은데 있었습니다. 다른 사람의 클라이언트에서 내 캐릭터가 골인했을 때도 RPC를 호출하여 골인 처리가 꼬이고 있었습니다.

void OnTriggerEnter(Collider other)
{
    if (!isMine) return;

    // 추락 시 마지막 저장위치로 이동
    if (other.CompareTag(Tags.Fall))
    {
        transform.position = lastPos;
        rigid.angularVelocity = Vector3.zero;
        StageSoundController.PlaySfx((int)StageSoundController.StageSfx.reset);
        return;
    }
    // 저장위치 갱신
    if (other.gameObject.CompareTag(Tags.SavePoint))
    {
        Vector3 savePos = other.GetComponent<SavePoint>().GetPos();
        if (lastPos == savePos) // 이미 저장된 위치라면 패스
            return;

        lastPos = savePos;
        StageSoundController.PlaySfx((int)StageSoundController.StageSfx.savePoint);
        return;
    }
    // 골인 지점 도착
    if (other.gameObject.CompareTag(Tags.Goal))
    {
        GameManager.Instance.Goal();
        return;
    }
}

 골인 처리는 OnTriggerEnter()에서 다루고 있었는데, 골인 시 처리 외에는 플레이어의 위치 갱신과 관련된 내용이었습니다. Transform 정보는 Photon Transfrom View를 통해 자동으로 동기화하므로, OnTriggerEnter() 맨 처음에 내 클라이언트가 아니라면 바로 나가도록 코드를 추가하여 해결했습니다(isMine은 PV.IsMine 값을 저장해둔 변수입니다). 

 

 

 

2. TrueOrFalse 스테이지

 해당 스테이지의 경우 씬에 배치된 TrueOrFalse 발판(이하 TOF 발판)들에서 일부를 랜덤하게 진짜로 설정하는 로직을 사용했는데, 문제는 이 TOF 발판 정보가 동기화되지 않아 서로 다른 맵을 플레이하고 있었습니다. 기존의 로직에 대한 정보는 이전 포스팅에 있습니다.

https://sam0308.tistory.com/61

 

[Unity][포트폴리오] 진짜 혹은 가짜(True or False) 발판

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다. ※ 해당 포스팅은 Unity 2021.3.15f1 버전을 기준으로 작성되었습니다. ※ 개발 중인 유니티 3D 포트폴

sam0308.tistory.com

 

 문제의 핵심은 랜덤 설정 로직을 클라이언트마다 각각 실행하면 동기화가 맞지 않는다는 점이었습니다. 처음엔 그냥 Initialize 전체를 RPC로 호출할까 했지만, Random.Range()의 결과는 RPC로 실행했다고 하더라도 서로 달라지기 때문에 해결책이 되지 못했습니다. 따라서 랜덤값을 뽑는 부분 이후, 해당 값을 통해 발판을 진짜로 설정하는 부분만을 별도의 함수로 작성하고 뽑은 랜덤 값을 매개변수로 받도록 했습니다. 해당 함수를 RPC로 호출하여 모든 클라이언트에서 발판 설정이 동일하도록 구현했습니다.

protected override void Initialize()
{
    // 마스터 클라이언트에서만 실행
    // 발판 설정 부분은 RPC로 실행하여 다른 클라이언트들과 동기화
    if (NetworkManager.Instance.IsMaster == false)
        return;

    // 랜덤 루트 설정
    // 5개의 발판 중 하나는 true 발판으로 설정
    // 이전 발판 1칸 앞 발판과 새로 지정한 발판 사이는 true 발판으로 설정
    Queue<int> que = new Queue<int>();
    for(int i = 0; i < tofPlatforms.Length; i += column) 
    {
        int idx = Random.Range(0, 5);

        // 첫행이 아닐 경우
        if(que.Count > 0)
        {
            int high = que.Peek() > idx ? que.Peek() : idx;
            int low = que.Peek() < idx ? que.Peek() : idx;
            PV.RPC(nameof(SetTrue), RpcTarget.All, i, low, high);
            // 큐 비우기
            que.Dequeue();
        }
        // 첫 행일 경우
        else
            PV.RPC(nameof(SetTrue), RpcTarget.All, i, idx, idx);

        que.Enqueue(idx);
    }
}

// 진짜 발판으로 설정
// 모든 클라이언트에서 동기화되도록 RPC로 실행
[PunRPC]
void SetTrue(int idx, int low, int high)
{
    for (int i = low; i <= high; i++)
        tofPlatforms[idx + i].IsTrue();
}

  기존의 Initialize 함수에 마스터 클라이언트에서만 실행하도록 조건문을 추가하고, 발판 설정 부분을 SetTrue()라는 별도의 RPC 함수로 작성하면서 Random.Range()로 뽑은 값을 매개변수로 받도록 했습니다. 해당 방법을 통해 모든 클라이언트에서 발판 설정이 동기화되도록 할 수 있었습니다.

 

 

 

3. 기타 이슈

캐릭터 애니메이션

 멀티플레이 용으로 추가한 2P용 캐릭터에서 Bool 값을 조건으로 하는 애니메이션이 동기화되지 않는 이슈가 있었습니다.

단순히 Photon Animator View에서 설정을 누락하여 발생한 이슈로, 설정 후 해결되었습니다.

 

사진 1. Photon Animator View 설정

 

씬 로딩 순서

 StageController 클래스의 Start()에서 NullReferenceException이 발생하는 이슈가 있었습니다. 타이머를 설정하는 부분에서 발생하는 이슈였습니다.

void Start()
{
    // 현재 스테이지를 액티브 씬으로 설정 후 캐릭터 생성
    SceneController.Instance.SetActiveScene();
    // null을 반환받았을 경우 예외 발생시킴
    GameObject p = InstantiatePlayer() ?? throw new Exception("캐릭터 생성 실패");

    // 스테이지 보여주기 코루틴 실행
    initialPos = mainCamera.gameObject.transform.position;
    StartCoroutine(ShowStage(p.transform));

    // 타이머 설정
    // 이슈 발생 부분
    UiController.Instance.SetTimeLimit(timeLimit);
}

사진 2. NullReferenceException

 

 원인은 씬 로딩 순서에 있었습니다. 기존의 스테이지 입장 함수에서 각종 오브젝트들을 포함한 스테이지 씬을 먼저 로드하고, UIController를 포함한 플레이어 씬을 그 뒤에 로드하도록 작성했었습니다. 타이머를 추가하면서 StageController의 Start()에서 타이머 설정 함수를 호출하도록 했는데, 여기서 아직 로딩이 끝나지 않은 플레이어 씬의 UIController에 접근하면서 발생하는 문제였습니다. 

 

 간단하게 스테이지 입장 함수에서 로드 순서를 바꾸어 플레이어 씬을 먼저 로드하고, 스테이지 씬을 나중에 로드하도록 수정하여 해결했습니다.