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

實作分享

Ryan Tseng
Photon Taiwan

--

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

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

本篇文章延續前回, 我們將使用 Unity 提供的免費素材來實作第一人稱模式的遊戲角色與動作操控.

創建 Prefab- My Robot Kyle

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

將 Unity 切換到場景 Kyle Test.
再將 Project 視窗裡面, \Assets\Photon\PhotonUnityNetworking\Demos\Shared Assets\Models 底下的 Robot Kyle 拖拉到 Hierarchy 視窗裡面, 並改名為 “My Robot Kyle”.

這邊要特别提醒一下: 使用 PUN 連線時, 只能動態生成放置在 Resources 資料夾底下的 Prefab, 而且, 在 Resources 資料夾底下的 Prefab 不能取同名.

所以, 在 Projects 視窗, 創建一個新資料夾, 取名為 “Resources”.
將 My Robot Kyle 拖拉到 Resources 資料夾底下. 此時 Unity 會跳出一個訊息詢問: 是否要建立為新的 Original Prefab 或是 Prefab Variant?
點選 Original Prefab 之後, Hierarchy 視窗裡面 My Robot Kyle 的圖示會轉變為藍色, 代表已建立為 Prefab.

接下來是 My Robot Kyle 的動作與操控設定.

My Robot Kyle 的操控設定與動作表現

簡化起見, 我們使用 Unity 的 CharacterController 來做角色的操控設定.
在 Hierarchy 視窗, 選取 My Robot Kyle, 然後在 Inspector 視窗, 點按 Add Component, 增加一個 component- Character Controller.
此時, 在 Scenes 視窗, My Robot Kyle 身體會被綠色膠囊踫撞器 (Capsule Collider) 包覆, 而此膠囊踫撞器的中心點是在 Kyle 的腳底位置.

將 Inspector 視窗, component- Character Controller 的 Center.Y 設為 1, 讓膠囊踫撞器的中心點移到 Kyle 的腰, 也就是一半身高的位置.

關於 My Robot Kyle 的動作表現, Unity 也有提供相對應的的 Animator Graph. 我們來做這部份的設定:
在 Hierarchy 視窗, 確認點選在 My Robot Kyle.
在 Inspector 視窗的 component- Animator, 點按 Controller 右邊的小圖示叫出 Select RuntimeAnimatorController 選單, 選取 Kyle Robot.
並勾選 Apply Root Motion.

最後, 在 Inspector 視窗, 點按右上角落的 Overrides, 再點選 Apply All, 將剛剛做的設定更新到 Prefab- My Robot Kyle 中.

接下來,
在 Project 視窗, Scripts 資料夾底下, 新增一個 C# Script, 取名為 “PlayerAnimatorManager”.
確認在 Project 視窗是選取 My Robot Kyle, 把 C# Script- PlayerAnimatorManager 拖拉 Inspector 視窗, 新增為 My Robot Kyle 的一個 component.

編輯 C# Script- PlayerAnimatorManager, 將預設產生的程式碼 class PlayerAnimatorManager 包含在自定義的 namespace 下 (例如 Com.ABCDE.MyApp).

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Com.ABCDE.MyApp
{
public class PlayerAnimatorManager : MonoBehaviour
{
void Start()
{
}

void Update()
{
}
}
}

在 class PlayerAnimatorManager 的最前面, 宣告一個變數 private Animator animator, 並在 void Start() 加入下列程式碼將變數 animator 指向 component- Animator 的位址.

private Animator animator;void Start()
{
animator = GetComponent<Animator>();
if (!animator)
{
Debug.LogError(
"PlayerAnimatorManager- Animator component 遺失", this);
}
}

再修改 void Update() 以取得玩家輸入的 horizontal 與 vertical 數值, 並轉換為 My Robot Kyle 的移動速度.
考量 My Robot Kyle 沒有後退的動作, 所以若 v (vertical 的數值) 小於 0, 就把它改為 0.
而 My Robot Kyle 的移動速度須為正值, 可將 h (horizontal 的數值) 與 v 取絕對值, 或做平方加總或任何能得出正值的方法均可.

void Update()
{
if (!animator)
{
return;
}
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
if (v < 0)
{
v = 0;
}
animator.SetFloat("Speed", h * h + v * v);
}

將 C# Script- PlayerAnimatorManager 存檔.

My Robot Kyle 的操控與動作表現都設定的差不多了, 我們回到 Unity 環境, 確認工作場景為 Kyle Test, 執行測試一下.
(測試執行的成果, 請參考下面 GIF 動畫圖檔)

才一開始, Kyle 就直接無限墜落. 這是因為在場景 Kyle Test 中, 沒有地板的緣故. 我們加個地板:
在 Hierarchy 視窗, 新增一個 GameObject/3D Object/Cube, 取名為 “floor”. 並設定其 Position 為 (0, -0.5, 0), Scale 為 (30, 1, 30).

再調整 Camera 位置以取得好的視野:
在 Hierarchy 視窗, 選取 Camera, 再按選單 GameObject/Align With View 讓相機與視角對齊.

再來, 把 My Robot Kyle 的 Pos.Y 提高為 0.1, 避免發生與地板之間的踫撞.

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

這次執行, My Robot Kyle 已不會無限墜落, 但是有其它問題: 1. My Robot Kyle 只會前進, 2. 相機沒有跟隨 My Robot Kyle 的位置.

我們先來解決 My Robot Kyle 只會前進不會轉彎的問題:
編輯 C# Script- PlayerAnimatorManager, 在 class PlayerAnimatorManager 的最前面, 宣告一個變數 private float directionDampTime;

[SerializeField]
private float directionDampTime = 0.25f;

並在 Update() 後面加一行指令, 設定 My Robot Kyle 的轉彎方向值.

animator.SetFloat("Direction", h, directionDampTime, 
Time.deltaTime);

需要特别說明的是, 設定轉彎方向值的指令, 帶入的參數除了 “Direction” 與 h 之外, 還多了兩個參數: directionDampTime 與 Time.deltaTime.
directionDampTime 是設定需要花多久的時間才能完成轉彎方向的數值.
Time.deltaTime 則能避免依玩家電腦的 frame rate 來計算數值變化量 (玩家電腦速度愈快, 遊戲執行的 frame rate 會愈高), 維持遊戲的公平性.

再執行測試看結果如何.
(設定 directionDampTime= 1, 測試執行的成果, 請參考下面 GIF 動畫圖檔)

(設定 directionDampTime= 5, 測試執行的成果, 請參考下面 GIF 動畫圖檔)

從上面兩次的執行結果可以看出來, directionDampTime 的數值愈大, My Robot Kyle 跑動的轉彎半徑也愈大.

再來, 我們讓 My Robot Kyle 在跑動時, 還能翻身跳躍.
編輯 C# Script- PlayerAnimatorManager, 在 Update() 裡面, 取得玩家輸入的 Horizontal 和 Vertical 數值之前, 加入下列程式碼:

AnimatorStateInfo stateInfo =
animator.GetCurrentAnimatorStateInfo(0);

// 只有在跑動時, 才可以跳躍.
if (stateInfo.IsName("Base Layer.Run"))
{
if (Input.GetButtonDown("Fire2"))
{
animator.SetTrigger("Jump");
}
}

這段程式碼的目的是讓 My Robot Kyle 僅在跑動狀態且非跳躍時, 才能做跳躍的動作.

存檔, 並再執行測試, 按滑鼠右鍵跳躍讓 My Robot Kyle 翻身跳躍.
(測試執行的成果, 請參考下面 GIF 動畫圖檔)

很好, 總括來看, My Robot Kyle 能向前與轉彎跑動, 並能翻身跳躍.

接下來, 我們來處理相機沒有跟隨 My Robot Kyle 位置的問題.
在 Hierarchy 視窗, 選擇在 My Robot Kyle, 在 Inspector 視窗點按 Add Component, 增加一個 component- CameraWork.

再將 component- Camera Work 的 Center Offset 設為 (0, 4, 0), 並勾選 Follow on Start.

最後, 在 Inspector 視窗, 點按右上角落的 Overrides, 再點選 Apply All, 將剛剛做的設定更新到 Prefab- My Robot Kyle 中.

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

這次執行的結果, 相機會跟隨 My Robot Kyle 的位置改變而移動, 清楚的照到舞台全貌.

為 My Robot Kyle 添加攻擊武器

是時候來為 My Robot Kyle 添加攻擊武器了, 我們來讓 My Robot Kyle 能從眼睛射出動感光波.

確認 Unity 工作場景是在 Kyle Test.
在 Hierarchy 視窗, 新增一個 GameObject/3D Object/Cube, 取名為 “Beam Left”, 並把其 Position 設為 (-0.06, 1.76, 0.9) 與 Scale 設為 (0.02, 0.02, 1.5)

在 Hierarchy 視窗, 展開 My Robot Kyle, 找到 My Robot Kyle/Root/Ribs/Neck/Head, 在其下新增一個 Empty GameObject, 取名為 “Beams”.
再把 Beam Left 拖拉到 Beams 之下. (注意. Beams Left 被拉到 Beams 之下後, 其 Position 和 Rotation 的數值會改變成相對於 My Robot Kyle 頭部的位置數值.)

將 Beam Left 複製一份, 並取名為 “Beam Right”. 再把 Beam Right 的 Position.Z 設為 -0.06, 其它不變.

接下來, 為了避免 Beam Left 與 Beam Right 兩者同時觸發碰撞事件, 增加程式處理的複雜度, 我們移除 Beam Right 的 component- Box Collider:
在 Hierarchy 視窗, 選取在 Beam Right, 將其在 Inspector 視窗裡面的 component- Box Collider 移除.

再將 Beam Left 的 component- Box Collider 的涵蓋範圍擴大能包含 Beam Right:
設定 Beam Left 的 component- Box Collider 的 Center 為 (3, 0, 0), Size 為 (7, 1, 1), 以及勾選 Is Trigger.

再來, 我們將 Beam Left 與 Beam Right 設為紅色, 看起來比較霸氣.
在 Project 視窗, Scenes 資料夾下, 新增一個 Material, 取名為 “Red Beam”, 顏色設為紅色.

接著, 在 Hierarchy 視窗, 分别選在 Beam Left 與 Beam Right, 再將 Material- Red Beam 拖拉到 Inspector 視窗, 即能將 Beam Left 與 Beam Right 設為紅色.

提醒一下, 只要有修改到 My Robot Kyle 的設定. 一定要點按, 在 Inspector 視窗右上角落的 Overrides | Apply All, 將設定更新到 Prefab- My Robot Kyle 中.

接下來, 我們來寫程式讓玩家能按鍵控制 My Robot Kyle 發射動感光波.

在 Project 視窗, Scripts 資料夾底下, 新增一個 C# Scripts, 取名為 “PlayerManager”, 並輸入下列程式碼, 存檔.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Com.ABCDE.MyApp
{
public class PlayerManager : MonoBehaviour
{
[Tooltip("指標- GameObject Beams")]
[SerializeField]
private GameObject beams;
bool IsFiring;

void Awake()
{
if (beams == null)
{
Debug.LogError(
"<Color=Red><a>指標- GameObject Beams 為空值</a></Color>",
this);
}
else
{
beams.SetActive(false);
}
}

void Update()
{
ProcessInputs();
if (beams != null && IsFiring != beams.activeSelf)
{
beams.SetActive(IsFiring);
}
}

void ProcessInputs()
{
// 按下發射鈕
if (Input.GetButtonDown("Fire1"))
{
if (!IsFiring)
{
IsFiring = true;
}
}
// 放開發射鈕
if (Input.GetButtonUp("Fire1"))
{
if (IsFiring)
{
IsFiring = false;
}
}
}
}
}

這整段程式碼主要目的, 是在偵測按鍵狀態來秀出或隱藏 GameObject- Beams.

回到 Unity, 確認工作場景為 Kyle Test.
在 Hierarchy 視窗, 選取 My Robot Kyle, 將 Project 視窗, Scripts 資料夾底下的 C# Script- PlayerManager 拉到 Inspector 視窗, 成為 My Robot Kyle 的一個 component.
再把 Hierarchy 視窗, My Robot Kyle 底下的 GameObject- Beams 拖拉給 Inspector 視窗裡面, component- PlayerManager 的 Beams.
這樣就把 GameObject-Beams 與 C# Script- PlayerManager 串起來了.

記得點按 Inspector 視窗右上角落的 Overrides |Apply All, 將設定更新到 Prefab- My Robot Kyle 中.

來測試執行看看, 點按滑鼠左鍵, My Robot Kyle 的眼睛會發射出動感光波.
(測試執行的成果, 請參考下面 GIF 動畫圖檔)

再來, 我們來處理被動感光波打到時的細節.
不同於挨到子彈類型的武器只會在事件發生時扣血一次, 被動感光波打到時, 除了在事件發生時扣血一次, 傷害量還會隨接觸到動感光波的時間而累積.

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

using Photon.Pun;(...)public class PlayerManager : MonoBehaviourPunCallbacks
{

並在 class PlayerManager 裡面, 宣告一個變數 Health, 來記錄玩家的血量, 初始值為 1f.

[Tooltip("玩家的血量")]
public float Health = 1f;

再實作 OnTriggerEnter() 和 OnTriggerStay() 兩個 Callback Functions.

void OnTriggerEnter(Collider other)
{
if (!photonView.IsMine)
{
return;
}
if (!other.name.Contains("Beam"))
{
return;
}
Health -= 0.1f;
}

void OnTriggerStay(Collider other)
{
if (!photonView.IsMine)
{
return;
}
if (!other.name.Contains("Beam"))
{
return;
}
Health -= 0.1f * Time.deltaTime;
}

在 OnTriggerEnter() 的程式碼中, 我們先判斷是否為玩家的角色觸發了此碰撞事件, 若是, 則再判斷是否為被動感光波打到, 若仍是, 就只能扣血了 (一次扣 0.1f).
OnTriggerStay() 的程式碼大致類似 OnTriggerEnter(), 差異只有在計害傷害量時, 有多乘上了 Time.deltaTime, 以確保遊戲的公平性.

最後, 當玩家的血扣光時, 代表遊戲結束, 得把玩家送出遊戲室, 回到遊戲場的入口. 說到把玩家送出遊戲室, 回到遊戲場的入口, 在前回的實作中, 我們在 class GameManager 已有寫一個 public void LeaveRoom(), 功能即是把玩家送出遊戲室, 回到遊戲場的入口. 我們想個辦法, 讓在 class PlayerManager 中, 也能直接呼叫這隻函式.

編輯 C# Script- GameManager, 在 class GameManager 裡面, 新增一個 static 變數 Instance, 並在 Start() 裡面, 將 Instance 設為 this.

public static GameManager Instance;(...)

void Start()
{
Instance = this;
}

再編輯 C# Script- PlayerManager, 在 Update() 的後面, 判斷當血量扣光 (即Health ≤ 0f 時), 就呼叫 LeaveRoom() 這隻函式.

void Update()
{
(...) if (Health <= 0f)
{
GameManager.Instance.LeaveRoom();
}
}

確認將 C# Script- GameManager 與 C# Script- PlayerManager 存檔.

扣血量的功能, 我們等到下回的實作, 也就是連網並同步全部在線玩家的動作狀態與血量時, 再來做測試驗證.

下次見囉.

--

--