This document is about: FUSION 2
SWITCH TO

Quiz Network

Level
BEGINNER

概述

Fusion Quiz Network 範例是共享模式的機智問答遊戲,可加入20名玩家,並且使用Photon Voice。玩家會被問到一系列冷知識問題,快速回答者會獲得分數。此Shared Mode範例展示了使用預設資料加入遊戲階段、切換主客戶端State AuthorityTick Timers的使用情況等。

下載

版本 發佈日期 下載
2.0.5 Mar 04, 2025 Fusion Quiz Network 2.0.5 Build 830

連線到Fusion

FusionConnection類別負責為遊戲階段建立NetworkRunner。它也儲存本機玩家的名稱和本機玩家將建立的遊戲階段遊戲的名稱(如果指定的話)。
FusionConnection是作為一個單一資料庫來執行的,這意味著它只能有一個執行個體。該執行個體位於Awake方法中,附有以下程式碼:

C#

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

Quiz Network的主選單中,當玩家選擇Create RoomJoin Room,玩家嘗試透過以下的開始引數來連線:

C#

public async void StartGame(bool joinRandomRoom)
{
    StartGameArgs startGameArgs = new StartGameArgs()
    {
        GameMode = GameMode.Shared,
        SessionName = joinRandomRoom ? string.Empty : LocalRoomName,
        PlayerCount = 20,
    };
    // ...
}
  • GameMode:使用的Game Mode,在這個案例中是Shared Mode,其中客戶端連線到Photon Cloud房間,每個玩家都對他們生成的網路物件擁有狀態授權。
  • SessionName:將建立的遊戲階段的名稱。如果沒有指定遊戲階段,或者joinRandomRoom為真,則對戰配對將嘗試讓玩家加入開啟的遊戲階段。如果這樣做失敗,則使用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包含名為TriviaPlayerNetworkBehaviour。每個玩家都有一系列使用Networked屬性的屬性。

C#

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

此屬性管理玩家名稱的顯示方式。它使用NetworkString,這是Fusion的一個獨特類型,用於處理具有強制限制的字串,在本例中為16個字元。它也使用第二個屬性OnChangedRender和函數的名稱OnPlayerNamed

C#

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

它更新作為TextMeshProUGUI物件的nameTexttext屬性。需要注意的是,當一個新玩家生成時,不會自動調用OnChangedRender方法。取而代之地,最好在NetworkObjectSpawned方法中更新這些屬性。

此外,當生成時,玩家會被新增到一個靜態清單中,該清單包含對所有TriviaPlayers的參照以及對本機TriviaPlayer的靜態參照,稍後將對此進行解釋。

冷知識管理器

Trivia Manager是一種NetworkBehaviour,用於處理遊戲開始後的調換問題順序和更新將詢問的問題,以及處理使用的TickTimer。當生成Trivia Manager時,遊戲開始;不需要遠端程序調用(RPC),因為當NetworkRunner生成Trivia Manager時,將為所有玩家生成它。

Shared Mode中,沒有一個主機端玩家像在Host Mode中那樣對場景具有狀態授權。然而,只有一個玩家可以對該物件具有狀態授權,在這種情況下,它就是Shared Mode Master Client。設定時,Trivia Manager被指定為主客戶端物件。

這意味著主客戶端將對該物件擁有狀態授權,而如果他們離開,狀態授權將移交給新的主客戶端。由於Trivia Manager執行IStateAuthorityChanged介面,因此當發生此移交時,它將調用StateAuthorityChanged

Trivia Manager也有一個TickTimer,用於更新遊戲的各種狀態。僅在FixedUpdateNetwork期間檢查TickTimer,該過程僅由具有狀態授權的玩家執行,因此TickTimer的視覺效果更新是在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 Manager透過增加FixedUpdateNetwork中的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參照沒有選擇由ChosenAnwswer指定的小於0的答案,則它們在Unity側定義的所選值將設定為ChosenAnswer。此外,TimerBonusScore被設定為基於TriviaManagerTickTimer的剩餘時間和Unity側定義值的值。

然後,在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,如果顯示問題,則TriviaPlayerRefs中的每個TriviaPlayer參照都會被迭代。如果他們已經回答了問題,totalAnswers將遞增,如果它與玩家數量匹配,則TriviaManagerGameState將變為TriviaGameState.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,如果所問問題的數量不再小於在Unity側設定的maxQuestions定義的一回合中的問題數量,則將TickTimer設定為TickerTime.None,停止它,並且TriviaManagerGameState設定為TriviaGameState.GameOver

透過設定GameStateOnTriviaGameStateChanged作為GameStateOnChangedRender屬性的一部分被觸發。在這種方法中,調用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!";
    }

    // ...
}

這將所有當前玩家帶到一個新的清單,該清單按分數排序,前三名玩家將被保留並提供給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:這個附加到TriviaPlayerNetworkBehaviour處理RecorderSpeaker的設定,以便與Photon Fusion一起使用。
三個先前提及的元件及它們的設定。

在遊戲中,切換一個圖標來指示本機玩家何時被錄製或其他玩家何時發言。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;
    }
}

Muted,一個具有OnChangedRender屬性的NetworkBool,稱為OnMuteChanged,它更新muteSpeakerIcon,這是一種顯示玩家被靜音的視覺效果表示。ToggleVoiceTransmission是一種透過Unity側ButtonOnClick事件觸發的函數,該函數為具有StateAuthority的玩家切換Muted值,並設定Recover.TransmitEnabledMuted相反。設定Recover.TransmitEnabled設定為偽,將禁止錄製本機玩家。

遊戲中一個玩家講話,另一個玩家靜音的預覽。

您可以在此閱讀更多以Photon Fusion設定Photon Voice的資訊。

第三方資產

Quiz Network範例包含多個Kenney提供的第三方資產,其使用一個CC0,授權意味著它們是公共領域的,可以用於專案,無論是商業專案還是其他專案,無需署名。

Back to top