This document is about: FUSION 2
SWITCH TO

Quiz Network

Level 4

Overview

The Fusion Quiz Network sample is shared mode quiz game for 20 players and utilizes Photon Voice. Players are asked a series of trivia questions, gaining points for answering quickly. This Shared Mode sample showcases joining a Game Session with preset data, switching Master Client State Authority, Tick Timers usage, and more.

Download

Version Release Date Download
2.0.0 Mar 28, 2024 Fusion Quiz Network 2.0.0 Build 466

Connecting to Fusion

The FusionConnection class is responsible for creating the NetworkRunner for Game Sessions. It also stores the name of the local player and the name of the Session Game that the local player will create, if specified.
FusionConnection is implemented as a singleton, meaning there can only be one instance of it. The instance is in the Awake method with the following code:

C#

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

On the main menu for Quiz Network, when a player chooses Create Room or Join Room, the player attempts to connect through the following start arguments:

C#

public async void StartGame(bool joinRandomRoom)
{
    StartGameArgs startGameArgs = new StartGameArgs()
    {
        GameMode = GameMode.Shared,
        SessionName = joinRandomRoom ? string.Empty : LocalRoomName,
        PlayerCount = 20,
    };
    // ...
}
  • GameMode: the Game Mode used, in this case Shared Mode, where clients connect to a Photon Cloud room and each player has State Authority over Network Objects they spawn.
  • SessionName: name of the Session that will be created. If no Session is specified or if joinRandomRoom is true, matchmaking will attempt to have the player join an open Session. If this fails, a new Session with a System.Guid will be created. Otherwise, the player will attempt to join a Session using LocalRoomName; if this session does not exist, they will create a new one with this name.
  • PlayerCount: defines the number of players allowed in the Session, in this case, 20. If the Session defined by LocalRoomName has 20 players, an error will be thrown if a player attempts to join it.

After this, a new NetworkRunner is instantiated and attempts to connect with these 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);
}
// ...

Since NetworkRunner.StartGame is an async function, there is a delay before StartGameResult is set. Once finished, if joining the Session has succeeded, it will display the session name; if it fails, an error screen will be shown.

Trivia Player

Once a player joins, their avatar is spawned. This NetworkObject contains a NetworkBehaviour named TriviaPlayer. Each player has a series of properties that utilize the Networked attribute.

C#

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

This property manages how the name of the player is shown. It uses a NetworkString which is a unique type to Fusion for handling string with an enforced limit, in this case 16 characters. It also uses a second attribute, OnChangedRender and the name of the function, OnPlayerNamed.

C#

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

It updates the text property of nameText which is a TextMeshProUGUI object. It's important to note, that when a new player spawns, the OnChangedRender method will not be called automatically. Instead, it's best to update these properties within the Spawned method of the NetworkObject.

Additionally, when spawned, players are added to a static list that contains references to all TriviaPlayers as well as a static reference to the local TriviaPlayer, which will be explained later.

Trivia Manager

Trivia Manager is a NetworkBehaviour that handles shuffling and updating what questions will be asked once the game has started as well as the TickTimer used. The game begins when a Trivia Manager is spawned; a remote procedural call (RPC) is not needed because when Trivia Manager is spawned by the NetworkRunner, it will be spawned for all players.

In Shared Mode, there is no host player that has state authority over the scene like in Host Mode however, only one player can have State Authority over this object, and in this case, it is the Shared Mode Master Client. When setting up the Trivia Manager is designated as a Master Client Object.

This means that the Master Client will have State Authority over this object, and if they were to leave, the State Authority will be transferred over to the new Master Client. Since Trivia Manager implements the IStateAuthorityChanged interface, it will call StateAuthorityChanged when this transfer occurs.

Trivia Manager also has a TickTimer which is used to update various states of the game. The TickTimer is only checked during FixedUpdateNetwork, which is only executed by the player with State Authority, so the visual update are for TickTimer is handled in Update since all players will execute this method.

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;
    }
}

When polling the remaining time on a TickTimer, the result can be a Nullable, represented by float?, meaning it can have a value or be null. These results are then handled differently.

Answering questions

When Trivia Manager updates the current question by incrementing CurrentQuestion in FixedUpdateNetwork, players simply answer the question by clicking a button, which will trigger 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();
        }
    }
}

In this method, first the GameState of TriviaManager is checked, just to make sure a question is being shown at this time. If the LocalPlayer reference of TriviaPlayer has not chosen an answer, specified by ChosenAnwswer being less than 0, then the value they've selected, defined on the Unity side, is set to ChosenAnswer. Additionally, TimerBonusScore is set to a value based on the remaining time of TriviaManager's TickTimer and a Unity-side defined value.

Then, within the FixedUpdateNetwork method of TriviaManager, the answer of every player is checked.

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 is checked, and if a question is being shown, each of the TriviaPlayer references in TriviaPlayerRefs are iterated through. If they have answered the question, totalAnswers is incremented and if it matches the number of players, then the GameState of TriviaManager goes to TriviaGameState.ShowAnswer and the answer is shown. This is why a reference to each TriviaPlayer is stored when each is spawned as mentioned previously.

Ending the Game

Trivia Manager also handles the end of the game through its FixedUpdateNetwork method.

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;
}

When the TickTimer expires, QuestionsAsked is checked, and if the number of questions asked is no longer less than the number of questions in a round, defined by maxQuestions, which is set on the Unity side, the TickTimer is set to TickerTime.None, stopping it, and the GameState of the TriviaManager is set to TriviaGameState.GameOver.

By setting GameState, OnTriviaGameStateChanged is triggered as part of GameState's OnChangedRender attribute. Inside this method, OnGameStateGameOver is called and the final scores are evaluated.

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!";
    }

    // ...
}

This takes all current players into a new list, which is sorted by score, the top 3 players being kept and provided to endGameObject.Show, which takes the winners and arranges them into the final, end game screen. Additionally, the Shared Mode Master Client is shown a button to start a new round of questions.

Photon Voice

Connected players can communicate over microphones through the use of Photon Voice. In this sample, the following components are used to achieve this:

  • Fusion Voice Client: This component is added to the NetworkRunner prefab and defines the initial setup for Photon Voice.
A preview of the `Fusion Voice Client` component.
  • Recorder: This component is added to the TriviaPlayer prefab and records the user's voice to send over the network
  • Speaker: Also added to the TriviaPlayer prefab, this component receives recorded audio from other players and plays it through the attached AudioSource component.
  • Voice Network Object: This NetworkBehaviour attached to TriviaPlayer handles settings up the Recorder and Speaker for use with Photon Fusion.
The three previously mentioned components and their settings.

In game, an icon is toggled to indicate when the local player is being recorded or when other players are speaking. The following code in the Update function of TriviaPlayer demonstrates this:

C#

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

First, the SpeakerInUse and IsSpeaking properties of VoiceNetworkObject indicate that a remote player is speaking; meanwhile, RecorderInUse and IsRecording indicates that the local player is speaking.

In this sample, the local player can prevent their audio from transmitted and show that they are muted to other players. This is achieved through the following:

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, a NetworkBool with the OnChangedRender attribute, called OnMuteChanged, which updates muteSpeakerIcon, a visual representation showing that the player is muted. ToggleVoiceTransmission is a function triggered through the OnClick event of a Button on the Unity side that toggles the value of Muted for the player with StateAuthority and sets Recover.TransmitEnabled to the opposite of Muted. Setting Recover.TransmitEnabled to false will prevent the local player from being recorded.

A preview of a player speaking and another muted in game.

You can read more about setting up Photon Voice with Photon Fusion here.

3rd Party Assets

The Quiz Network sample includes several third party assets provided by Kenney that are using a CC0, license meaning they are public domain and can be used in projects, commercial or otherwise, without attribution.

Back to top