PUN 2 | 連線射擊遊戲 (4/4)

實作分享

Ryan Tseng
Photon Taiwan
29 min readMay 24, 2019

--

我們將分成四篇文章, 使用 PUN 2 來實作一個簡單的連線射擊遊戲.

在前三篇文章中, 我們在新專案匯入 PUN 2 套件, 與 Photon Cloud 建立連線及加入遊戲室. 又創建了遊戲室場景, 能隨同時在線的遊戲玩家人數而更新場景 (最多四人). 並使用 Unity 提供的免費素材來實作第一人稱模式的遊戲角色與動作操控.
[延伸閱讀: PUN 2 | 連線射擊遊戲 (1/4)| 實作分享]
[延伸閱讀: PUN 2 | 連線射擊遊戲 (2/4)| 實作分享]
[延伸閱讀: PUN 2 | 連線射擊遊戲 (3/4)| 實作分享]

本篇為系列最後一篇文章, 我們來處理連線網路時, 玩家角色的移動, 操控, 動作表現與血量的即時同步等細節.

PhotonView 是串接多人網路即時連線環境的核心, 設定好在遊戲中要觀察的元件/ Components 與其同步規則之後, 當遊戲進行時, 只要有資料變動, 它就會自動的做各個 Client 端的同步.

接下來, 我們將依靠 PhotonView 來大幅減少在網路環境做資料同步處理的麻煩問題 (例如: 資料同步效率, 處理網路延遲等).

同步玩家角色的移動, 操控和動作表現

首先, 我們為 Prefab- My Robot Kyle 增加一個 PhotonView component.

在 Unity 開啟前回實作的專案 pun_basic, 並將工作場景切換到 Kyle Test.
在 Hierarchy 視窗, 選取 GameObject- My Robot Kyle.
在 Inspector 視窗, 點按 Add Component, 增加一個 component- PhotonView, 並設定 component- PhotonView 的 Observed Option 為 Unreliable On Change*.

* Observed Option 設為 Unreliable On Change 時, PhotonView 不會確認封包是否有被送達, 且僅在有變更時才發送訊息.

接下來, 需要在 component- PhotonView 設定觀察哪些元件/Components 與其同步規則.

第一個需要同步的, 是各玩家角色的位置, 移動與轉向等資訊.
這些資訊的同步, 可以藉由 PhotonTransformView 來輕鬆達成.

在 Hierarchy 視窗, 確認選取在 GameObject- My Robot Kyle.
在 Inspector 視窗, 點按 Add Component, 增加一個 component- PhotonTransformView.

My Robot Kyle 只會有移動和旋轉的數值改變, 不會放大或縮小,
所以, 將 component- PhotonTransformView 的 Synchronize Options 的 Position 與 Rotation 勾選起來.
再將 component- PhotonTransformView 拖拉到 component- PhotonView 的 Observed Components 裡面, 加為第一個被觀察的元件.

這樣一來, PhotonView 即會自動同步玩家角色的位置, 移動與轉向等資訊.

再來, 各玩家角色動作表現的資訊, 也需要同步.
這些資訊的同步, 則可以藉由 PhotonAnimatorView 來輕鬆達成.

在 Hierarchy 視窗, 確認選取在 GameObject- My Robot Kyle.
在 Inspector 視窗, 點按 Add Component, 增加一個 component- PhotonAnimatorView.

My Robot Kyle 只有跑步, 轉彎與跳躍三種動作, 所以,
將 component- PhotonAnimatorView 的 Synchronize Parameters 設為:
Speed -> Discrete*
Direction -> Discrete
Hi -> Disabled
Jump -> Discrete
再將 component- PhotonAnimatorView 拖拉到 component- PhotonView 的 Observed Components 裡面, 加為第二個被觀察的元件.

這樣一來, PhotonView 即會自動同步玩家角色動作表現的資訊.

* 設為 Discrete 時, 該屬性的數值會以每秒 10 次的頻率做同步. 程式可以在 OnPhotonSerialzeView() 讀/寫該屬性數值.

多人網路即時連線的用戶輸入管理

在多人網路即時連線的遊戲環境中, 需要有個機制來分辨~ 哪些動作或事件是和玩家角色產生了踫撞或觸發造成? 哪些又是發生在其他玩家的角色上?
這個問題, 可以靠 PhotonView 的 IsMine 屬性來解決. 唯有是在玩家角色, 也就是本人時, IsMine 的值才會為真, 在其它狀況時, 其值均為否.

編輯 C# Script- PlayerAnimatorManager.
在宣告 class PlayerAnimatorManager 之前, 加上 “using Photon.Pun;”,
並把 class PlayerAnimatorManager 改為繼承 MonoBehaviourPun.

並在 Update() 的最前面, 加入下列程式碼~ 依 PhotonView.IsMine 的值, 控制僅在為玩家角色本人時, 才會往下執行 My Robot Kyle 的跑跳等動作表現.

另外, 會多判斷 PhotonNetwork.IsConnected 的值, 是為了開發與除錯的目的, 方便測試一些和網路無關的功能.

讓 Camera 跟隨玩家的角色

每個玩家的 Camera 也需跟隨玩家自己的角色, 以取得最佳視野.

編輯 C# Script- PlayerManager, 在宣告 class PlayerManager 之前, 加上 “using Photon.Pun.Demo.PunBasics;”,

並加入下列程式碼實作 Start().

這段程式碼的功能是取得 component- CameraWork 的位址, 並在 IsMine 為真時, 讓 CameraWork 做 OnStartFollowing().

另外, 既然已在程式中做 OnStartFollowing() 了, 原本在 Prefab- My Robot Kyle 的 component CameraWork 勾選的 Follow On Start 就可以取消了.

這樣一來, 每個玩家的 Camera 都會跟隨玩家自己的角色了.

同步玩家角色發射武器的動作

目前我們只觀察 PhotonTransformView 與 PhotonAnimatorView 兩個元件, 並沒有包含其他玩家角色發射動感光波動作的資訊.

我們來處理這部份的同步.

編輯 C# Script- PlayerManager.
將 Update() 裡面, 改為僅在 IsMine 為真時, 才做 ProcessInputs().

再將 class PlayerManager 改為除了繼承 MonoBehaviourPunCallbacks 之外, 也繼承 IPunObservable.

在前面有提到, My Robot Kyle 的 Speed, Jump 和 Jump 等動作, 是以每秒 10 次的頻率做同步. 程式可以在 OnPhotonSerialzeView() 讀/寫該屬性數值.
我們就實作 OnPhotonSerializeView(), 把 IsFiring 的狀態同步也加進去.
加入下列程式碼:

再來, 很重要的, 把 component- PlayerManager 加為 component- PhotonView 的 Observed Components 的第三個被觀察的元件.

回到 Unity 環境, 確認工作場景為 Kyle Test
在 Hierarchy 視窗, 選取 GameObject- My Robot Kyle.
在 Inspector 視窗, 將 component- PhotonView 的 Observed Components 增加一項, 再把 component- PlayerManager 拖拉到 Observed Components, 加為第三個觀察元件.

這樣一來, 各玩家角色發射動感光波的動作就會被同步了.

[經驗分享-1]
小編在這步驟曾經遇到一個問題~ 將 component- PlayerManager 拉進 Observed Components 時, 其值竟然變成 None.

會發生這問題是因為 component- PlayerManager 的 Beams 因不明原因變成 None 了. 只要重新把 GameObject- Beams 拖拉到 component- PlayerManager 的 Beams 之後, 再把 component- PlayerManager 拖拉到 Observed Components, 即能解決此問題.
最後, 記得要按右上角 Overrides 更新 Prefab 的設定喔.

同步玩家角色的血量

玩家角色被動感光波打中會扣血, 所以, 各玩家角色的血量, 也要被同步.
同樣的, 我們也在 OnPhotonSerializeView(), 把玩家角色血量的數值同步加進去.

修改 C# Script- PlayerManager, 在 OnPhotonSerializeView() 多加上 Health 的更新.

這樣, 各玩家角色的血量數值也會被同步了.

再來, 我們找個適當的地方來動態生成玩家的角色, 以及處理遊戲場景切換的功能.

動態生成玩家的角色

在玩家一進入遊戲室時, 我們應該就要動態生成玩家的角色.
我們在 GameManager 的 Start() 來做這件事.

編輯 C# Script- GameManager, 在 class GameManager 的前面, 加個變數 playerPrefab.

並修改 Start(), 加入下列程式碼, 來動態生成玩家的角色.

在動態生成玩家角色時, 我們將其啟始座標設在 (0f, 5f, 0f) 是要避免和其他玩家發生踫撞, 而且, 新生成的玩家自空中落下進入遊戲場, 看起來也比較帥.

回到 Unity, 將工作場景切換到 Room for 1.
在 Hierarchy 視窗, 選取 GameObject- Game Manager.
將 Project 視窗, Resources 資料夾底下的 Prefab- My Robot Kyle 拖拉到 Inspector 視窗, component- Game Manager 的 Player Prefab 裡面.
再按右上角 Overrides 更新 Prefab- Game Manager 的設定.

到此, 我們將 Unity 工作場景切換到 Launcher, 並執行做個測試.
(測試結果, 請參考下面 GIF 動畫圖檔)

我們可以看到, 玩家點按 Play 進入遊戲場後, My Robot Kyle 自天而降落在遊戲場的正中央, 看起來還不錯, 是符合預期的行為.

我們再多做個測試, 讓第二位玩家進入遊戲場.
(測試結果, 請參考下面 GIF 動畫圖檔)

我們可以看到, 在第二位玩家進入遊戲場時, 原本在牆邊第一位玩家的角色也被重新生成而落在場中央的位置. 這就不是合理的行為了.

會發生這個問題, 是因為在第二位玩家進入遊戲時, Game Manager 在 OnPlayerEnteredRoom() 重載場景後, 使得第一位玩家角色的 instance 被一殺掉, 而後又在 Game Manager 的 Start() 裡面, 呼叫 PhotonNetwork.Instantiate() 重建出來.

我們修改一下程式, 當有玩家離開遊戲室, 或新玩家加入遊戲室而使得場景重載時, 已在遊戲場中的玩家角色不能受到影響.

編輯 C# Script- PlayerManager.
在 class PlayerManager 的前面, 加個變數 LocalPlayerInstance, 來記錄已存在在場景中玩家角色的 instance.

修改 Awake(), 增加下列程式碼~ 把玩家角色的 instance 記錄起來, 並呼叫 DontDestroyOnLoad() 標註玩家角色的 instance 不會在重載場景時被砍殺掉.

另外, 在 C# Script- GameManager 的 Start() 裡面, 我們在呼叫 PhotonNetwork.Instantiate() 之前, 需要判斷唯有 PlayerManager.LocalPlayerInstance 為 null 時, 才做動態生成.

存檔, 回到 Unity, 再執行做一次測試.
(測試結果, 請參考下面 GIF 動畫圖檔).

我們可以看到, 在第二位玩家進入遊戲場時, 遊戲場景會自動擴大, 原本第一位玩家的角色沒有被影響了.

這裡還有一個問題要考慮~ 當有玩家離開遊戲室, 使得場景重載而空間縮小, 剛好站在邊界之外的玩家角色, 就會掉落無底深淵.
(測試結果, 請參考下面 GIF 動畫圖檔).

要解決這個問題, 只要在遊戲場景重載時, 在 OnLevelWasLoaded() 裡面判斷遊戲角色的腳底是否有地板存在即可.

但因為 Unity 5.4 (含) 以後版本, 導入新的 Scene Management 機制, 不再支援包括 OnLevelWasLoaded() 在内的某些 Callbacks 函式. 這邊得考慮 Unity 的版本, 而有不同的作法.

編輯 C# Script- PlayerManager.
修改 Start(), 在其最後面加上下列程式碼.

這段程式碼的目的是在當 Unity 為 5.4 (含) 以後的版本時, 就透過 SceneManager 登註當發生場景重載時的行為, 也就是呼叫 CalledOnLevelWasLoaded().

若上一段程式碼的寫法不夠直覺, 改為下列寫法亦可.

以及, 在 C# Script- PlayerManager 的後面, 加上下列程式碼.

這段程式碼目的有二, 一是當在 Unity 5.4 以前的版本時, 在 OnLevelWasLoaded() 時, 去呼叫 CalledOnLevelWasLoaded().
二是實作 CalledOnLevelWasLoaded(), 呼叫 Physics.Raycast() 來判斷遊戲角色的腳底是否有地板存在.

存檔, 回到 Unity 環境, 再執行測試一次.
(測試結果, 請參考下面 GIF 動畫圖檔).

我們可以看到, 位在牆邊的玩家角色在其他玩家離開遊戲室後, 因場景縮小的緣故, 而重新生成並降落在舞台中間.

到此, 包括動態生成玩家角色與其操控和動作表現的資訊同步, 都已經完成了.

接下來, 我們要在玩家角色的頭頂顯示名字與血條.
這些和畫面呈現相關的部份, 並不需要透過網路來做同步. 我們將會實作一個 Prefab-Player UI 來維護包括名字欄位與血條的相對位置, 與血條數值變化的呈現. 然後讓 PlayerManager 與此 Player UI 串接起來後, 由 PlayerManager 來控制 Player UI 的顯示位置.

創建 Prefab- Player UI

在 Unity, 將工作場景切換到 Launcher. (或其它内含有 GameObject- Canvas 的工作場景, 例如 Room for 1, Room for 2~4 等均可)
在 Hierarchy 視窗, GameObject- Canvas 底下, 新增一個 GameObject\UI\Slider UI, 並取名為 “Player UI”.
再將 Inspector 視窗裡面, component- Rect Transform 的 Vertical anchor 設為 Middle, Horizontal anchor 設為 Center, Width 設為 80, Height 設為 15.

在 Hierarchy 視窗, 選取 GameObject- Player UI 底下的 Background.
將 Inspector 視窗裡面的 component- Image 的 Color 設為紅色.

在 Hierarchy 視窗, 選取 GameObject- Player UI 底下 Fill Area 的 Fill.
將 Inspector 視窗裡面的 component- Image 的 Color 設為綠色.

在 Hierarchy 視窗, GameObject- Player UI 底下, 新增一個 GameObject\UI\Text, 取名為 “Player Name Text”.
並把 Inspector 視窗, component- Rect Transform 的 Width 設為 160, Height 設為 50, 以及 component- Text 的 Alignment 設為 Middle.

再把 Hierarchy 視窗的 GameObject- Player UI 拖拉到 Project 視窗裡面, Prefab 資料夾底下, 建為一個 Prefab.
最後, 再把 Hierarchy 視窗的 GameObject- Player UI 砍掉, 不再需要它了.

接著, 在 Inspector 視窗, Scripts 資料夾底下, 新增一個 C# Script, 取名為 “PlayerUI”.

編輯 C# Script- PlayerUI.
輸入下列程式碼, 先把需要的變數宣告出來.

然後, 在 Project 視窗, Prefab 資料夾底下, 選取 Prefab- Player UI.
在 Inspector 視窗, 開啟 Prefab- Player UI 的内容頁面.
將 Project 視窗, Scripts 資料夾底下的 C# Script- PlayerUI 拉到 Inspector 視窗, 建立為 Prefab- Player UI 的一個 component.

再將 Hierarchy 視窗, GameObject- Player Name Text 拖拉到 Inspector 視窗, component- Player UI 的 Player Name Text.
以及, 在 Inspector 視窗裡面, 將 component- Slider 拖拉到 component Player UI 的 Player Health Slider.

到此, 我們就創建完成 Prefab- Player UI 了.

接下來, 我們要讓 Player Manager 與 Player UI 串接起來, 再由 Player Manager 來控制 Player UI 的顯示位置.

串接玩家角色與 Player UI

首先, 我們先實作設定玩家角色名字欄位内容的功能.

編輯 C# Script- Player UI, 在 class PlayerUI 裡面新增一個變數 target.

再輸入下列程式碼實作 SetTarget().

這段程式碼會檢查傳入的 PlayerManager instance 非為空值時, 將字串欄位的内容設為玩家的名字.

另外, 也需要實作 Update(), 更新血條的數值.

接下來, 我們要在 PlayerManager 裡面, 找個適當時機, 把 Player UI 動態生成出來, 並串接在一起.

編輯 C# Script- PlayerManager, 在 class PlayerManager 增加一個變數 PlayerUIPrefab.

並修改 Start(), 動態生成出 Player UI instance, 並用 SendMessage 的作法呼叫其 SetTarget(), 進一步設定好名字欄位的内容.

理論上, 在玩家角色離開遊戲室時, Photon 會清除掉全部相關的 instance.
預防萬一 Photon 沒有把全部相關的 instance 清除乾淨, 我們自己來檢查 Player UI instance 的狀況.

編輯 C# Script- PlayerUI, 修改 Update(), 只要發現記錄的 Player Manager instance 為空值時, 就把自身的 instance 也砍除掉.

另外, 當有場景重載時, 玩家角色可能會被迫重新生成. 在這個時候, 我們也需要做 SetTarget() 來重新調整字串欄位與血條的數值.

編輯 C# Script- PlayerManager, 在 CalledOnLevelWasLoaded() 的最後面, 加上下列程式碼, 再做一次 SetTarget().

再來, 在 Unity 環境, UI 元件都得依存在 GameObject- Canvas 之下.
因此, 當 Player UI 被動態生成出來時, 我們需要將 Player UI instance 的 parent 設為 Canvas.

編輯 C# Script- PlayerUI, 輸入下列程式碼, 實作 Awake() 來設定 Player UI instance 的 parent.

好的, 到此我們已將玩家角色與 Player UI 串接起來.

回到 Unity, 切換工作場景為 Launcher, 執行測試.
(測試結果, 請參考下面 GIF 動畫圖檔).

我們可以看到, 玩家角色頭頂出現名字與血條, Camera 也有跟隨玩家角色. 很好, 這是符合預期的行為.

我們再多做個測試, 讓第二位玩家進入遊戲場.
(測試結果, 請參考下面 GIF 動畫圖檔).

我們可以看到, 在第二位玩家進入遊戲場後, 兩位玩家的名字欄位疊在一起.
我們來修改這個問題.

編輯 C# Script- PlayerUI, 在 class PlayerUI 增加幾個變數:

再修改 SetTarget(), 加入下列程式碼~ 根據 CharacterController 的 Height 值, 來調整 Player Name Text 的高度.

並輸入下列程式碼實作 LateUpdate(), 調整 Camera 的位置, 能跟隨玩家角色移動.

回到 Unity, 切換工作場景為 Launcher, 執行測試.
(測試結果, 請參考下面 GIF 動畫圖檔).

結果看起來不錯, 兩位玩家角的的名字欄位沒有疊在一起了. 其中一位玩家角色發射動感光波, 另位玩家角色被打中時, 血條的血量也有遞減. 到血量歸零時, 就被自動帶離開遊戲室, 回到遊戲場入口了.

[經驗分享-2]
小編在實作過程中, 曾經遇到下面 GIF 動畫圖檔的問題~ My Robot Kyle 發射動感光波時, 會讓自己扣血.

會發生這個問題, 是因為 GameObject- Beam Left 的 component- Box Collider和 My Robot Kyle 身體其它部位發生踫撞而造成的.
將 component- Box Collider 的 Center 設為 (3, 0, 0.05), Size 設為 (7, 1, 0.9) 即可避開此問題.

到此我們終於完成這個連線射擊遊戲. 藉由 PUN 2 提供的便利功能, 大幅減少了處理網路連線與資料同步的麻煩問題, 讓我們能把心思與精力放在遊戲本體的開發上面.

不止 PUN 2, Photon 還有一系列的產品能支援更多 CCUs 與更短的延遲, 各類型的多人網路即時連線的遊戲均有對應的產品. 有機會再來為大家做介紹.

歡迎各位朋友來逛我們的 粉絲團: https://www.facebook.com/photoncloudtw/
如果還有什麼疑問的話, 可以來我們的粉絲團討論唷 😃~

--

--