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

實作分享

Ryan Tseng
Apr 15, 2019 · 19 min read

在前面文章介紹過 PUN 2 新版本的新功能, 從這篇開始會有一系列文章詳細的說明 PUN 2 的使用.

延伸閱讀: PUN 2 — 新版本新功能之入門介紹

建議讀者:
1. 具備基礎的 Unity Editor 操作知識, 有遊戲開發經驗者尤佳.
2. 使用 Unity 2018.1 以後的版本.

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


開啟新專案, 匯入 PUN 2 套件

首先, 在 Unity 建立一個新專案, 叫做 pun_basic.
開啟 Asset Store, 找到 PUN 2 — FREE, 下載並且匯入.

在 Import Unity Package 視窗, 點按 Import 全部匯入.

匯入完成後, 可以看到在 Project 視窗, Assets 資料夾底下多了一個叫做 Photon 的資料夾.
另外會跳出 PUN Wizard 視窗, 提示用戶輸入 AppId 或 Email 帳號.

如何取得 AppId 呢?
如果是 Photon Cloud 的新用戶, 可以直接輸入 Email, 並按 Setup Project.
PUN Wizard 會在 Photon Cloud 建立帳號與應用程式, 並自動把應用程式的 AppId 帶入到 PUN Wizard.

或是直接到 Photon Cloud 註冊/登入, 在 Cloud Dashboard 點按 建立新 應用程式.

Photon Type 選 “Photon PUN”.
名稱可隨意輸入.
按 建立 建立新的應用程式.

將新應用程式的應用程式 ID 複製起來.

回到 Unity, 貼在 PUN Wizard 上, 並按 Setup Project.
當 PUN Wizard 出現 “Done! All connection settings can be edited in the PhotonServerSettings now.”, 代表專案已經與 Photon Cloud 的服務串接起來. 這時候可以按 Close 關掉 PUN Wizard.


Photon Cloud

開始動手寫程式之前, 先介紹一下 Photon Cloud 的功能:

Photon Cloud 是由 Exit Games 在全球各地運營的雲端服務, 提供無比的彈性與擴充性, 不論你是獨立製作開發者或 AAA Studio, 都能讓你在全球各地開發及推出即時多人連線遊戲.

Photon Unity Networking (PUN) 是 Photon Cloud 的製作團隊, 特别為了能在 Unity 上做網路遊戲開發而設計的套件, 主要重點為強化多人網路即時連線的功能, 相容於原本的 Unity Networking, 而且網路底層全部重新設計過, 配合在全球佈署的 Photon Cloud 來做為遊戲的後端伺服器, 應用上非常廣泛.


建立連線, 創建/加入遊戲室

在 Project 視窗, Scenes 資料夾底下, 將預設的場景 Sample Scene 改名叫做 “Launcher”.

在 Project 視窗, 另外創建一個新資料夾叫做 “Scripts”, 並在 Scripts 底下創建一個新的 C # Script, 取名也叫做 “Launcher”.

在 Hierarchy 視窗, 創建一個 Empty GameObject, 改名也叫做 “Launcher”.

把 C# Script- Launcher 拉到 GameObject- Launcher 的 Inspector 視窗裡, 加為 GameObject- Launcher 的一個 component.

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

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

namespace Com.ABCDE.MyApp
{
public class Launcher : MonoBehaviour
{
// 遊戲版本的編碼, 可讓 Photon Server 做同款遊戲不同版本的區隔.
string gameVersion = "1";

void Awake()
{
// 確保所有連線的玩家均載入相同的遊戲場景
PhotonNetwork.AutomaticallySyncScene = true;
}

void Start()
{
Connect();
}

// 與 Photon Cloud 連線
public void Connect()
{
// 檢查是否與 Photon Cloud 連線
if (PhotonNetwork.IsConnected)
{
// 已連線, 嚐試隨機加入一個遊戲室
PhotonNetwork.JoinRandomRoom();
}
else
{
// 未連線, 嚐試與 Photon Cloud 連線
PhotonNetwork.GameVersion = gameVersion;
PhotonNetwork.ConnectUsingSettings();
}
}
}
}

這段程式的功能, 主要是在 Unity 的 Start() 階段, 檢查有否連上 Photon Cloud. 若有, 則隨機加入一個遊戲室. 若否, 則與 Photon Cloud 建立連線. 並搭配做遊戲版本的控制, 以及確保全部連線玩家均載入相同的遊戲場景.

Namespace (Com.ABCDE.MyApp): Namespace 的字串可隨意定義, 建議結合網域與程式名稱組合出唯一的字串. 善用 namespace 的機制, 將可以有效避免自己程式的物件與其他廠商定義的物件發生撞名的困擾.

測試執行之前, 從選單 Window/Photon Unity Networking/Highlight Server Settings 叫出 PhotonServer Settings, 並把 PUN Logging 設為 Full.

執行後, 開啟 Console 視窗, 會發現已有數行的訊息記錄, 其中一行是 “PUN got region list. Going to ping minimum regions, based on this previous result summary:”, 這代表程式已經有連上 Photo Cloud 了.

再多測試一個不同狀況: 把網路斷線, 再執行一次.
這次得到不同的訊息記錄: “Connect() to ‘ns.exitgames.com’ () failed: System.Net.Sockets.SocketException (0x80004005): Could not resolve host ‘ns.exitgames.com’..”.

看來我們得在 Connect() 呼叫 PhotonNetwork.ConnectUsingSettings() 之後, 處理因網路異常導致無法與 Photon Cloud 建立連線的狀況.

另外, 當呼叫 PhotonNetwork.JoinRandomRoom() 之後, 無論成功或失敗, 也有後續的狀態要做處理.

實作 PUN Callbacks

PUN 2 提供有數個 callbacks 介面, 方便讓我們做連線與遊戲狀態的同步處理.

編輯 C# Script- Launcher.
在宣告 class Launcher 之前, 加上 “using Photon.Realtime;”. 並把 class Launcher 改為繼承 MonoBehaviourPunCallbacks. 如此一來, 即可直接使用 PUN 2 設定好的 Events 與 Callback Functions 介面.

using Photon.Realtime;

(...)

public class Launcher: MonoBehaviourPunCallbacks

再把 OnConnectedToMaster(), OnDisconnected(), OnJoinRandomFailed() 與 OnJoinedRoom() 共四個 Callback Function 實作出來.

public override void OnConnectedToMaster()
{
Debug.Log("PUN 呼叫 OnConnectedToMaster(), 已連上 Photon Cloud.");

// 確認已連上 Photon Cloud
// 隨機加入一個遊戲室
PhotonNetwork.JoinRandomRoom();
}
public override void OnDisconnected(DisconnectCause cause)
{
Debug.LogWarningFormat("PUN 呼叫 OnDisconnected() {0}.", cause);
}
public override void OnJoinRandomFailed(short returnCode, string message)
{
Debug.Log("PUN 呼叫 OnJoinRandomFailed(), 隨機加入遊戲室失敗.");

// 隨機加入遊戲室失敗. 可能原因是 1. 沒有遊戲室 或 2. 有的都滿了.
// 好吧, 我們自己開一個遊戲室.
PhotonNetwork.CreateRoom(null, new RoomOptions());
}
public override void OnJoinedRoom()
{
Debug.Log("PUN 呼叫 OnJoinedRoom(), 已成功進入遊戲室中.");
}

當程式連上 PhotonCloud, 會進入到 OnConnectedToMaster() 中, 在此我們呼叫 PhotonNetwork.JoinRandomRoom() 隨機加入一個遊戲室.

若無法隨機加入遊戲室, 則程式會進入到 OnJoinRandomFailed(), 在此我們呼叫 PhotonNetwork.CreateRoom() 創建一個新的遊戲室.

好, 測試執行看看結果: 從訊息記錄可以清楚看到, 程式連上 Photon Cloud -> 隨機加入遊戲室失敗 -> 已成功進入遊戲室中.

Expose Fields in Unity Inspector

我們再改一下程式, 讓遊戲室玩家人數上限的控制變數能直接顯示在 Unity 的 Inspector 視窗中, 方便我們隨時做修改與測試.

編輯 C# Script- Launcher, 在 class Launcher 的最前面, 加入下列程式碼:

[Tooltip("遊戲室玩家人數上限. 當遊戲室玩家人數已滿額, 新玩家只能新開遊戲室來進行遊戲.")]
[SerializeField]
private byte maxPlayersPerRoom = 4;

並把在 OnJoinRandomFailed() 裡面的 PhotonNetwork.CreateRoom() 帶入玩家人數上限的控制變數.

PhotonNetwork.CreateRoom(null, new RoomOptions { MaxPlayers = 
maxPlayersPerRoom });

存檔, 並等待 Unity 編譯完成, 即會在 Inspector 視窗看到多出一個叫做 “Max Players Per Room” 的欄位. 日後若要增加遊戲室的玩家人數上限, 就不需要改程式, 直接改這個欄位的值即可.

增加 Play Button

目前, 我們的程式會在執行後自動連線 Photon Cloud, 並隨機加入一個遊戲室(或新開一個遊戲室). 讓我們把它改成: 按了某個按鈕後, 程式才會開始與 Photon Cloud 連線.

開啟 Unity, 在 Hierarchy 視窗, 增加一個 GameObject/UI/Button, 取名叫做 “Play Button”, 並將 Inspector 視窗裡面 Rect Transform 的 Pos X, Pos Y, Pos Z 歸零, Width 與 Height 均設為 100.
再把 Play Button 底下 Text 的 Text 值改成為 “Play”.

將 GameObject- Launcher 拉到 Play Button 的 On Click(), 並從下拉選單中選取 “Launcher.Connect()”.

如此一來, 當玩家點按了 Play Button, 即會觸發執行 Launcher.Connect(). 原本在 C# Script- Launcher Start() 裡面呼叫 Connect() 的程式碼即可刪掉了.

顯示 Player Name

接下來, 我們利用 Unity 的 PlayerPrefs 機制來記錄遊戲玩家的名稱.

開啟 Unity, 在 Project 視窗, Scripts 目錄下, 新增一個 C# Script, 取名為 “PlayerNameInputField”.

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

using System.Collections;
using System.Collections.Generic;
using Photon.Pun;
using UnityEngine;
using UnityEngine.UI;

namespace Com.ABCDE.MyApp
{
[RequireComponent(typeof(InputField))]
public class PlayerNameInputField : MonoBehaviour
{
const string playerNamePrefKey = "PlayerName";

void Start()
{
string defaultName = string.Empty;
InputField _inputField =
this.GetComponent<InputField>();
if (_inputField != null)
{
if (PlayerPrefs.HasKey(playerNamePrefKey))
{
defaultName = PlayerPrefs.
GetString(playerNamePrefKey);
_inputField.text = defaultName;
}
}
// 設定遊戲玩家的名稱
PhotonNetwork.NickName = defaultName;
}

public void SetPlayerName(string value)
{
if (string.IsNullOrEmpty(value))
{
Debug.LogError("Player Name is null or empty");
return;
}
// 設定遊戲玩家的名稱
PhotonNetwork.NickName = value;
PlayerPrefs.SetString(playerNamePrefKey, value);
}
}
}

這段程式的功能, 主要是取得玩家在 InputField 輸入的名稱字串或從 PlayerPrefs 中撈出已儲存的名稱字串後, 設定遊戲玩家的名稱.

接下來, 回到 Unity, 在 Hierarchy 視窗, 新建一個 GameObject/UI/InputField, 取名為 “Name InputField”, 並將 Rect Transform 的 PosY 設為 65, Height 設為 30, 讓它排列在 Play Button 的上方.

將 Name InputField 底下 PlaceHolder 的 Text 值改為 “Enter your Name…”.

再將 C# Script- PlayerNameInputField 拖拉到 Name InputField 的 Inspector 視窗, 加為 Name InputField 的一個 component.

再把 Inspector 視窗裡面的 Component- PlayerNameInputField 拉到 Component- InputField 的 On Value Change, 並在下拉選單中選取 PlayerNameInputField.SetPlayerName().

將場景 Launcher 存檔.

如此一來, 當玩家在 Name InputFiled 輸入字串後, 即會觸發執行 PlayerNameInputField.SetPlayerName(), 把遊戲玩家的名字寫入到 PlayerPrefs 中.

顯示 Connection Progress

再來, 我們要在程式與 Photon Cloud 連線時, 將 Name InputField 與 Play Button 隱藏起來, 改為顯示 “Connecting…” 的字串. (之後會在遊戲角色上面顯示遊戲玩家的名字. )

開啟 Unity, 在 Hierarchy 視窗, 新建一個 GameObject/UI/Panel, 取名為 “Control Panel”. 並將 Inspector 視窗裡面的 Image 與 Canvas Renderer 兩個 components 砍掉, 只留下 Rect Transform.

再將 Play Button 與 Name InputField 兩個 GameObject 拖拉到 Control Panel 之下.

在 Hierarchy 視窗, 另外新建一個 GameObject/UI/Text, 取名為 “Progress Label”. 並把其 Inspector 視窗裡面, Text component, Alignment 設為 center align 與 middle align, Text 值設為 “Connecting…”, Color 設為白色或其它醒目的顏色.

將場景 Launcher 存檔.

接下來, 我們把 Control Panel 與 Progress Label 兩個 GameObject 的顯示/隱藏控制變數直接顯示在 Unity 的 Inspector 視窗中, 方便我們隨時做修改與測試.

編輯 C# Script- Launcher, 在 class Launcher 的最前面, 加入下列程式碼:

[Tooltip("顯示/隱藏 遊戲玩家名稱與 Play 按鈕")]
[SerializeField]
private GameObject controlPanel;
[Tooltip("顯示/隱藏 連線中 字串")]
[SerializeField]
private GameObject progressLabel;

在 Start() 中加入 下列程式碼:

progressLabel.SetActive(false);
controlPanel.SetActive(true);

在 Connect() 中加入 下列程式碼:

progressLabel.SetActive(true);
controlPanel.SetActive(false);

在 OnDisconnected() 中加入 下列程式碼:

progressLabel.SetActive(false);
controlPanel.SetActive(true);

將 C# Script- Launcher 存檔, 並回到 Unity.
等 Unity 編譯完, 在 GameObject- Launcher 的 Inspector 視窗裡面多出 Control Panel 與 Progress Label 兩個欄位.
再分别把 GameObject- Control Panel 與 Progress Label 拖拉到對應的欄位中.
將場景 Launcher 存檔.

來測試執行一下.

程式執行後, 即會自 PlayerPrefs 撈出先前輸入的名稱字串, 並顯示在 Name InputField 中.

點按 Play Button 後, Name InputField 與 Play Button 兩者均隱藏起來, 並顯示 “Connecting…”

到此, 我們完成了第一部份的實作, 下回我們會實作遊戲室的場景, 並控制登入的遊戲玩家人數.

下次見囉.

Photon Taiwan

有關多人連線, 即時連線, 遊戲設計的資訊與技術主題~ 多多交流喔!

Ryan Tseng

Written by

Photon Taiwan

有關多人連線, 即時連線, 遊戲設計的資訊與技術主題~ 多多交流喔!

More From Medium

More on Multiplayer from Photon Taiwan

More on Multiplayer from Photon Taiwan

Photon Bolt 特有功能介紹

More on Game Development from Photon Taiwan

More on Game Development from Photon Taiwan

如何挑選適當的 Photon 伺服端產品方案?

More on Pun 2 from Photon Taiwan

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade