Fusion Impostor 遊戲開發導讀

瞭解 Fusion SDK 開發規則與架構引導

Steven Hu
Photon Taiwan
20 min readMar 24, 2023

--

此篇文章中,我們將介紹一個以 Fusion SDK 制作的超熱門社交推理類型遊戲: Fusion Impostor。此範例使用 Host Mode 拓撲,展示如何開發一個一局可容納 10名玩家的遊戲,並且結合使用 Photon Voice SDK 來進行通話。

此範例具有豐富的功能和特點,其中包括語音通信、網路化的遊戲狀態機、可定制的遊戲設置等。通常, 愈是簡單的房間設置和讓玩家加入的方法,愈是能讓遊戲具有更高的可玩性,所以此範例也是採用簡單的設計方式,開發者也可以直接深入網路設計的核心。在遊戲開發中,使用網路狀態機來同步和控制遊戲邏輯是很重要的,所以此範例也包含模組化互動系統的多種 mini game的船員任務,希望開發者們瞭解用法後,更能讓自己的專案有更多的發想空間。

主要亮點

  • 遊戲大廳和遊戲中的語音通信
  • 完全網路化的遊戲狀態機和系統
    涵蓋遊戲前準備、遊戲進行、會議和遊戲結果
  • 共享(同步)的互動點,如任務站和船員的屍體
  • 可定制的遊戲設置(冒名者 Imposter 數量、移動速度、玩家碰撞等)
  • 世界中的物體狀態同步,如:門的狀態
  • 基於模組化互動系統的多種船員任務 (Task, Mini Game)
  • 使用Photon Voice 處理各種語音通信類型
  • 使用代號(Room Code)設置房間,可讓玩家自行輸入後直接進入房間
  • 區域設置、暱稱和麥克風選擇

本篇文章是為進階網路遊戲開發者提供一個簡潔的的引導。希望各位能在學習這個範例的過程中,也能感受到遊戲開發的樂趣。歡迎繼續踏上此次的學習之旅,探索網路遊戲開發的奧秘!😄
所以, 趕快下載來使用吧 !!

範例下載

主要功能簡介

玩家

玩家的行為, 用了三個不同的組件, 各有其用途.

  • PlayerObject
    存儲與該物件關聯的 PlayerRef的引用,包括房間索引、暱稱和選定顏色
  • PlayerMovement
    負責玩家移動和輸入,以及遊戲所需的數據和方法
  • PlayerData
    主要處理材質、設置動畫屬性和實例化暱稱UI

PlayerRegistry

存儲每個玩家在房間中的引用,並提供對玩家進行選擇和操作的實用方法

遊戲狀態 GameState

遊戲邏輯的流程和行為由 GameState NetworkBehaviour 控制

加入遊戲

用戶可以使用房間代號加入或主持(Host)一個房間

遊戲前

進入房間後, 遊戲正式開玩之前的階段,玩家可以選擇顏色、設定麥克風,主持人可以自定義遊戲設置並負責開始遊戲

輸入處理

網路化輸入是在 PlayerInputBehaviour.cs 進行檢查,並在此執行輸入篩選,以進行輸入身份的分類判別。之後, 會在 PlayerMovement.cs 進行服務端(Host Input)檢查,然後執行輸入操作.

鍵盤和滑鼠

W A S D 鍵行走, E 鍵互動, Enter鍵開始遊戲 (僅在遊戲前階段作為主持人), 左鍵行走,點按UI中的按鈕可進行互動

可互動物件

遊戲中有多個可互動的物件,
例如顏色選擇機、設置機、緊急按鈕、任務和屍體😅

任務

地圖上分布著 14個任務站,包括 5個獨特的任務小遊戲,
調整恆溫器滑動滑塊匹配圖案按數字順序下載文件

語音

Fusion Impostor 遊戲範例使用 Photon Voice SDK v2 提供語音通信功能,
包括 FusionVoiceNetwork 和 VoiceNetworkObject 兩隻程式的使用

(注意) 遊戲範例使用 Unity 2021.3 版本開發

以上這段, 即是 Fusion Impostor遊戲範例的主題功能簡介.

接下來我們來為幾個重點大項目做些解說.

資料夾結構

主要的腳本資料夾 /Scripts 下有一個名為 Networking 的子資料夾,包含示例中的主要網路實現以及網路化狀態機。其他子資料夾如 Player Managers,分別包含遊戲行為和管理的邏輯。

加入遊戲

用戶可以使用房間代碼加入或創建一個房間。
如果用戶選擇創建房間,則輸入房間代號是可用的。
一旦進入房間,屏幕底部將顯示用於加入的代號。
房間代號可以由:runner.SessionInfo.Name 來取得。
NetworkStartBridge 充當 NetworkDebugStart 的中間介面。
如果沒有指定特定的房間代號,StartHost() 將從 RoomCode 中獲取一個隨機的 4個字元的字串。我們來看看 RoomCode 是怎麼做事的:

RoomCode.cs

定義了一個名為 RoomCode 的靜態類別,用於生成遊戲房間代號。
這個類別有一個靜態方法 Create(),該方法接受一個可選參數 length(代號的長度),預設值為 4。

  1. Create() 方法內部定義了一個字元組 chars,該字元組包含了用於生成房間代號的字元。
    特別的地方是,字集中沒有容易混淆的字符(如 I、O、1、0 等)。
  2. 方法內部還定義了一個空字串 str,用於存儲生成的房間代號。
  3. 接下來,使用一個 for 迴圈來生成房間代號。
    迴圈從 0 開始,執行次數等於 length(代碼的長度)。
  4. 在每次迴圈中,從 chars 字組中隨機選取一個字元,並將其添加到 str 字串中。Random.Range(0, chars.Length) 函數用於生成一個介於 0(包含)和 chars.Length(不包含)之間的隨機整數。
  5. 最後,當迴圈結束時,返回生成的房間代號(str 字串)。

這個簡單的類別可用於在遊戲中生成唯一的、不容易混淆的房間代號,以便玩家可以簡單地加入到其所指定的遊戲房間中。

遊戲狀態

遊戲邏輯的流程和行為由 GameState NetworkBehaviour控制。
GameState為遊戲階段定義了一個列舉,網路化的 StateMachine 將其用作狀態。
StateMachine<T>定義了一個包含 3個階段的 StateHooks類:onEnter、onExit 和 onUpdate。
在使用 StateMachine 類時,每個列舉狀態都可能具有 StateHooks,用於定義進入、退出和更新狀態時發生的操作。

GameState.cs

這段程式碼 主要目的是在多人遊戲中管理遊戲的不同狀態。這個 GameState 類別繼承自 NetworkBehaviour,負責控制遊戲中的狀態轉換。

程式碼首先定義了一個 EGameState 列舉,表示遊戲的不同狀態,如遊戲前(Pregame)、遊戲中(Play)、會議中(Meeting)、投票結果(VoteResults)、船員勝利(CrewWin)和假冒者(殺手)勝利(ImpostorWin)等。

接著,類別中定義了一些變數,包括當前狀態、上一個狀態、延遲計時器以及延遲狀態。

StateMachine 是一個用於管理狀態轉換的實例。在 Spawned() 函數中,設置了不同狀態之間轉換時的行為。例如,在進入 Pregame 狀態時,會初始化各個玩家的位置和狀態;在進入 Play 狀態時,會將玩家分配到隨機的初始位置,並為假冒者(殺手)分配任務等。

FixedUpdateNetwork() 函數則用於在每個網路更新時檢查是否應該更新狀態。如果是伺服器,並且延遲計時器到期,則會更新狀態。如果是客戶端,則只需根據收到的狀態更新來更新本地狀態。

Server_SetState() 函數用於在伺服器上設置新狀態,而 Server_DelaySetState() 函數則用於在指定延遲後設置新狀態。
這樣可以確保在遊戲的關鍵時刻,例如投票結果公佈後,給予玩家足夠的時間在進入下一個狀態之前做出反應。

整體來說,這段程式碼用於管理遊戲的狀態轉換,並根據狀態的改變來更新遊戲世界,使多人遊戲保持同步。

處理輸入

PlayerInputBehaviour.cs 腳本中,網路輸入會不斷的查詢及傳送。在這裡還會進行輸入阻擋並篩選,以排除非本機玩家的輸入, 確保只會使用到本機玩家的輸入。此外,在執行輸入之前,PlayerMovement.cs 會進行服務器端檢查。本地輸入輪詢(polling) 使用 FixedInput.cs 類完成。

PlayerInputBehaviour.cs

定義了 PlayerInputBehaviour 的類別,該類別繼承自 Fusion.Behaviour 並實現了 INetworkRunnerCallbacks 界面。此類別用於處理玩家的輸入並將其同步到遊戲的網路層。

  1. OnInput(NetworkRunner runner, NetworkInput input) 方法在玩家有輸入時被呼叫。它檢查玩家當前的狀態和遊戲狀態,並根據按鍵狀態設置 frameworkInput 的按鈕值。最後,再將按鈕值存儲在 input 中。
  2. OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) 是一個空方法 (empty method),用於實現 INetworkRunnerCallbacks 界面。
  3. 接下來的其他方法,也都是 INetworkRunnerCallbacks 界面所需的空方法,這些方法在特定事件發生時被呼叫,例如玩家加入或離開遊戲、與服務器連接或斷開連接等。
  4. 在這個類別中,這些空方法 (empty method),主要用於實現界面,並未進行任何實際操作。而一般正式遊戲就會看須要而加入適當的程式來處理相對應的事項。

PlayerInputBehaviour 類別用於處理玩家的輸入並將其同步到遊戲的網路層。通過實現 INetworkRunnerCallbacks 界面,它可以在特定事件發生時被呼叫。然而,在這個類別中,我們只專注於處理玩家的輸入,所以直接使用空方法。

玩家

玩家的行為由三個不同的組件定義:
PlayerObject, PlayerMovement, PlayerData

PlayerObject

這是一個玩家物件(PlayerObject)類別,它繼承自 NetworkBehaviour,用於表示遊戲中的玩家,包含與該物件相關的 PlayerRef 的引用、玩家在房間中的索引、暱稱和選定顏色。PlayerObject 也是呼叫 Rpc_Kill 方法的入口點。

  1. 在 PlayerObject 類別內,定義了一些變數:
    Local:表示本地玩家。
    Ref、Index、Nickname 和 ColorIndex:用於保存玩家的基本資訊,並使用 Networked 屬性實現網路同步。
    Controller、VoiceObject 和 KillRadiusTrigger:這些是玩家物件所需的組件引用。
  2. Server_Init() 方法:這個方法在伺服器上初始化玩家物件,設置玩家的 PlayerRef、索引和顏色。
  3. Spawned() 方法:這個方法在玩家物件生成時被呼叫。它首先呼叫基礎的 Spawned() 方法,然後根據物件的 StateAuthority 和 InputAuthority,對玩家物件進行設置。
  4. Rpc_SetNickname() 和 Rpc_SetColor() 方法:這兩個方法都是遠程呼叫(RPC)方法,允許玩家在網路上設置自己的昵稱和顏色。
  5. Rpc_Kill() 方法:這是一個 RPC 方法,用於處理玩家之間的擊殺行為。它根據距離判定是否可以擊殺,如果可以,則將被擊殺玩家設為死亡狀態,並在遊戲場景中生成一個 “死亡玩家物件”。
    此外,還會為擊殺者設置擊殺計時器。
  6. NicknameChanged() 和 ColorChanged() 方法:這兩個方法是 Networked 變數的 OnChanged 事件處理程序。當玩家的昵稱或顏色發生變化時,會更新玩家物件的顯示。

PlayerMovement

負責玩家的移動和輸入。它還包含遊戲所需的數據和方法,尤其是IsDead(是否死亡)、IsSuspect(是否可疑)和 EmergencyMeetingUses(緊急會議使用次數)屬性。

PlayerData

玩家的視覺組件。主要負責處理材質、設置動畫屬性並實例化暱稱 UI。

可互動物件

遊戲中的可互動物件,幫助玩家在遊戲中互動和參與各種活動。

Interactable.cs
定義了 Interactable 的抽象類別,該類別繼承自 NetworkBehaviour。用來定義遊戲中可與玩家互動的物件的基類。所有具有互動功能的物件,如死去的玩家或其他遊戲物件,都繼承自這個類別。

  1. 定義了兩個 bool 變數 isInteractionInstant 和 isGhostAccessible。isInteractionInstant 用於標記互動是否立即生效,
    isGhostAccessible 用於標記物件是否可以被幽靈玩家訪問。
  2. 定義了一個抽象屬性 CanInteract,它是一個 bool 變數,用於確定玩家是否可以與物件互動。這個屬性必須在衍生類別中實現。
  3. 定義了一個抽象方法 Interact(),該方法用於實現物件與玩家的互動功能。這個方法必須在衍生類別中實現。

Interactable 類別是一個基礎類別,它為遊戲中需要與玩家互動的物件提供了統一的界面。開發人員可以通過繼承 Interactable 類別,實現具體物件的互動功能。由於 Interactable 是一個抽象類別,開發人員需要在派生類中實現 CanInteract 屬性和 Interact() 方法,以確保物件能夠正確地與玩家互動。

顏色機器:

位於預遊戲房間中央的桌子上。玩家可以從12種預設顏色中選擇一個未被其他玩家選擇的顏色。

設置機器:

位於預遊戲房間的頂部,房主可以在此選擇遊戲設置並開始遊戲。

緊急按鈕:

每一回合緊急按鈕可以按有限次數來召集會議。

EmergencyButton.cs
定義了 EmergencyButton 的類別,繼承自先前定義的 Interactable 類別。EmergencyButton 類別表示遊戲中的緊急按鈕,玩家可以與之互動來召開緊急會議。

  1. 實現了 Interactable 類別中的抽象屬性 CanInteract。在此類別中,條件為本地玩家必須存活(PlayerMovement.Local.IsDead 為 false)並且有緊急會議次數(PlayerMovement.Local.EmergencyMeetingUses > 0)。
  2. 實現了 Interactable 類別中的抽象方法 Interact()。在此類別中,當玩家與緊急按鈕互動時,呼叫 GameManager 實例的 Rpc_CallMeeting() 方法,並將 Runner.LocalPlayer 和 null 作為參數傳遞。此 null 參數表示在此場景中沒有特定的互動物件,而是通過緊急按鈕發起的會議。

EmergencyButton 類別用於實現遊戲中的緊急按鈕功能。通過繼承 Interactable 類別,它實現了與玩家的交互功能。在玩家與緊急按鈕互動時,可以觸發遊戲中的緊急會議。

任務:

地圖上分布著14個任務站,提供5個獨特的任務小遊戲供船員們完成。

屍體:

被謀殺玩家的屍體可以被船員自由地報告召集會議,或者詐騙者試圖掩蓋他們的行蹤。

DeadPlayer.cs
它定義了一個名為 DeadPlayer 的類別,繼承自 Interactable。
該類別表示遊戲中死去的玩家物件,用於處理與死去玩家的互動。

  1. 在類別內部,定義了一個 Renderer 類型的數組 modelMeshes,用於存儲死去玩家物件的顏色。
  2. 使用 Networked 屬性同步 PlayerRef,並在 OnRefChanged 事件處理程序中處理 Ref 的變更。
  3. Spawned() 方法:這個方法在物件生成時被呼叫。它首先呼叫基類的 Spawned() 方法,然後將物件添加到 GameManager 的管理中。
  4. SetColour() 方法:這個方法用於設置死去玩家物件的顏色。它實例化一個新的材質並設置顏色,然後遍歷 modelMeshes 中的所有 Renderer,將顏色應用到對應的材質上。
  5. OnRefChanged() 方法:這是一個靜態方法,用於處理 PlayerRef 發生變更的情況。當 PlayerRef 發生變化時,將死去玩家的顏色設置為與生存玩家相同。
  6. Interact() 方法:這是一個覆寫的方法,用於處理與死去玩家物件的交互。當玩家與死去玩家交互時,呼叫 GameManager 實例的 Rpc_CallMeeting() 方法。
  7. CanInteract 屬性:這是一個覆寫的屬性,用於確定玩家是否可以與死去玩家物件交互。只有當本地玩家存活時(IsDead 為 false),才能與死去玩家物件交互。

這個類別主要用於表示遊戲中死去的玩家物件,並處理與該物件的交互。通過繼承 Interactable 類別,它實現了與玩家的交互功能。在玩家與死去玩家物件交互時,可以觸發遊戲中的相應事件,例如召開緊急會議。

任務 (Task)種類

在地圖上可以找到各種任務站。當船員在範圍內時,可以與它們互動。

調節溫度(TemperatureTask.cs

通過按上下箭頭使兩個數字相等。

滑塊(SlidersTask.cs

將每個滑塊拖動至與紅色輪廓對齊。當定位正確時,滑塊將被鎖定。

圖案匹配(PatternMatchTask.cs

按右邊面板上的按鈕,使其與左邊面板上閃爍的燈光序列相匹配。

數字順序(NumberSequenceTask.cs

按從小到大的數字順序(1–10)。

下載文件(DownloadTask.cs

按下載按鈕,等待進度條填充以完成任務。

任務說明: 圖案匹配 MiniGame

因為篇幅的關係, 這裡以五個小遊戲其中的 — 圖案匹配 — 來舉列說明, 其它程式的寫法架構也會很類似.

直接來看 PatternMatchTask.cs 程式 , 它是遊戲中的一個任務,名為 “Pattern Match”。該任務要求玩家按照顯示的模式(顏色和音調組合)重複按鈕。以下是各部分的詳細解釋。

類別聲明:

該程式碼定義了一個名為 PatternMatchTask 的類別,該類別繼承自 TaskBase,表示它是一個遊戲中的任務。

變數和屬性:

  • pitches:一個浮點數數組,表示音調的值,共有9個。
  • Name:重寫基類的屬性,返回任務的名稱,即 "Pattern Match"。
  • UI相關的變數,例如patternStageLightspatternSquaresmatchStageLightsmatchButtons,用於存儲和控制UI。
  • 顏色變數,例如stageLightOffstageLightOnpatternSquareOffpatternSquareOnpatternSquareWrong,用於設定顏色。
  • patternmatch:用於存儲模式和玩家的輸入。

方法:(這裡用了很多不同的協程 Coroutine 來達成)

  • ResetTask():重置任務,將所有 UI 元素和變數恢復到初始狀態,並在適當情況下啟動顯示模式的協程。
  • OnEnable():當物件啟用時,啟動顯示模式的協程。
  • ShowPattern():協程,用於顯示模式。在顯示過程中,會向模式中添加隨機的數字,並將對應的顯示元素顏色和音調播放出來。顯示完成後,啟用按鈕,讓玩家進行輸入。
  • PressMatch(int index):玩家按下按鈕時呼叫的方法。將玩家的輸入添加到 match 列表中,並根據輸入是否正確或任務是否完成來啟動相應的協程。
  • WrongInput():協程,用於處理玩家輸入錯誤的情況。在錯誤提示結束後,重置任務。
  • DelayCompleted():協程,用於在任務完成後的延遲。在延遲結束後,呼叫 Completed() 方法。

所以, 這個 PatternMatchTask 類實現了一個名為 "Pattern Match" 的任務。玩家需要按照遊戲顯示的模式(顏色和音調組合)來按下按鈕。該類包含了一系列的變數、屬性和方法,以實現遊戲任務的邏輯和UI互動。

圖案匹配(任務小遊戲)的過程如下:

  1. 遊戲顯示模式:遊戲會生成一個隨機的模式,並將其顯示給玩家。模式由顏色和音調組成。
  2. 玩家輸入:顯示模式後,玩家需要按照顯示的模式按下按鈕。
  3. 輸入檢查:遊戲會檢查玩家的輸入是否與模式相配。如果不對,遊戲將顯示錯誤提示,並重新開始任務。如果相同,遊戲將繼續顯示新的模式,直到指定的長度(本例中為5)達成。

當達到指定長度時,遊戲將認為任務完成,並執行 DelayCompleted() 協程,最後呼叫Completed() 方法。

該類涵蓋了遊戲任務的主要邏輯,並與Unity和Fusion相關的UI元素互動,以提供玩家一個具有限時挑戰性和趣味性的遊戲體驗。

並且因為這些狀態不用跟其它玩家同步,只會在本機執行,所以直接用協程Coroutine 來進行就好,非常方便有效。

語音:

在 Fusion Impostor中,使用 Photon Voice SDK v2 提供的兩個程式來實現語音功能,用法如下:

  • FusionVoiceNetwork 添加到 PrototypeRunner.prefab 中。
  • 在玩家預制件( Player.prefab )上使用 VoiceNetworkObject,並將 Speaker 作為給定 prefab 的子物件。

VoiceNetworkObject.cs 說明:

定義了 VoiceNetworkObject 的類別,繼承自 Fusion.NetworkBehaviour
此類別用於在遊戲中設置和管理 Photon Voice 的相關組件,像是RecorderSpeaker 組件(分別用於處理玩家語音的錄製和播放)。通過這個類別,遊戲中的玩家就可以使用語音聊天功能。

在遊戲開發過程中,開發者可以直接將 VoiceNetworkObject 添加到需要語音功能的遊戲物件上,並根據需要配置相應的 RecorderSpeaker 組,就如此範例一樣的作法。這樣,玩家在遊戲中就能夠使用語音聊天功能,與其他玩家進行實時語音交流,提高遊戲的互動性和沉浸感。

小結

在本篇文章中,我們向您介紹了 Fusion Impostor遊戲範例,並深入講解了其中幾個重要的核心架構及簡單應用的場合。若為網路程式的進階者,相信在熟悉此範例的過程中,對於使用 Fusion SDK 做網路遊戲開發會有更深入的瞭解。🚀

此範例涵蓋了 Fusion SDK 和 Photon Voice SDK, 對於還不熟悉 Photon 產品的朋友,可能有些地方會混淆; 所以, 若是網路程式的初學者,我們會建議先看看我們過往的 Fusion SDK 的一些使用引導, 有了初步的經驗後, 再來看這篇會更適合喔. 😀

所以,通掌握這些新知識與技術架構,將能夠更快速、有效地開發具有高度互動性和吸引力的遊戲,快速地成為網路遊戲的開發高手,並獲得更多的成就解鎖與快樂的心情喔 !! 🎉

有開發上的疑問嗎? 直接到 粉絲團 發訊息 (private message) 來討論吧 !
https://www.facebook.com/photoncloudtw/

學習順利,下次見!

--

--