퀴즈 네트워크
개요
Fusion Quiz Network 샘플은 최대 20명의 플레이어를 위한 공유 모드 퀴즈 게임이며 Photon Voice
를 사용합니다. 플레이어는 일련의 퀴즈 질문을 받고, 빠르게 답변하여 점수를 획득합니다. 이 공유 모드
샘플은 사전 설정된 데이터를 사용한 게임 세션에 참여하고, 마스터 클라이언트 상태 권한
을 전환하며, Tick Timers
사용 등을 보여줍니다.
다운로드
버전 | 릴리즈 일자 | 다운로드 | |
---|---|---|---|
2.0.1 | Jun 19, 2024 | Fusion Quiz Network 2.0.1 Build 576 |
Fusion 연결
FusionConnection
클래스는 게임 세션을 위한 NetworkRunner
를 생성하는 역할을 합니다. 또한 로컬 플레이어의 이름과 로컬 플레이어가 생성할 세션 게임의 이름을 저장합니다.
FusionConnection
은 싱글톤으로 구현되어 하나의 인스턴스만 존재할 수 있습니다. 인스턴스는 다음과 같은 코드로 Awake
메서드에서 설정됩니다:
C#
private void Awake()
{
// ...
if (Instance != null)
{
Destroy(gameObject);
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
퀴즈 네트워크의 메인 메뉴에서 플레이어가 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
: 사용되는게임 모드
, 이 경우 각 플레이어가 생성한 네트워크 오브젝트에 대해 상태 권한을 가지는공유 모드
입니다.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; }
이 속성은 플레이어 이름이 표시되는 방식을 관리합니다. 이는 Fusion에서 문자열을 처리하는 고유한 유형인 NetworkString
을 사용하여, 이 경우 16자로 제한됩니다. 또한 OnChangedRender
라는 두 번째 속성과 함수 이름인 OnPlayerNameChanged
를 사용합니다.
C#
void OnPlayerNameChanged()
{
nameText.text = PlayerName.Value;
}
이는 TextMeshProUGUI
객체인 nameText
의 text
속성을 업데이트합니다. 새 플레이어가 스폰 될 때 자동으로 OnChangedRender
메서드가 호출되지 않는다는 점이 중요합니다. 대신, 이러한 속성은 NetworkObject
의 Spawned
메서드 내에서 업데이트하는 것이 좋습니다.
또한, 스폰 시 모든 TriviaPlayers
에 대한 참조를 포함하는 정적 리스트와 로컬 TriviaPlayer
에 대한 정적 참조에 추가됩니다. 이는 나중에 설명할 것입니다.
퀴즈 관리자
Trivia Manager
는 게임이 시작된 후 어떤 질문이 제시될지를 섞고 업데이트하는 역할을 하는 NetworkBehaviour
입니다. 게임은 Trivia Manager
가 스폰 될 때 시작됩니다. 원격 프로시저 호출(RPC)이 필요하지 않은 이유는 NetworkRunner
가 Trivia Manager
를 스폰 할 때 모든 플레이어에게 스폰 되기 때문입니다.
공유 모드
에서는 호스트 모드
와 달리 씬에 대한 상태 권한을 가진 호스트 플레이어가 없습니다. 그러나 하나의 플레이어만 이 오브젝트에 대한 상태 권한을 가질 수 있으며, 이 경우 공유 모드 마스터 클라이언트
입니다. 설정 시 Trivia Manager
는 마스터 클라이언트 오브젝트로 지정됩니다.
이는 마스터 클라이언트가 이 오브젝트에 대한 상태 권한을 가지며, 이들이 떠날 경우 상태 권한이 새로운 마스터 클라이언트로 전환된다는 것을 의미합니다. Trivia Manager
는 IStateAuthorityChanged
인터페이스를 구현하여 이 전환이 발생할 때 StateAuthorityChanged
를 호출합니다.
Trivia Manager
는 게임의 다양한 상태를 업데이트하는 데 사용되는 TickTimer
도 가지고 있습니다. TickTimer
는 상태 권한을 가진 플레이어만 실행하는 FixedUpdateNetwork
동안만 확인되므로 모든 플레이어가 실행하는 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보다 작음), 유니티 측에서 정의된 값이 ChosenAnswer
에 설정됩니다. 또한, TimerBonusScore
는 TriviaManager
의 TickTimer
의 남은 시간과 유니티 측에서 정의된 값을 기반으로 설정됩니다.
그런 다음, 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
를 확인하여 질문이 표시되고 있는지 확인합니다. 각 TriviaPlayer
참조를 TriviaPlayerRefs
에서 반복합니다. 질문에 답한 경우 totalAnswers
를 증가시키고 플레이어 수와 일치하면 TriviaManager
의 GameState
가 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
가 확인되고, 유니티 측에서 설정된 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
:TriviaPlayer
에 부착된 이NetworkBehaviour
는Photon Fusion
에서Recorder
와Speaker
를 설정합니다.
게임 내에서 로컬 플레이어가 녹음되고 있거나 다른 플레이어가 말하고 있을 때 아이콘이 전환됩니다. 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
를 호출하여 플레이어가 음소거되었음을 보여주는 시각적 표현을 업데이트합니다. ToggleVoiceTransmission
은 유니티 측에서 Button
의 OnClick
이벤트를 통해 트리거 되는 함수로, StateAuthority
가 있는 플레이어에 대해 Muted
값을 전환하고 Recorder.TransmitEnabled
를 Muted
의 반대로 설정합니다. Recorder.TransmitEnabled
를 false로 설정하면 로컬 플레이어의 녹음이 방지됩니다.
Photon Fusion과 함께 Photon Voice 설정에 대해 더 알아보세요.
타사 에셋
Quiz Network 샘플에는 Kenney가 제공한 여러 서드파티 에셋이 포함되어 있으며, CC0 라이선스를 사용하여 퍼블릭 도메인에 속하며, 상업적 용도나 기타 용도로 사용 시 출처를 밝히지 않아도 됩니다.
Back to top