Quiz Network
概要
Fusion Quiz Networkサンプルは、20人のプレイヤー向けの共有モードのクイズゲームであり、Photon Voiceを利用しています。プレイヤーは一連のトリビア質問に答え、迅速に回答することでポイントを獲得します。この共有モードサンプルは、プリセットデータを使用してゲームセッションに参加し、マスタークライアントのState Authorityを切り替え、Tick Timersの使用などを示しています。
ダウンロード
| Version | Release Date | Download |
|---|---|---|
| 2.0.5 | 3月 04, 2025 | Fusion Quiz Network 2.0.5 |
Fusionへの接続
FusionConnectionクラスは、ゲームセッションのためのNetworkRunnerを作成する責任を担っています。また、ローカルプレイヤーの名前と、ローカルプレイヤーが作成するセッショングameの名前(指定された場合)を保存します。
FusionConnectionはシングルトンとして実装されており、インスタンスは一つだけ存在できます。インスタンスはAwakeメソッド内で以下のコードによって作成されます:
C#
private void Awake()
{
// ...
if (Instance != null)
{
Destroy(gameObject);
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
Quiz Networkのメインメニューで、プレイヤーが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: 使用されるゲームモード、この場合は共有モードで、クライアントはPhoton Cloudルームに接続し、各プレイヤーが生成したネットワークオブジェクトに対してステート権限を持ちます。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; }
このプロパティは、プレイヤーの名前の表示方法を管理します。これは、16文字の制限が強制される文字列を扱うためにFusionに特有の型であるNetworkStringを使用しています。また、2つ目の属性OnChangedRenderと、関数名OnPlayerNamedも使用されています。
C#
void OnPlayerNameChanged()
{
nameText.text = PlayerName.Value;
}
nameTextはTextMeshProUGUIオブジェクトであり、そのtextプロパティが更新されます。重要な点として、新しいプレイヤーが生成されると、OnChangedRenderメソッドは自動的には呼び出されないことに注意してください。代わりに、これらのプロパティはNetworkObjectのSpawnedメソッド内で更新するのが最適です。
加えて、プレイヤーが生成されると、すべてのTriviaPlayerへの参照を含む静的リストに追加されます。また、ローカルTriviaPlayerへの静的参照も保持され、その詳細については後ほど説明します。
トリビアマネージャー
Trivia Managerは、ゲームが開始された後に出題される質問のシャッフルや更新、および使用されるTickTimerを管理するNetworkBehaviourです。ゲームはTrivia Managerが生成されることで始まります。リモート手続き呼び出し(RPC)は必要ありません。というのも、Trivia ManagerがNetworkRunnerによって生成される際、すべてのプレイヤーに対して同時に生成されるからです。
共有モードでは、ホストモードのようにシーンに対してステート権限を持つホストプレイヤーは存在しませんが、このオブジェクトに対しては1人のプレイヤーのみがステート権限を持つことができ、今回は共有モードマスタークライアントがその役割を果たします。Trivia Managerを設定する際には、マスタークライアントオブジェクトとして指定されます。
これは、マスタークライアントがこのオブジェクトに対してステート権限を持ち、彼らが離脱した場合、ステート権限が新しいマスタークライアントに移転されることを意味します。Trivia ManagerはIStateAuthorityChangedインターフェースを実装しているため、この移転が発生するとStateAuthorityChangedが呼び出されます。
また、Trivia Managerには、ゲームのさまざまな状態を更新するために使用されるTickTimerがあります。TickTimerはFixedUpdateNetwork内でのみチェックされ、これはステート権限を持つプレイヤーによってのみ実行されるため、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();
}
}
}
このメソッドでは、まずTriviaManagerのGameStateがチェックされ、現在質問が表示されていることを確認します。次に、TriviaPlayerのLocalPlayer参照が回答を選択していない場合(ChosenAnswerが0未満の場合)、Unity側で定義された値がChosenAnswerに設定されます。さらに、TimerBonusScoreはTriviaManagerのTickTimerの残り時間に基づいて、Unity側で定義された値を設定します。
その後、TriviaManagerのFixedUpdateNetworkメソッド内で、すべてのプレイヤーの回答がチェックされます。
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が増加し、その数がプレイヤーの数と一致する場合、TriviaManagerのGameStateはTriviaGameState.ShowAnswerに移行し、回答が表示されます。このため、各TriviaPlayerが生成される際にその参照が保存されていることが重要です(前述の通り trivia-player のセクション参照)。
ゲームの終了
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に設定され、停止します。そして、TriviaManagerのGameStateはTriviaGameState.GameOverに設定されます。
GameStateを設定することで、GameStateのOnChangedRender属性の一部として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の初期設定を定義します。
Recorder: このコンポーネントはTriviaPlayerプレハブに追加され、ユーザーの声を録音してネットワーク経由で送信します。Speaker: こちらもTriviaPlayerプレハブに追加されるコンポーネントで、他のプレイヤーから録音された音声を受信し、接続されたAudioSourceコンポーネントを通じて再生します。Voice Network Object: このNetworkBehaviourはTriviaPlayerに取り付けられ、RecorderとSpeakerの設定を行い、Photon Fusionで使用できるようにします。
ゲーム内では、アイコンが切り替わってローカルプレイヤーが録音されているときや、他のプレイヤーが話しているときに示されます。TriviaPlayerのUpdate関数内の次のコードがこれを示しています:
C#
private void Update()
{
speakingIcon.enabled = (_voiceNetworkObject.SpeakerInUse && _voiceNetworkObject.IsSpeaking) || (_voiceNetworkObject.RecorderInUse && _voiceNetworkObject.IsRecording);
}
まず、VoiceNetworkObjectのSpeakerInUseおよびIsSpeakingプロパティは、リモートプレイヤーが話していることを示します。一方、RecorderInUseおよびIsRecordingは、ローカルプレイヤーが話していることを示します。
このサンプルでは、ローカルプレイヤーは自分の音声が送信されるのを防ぎ、他のプレイヤーにミュートされていることを示すことができます。これは以下のように実現されています:
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側のButtonのOnClickイベントを通じてトリガーされる関数で、StateAuthorityを持つプレイヤーのMutedの値を切り替え、Recover.TransmitEnabledをMutedの逆の値に設定します。Recover.TransmitEnabledをfalseに設定すると、ローカルプレイヤーの音声が録音されないようになります。
こちらで、Photon FusionとPhoton Voiceの設定についてさらに詳しく読むことができます。
サードパーティアセット
Quiz Networkサンプルには、Kenneyによって提供されているいくつかの第三者アセットが含まれており、これらはCC0ライセンスの下で使用されています。これは、公共のドメインであり、クレジットなしで商業的プロジェクトを含むプロジェクトで使用できることを意味します。
You can read more about setting up Photon Voice with Photon Fusion here.