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

實作分享

Ryan Tseng
Photon Taiwan

--

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

預定實作的遊戲流程為:
1. 遊戲啟動時, 玩家能見證與遊戲伺服器連線過程及進度.
[延伸閱讀: PUN 2 | 連線射擊遊戲 (1/4)]
2. 遊戲室場景會隨著同時在線人數擴大. (即本篇文章)
3. 玩家可以操控遊戲角色跑/跳/轉身與射擊.
[延伸閱讀: PUN 2 | 連線射擊遊戲 (3/4)]
4. 全部在線玩家的角色動作與血量, 都會被同步. 當角色血量歸零, 遊戲即結束. 玩家會自動被帶回到遊戲場, 隨時可以再啟動新一場的遊戲.
[延伸閱讀: PUN 2 | 連線射擊遊戲 (4/4)]

在第一篇文章中, 我們在新專案匯入 PUN 2 套件, 並和 Photon Cloud 建立連線與加入遊戲室. 本篇文章延續前回, 我們會創建遊戲室場景, 並能隨同時在線的遊戲玩家人數而更新場景 (最多四人).

創建第一個遊戲室場景

首先, 在 Unity 開啟前回實作的專案 pun_basic.
在 Project 視窗, Scenes 資料夾底下, 創建一個新場景, 取名叫做 “Room for 1”.

將 Unity 切換到場景 Room for 1.
場景 Room for 1 目前是空無一物的, 我們先為它打造一塊地板.
在 Hierarchy 視窗, 新增一個 GameObject/3D Object/Cube, 取名叫做 “floor”, 並將其 Inspector 視窗裡面 Transform 的 Position 歸零, Scale 設為 (20, 1, 20).

地板有了, 還需要四面牆. 我們另外新增四個 GameObject/3D Object/Cube, 分别取名為 “Wall 1”, “Wall 2”, “Wall 3” 與 “Wall 4”.
並分别將其 Inspector 視窗裡面的 Transform 設為:
Wall 1: Position → (0, 2, -10), Rotation → (0, 0, 0), Scale → (20, 3, 1)
Wall 2: Position → (0, 2, 10), Rotation → (0, 0, 0), Scale → (20, 3, 1)
Wall 3: Position → (-10, 2, 0), Rotation → (0, 90, 0), Scale → (20, 3, 1)
Wall 4: Position: (10, 2, 0), Rotation → (0, 90, 0), Scale → (20, 3, 1)
將場景 Room for 1 存檔, 這樣一來, 我們第一個的遊戲室場景就完成了.

接下來, 我們來處理玩家加入或離開遊戲等操作介面相關的功能.

考量我們將在這個遊戲打造四個不同的遊戲室場景, 而這些遊戲室場景之間, 只有地板與四面牆大小的差異, 其它諸如操作介面等功能都是相同的. 因此, 我們將利用 Unity 的 Prefab 機制, 把遊戲操作介面與管理功能打造從能重覆利用的 prefab, 方便後面的開發.

我們在前回, 已有做了一個按鈕 Play Button, 玩家點按後即會連上 Photon Cloud 並加入或創建一個遊戲室. 目前還缺少讓玩家主動選擇離開遊戲的操作介面, 我們就從這裡下手.

創建 Prefab- Game Manager

在 Unity, 確認目前工作場景為 Room for 1.
在 Hierarchy 視窗, 新增一個 GameObject, 取名為 “Game Manager”.

在 Project 視窗, Scripts 資料夾下, 新增一個 C# Script, 取名為 “GameManager”
並把 C# Script- GameManager 拖拉到 GameObject- Game Manager 的 Inspector 視窗, 加為 Game Manager 的一個 component.

接下來,
在 Project 視窗, Assets 資料夾底下, 新增一個資料夾, 取名叫做 “Prefab”.
把 Hierarchy 視窗裡面的 GameObject- Game Manager 拖拉到 Prefab 資料夾底下. 此時, 原本在 Hierarchy 視窗裡面的 GameObject- Game Manager 會變為藍色, 代表它已經轉成為一個 prefab 了.

編輯 C# Script- GameManager, 輸入下列程式碼, 並存檔.

using System.Collections;
using System.Collections.Generic;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Com.ABCDE.MyApp
{
public class GameManager : MonoBehaviourPunCallbacks
{
// 玩家離開遊戲室時, 把他帶回到遊戲場入口
public override void OnLeftRoom()
{
SceneManager.LoadScene(0);
}
public void LeaveRoom()
{
PhotonNetwork.LeaveRoom();
}
}
}

這段程式的功能, 主要是實作出 public method LeaveRoom(), 暫時只呼叫 PhotonNetwork.LeaveRoom() 讓玩家離開遊戲室, 其它諸如儲存資料等細節功能會在後面文章中補上.
而在玩家離開遊戲室時, 程式會進入 Callback Function OnLeftRoom() 中, 在此, 搭配 Build Setting 的設定 (本篇文章後面會講到), 我們呼叫 Unity SceneManager 載入第一個場景, 把玩家導回到場景 Launcher, 即遊戲場入口.

創建 Prefab- Quit Room Button

回到 Unity 環境, 確認目前工作場景為 Room for 1.
在 Hierarchy 視窗, 新增一個 GameObject/UI/Panel, 取名為 “Top Panel”. 並將 Inspector 視窗裡面的 Image 與 Canvas Renderer 兩個 components 砍掉, 只留下 Rect Transform.
Rect Transform 的 Anchor Presets 設為: (同時壓住 Alt 與 Shift 鍵) top 與 stretch, Height 則設為 50.

在 Top Panel 之下, 新增一個 GameObject/UI/Button, 取名叫做 “Leave Button”. 並把其下 Text 在 Inspector 視窗裡面, Text component 的 Text 值設為 “Leave Game”.

在 Hierarchy 視窗, 點選在 Leave Button.
將 Project 視窗裡面, Scripts 資料夾底下, C# Script- GameManager 拉到 Inspector 視窗, 加為 Leave Button 的一個 component.
再把 Inspector 視窗裡面 Component- Game Manager 拉到 Component- Button 的 OnClick(), 選取 GameManager.LeaveRoom().
這樣一來, 當點按 Leave Button 時, 就會觸發執行 GameManager.LeaveRoom().

再把 Leave Button 拖拉到 Prefab 資料夾下, 轉成為 prefab.

好的, 我們已完成場景 Room for 1 的架構, 存檔.
接下來, 依樣畫葫蘆, 我們來製作另外的三個場景.

創建其它的遊戲室場景

首先, 在 Projects 視窗裡面, Scenes 資料夾底下, 點選 Room for 1.
再點按選單 Edit/Duplicate, 把場景 Room for 1 複製一份. 再將複製出來的場景改名叫做 Room for 2.

將 Unity 的工作場景切換到 Room for 2.
並把 floor 與四面牆的尺寸設為:
floor → Scale: (36, 1, 36)
Wall 1 → Position: (0, 2, -18.5), Rotation: (0, 0, 0), Scale: (36, 3, 1)
Wall 2 → Position: (0, 2, 18.5), Rotation: (0, 0, 0), Scale: (36, 3, 1)
Wall 3 → Position: (-18.5, 2, 0), Rotation: (0, 90, 0), Scale: (36, 3, 1)
Wall 4 → Position: (18.5, 2, 0), Rotation: (0, 90, 0), Scale: (36, 3, 1)
再將場景 Room for 2 存檔

同樣的步驟, 做出場景 Room for 3.
並將 floor 與四面牆的尺寸設為:
floor → Scale: (50, 1, 50)
Wall 1 → Position: (0, 2, -25), Rotation: (0, 0, 0), Scale: (50, 3, 1)
Wall 2 → Position: (0, 2, 25), Rotation: (0, 0, 0), Scale: (50, 3, 1)
Wall 3 → Position: (-25, 2, 0), Rotation: (0, 90, 0), Scale: (50, 3, 1)
Wall 4 → Position: (25, 2, 0), Rotation: (0, 90, 0), Scale: (50, 3, 1)
再將場景 Room for 3 存檔

同樣的步驟, 做出場景 Room for 4.
並將 floor 與四面牆的尺寸設為:
floor → Scale: (60, 1, 60)
Wall 1 → Position: (0, 2, -30), Rotation: (0, 0, 0), Scale: (60, 3, 1)
Wall 2 → Position: (0, 2, 30), Rotation: (0, 0, 0), Scale: (60, 3, 1)
Wall 3 → Position: (-30, 2, 0), Rotation: (0, 90, 0), Scale: (60, 3, 1)
Wall 4 → Position: (30, 2, 0), Rotation: (0, 90, 0), Scale: (60, 3, 1)
再將場景 Room for 4 存檔.

設定場景編譯清單

接著, 點按選單 File/Build Settings, 叫出 Unity 的 Build Settings 視窗.
再把 Project 視窗, Scenes 資料夾底下, 按順序將 Launcher, Room for 1, …, Room for 4, 逐個拖拉到 Scenes In Build 裡面.

好的, 我們已把遊戲全部的場景都準備好了.
接下來, 我們要隨在線的玩家人數變化, 自動載入對應的場景.

隨在線玩家人數變化, 自動載入對應場景

我們分兩段來實作, 先做載入對應場景的功能.
編輯 C# Script- GameManager, 輸入下列程式碼, 並存檔.

void LoadArena()
{
if (!PhotonNetwork.IsMasterClient)
{
Debug.LogError("我不是 Master Client, 不做載入場景的動作");
}
Debug.LogFormat("載入{0}人的場景",
PhotonNetwork.CurrentRoom.PlayerCount);
PhotonNetwork.LoadLevel("Room for " +
PhotonNetwork.CurrentRoom.PlayerCount);
}

在 LoadArena() 中, 我們檢查玩家是否為 Master Client.
若為 Master Client, 則呼叫 PhotonNetwork.LoadLevel() 去載入對應的場景. (在線玩家人數為兩人, 則載入場景 Room for 2, … 以此類推. 最多為四人)
非為 Master Client 的玩家也不用擔心, 在前篇文章中, 我們在 C# Script- Launcher 的 Awake() 裡面, 有設定 PhotonNetwork.AutomaticallySyncScene 為 true, 這能讓其他非為 Master Client 的玩家在 Master Client 載入場景後, 也同步更新為新的場景.

接下來就是掌握在線玩家人數變化的時機, 去呼叫 LoadArena(), 執行載入對應場景的功能.
我們繼續在 C# Script- GameManager 輸入下列程式碼, 並存檔.

public override void OnPlayerEnteredRoom(Player other)
{
Debug.LogFormat("{0} 進入遊戲室", other.NickName);
if (PhotonNetwork.IsMasterClient)
{
Debug.LogFormat("我是 Master Client 嗎? {0}",
PhotonNetwork.IsMasterClient);
LoadArena();
}
}
public override void OnPlayerLeftRoom(Player other)
{
Debug.LogFormat("{0} 離開遊戲室", other.NickName);
if (PhotonNetwork.IsMasterClient)
{
Debug.LogFormat("我是 Master Client 嗎? {0}",
PhotonNetwork.IsMasterClient);
LoadArena();
}
}

在這段程式中, 我們實作了 Callback Function OnPlayerEnteredRoom() 與 OnPlaterLeftRoom().
顧名思義, 當有玩家連線上來進入遊戲室時, 程式就會進入到 OnPlayerEnteredRoom() 中, 在此我們控制僅讓 Master Client 玩家的程式去呼叫 LoadArena().
同樣的, 當有玩家離開遊戲室時, 程式會進入到 OnPlayerLeftRoom() 中, 在此我們也是控制僅讓 Master Client 玩家的程式去呼叫 LoadArena().

到此, 我們的程式已能隨在線遊戲玩家人數的變化, 自動載入對應的遊戲場景.

載入初始的遊戲場景

接下來, 針對第一個進入遊戲室的玩家, 也需要載入初始的場景.
我們編輯 C# Script- Launcher, 在 OnJoinedRoom() 裡面, 輸入下列程式碼, 並存檔.

if (PhotonNetwork.CurrentRoom.PlayerCount == 1)
{
Debug.Log("我是第一個進入遊戲室的玩家");
Debug.Log("我得主動做載入場景 'Room for 1' 的動作");
PhotonNetwork.LoadLevel("Room for 1");
}

我們回到 Unity 環境, 將工作場景切換到 Launcher, 並執行測試, 看看目前為止的成果如何. (測試執行的成果, 請參考下面 GIF 動畫圖檔)

首先, 按 Play 與 Photon Cloud 連線, 會看到遊戲場景轉變為 Room for 1, 上面有一個 Leave Game 按鈕. 並在 Console 視窗能看到載入 Room for 1 的訊息. 程式執行到這邊為止的動作符合我們的預期.

但是, 接下來, 點按 Leave Game 離開遊戲室時, 會發現程式離開遊戲室後, 又自動再進入遊戲室. 這就不是我們預期的程式動作. 我們查看一下 Console 視窗裡面的訊息記錄, 看看發生了什麼事.

從訊息記錄可以看出, 點按 Leave Game 離開遊戲室後, 程式來到 OnConnectedToMaster(). 而在 C# Script- Launcher 的 OnConnectedToMaster() 中, 我們是讓程式一連上 Photon Cloud 時, 就呼叫 PhotonNetwork.JoinRandomRoom() 隨機加入一個遊戲室. 因此, 也就發生程式又進入到遊戲室並載入場景 Room for 1 的動作.

因此, 我們得加個 flag, 當玩家主動按 Play 時, 才去呼叫 PhotonNetwork.JoinRandomRoom() 加入遊戲室.
我們修改一下程式碼, 在 C#Script- Launcher, class Launcher 的最前面, 加上

bool isConnecting;

並在 Connect() 的最前面, 加上

isConnecting = true;

以及, 在 OnConnectedToMaster() 中, 呼叫 PhotonNetwork.JoinRandomRoom() 前, 先判斷 isConnecting 是否為 true.

if (isConnecting)
{
PhotonNetwork.JoinRandomRoom();
}

再執行測試一次, 看看成果如何? (測試執行的成果, 請參考下面 GIF 動畫圖檔)

這一次沒問題了, 在點按 Leave Game 之後, 程式會停在遊戲場入口, 也就是 Play 按鈕的畫面. 此時再點按 Play, 則又會再度進入場景 Room for 1 的畫面.

再來, 我們測試有複數玩家進入遊戲室的場景同步情況如何?
我們把在 Unity 環境跑的程式當作第一位玩家, 另外把程式編譯出執行檔並執行起來, 當作是第二位玩家. 再分别連上 Photon Cloud 進入遊戲室.

從測試執行的 GIF 動畫圖檔可以看到, 在第二位玩家進入遊戲室後, 在線的兩位玩家同步都更新了場景. 而在第二位玩家離開遊戲室後, 留在遊戲室的玩家也再度更新了場景.

到此, 我們完成了第二部份的實作, 下回我們會實作遊戲角色與其動作操控.
下回見囉.

--

--