Quiz Network
概要
Fusion Quiz Networkサンプルは、20人のプレイヤー向けの共有モードのクイズゲームであり、Photon Voice
を利用しています。プレイヤーは一連のトリビア質問に答え、迅速に回答することでポイントを獲得します。この共有モード
サンプルは、プリセットデータを使用してゲームセッションに参加し、マスタークライアントのState Authority
を切り替え、Tick Timers
の使用などを示しています。
ダウンロード
バージョン | リリース日 | ダウンロード | |
---|---|---|---|
2.0.1 | Jun 19, 2024 | Fusion Quiz Network 2.0.1 Build 576 |
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.