Quiz Network
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.1 | Jun 19, 2024 | Fusion Quiz Network 2.0.1 Build 576 |
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
: theGame Mode
used, in this caseShared 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 ifjoinRandomRoom
is true, matchmaking will attempt to have the player join an open Session. If this fails, a new Session with aSystem.Guid
will be created. Otherwise, the player will attempt to join a Session usingLocalRoomName
; 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 byLocalRoomName
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 theNetworkRunner
prefab and defines the initial setup forPhoton Voice
.
Recorder
: This component is added to theTriviaPlayer
prefab and records the user's voice to send over the networkSpeaker
: Also added to theTriviaPlayer
prefab, this component receives recorded audio from other players and plays it through the attachedAudioSource
component.Voice Network Object
: ThisNetworkBehaviour
attached toTriviaPlayer
handles settings up theRecorder
andSpeaker
for use withPhoton Fusion
.
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.
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