This document is about: FUSION 2
SWITCH TO

퀴즈 네트워크

Level 4

개요

Fusion Quiz Network 샘플은 최대 20명의 플레이어를 위한 공유 모드 퀴즈 게임이며 Photon Voice를 사용합니다. 플레이어는 일련의 퀴즈 질문을 받고, 빠르게 답변하여 점수를 획득합니다. 이 공유 모드 샘플은 사전 설정된 데이터를 사용한 게임 세션에 참여하고, 마스터 클라이언트 상태 권한을 전환하며, Tick Timers 사용 등을 보여줍니다.

다운로드

버전 릴리즈 일자 다운로드
2.0.1 Jun 19, 2024 Fusion Quiz Network 2.0.1 Build 576

Fusion 연결

FusionConnection 클래스는 게임 세션을 위한 NetworkRunner를 생성하는 역할을 합니다. 또한 로컬 플레이어의 이름과 로컬 플레이어가 생성할 세션 게임의 이름을 저장합니다.
FusionConnection은 싱글톤으로 구현되어 하나의 인스턴스만 존재할 수 있습니다. 인스턴스는 다음과 같은 코드로 Awake 메서드에서 설정됩니다:

C#

private void Awake()
{
    // ...
    if (Instance != null)
    {
        Destroy(gameObject);
    }
    Instance = this;
    DontDestroyOnLoad(gameObject);
}

퀴즈 네트워크의 메인 메뉴에서 플레이어가 Create Room 또는 Join Room을 선택하면 다음 시작 인수를 통해 연결을 시도합니다:

C#

public async void StartGame(bool joinRandomRoom)
{
    StartGameArgs startGameArgs = new StartGameArgs()
    {
        GameMode = GameMode.Shared,
        SessionName = joinRandomRoom ? string.Empty : LocalRoomName,
        PlayerCount = 20,
    };
    // ...
}
  • GameMode: 사용되는 게임 모드, 이 경우 각 플레이어가 생성한 네트워크 오브젝트에 대해 상태 권한을 가지는 공유 모드입니다.
  • SessionName: 생성될 세션의 이름입니다. 세션이 지정되지 않았거나 joinRandomRoom이 true인 경우, 매치메이킹은 플레이어가 열린 세션에 참여하도록 시도합니다. 이것이 실패하면 System.Guid를 사용하여 새 세션이 생성됩니다. 그렇지 않으면 플레이어는 LocalRoomName을 사용하여 세션에 참여하려고 시도합니다. 이 세션이 존재하지 않으면 이 이름으로 새 세션이 생성됩니다.
  • PlayerCount: 세션에 허용되는 플레이어 수를 정의합니다. 이 경우 20명입니다. LocalRoomName으로 정의된 세션에 20명의 플레이어가 있는 경우, 플레이어가 이 세션에 참여하려고 시도하면 오류가 발생합니다.

이후 새로운 NetworkRunner가 인스턴스화되어 이러한 StartGameArgs로 연결을 시도합니다.

C#

// ...
NetworkRunner newRunner = Instantiate(_networkRunnerPrefab);

StartGameResult result = await newRunner.StartGame(startGameArgs);

if (result.Ok)
{
    roomName.text = "Room:  " + newRunner.SessionInfo.Name;
    GoToGame();
}
else
{
    roomName.text = string.Empty;

    GoToMainMenu();

    errorMessageObject.SetActive(true);
    TextMeshProUGUI gui = errorMessageObject.GetComponentInChildren<TextMeshProUGUI>();
    if (gui) {
        gui.text = result.ErrorMessage;
    }

    Debug.LogError(result.ErrorMessage);
}
// ...

NetworkRunner.StartGame은 비동기 함수이므로 StartGameResult가 설정되기까지 지연이 발생합니다. 완료되면 세션 참여에 성공한 경우 세션 이름이 표시되고, 실패한 경우 오류 화면이 표시됩니다.

퀴즈 플레이어

플레이어가 참여하면 아바타가 스폰 됩니다. 이 NetworkObject에는 TriviaPlayer라는 NetworkBehaviour가 포함되어 있습니다. 각 플레이어는 Networked 속성을 사용하는 일련의 속성을 가지고 있습니다.

C#

[Networked(), OnChangedRender(nameof(OnPlayerNameChanged))]
public NetworkString<_16> PlayerName { get; set; }

이 속성은 플레이어 이름이 표시되는 방식을 관리합니다. 이는 Fusion에서 문자열을 처리하는 고유한 유형인 NetworkString을 사용하여, 이 경우 16자로 제한됩니다. 또한 OnChangedRender라는 두 번째 속성과 함수 이름인 OnPlayerNameChanged를 사용합니다.

C#

void OnPlayerNameChanged()
{
    nameText.text = PlayerName.Value;
}

이는 TextMeshProUGUI 객체인 nameTexttext 속성을 업데이트합니다. 새 플레이어가 스폰 될 때 자동으로 OnChangedRender 메서드가 호출되지 않는다는 점이 중요합니다. 대신, 이러한 속성은 NetworkObjectSpawned 메서드 내에서 업데이트하는 것이 좋습니다.

또한, 스폰 시 모든 TriviaPlayers에 대한 참조를 포함하는 정적 리스트와 로컬 TriviaPlayer에 대한 정적 참조에 추가됩니다. 이는 나중에 설명할 것입니다.

퀴즈 관리자

Trivia Manager는 게임이 시작된 후 어떤 질문이 제시될지를 섞고 업데이트하는 역할을 하는 NetworkBehaviour입니다. 게임은 Trivia Manager가 스폰 될 때 시작됩니다. 원격 프로시저 호출(RPC)이 필요하지 않은 이유는 NetworkRunnerTrivia Manager를 스폰 할 때 모든 플레이어에게 스폰 되기 때문입니다.

공유 모드에서는 호스트 모드와 달리 씬에 대한 상태 권한을 가진 호스트 플레이어가 없습니다. 그러나 하나의 플레이어만 이 오브젝트에 대한 상태 권한을 가질 수 있으며, 이 경우 공유 모드 마스터 클라이언트입니다. 설정 시 Trivia Manager는 마스터 클라이언트 오브젝트로 지정됩니다.

이는 마스터 클라이언트가 이 오브젝트에 대한 상태 권한을 가지며, 이들이 떠날 경우 상태 권한이 새로운 마스터 클라이언트로 전환된다는 것을 의미합니다. Trivia ManagerIStateAuthorityChanged 인터페이스를 구현하여 이 전환이 발생할 때 StateAuthorityChanged를 호출합니다.

Trivia Manager는 게임의 다양한 상태를 업데이트하는 데 사용되는 TickTimer도 가지고 있습니다. TickTimer는 상태 권한을 가진 플레이어만 실행하는 FixedUpdateNetwork 동안만 확인되므로 모든 플레이어가 실행하는 Update에서 시각적 업데이트가 처리됩니다.

C#

public void Update()
{
    // Updates the timer visual
    float? remainingTime = timer.RemainingTime(Runner);
    if (remainingTime.HasValue)
    {
        float percent = remainingTime.Value / timerLength;
        timerVisual.fillAmount = percent;
        timerVisual.color = timerVisualGradient.Evaluate(percent);
    }
    else
    {
        timerVisual.fillAmount = 0f;
    }
}

TickTimer에서 남은 시간을 확인할 때, 결과는 Nullable일 수 있으며, 이는 float?로 표현됩니다. 이는 값이 있거나 null일 수 있음을 의미합니다. 이러한 결과는 각각 다르게 처리됩니다.

질문에 답하기

Trivia ManagerFixedUpdateNetwork에서 CurrentQuestion을 증가시켜 현재 질문을 업데이트하면, 플레이어는 버튼을 클릭하여 간단히 질문에 답할 수 있으며, 이는 PickAnswer를 트리거 합니다.

C#

public void PickAnswer(int index)
{
    // If we are in the question state and the local player has not picked an answer...
    if (GameState == TriviaGameState.ShowQuestion)
    {
        // For now, if Chosen Answer is less than 0, this means they haven't picked an answer.
        // We don't allow players to pick new answers at this time.
        if (TriviaPlayer.LocalPlayer.ChosenAnswer < 0)
        {
            _confirmSFX.Play();

            TriviaPlayer.LocalPlayer.ChosenAnswer = index;

            // Colors the highlighted question cyan.
            answerHighlights[index].color = Color.cyan;

            float? remainingTime = timer.RemainingTime(Runner);
            if (remainingTime.HasValue)
            {
                float percentage = remainingTime.Value / this.timerLength;
                TriviaPlayer.LocalPlayer.TimerBonusScore = Mathf.RoundToInt(timeBonus * percentage);
            }
            else
            {
                TriviaPlayer.LocalPlayer.TimerBonusScore = 0;
            }
        }
        else
        {
            _errorSFX.Play();
        }
    }
}

이 메서드에서는 먼저 현재 질문이 표시되고 있는지 확인하기 위해 TriviaManagerGameState를 확인합니다. TriviaPlayerLocalPlayer 참조가 답변을 선택하지 않은 경우(ChosenAnswer가 0보다 작음), 유니티 측에서 정의된 값이 ChosenAnswer에 설정됩니다. 또한, TimerBonusScoreTriviaManagerTickTimer의 남은 시간과 유니티 측에서 정의된 값을 기반으로 설정됩니다.

그런 다음, TriviaManagerFixedUpdateNetwork 메서드 내에서 모든 플레이어의 답변이 확인됩니다.

C#

// ...
// We check to see if every player has chosen answer, and if so, go to the show answer state.
if (GameState == TriviaGameState.ShowQuestion)
{
    int totalAnswers = 0;
    for (int i = 0; i < TriviaPlayer.TriviaPlayerRefs.Count; i++)
    {
        if (TriviaPlayer.TriviaPlayerRefs[i].ChosenAnswer >= 0)
        {
            totalAnswers++;
        }
    }

    if (totalAnswers == TriviaPlayer.TriviaPlayerRefs.Count)
    {
        timerLength = 3f;
        timer = TickTimer.CreateFromSeconds(Runner, timerLength);
        GameState = TriviaGameState.ShowAnswer;
    }
}

GameState를 확인하여 질문이 표시되고 있는지 확인합니다. 각 TriviaPlayer 참조를 TriviaPlayerRefs에서 반복합니다. 질문에 답한 경우 totalAnswers를 증가시키고 플레이어 수와 일치하면 TriviaManagerGameStateTriviaGameState.ShowAnswer로 전환되고 답이 표시됩니다. 이 때문에 각 TriviaPlayer의 참조가 생성될 때 저장됩니다.

게임 종료

Trivia Manager는 또한 FixedUpdateNetwork 메소드를 통해 게임 종료를 처리합니다.

C#

// When the timer expires...
if (timer.Expired(Runner))
{
    // If we are showing a question, we then show an answer...
    if (GameState == TriviaGameState.ShowQuestion)
    {
        timerLength = 3f;
        timer = TickTimer.CreateFromSeconds(Runner, timerLength);
        GameState = TriviaGameState.ShowAnswer;
        return;
    }
    else if (QuestionsAsked < maxQuestions)
    {
        TriviaPlayer.LocalPlayer.ChosenAnswer = -1;

        CurrentQuestion++;
        QuestionsAsked++;

        timerLength = questionLength;
        timer = TickTimer.CreateFromSeconds(Runner, timerLength);
        GameState = TriviaGameState.ShowQuestion;
    }
    else
    {
        timer = TickTimer.None;
        GameState = TriviaGameState.GameOver;
    }

    return;
}

TickTimer가 만료되면 QuestionsAsked가 확인되고, 유니티 측에서 설정된 maxQuestions보다 질문 수가 더 이상 적지 않으면 TickTimerTickerTime.None으로 설정되어 타이머가 중지되고 TriviaManagerGameStateTriviaGameState.GameOver로 설정됩니다.

GameState를 설정하면 GameStateOnChangedRender 속성의 일부로 OnTriviaGameStateChanged가 트리거 됩니다. 이 메서드 내부에서 OnGameStateGameOver가 호출되고 최종 점수가 평가됩니다.

C#

private void OnGameStateGameOver()
{
    // ...

    // Sorts all players in a list and keeps the three highest players.
    List<TriviaPlayer> winners = new List<TriviaPlayer>(TriviaPlayer.TriviaPlayerRefs);
    winners.RemoveAll(x => x.Score == 0);
    winners.Sort((x, y) => y.Score - x.Score);
    if (winners.Count > 3)
    {
        winners.RemoveRange(3, winners.Count - 3);
    }

    endGameObject.Show(winners);

    if (winners.Count == 0)
    {
        triviaMessage.text = "No winners";
    }
    else
    {
        triviaMessage.text = winners[0].PlayerName.Value + " Wins!";
    }

    // ...
}

현재 모든 플레이어를 새로운 목록으로 가져와 점수에 따라 정렬하고 상위 3명의 플레이어를 유지하여 endGameObject.Show에 제공하여 승자를 보여줍니다. 또한 Shared Mode Master Client에게는 새로운 질문 라운드를 시작하는 버튼이 표시됩니다.

Photon Voice

연결된 플레이어들은 Photon Voice를 통해 마이크를 통해 통신할 수 있습니다. 이 샘플에서는 다음과 같은 컴포넌트를 사용하여 이를 구현합니다:

  • Fusion Voice Client: 이 컴포넌트는 NetworkRunner 프리팹에 추가되며 Photon Voice의 초기 설정을 정의합니다.
`Fusion Voice Client` 컴포넌트 미리 보기.
  • Recorder: 이 컴포넌트는 TriviaPlayer 프리팹에 추가되어 사용자의 음성을 녹음하여 네트워크로 전송합니다.
  • Speaker: TriviaPlayer 프리팹에 추가되어 다른 플레이어의 녹음된 오디오를 받아 AudioSource 컴포넌트를 통해 재생합니다.
  • Voice Network Object: TriviaPlayer에 부착된 이 NetworkBehaviourPhoton Fusion에서 RecorderSpeaker를 설정합니다.
이전에 언급된 세 가지 컴포넌트와 그 설정.

게임 내에서 로컬 플레이어가 녹음되고 있거나 다른 플레이어가 말하고 있을 때 아이콘이 전환됩니다. TriviaPlayerUpdate 함수에서 이를 다음과 같이 구현합니다:

C#

private void Update()
{
    speakingIcon.enabled = (_voiceNetworkObject.SpeakerInUse && _voiceNetworkObject.IsSpeaking) || (_voiceNetworkObject.RecorderInUse && _voiceNetworkObject.IsRecording);
}

먼저, VoiceNetworkObjectSpeakerInUseIsSpeaking 속성은 원격 플레이어가 말하고 있음을 나타냅니다. 한편, RecorderInUseIsRecording은 로컬 플레이어가 말하고 있음을 나타냅니다.

이 샘플에서는 로컬 플레이어가 자신의 오디오 전송을 방지하고 다른 플레이어에게 음소거되었음을 표시할 수 있습니다. 이는 다음과 같이 구현됩니다:

C#

[Networked(), OnChangedRender(nameof(OnMuteChanged))]
public NetworkBool Muted { get; set; }

public void OnMuteChanged()
{
    muteSpeakerIcon.enabled = Muted;
}

public void ToggleVoiceTransmission()
{
    if (HasStateAuthority)
    {
        Muted = !Muted;
        _recorder.TransmitEnabled = !Muted;
    }
}

MutedOnChangedRender 속성이 있는 NetworkBool이며, OnMuteChanged를 호출하여 플레이어가 음소거되었음을 보여주는 시각적 표현을 업데이트합니다. ToggleVoiceTransmission은 유니티 측에서 ButtonOnClick 이벤트를 통해 트리거 되는 함수로, StateAuthority가 있는 플레이어에 대해 Muted 값을 전환하고 Recorder.TransmitEnabledMuted의 반대로 설정합니다. Recorder.TransmitEnabled를 false로 설정하면 로컬 플레이어의 녹음이 방지됩니다.

게임 내에서 한 플레이어가 말하고 다른 플레이어가 음소거된 미리 보기.

Photon Fusion과 함께 Photon Voice 설정에 대해 더 알아보세요.

타사 에셋

Quiz Network 샘플에는 Kenney가 제공한 여러 서드파티 에셋이 포함되어 있으며, CC0 라이선스를 사용하여 퍼블릭 도메인에 속하며, 상업적 용도나 기타 용도로 사용 시 출처를 밝히지 않아도 됩니다.

Back to top