# 114-1 Metaverse 課程講義 [TOC] :::success 請善用瀏覽器的關鍵字搜尋功能:```Ctrl+F``` ::: # 課程資源 * [課程往年購買的素材包](https://drive.google.com/drive/folders/1HzvexBlurS1gE15yS91XBQlEzpq2Qfa9) * [2025/9/11 Peak VR 教學素材](https://drive.google.com/drive/folders/1d3sV0a9cEHCB7Tc2msE-QUjHIbZO7VMF?usp=sharing) * [Peak是?](https://www.youtube.com/watch?v=D6io5XZWBHk) {%youtube D6io5XZWBHk %} # 2025/09/11 Unity basic ## 1. 創建URP專案 * **安裝Unity Editor** 1. 點選 Unity Hub 左側的 `Installs` 標籤 ![image](https://hackmd.io/_uploads/B1hrk22cxe.png) 2. 會看到已經安裝的 Editor,點選右上角 `Install Editor` 按鈕可以安裝新的 ![image](https://hackmd.io/_uploads/rkp4x2h9ee.png) 3. 會顯示一些官方建議的版本,但通常沒有我們需要的版本,點選 `Archive` 標籤內的 [download archive](https://unity.com/releases/editor/archive) 連結,即可看到所有過去釋出的版本 ![image](https://hackmd.io/_uploads/ryBQZhnqgg.png) ![image](https://hackmd.io/_uploads/Bk_0-n39eg.png) 4. 若要將專案打包到 Quest 頭盔 ( Android 系統 ) 上執行,需在下載 Editor 的時候新增 Android Build Support 的 module ![image](https://hackmd.io/_uploads/Hyzo4wFieg.png) >[!Warning]注意 >本教學使用的版本是 `2022.3.20f1`,使用不同版本的 Editor 可能產生預期外的問題,但通常只要中版本號之前是相同的 (也就是2022.3.x) 就不會有大問題 >![image](https://hackmd.io/_uploads/ryPSz339ge.png) * **創建專案** 打開 Unity Hub,建立一個空的 Universal 3D Unity 專案,版本請選擇 2022.3.x 以上的LTS 版,並按下 Create Project: ![image](https://hackmd.io/_uploads/rJ_Hoi2qle.png) >[!tip] 小提醒 >如欲使用版本控制,請勾選右下角的"Use Unity Version Control"。 ## 2. Unity 介面 ![image](https://hackmd.io/_uploads/rJPsn_pcxg.png) ### Scene View * 編輯遊戲場景的主要工作區,可以在 3D 或 2D 視角中拖曳、旋轉、縮放物件 ![image](https://hackmd.io/_uploads/r1nbaOaqge.png) * 可以透過右上方![image](https://hackmd.io/_uploads/rygbQtT5xl.png)圖標的下拉式選單內的3D Icons的拉桿調整 scene view 中的圖標大小,以免遮擋要操作的物件 ![image](https://hackmd.io/_uploads/Skh2ftp5gg.png) ### Game View * 顯示相機 (Camera) 拍攝到的畫面,即遊玩時顯示的實際效果。 ![image](https://hackmd.io/_uploads/Sk6zpdaclx.png) ### Hierarchy * 列出當前場景 (Scene) 中的所有物件(GameObject),以樹狀結構顯示「父物件 / 子物件」關係 ![image](https://hackmd.io/_uploads/S1_YrFT5xl.png) >[!Tip]小提醒 當點擊![image](https://hackmd.io/_uploads/rkPwUFTcll.png)圖示變成![image](https://hackmd.io/_uploads/BylqIFa5xe.png)之後,該物件會在 scene view 中不可見,但實際上並未被關閉,在 game view 依然會正常顯示以及執行物件上的元件 ( component ),可用於關閉暫時不需要編輯但是遮擋視線的物件。 ### Inspector * 顯示選中物件的屬性與元件 (Component),可以編輯 Transform、材質、腳本參數等。 ![image](https://hackmd.io/_uploads/H1WTDFa5gl.png) ### Project * 顯示專案資料夾內的所有資源(Assets)包括:Prefab、材質 (Material)、貼圖 (Texture)、音效 (Audio)、腳本 (C# Scripts)。 ![image](https://hackmd.io/_uploads/B17ddYa9le.png) ### Console * 顯示程式輸出 (Debug.Log)、錯誤訊息 (Error)、警告 (Warning) * 開關 ![image](https://hackmd.io/_uploads/S1HjKtpcgl.png) 這三個圖示可以選擇要顯示哪些類型的訊息 ![image](https://hackmd.io/_uploads/SysetYaceg.png) >[!Tip]小提醒 >若 ![image](https://hackmd.io/_uploads/ByY1qKp5xl.png) 按鈕被開啟,執行專案時一旦發生 Error 就會自動暫停專案,別誤認為是程式邏輯卡死了>< ### Layout * 有各種預設的版面配置,也可以儲存自己的配置 ![image](https://hackmd.io/_uploads/BJQ72Y6clg.png) ## 3. 遊戲物件 - GameObject ### 創建物件 1. 在 Hierarchy 視窗中按右鍵 2. 選擇 3D Object 或 2D Object 3. 選擇要建立的物件(Cube、Sphere、Plane、Sprite...) * 一些primitive object ![image alt](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSyntqYQQYjx9Xompvd8Pb6rDkdgCEXkp1c2Q&s) >[!Note]註解 >GameObject 是 Unity 世界中的「基本單位」,它本身只是「一個空殼」,需要透過 Component(元件) 來賦予功能 ### 啟用 / 禁用 ( Active / Inactive ) * Inspector 介面中左邊的核取框就是遊戲物件的 啟用/禁用 開關 ![image](https://hackmd.io/_uploads/ByNdyWC9le.png) * 物件本身需要被啟用且所有父物件皆啟用才算真正有效的啟用 ### 靜態遊戲物件 ( Static ) * 勾選 Static 表示這個物件在遊戲執行中不會移動/旋轉/縮放。 ![image](https://hackmd.io/_uploads/SyxuZZC5gl.png) * Unity 可以針對靜態物件做最佳化(如:光照貼圖、遮擋剔除、靜態批次)。 * 可以展開選單,選擇只啟用某些 Static 類型(例如 Batching Static、Occluder Static) ![image](https://hackmd.io/_uploads/B1htbbRqxl.png) ### 標籤 ( Tag ) * Tag 是給物件做分類與識別用的字串標籤,有一些內建的 Tag ,也可以自己新增 ![image](https://hackmd.io/_uploads/ryjtQWCceg.png) * 常用於程式判斷,例如: ```C# using UnityEngine; public class DetectEnemy : MonoBehaviour { private void OnTriggerEnter(Collider other) { if (other.CompareTag("Enemy")) { // 偵測到敵人 Debug.Log("敵人進入觸發區!"); } } } ``` ### 層 ( Layer ) * Layer 是整數 ID,常用於以下用途: * 相機的 Culling Mask:決定相機要不要渲染某些 Layer 的物件 ![image](https://hackmd.io/_uploads/r1QWHWA5gx.png) * 物理碰撞矩陣 ( Edit -> Project setting -> Physics -> Layer Collision Matrix ):決定哪些 Layer 會互相碰撞 ![image](https://hackmd.io/_uploads/B1LkIZA5el.png) ### 匯入模型 * 通常使用以下兩種格式的3D模型: 1. FBX (.fbx) → 最常用,支援動畫與材質 2. OBJ (.obj) → 靜態模型,附帶 .mtl 材質檔 * 將下載來的模型拖入 project 介面 ![image](https://hackmd.io/_uploads/Syyy19p9eg.png) 有些 FBX 檔案會夾帶動畫,展開後就會看到像是 ![image](https://hackmd.io/_uploads/rJENk5a5ge.png) 這樣的動畫素材在其中 * 若是 FBX 帶有材質 ( Texture ) 下載下來應該會看到 ![image](https://hackmd.io/_uploads/Sy8Akqp5ex.png) 兩個資料夾,建議把這兩個資料夾一同匯入到一個資料夾中方便整理 ![image](https://hackmd.io/_uploads/Bk4Fl56clg.png) FBX 檔案會在 source 資料夾中,並且自動綁上材質 ![image](https://hackmd.io/_uploads/B11Rx5Tcee.png) * 將 FBX 拖入 Scene view 或 Hierarchy 即可將模型匯入場景 ![image](https://hackmd.io/_uploads/ryAX-q6qgg.png) >[!Caution] 若無法正確顯示材質,請將材質 ( Material ) 轉換成 URP 格式 1. 當匯入的物件呈現粉紅色 ( 無法正確顯示材質 ) , 2. 請前往 Window -> Rendering -> Render Pipeline Converter 3. 左上選取 Built-in to URP 4. 全部勾選,並點擊 Initialize And Convert ![image](https://hackmd.io/_uploads/BJFcPpbTp.png) ## 4. 基本操作 ### 視角移動 #### 滑鼠操作 * 右鍵 + 滑鼠移動 → 環顧四周 (look around) * 滾輪 → 拉近/拉遠(縮放) * 中鍵拖曳 → 平移 (pan) * 在 Hierarchy 中雙擊物件 → 聚焦到選取的物件 #### 鍵盤操作 * W / A / S / D → 向前 / 左 / 後 / 右 移動鏡頭(需按住右鍵) * Q / E → 向下 / 向上 移動鏡頭(需按住右鍵) * F → 聚焦到選取的物件 ### 物件移動、旋轉、縮放 #### View Tool - 視圖工具 ( 快捷鍵 Q ) * 移動畫布,不影響物件 #### Move Tool - 移動工具 ( 快捷鍵 W ) * 顯示三條軸(紅 X、綠 Y、藍 Z) * 拖曳箭頭即可沿著該軸移動物件 * 拖曳中心方塊,可在平面上移動 #### Rotate Tool - 旋轉工具 ( 快捷鍵 E ) * 出現三個圓環(紅 X、綠 Y、藍 Z) * 拖曳圓環即可繞著對應軸旋轉 * 灰色外圈可以依視線為轉軸旋轉 * 按住物體可以自由旋轉 #### Scale Tool - 縮放工具 ( 快捷鍵 R ) * 三條軸上有方塊 * 拖曳單一方塊 → 沿著軸向縮放 * 拖曳中心方塊 → 等比例縮放 ### 座標系統 ( Global / Local ) * Scene view 的工具列可以選取操作物件時所使用的座標系 ![image](https://hackmd.io/_uploads/BJjm05p5xe.png) * Global ![image](https://hackmd.io/_uploads/BkQnCc65eg.png) * Local ![image](https://hackmd.io/_uploads/rk2TA56qel.png) ### 播放 ( Play )、暫停 ( Pause )、下一幀 ( Step ) Unity Editor 的中心上方有三個這樣的按鈕 ![image](https://hackmd.io/_uploads/BJNDJsTcle.png) 功能分別如下: * Play 模式(播放遊戲) 在 Play 模式下,專案會開始運行,動畫、物理、程式碼都會開始運作,讓你看到遊戲中物件隨時間變化的實際狀態 >[!Warning]注意 >在 Play mode 下的所有修改,包含位置、數值等等,在專案結束後都會消失,如果要對物件做永久的修改,請先退出 Play mode 再開始修改 * Pause 模式(暫停遊戲) 可以將專案暫停,所有的動畫、物理、程式碼都會停下來,可以將狀態定格觀察inspector中的數值 * Step 模式(下一幀) 在暫停的狀態下,可以透過這個按鍵一幀一幀的模擬,也是方便用於Debug ## 5. 元件 - Component :::success Component(元件) 是賦予 GameObject 功能的「零件」 ::: ### 有效化 / 無效化 ( Enable / Disable ) * Inspector 介面中 部分 Component 圖標旁邊會有核取欄,可以 開啟 / 關閉 該元件的作用 ![image](https://hackmd.io/_uploads/Syd8cW0qll.png) ### 基本屬性 * Transform ![image](https://hackmd.io/_uploads/H1ouQopqxx.png) * RectTransform(UI 專用) ![image](https://hackmd.io/_uploads/By_6QoTcll.png) ### 物理 * 剛體 ( Rigidbody ) 控制重力、慣性、速度等等的計算 ![image](https://hackmd.io/_uploads/S1ruEsT5eg.png) * 碰撞器 ( Collider ) 負責物件之間的碰撞以及作為觸發器 ( Trigger ) 使用 * 有各式各樣的Collider,由於 XR 屬於 3D 專案,所以不要使用 2D 的 Collider ![image](https://hackmd.io/_uploads/HJylriTqex.png) * 不同的 Collider 有不同的參數可以設定 ![image](https://hackmd.io/_uploads/SJEiSoT5le.png) ![image](https://hackmd.io/_uploads/SJVeIspcge.png) * 如果 is Trigger 被勾選,此碰撞器將不再計算碰撞,而是轉變成可以檢測物件是否 進入 / 停留 / 離開 這個範圍 ![image](https://hackmd.io/_uploads/B1qJKia9ex.png) >[!Warning]注意 >要觸發 Trigger 必須雙方物件都有 collider ( 無論是否切換為 Trigger ) ,被觸發的物件必須勾選 is Trigger ,且必須有任一方含有 Rigidbody ### 渲染 * 渲染器 ( MeshRenderer / SkinnedMeshRenderer ) * 顯示 3D 模型 (Mesh) 的外觀,需要搭配 MeshFilter(決定模型形狀)+ Material(決定材質與貼圖),常用於靜態物件(牆壁、道具) ![image](https://hackmd.io/_uploads/BkM2Aoaqgg.png) * 可以開關是否要投影出影子 ![image](https://hackmd.io/_uploads/SJJ9gna9gx.png) * on ![image](https://hackmd.io/_uploads/ryonk26cxx.png) * off ![image](https://hackmd.io/_uploads/BkHZlh65xl.png) * shadow only ![image](https://hackmd.io/_uploads/HJYwe3pcll.png) * two sides 有時候模型的面與面之間會漏光,可以使用這個來讓 Mesh 的內外側都投影出陰影來避免漏光 ![image](https://hackmd.io/_uploads/ryVEW2a9gl.png) ![image](https://hackmd.io/_uploads/rkIUb2pclx.png) * 光照 ( Directional、Point、Spot、Area ) * Directional ![image](https://hackmd.io/_uploads/HJiAG3Tqee.png) * Point ![image](https://hackmd.io/_uploads/ryNWm3a5ex.png) * Spot ![image](https://hackmd.io/_uploads/Hy9Imn69gl.png) * Area 矩形的光源,較少使用,需要 Bake Lightmap * 相機 ( Camera ) 拍攝遊戲場景並輸出到 Game 視窗 ![image](https://hackmd.io/_uploads/SyIww3a5ge.png) 有蠻多參數可調整,但主要會用到的有以下幾樣: * 投影 ( Projection ): * Perspective(透視,相機遠小近大) ![image](https://hackmd.io/_uploads/B11Ltn6qle.png) * Orthographic(正交,常用於 2D) ![image](https://hackmd.io/_uploads/rJcLF2T5eg.png) * 視角廣度 ( Field of View ) (FOV) ![image](https://hackmd.io/_uploads/r1uOuhaqgx.png) * 可見距離範圍 ( Clipping Planes ) * 近裁切 * 遠裁切 ![image](https://hackmd.io/_uploads/SkZPd36qxe.png) * Post processing ( 後處理 ) 要使後處理生效於 Game view 需要勾選 ![image](https://hackmd.io/_uploads/HJq0Yhpcge.png) >[!Tip]小提醒 > 若使用 XR 的套件,使用 Camera Rig 的預製件 ( Prefab ) 的話,通常會包含一個 Camera 在 Hierarchy 中負責追蹤 Center Eye 的 Game Object 上,場景預設的 Camera 記得刪除,避免 Game view 的畫面被搶走 ### 音效 * AudioSource * AudioClip → 指定要播放的聲音檔 * Play On Awake → 遊戲一開始自動播放 * Loop → 自動重播 * Volume → 音量(0.0 ~ 1.0) * Pitch → 聲音速度與音高(1.0 = 原本,0.5 = 低沉,2.0 = 加速) * Spatial Blend * 2D(0):音量固定,不受位置影響 * 3D(1):音量隨距離衰減(模擬空間感) * 3D Sound Settings * Min Distance:應量開始受距離影響的最小距離 * Max Distance:聲音隨距離變化的最大距離 ( 超過就聽不到 ) * Spread:聲音在左右聲道平衡的參數,範圍是 0-360,通常設定 0-180 * 0:聲音像是從單一個點播放 * 180:左右聲道音量完全平衡 * 360:和 0 的效果一樣但左右相反 ![image](https://hackmd.io/_uploads/B1tRB10cxg.png) * AudioListener * 通常掛在主攝影機 * 接收 AudioSource 聲音並輸出到玩家的喇叭或耳機 * 一個場景只能有一個 AudioListener ### UI * Canvas * UI的根節點,可以設定以下三種渲染方式: 1. Screen Space - Overlay → 直接覆蓋在輸出的畫面上 2. Screen Space - Camera → 跟隨指定相機位置 3. World Space - 實際出v限於 3D 場景中 ( XR使用最多的一種 ) * RectTransform * 取代一般Transform,用於控制UI位置、大小、對齊 * 常用UI元件 * Text / TextMeshPro → 顯示文字(建議用 TextMeshPro,效果更清晰) * Image → 顯示圖片(常用於背景、圖示) * Button → 可點擊,支援 OnClick() 事件 * Toggle → 勾選框 * Slider → 數值調整(音量控制、耐力條) * Scrollbar → 卷軸 * Dropdown → 下拉選單 * InputField (TMP_InputField) → 文字輸入 ### 動畫 * Animator ![image](https://hackmd.io/_uploads/ry6aygR5xl.png) * Animator Controller * 狀態機 ![image alt](https://europe1.discourse-cdn.com/unity/original/3X/f/6/f64b0fd419110ab58131eb1c88f0c406241496fc.png =70%x) * Blend Tree ![image alt](https://www.gamedesigndecal.com/pages/labs/lab8/images/image7.png =50%x) >[!Note]註解 >Unity 的動畫系統很多內容,不容易一次學好,建議要用到什麼功能再去查詢使用的方法, Youtube 上有很多學習資源,如:[iHeartGameDev 的 Unity's Animation System](https://www.youtube.com/playlist?list=PLwyUzJb_FNeTQwyGujWRLqnfKpV-cj-eO) > ## 6. C# Script ( 也是元件 ) :::success Unity 採用 C# 作為主要語言, C# 腳本可以以元件的形式掛載到物件上和其他元件互動 雖然圖形介面就可以建立場景、調整物件,也有許多內建的元件可以使用,但要實現特定的遊戲邏輯就必須用程式控 ::: ### 創建 C# Script 1. Project 介面點右鍵 -> Create -> C# script ![image](https://hackmd.io/_uploads/ryVPOxA9el.png) 2. 幫新增的腳本命名 ![image](https://hackmd.io/_uploads/BykadeC5ex.png) 3. 新的 C# Script 會自動把 class 名稱設為 C# 腳本剛建立時的名稱 ![image](https://hackmd.io/_uploads/r1HKYeA9le.png) >[!Warning]注意 > 1. 腳本名稱必須與 class 名稱相同,否則會報錯 > 2. 專案內不可以有兩個同名的 class > 3. 如果在建立 C# 腳本後去改動他的名稱,程式碼內的 class 名稱不會自動同步,需手動修改 > 4. 要掛載在 Game Object 上當作元件的腳本必須由 MonoBehaviour 作為基類 ( Base Class ) >[!Important]MonoBehaviour >MonoBehaviour 是 Unity 所有可掛在 GameObject 上腳本的基底類別。它提供了生命週期方法(例如 Start(), Update())和與 Unity 引擎互動的功能(像是 Instantiate、Coroutine、OnCollisionEnter 等) ### 常用的生命週期函式 * **Awake()** * 觸發時間:腳本載入時(比 Start 更早)。 * 用途:初始化變數、載入資源、設定預設值。 * 特點:即使物件未啟用 (SetActive(false)),Awake 仍會執行。 * **OnEnable()** * 觸發時間:當物件或腳本被啟用時。 * 用途:重新啟用計時器、事件監聽。 * 特點:每次啟用物件都會呼叫(不像 Awake 只執行一次)。 * **Start()** * 觸發時間:第一幀 Update 之前。 * 用途:需要等其他物件初始化完成後再做的設定。 * 特點:若物件一開始停用,啟用時才會執行。 * **Update()** * 觸發時間:每一幀都執行(Frame-based)。 * 用途:處理玩家輸入、檢查狀態、一般遊戲邏輯。 * 特點:執行頻率 = FPS,非物理相關邏輯適用。 * **FixedUpdate()** * 觸發時間:固定時間間隔(預設 0.02 秒一次)。 * 用途:物理運算(Rigidbody 移動、力、碰撞)。 * 特點:與 FPS 無關,可能一幀內執行多次。 * **LateUpdate()** * 觸發時間:每一幀結束時(所有 Update() 之後)。 * 用途:相機跟隨角色、收尾運算。 * 特點:避免角色移動與相機抖動。 * **OnDisable()** * 觸發時間:物件或腳本被停用時。 * 用途:移除事件監聽、暫停任務。 * **OnDestroy()** * 觸發時間:物件或腳本被刪除時。 * 用途:釋放資源、取消訂閱事件。 * 特點:遊戲結束(停止 Play 模式)時也會呼叫。 ### 變數 #### 常用型別 數值型別:int, float, double 布林值:bool 字串:string 向量:Vector2, Vector3 顏色:Color 遊戲物件 / 元件:GameObject, Transform, Rigidbody 等 集合:Array[], List<T>, Dictionary<TKey, TValue> * 範例: ```C# int hp = 100; // 整數 float speed = 5.5f; // 浮點數 string playerName = "Hero"; // 字串 bool isAlive = true; // 布林值 ``` #### 存取修飾詞 * public:公開,可在 Inspector 調整,也能被其他腳本存取 * private:私有,只有在這個類別內可使用 * [SerializeField] private:私有但可在 Inspector 編輯(常用寫法) ```C# public int hp = 100; // Inspector 可見 private float speed = 5f; // Inspector 不可見 [SerializeField] private string playerName = "Hero"; // Inspector 可見,但仍保持 private ``` #### 靜態與實例變數 * 實例變數(一般變數):每個物件有自己的數值。 * 靜態變數(static):整個類別共享一份,常用於紀錄全域狀態(例如分數)。 ```C# public class GameManager : MonoBehaviour { public static int score = 0; // 全遊戲共用 } ``` ### 訪問其他腳本 #### 透過Inspector直接訪問 * 範例: ```C# // Player.cs using UnityEngine; public class Player : MonoBehaviour { public int hp = 100; } // UIManager.cs using UnityEngine; public class UIManager : MonoBehaviour { public Player player; // Inspector 拖入 Player 物件 void Update() { Debug.Log("玩家血量:" + player.hp); } } ``` 如此一來,我們就可以將掛載 Player 的物件拖曳到某個掛載在某物件 ( 可以是同一個也可以是不同個物件 ) 上的 UIManager 元件裡面 ![image](https://hackmd.io/_uploads/ryxbfM05eg.png) #### 訪問同一物件上的其他腳本 ( `GetComponent<T>()` ) * 如果 Player.cs 與 UIManager.cs 掛在同一個物件上,可以使用 `GetComponent<Player>()` ```C# // 同一個 GameObject 上的另一個腳本 public class PlayerAttack : MonoBehaviour { private Player player; void Start() { player = GetComponent<Player>(); // 取得 Player 腳本 Debug.Log("初始血量:" + player.hp); } } ``` #### 快速找到場景中的某個腳本 ( `FindObjectOfType<T>()` ) * 如果場景物件多,效能較差 ```C# public class UIManager : MonoBehaviour { private Player player; void Start() { player = FindObjectOfType<Player>(); // 自動尋找場景中的 Player } void Update() { Debug.Log("血量:" + player.hp); } } ``` >[!Note]註解 >還有其他種方法可以訪問其他腳本,例如:`GameObject.Find()` + `GetComponent<T>()` 或是使用靜態變數 (static) 之類的方法 # 2025/09/11 Unity (VR) Peak ## 1. 創建一個URP專案 打開 Unity Hub,建立一個空的 Universal 3D Unity 專案,版本請選擇 2022.3.x 以上的LTS 版,並按下 Create Project: ![image](https://hackmd.io/_uploads/rJ_Hoi2qle.png) >[!tip] 小提醒 >如欲使用版本控制,請勾選右下角的"Use Unity Version Control"。 ## 2. 安裝套件 ![image](https://hackmd.io/_uploads/S18x65Rcex.png =40%x) 1. Window -> Asset Store -> 搜尋 [Meta XR All-in-One SDK](https://assetstore.unity.com/packages/tools/integration/meta-xr-all-in-one-sdk-269657) ![image](https://hackmd.io/_uploads/SJJR6q0qel.png) ![image](https://hackmd.io/_uploads/rybU0909ll.png) 2. Window -> Package Manager ![image](https://hackmd.io/_uploads/rywiA9C5le.png) 3. 把 Packages 切換為 My Assets ![image](https://hackmd.io/_uploads/HynbkiRcge.png) 4. 找到 Meta XR All-in-One SDK 並安裝 ![image](https://hackmd.io/_uploads/rywaJsC9gx.png) 5. 裝完會要求重開 Editor ![image](https://hackmd.io/_uploads/S1G0boC9lx.png) 6. 重啟完畢後再次開啟 Package Manager 將 Packages 切換為 In Project 會看到許多 Meta 開頭的 Package 被加入了專案,如果想了解各個 Package 分別有哪些功能,可以點他的 Samples 標籤並下載 Example Scenes 來看官方的示範場景 ![image](https://hackmd.io/_uploads/rkXYXiA9xx.png) ### 安裝 XR Plugin Management 1. Edit -> Project Settings -> XR Plugin Management ![image](https://hackmd.io/_uploads/SkiLEoCqxg.png) 2. 將 Windows / Mac / Linux 與 Android 的 Plug-in Provider 都勾選為 ![image](https://hackmd.io/_uploads/BkJLriCcxe.png) ![image](https://hackmd.io/_uploads/B1CvSjRcle.png) ### Project Setup * 以往再開始開發之前還要將專案的一些選項調整到適合 XR 開發,現在 Meta 提供了一鍵調整專案設定的工具,可以快速完成專案的初期設定 1. Meta XR Tools -> Project Sutup Tool ![image](https://hackmd.io/_uploads/Sy7h8oAqgg.png) 2. 將三個平台的設定檔都按 Fix All / Apply All 自動修正專案設定 ![image](https://hackmd.io/_uploads/SyWODjRcex.png) ### Quest Link :::success Quest Link 讓 Meta Quest 系列(Quest 2 / Quest 3 / Quest Pro) 可以透過 USB-C 傳輸線 或 Wi-Fi(Air Link) 連接到電腦執行 PC VR 遊戲或直接在 Unity、Unreal 這些開發環境中 Play Mode 測試 ::: * 電腦端下載 [Quest Link App](https://www.meta.com/en-gb/help/quest/1517439565442928/) 並在 Quest 內開啟 Link ,進入 Quest Link 的白色大廳,連上之後才能進行 Play Mode 測試 ![image](https://hackmd.io/_uploads/S1Oxg4JRa.png) * 有些功能需要在 Quest Link -> 設定 -> 測試版 中開啟 **開發人員執行期間功能** 才能在 Play Mode 測試的時候啟用 ![image](https://hackmd.io/_uploads/B1YLifloxx.png) ## 3. 匯入教學的 Unity Package / TagManager * 點兩下或將 `peak.unitypackage` ( 在今日課程的素材裡面 ) 拖曳到 Project 介面中 ![image](https://hackmd.io/_uploads/rJ6Aezxill.png) * 在 Project 介面按右鍵 -> Show in Explorer ![image](https://hackmd.io/_uploads/SytDbzxilx.png) * 把雲端素材內的 TagManager.asset 複製到 ProjectSettings 資料夾中,取代原有檔案,以避免遺失 Tag、Layer 等資料 ![image](https://hackmd.io/_uploads/S1QfGMgslx.png) ## 4. 認識 Building Blocks :::success Building blocks 是 Meta All-in-One SDK 推出的一套 **預製組件** ,幫助開發者快速建立 XR 相關功能,使用方法多是拖曳需要方塊到場景中,省去重複建構常見的 XR 功能的過程 ::: ![image](https://hackmd.io/_uploads/BySecl1jgx.png) ## 5. 新增場景,加入 VR 核心功能 ( Core ) ### 建立一個新的空場景 ( Scene ) * 記得先把場景中的 **Main Camera** 刪除 ### Camera Rig * 任何 VR 專案的基礎 * 相機會自動跟隨玩家的頭部移動 ![image](https://hackmd.io/_uploads/HylGVbysle.png) 拖入 Hierarchy 會長這樣,這些 TrackingSpace 下 Anchor 會自動追蹤相應的位置,如果有物件需要綁定在這些位置下,可以選擇用腳本追蹤這些 Anchor 位置或是直接放到 Anchor 的下層: ![image](https://hackmd.io/_uploads/SJd3E-kolg.png) ### Controller Tracking * 追蹤 **控制器 (Controllers)** 的位置與按鈕輸入 ![image](https://hackmd.io/_uploads/ByXm4-yill.png) 拖入後會自動把左右手的 Controller Tracking 元件放到對應的 Anchor 下 ![image](https://hackmd.io/_uploads/SJ-sBW1jex.png) ### Hand Tracking * 追蹤玩家的 **雙手** ,不用控制器 ![image](https://hackmd.io/_uploads/S1yNVWkigx.png) 加入後也會自動把兩手的 Hand Tracking 元件放到對應的 Anchor 下 ( 比 Controller Tracking 再上一層) ![image](https://hackmd.io/_uploads/rJF88Wkjeg.png)* ### Passthrough ( 補充,這個教學不會用到 ) * 讓玩家透過 Quest 的攝影機看到現實世界,取代原本的 skybox 再與虛擬物件結合,是 **MR ( 混合實境 )** 的核心 ![image](https://hackmd.io/_uploads/HJOHDZksge.png) ## 6. 設計場景、加上碰撞器 * 可以從 Prefabs 資料夾把預製的 Environment 拉到場景中,可以節省一些時間 ![image](https://hackmd.io/_uploads/HkS-XGxill.png) * 如果想要拓展場景可以匯入課程素材內的 `PolygonNature.unitypackage` * 匯入後點兩下 PolygonNature 資料夾的 ![image](https://hackmd.io/_uploads/BJ2nmzlolg.png) 並按 `import` * 完成之後可以在 PolygonNature/Prefabs 資料夾內看到不同類型預製件的資料夾 ![image](https://hackmd.io/_uploads/ByYFVMgoex.png) ![image](https://hackmd.io/_uploads/SyL5Ezeilg.png) * 需將所有可攀爬的物件加上 Collider 並將 Layer 設為 climbable ![image](https://hackmd.io/_uploads/B1IWSzgoge.png) ## 角色移動 ( Locomotion ) 介紹 :::success Locomotion:翻譯為 **運動能力** ,在 VR 領域指玩家在虛擬世界中的移動方式 VR 中移動方式必須兼顧: * 沉浸感(是否真實自然) * 舒適度(是否容易 **動暈** ) * 操作便利(是否容易上手) ::: Locomotion 又可以分三個方向討論: * 移動 * 旋轉 * 跳躍 ### 移動 #### 傳送 ( Teleportation ) * 玩家指向要去的位置,觸發按鈕,瞬間移過去 * 優點:舒適度高,不容易暈 * 缺點:沉浸感低 #### 搖桿移動 ( Joystick Movement ) * 使用搖桿控制玩家像一般 3D 遊戲那樣平滑移動 * 優點:沉浸感高、操作直覺 * 缺點:容易產生 **動暈症** #### 抓攀移動 ( Climbing ) * 玩家用手抓住可互動的物體(牆壁、繩索 ), * 優點:自然、有臨場感 * 缺點:通常只用於特殊場景 >[!Note]註解 >其他移動方式還有:手臂擺動 ( ArmSwinger ) 、頭部晃動 ( Head Bobbing ) 、模擬交通工具等等,大家可以發揮創意去思考有哪些不論是現實或想像的移動方式可以實作在 XR 的世界中 ### 旋轉 #### 快速轉向 ( Snap Turn ) * 玩家按搖桿左右,畫面瞬間旋轉 15° / 30° / 45° * 優點:減少暈眩 * 缺點:旋轉感覺不連續 #### 平滑轉向 ( Continuous Turn ) * 玩家使用搖桿控制旋轉(像第一人稱遊戲的滑鼠視角)。 * 優點:沉浸感高 * 缺點:容易造成 **動暈症** ### 跳躍 #### 按鍵觸發跳躍 ( Button-triggered Jump ) * 玩家按下控制器按鈕(如 A / X / Grip),角色就會跳躍 * 優點:操作簡單 * 缺點:容易暈 #### 動作觸發跳躍 ( Motion-triggered Jump ) * 偵測玩家 Y軸加速度 或 手臂揮動,觸發跳躍 * 優點:沉浸感高,比按按鈕自然 * 缺點:沒設計好容易誤觸、容易累 #### 輔助式跳躍 ( Assisted Jump ) * 玩家透過 抓取 + 拉動(Climbing Locomotion) 進行跳躍 * 優點:動作合理,減少暈眩、沉浸感高 * 缺點:使用場景有限 ## 角色移動 ( Locomotion ) 實作 在這個專案裡面,我們選擇 **搖桿移動 ( Joystick Movement )** + **抓攀移動 ( Climbing )** 來滿足我們爬山的需求,並使用 **快速轉向 ( Snap Turn )** 來減少轉向時的暈眩,為了方便在岩石間移動與攀爬,也會使用 **按鍵觸發跳躍 ( Button-triggered Jump )** 以及 **輔助式跳躍 ( Assisted Jump )** ### 添加 Rigidbody 、 Collider 為 Camera Rig 物件加上 Rigidbody 以及 Capsule Collider 並如圖設定參數 ![image](https://hackmd.io/_uploads/SyPqAEkoll.png) #### Rigidbody * Freeze 3個方向的 Rotation 避免Capsule形狀傾倒 * 勾選 Use Gravity 這個物件才會開始受重力模擬的作用 #### Capsule Collider * 膠囊形狀的碰撞器,常用於玩家身體 * 可調整 Radius 與 Height 來改變膠囊的形狀 ### PlayerLocomotion.cs #### 搖桿移動 ( Joystick Movement ) 與一般 3D 不同在於我們轉頭的時候,實際上不是去旋轉最外層的玩家遊戲物件,而是內部掛有 Camera 的 CenterEyeAnchor 在旋轉,遊戲物件的前方不一定是頭盔朝向的前方,所以我們在使用搖桿移動的時候也要考慮頭盔的方向 ```C# public Transform head; // 指向 Quest 頭盔 transform public float moveSpeed = 3f; private Rigidbody rb; // 玩家的遊戲物件掛載的 Rigidbody void Update() { // 以頭盔面向計算移動方向(不影響角色本身 rotation) Transform moveRef = head ? head : transform; float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); Vector3 move = (moveRef.right * h + moveRef.forward * v).normalized; Vector3 velocity = new Vector3(move.x * moveSpeed, rb.velocity.y, move.z * moveSpeed); rb.velocity = velocity; } ``` #### 快速轉向 ( Snap Turn ) 右手控制桿朝 左 / 右 一次,只能做一次轉向,直到控制桿回到中間才能再做下一次的轉向 >[!Warning]注意 >當 Rigidbody 有啟用 interpolation 會造成轉向有機會被復原,所以這裡在計算轉向之前,先把 Interpolation 設為 None ,等到轉動之後才恢復原本的 Interpolation 模式 ```C# public float snapTurnAngle = 45f; // Snap 轉向角度 public float snapTurnThreshold = 0.7f; // 右搖桿 X 軸超過此值才觸發 private bool snapTurnReady = true; // 紀錄控制桿是否回到中間 private Rigidbody rb; // 玩家的遊戲物件掛載的 Rigidbody void Update() { // 右手搖桿 snap turn(執行時暫時關閉 interpolation) if (snapTurnReady && rb != null) { if (OVRInput.GetDown(OVRInput.RawButton.RThumbstickLeft)) { var prevInterp = rb.interpolation; rb.interpolation = RigidbodyInterpolation.None; rb.MoveRotation(Quaternion.AngleAxis(-snapTurnAngle, Vector3.up) * rb.rotation); rb.interpolation = prevInterp; snapTurnReady = false; } else if (OVRInput.GetDown(OVRInput.RawButton.RThumbstickRight)) { var prevInterp = rb.interpolation; rb.interpolation = RigidbodyInterpolation.None; rb.MoveRotation(Quaternion.AngleAxis(snapTurnAngle, Vector3.up) * rb.rotation); rb.interpolation = prevInterp; snapTurnReady = false; } } // 只有在左右按鍵都沒按下才允許再次 snap if (!OVRInput.Get(OVRInput.RawButton.RThumbstickLeft) && !OVRInput.Get(OVRInput.RawButton.RThumbstickRight)) { snapTurnReady = true; } } ``` #### 按鍵觸發跳躍 ( Button-triggered Jump ) 為了避免在空中連跳,需要使用一個射線來偵測腳下是否有地面,另外為了避免玩家貼著斜率很大的牆面一路連跳上去,也要判斷腳下的這個地面傾斜的程度 ```C# public float jumpForce = 5f; public LayerMask groundMask; public float groundCheckDistance = 1.2f; public Transform groundCheck; void Update() { // 跳躍(禁止空中跳躍,並確認腳下平面夠平緩) bool jumpInput = Input.GetKeyDown(KeyCode.Space) || Input.GetButtonDown("Jump") || OVRInput.GetDown(OVRInput.Button.SecondaryThumbstick); if (jumpInput) { float cheatGroundCheckDistance = groundCheckDistance; if (head) // 如果頭的位置存在,紀錄頭盔的Local Y 座標作為跳躍檢測的高度 { cheatGroundCheckDistance = transform.InverseTransformPoint(head.position).y; } RaycastHit hit; float jumpCheckDistance = cheatGroundCheckDistance + 0.1f; if (Physics.Raycast(groundCheck.position, Vector3.down, out hit, jumpCheckDistance, groundMask)) { // 雖然叫 slope 但其實是地面與正 Y 方向的餘弦值,越接近1代表越平坦 float slope = Vector3.Dot(hit.normal, Vector3.up); if (slope > 0.7f) { rb.AddForce(Vector3.up * jumpForce, ForceMode.VelocityChange); } } } } ``` #### 把 PlayerLocomotion.cs 加到 Camera Rig 上 * 把 CenterEyeAnchor 拖到 Head Reference 以及 Ground Check 的位置 * Ground Mask 是 Layer Mask ,可以指定會被判斷為地面的 Layer 有哪些 ![image](https://hackmd.io/_uploads/B1FDWryjeg.png) ### OVRClimbingRig.cs 用於實現 抓攀移動 ( Climbing ) 以及 輔助式跳躍 ( Assisted Jump ) ,由於邏輯比較複雜,只會說明腳本大概做了什麼以及一些參數的意義 ```plaintext 初始化: leftHand, rightHand, rigRb, staminaManager 等參考 left, right 兩隻手的狀態(是否抓取、抓取點、速度紀錄等) 每幀 Update: 根據 grip 或手部追蹤,決定是否要抓取 分別處理左手和右手的抓取邏輯 HandleHand(每隻手): 如果 grip 超過門檻且有體力: 嘗試在手附近找到可攀爬物體 如果找到: 如果另一隻手正在抓取,強制放開另一隻手 設定當前手為抓取狀態,記錄抓取點、初始手位置 觸覺回饋 如果 grip 不足或沒體力且正在抓取: 計算手的拋出速度 限制最大速度 把拋出速度加到玩家剛體 取消抓取狀態 每 FixedUpdate(物理更新): 檢查是否有手在抓取 更新 isClimbing 狀態,通知 staminaManager 如果有手在抓取: 計算手的移動偏移 用偏移算出速度,限速 剛開始抓取時平滑過渡速度 設定剛體速度 記錄最後速度 記錄手的速度(用於拋出) ComputeClimbOffset: 計算手目前位置和初始抓取位置的偏移 如果手離抓取點太遠,限制最大偏移 TryFindClimbable: 在手附近做球體檢測,找最近的可攀爬物體 用 Raycast 拿到精確的碰撞點和法線 ``` #### 把 OVRClimbingRig.cs 加到 Camera Rig 上 * 把左右手的 Anchor 分別放到對應的位置 * Camera Rig 本身拖到 Rig Rb 內 * Climbable Mask 也是 Layer Mask,用來區分可以被手抓住攀爬的物件 ![image](https://hackmd.io/_uploads/Sk2omSkoll.png) * 在雙手的 Anchor 上加上一個 Sphere Collider 手感會更好 ![image](https://hackmd.io/_uploads/H1wVq1loll.png) >[!Tip]小提醒 >若手加上了 collider 可能發生手推到玩家自身軀幹碰撞器的問題,可以透過設定 Layer 配合 Collision Matrix 來限定手可以碰撞的物件 ### Hand Pose 為了更進一步增加沉浸感,我們要讓玩家可以在手部追蹤 ( Hand Tracking ) 的情境下也可以攀爬,所以我們要定義爬牆的 Hand Pose ,當玩家的手在牆上做出指定的姿勢,也能觸發爬牆的功能 * 建立 Shape ,在 Project 介面按右鍵 Create -> Meta -> Interaction SDK Pose Detection -> Shape ,並取名為 Climb ![image](https://hackmd.io/_uploads/By8DCgljlx.png) * 依下圖設定 Climb 的姿勢,( 食指、中指、無名指捲曲 ) ![image](https://hackmd.io/_uploads/rJbVy-xjgl.png) * 在場景中新增 Poses 空物件作為 pose 的資料夾,並在下層在建立兩個空物件 Left Climb Pose 與 Right Climb Pose 來裝負責手勢辨識的腳本 ![image](https://hackmd.io/_uploads/SJ2gl-ljeg.png) >[!Warning]注意 >接下來的步驟在左右手都要做,這裡只展示一隻手的,另一隻手請把左右相反照做一次 * 在兩個 Climb Pose 物件內加上 Shape Recognizer Active State 、 Active State Selector 、 Selector Unity Event Wrapper 三個元件並照下圖將他們連接,並把 Climb shape 加入 Shape Recognizer Active State 的 shapes 中 ![image](https://hackmd.io/_uploads/r1KnZbxjgl.png) * 把 LeftInteractions 與下層的 Features 拉到 Shape Recognizer Active State 的 Hand 與 Finger Feature State Provider 欄位 ( Finger Feature State Provider 在加入元件的時候會自己填入但可能是錯的手,所以必須再拉一次 ) ![image](https://hackmd.io/_uploads/SkiMNbxjeg.png) * 最後到 Selector Unity Event Wrapper 把 `When Selected()` 與 `When Unselected()` Event 各新增一個監聽器並拉入 Camera Rig 到物件欄位,分別加上 OVRClimbingRig 中對應手的 SetHandTrackedGrab ,並分別勾選與不勾選,表示傳入 SetHandTrackedGrab 的 boolean 值分別是 True 和 False ![image](https://hackmd.io/_uploads/S1XZr-eiex.png) ### CameraSpringUp.cs 如果現在執行專案去爬牆會發現,爬到一個平台邊緣會爬不上去,原因是身下拖著一個身高長的 Capsule Collider,當玩家爬上平台邊緣的時候會被卡住,為了解決這個問題,我們又寫了一個腳本,他的目標是在攀爬的時候把 Capsule Collider 的身高縮短,停止攀爬才逐漸長回來 * 把 CameraSpringUp.cs 加到 Camera Rig 上,並且把 Camera Rig 拉到 Capsule Collider 的欄位,CenterEyeAnchor 拖到 Head 的欄位 ![image](https://hackmd.io/_uploads/Skkej1gsle.png) ## 可抓取物件 ( Grabbable ) ### 先把一隻卯咪放到場景中 * 這隻卯咪可以在 Project 介面 Assets/Models/Cat Model/source 找到 ![image](https://hackmd.io/_uploads/S1tbvS1oee.png) ### Building Blocks - Grab interaction * 直接把這個方塊拖曳到希望可抓取的物件上 ![image](https://hackmd.io/_uploads/HkMYdH1ixl.png) * 這一個動作會發生兩件事 1. Building blocks 會自動將物件綁上可抓取的組件 ( Grabbable ) 並加上可互動物件 ( Grab Interactable ) 2. Building blocks 發現你的 Camera Rig 上沒有互動器 ( Interactor ) 便幫你加上 ![image](https://hackmd.io/_uploads/BkSbFByjgl.png) >[!Warning]注意 >Building blocks 發現你的 Camera Rig 上沒有互動器 ( Interactor ) 幫你加上時,他會一次將含有所有互動功能的預製件放到 Camera Rig 下層,其中的 Locomotor 會和我們寫的 Locomotion 衝突,必須把他禁用 >![image](https://hackmd.io/_uploads/BJ9bgIJoex.png =70%x) ### 其他抓取模式 * Distance Grab:可以從遠處把指到的物件取過來 * Touch Hand Grab:讓物件可以核玩家的手可以物理上的互動 ![image](https://hackmd.io/_uploads/ryhBgUJoeg.png) >[!Note]註解 >* Grab Interactable:在互動框架中標註 **這是可以被抓的互動物件** >* Grabbable:在物理/行為角度上控制抓到後物體怎麼動、如何處理拋擲或旋轉等 ## 耐力條 ( Stanima ) 要說 Peak 這遊戲的精髓應該就是耐力條機制吧,不是耐力條機制的話...一定是耐力條機制啦👍,攀爬的時候耐力值會下降,當耐力值用光了玩家便會抓不住牆壁,從太高的地方摔下則會導致耐力條因為受傷而變短,進而產生了遊戲的難度,玩家就開始得動腦制定攀爬的策略 ### 耐力條UI * 可以從 Prefabs 資料夾找到,把他拖曳到場景中,由於他的 Render Mode 目前是 `Screen Space-Overlay` 所以我們只能在電腦中的 Game View 看到他 ![image](https://hackmd.io/_uploads/Bkg48I1jgg.png) * 把 Render Mode 設為 World Space 並放到 Hand Anchor 下,調整位置讓攀爬的時候方便看到 ![image](https://hackmd.io/_uploads/HJ7MtLJoex.png) ### ClimbStaminaManager.cs 這個腳本負責耐力條的邏輯,提供外部可以取得耐力條的狀態,也有公開方法可以呼叫腳本內的 受傷 / 恢復 方法來跟耐力條互動 * 場景中新增一個空物件取名 Stanima Manager 並將 ClimbStaminaManager.cs ![image](https://hackmd.io/_uploads/rJPQiU1oxl.png) * 將 UI 下的這三個含 Slider 的物件依序拖入 Climb Stamina Manager ![image](https://hackmd.io/_uploads/SyRk9Uksll.png) * 把 Stamina Manager 物件拉到 Camera Rig 的 OVR Climbing Rig 內的 Stamina Manager 格中,這樣一來 OVRClimbingRig.cs 就會在耐力條用完的時候鬆手 ![image](https://hackmd.io/_uploads/Sy1-n8Jogx.png) ## 受傷、恢復 接著要加上讓玩家受傷以及恢復的功能,並且我們會使用到 **音效 ( Audio )** 、 **後處理 ( Post Processing )** 以及 **粒子系統 ( Particle System )** 來產生聽覺以及視覺效果 ### 音效 ( Audio ) * 新增 音效源 ( Audio Source ) 到 Camera Rig 中,確認 Play On Awake 與 Loop 沒有勾選,可以將 Spatial Blend 提高,將來有其他玩家可以聽到聲音從你的方向發出 ![image](https://hackmd.io/_uploads/Hk1Eu2ksee.png) * 後面會有腳本負責在受傷的時候使用 AudioSource.PlayOneShot() 方法來套用這個 Audio Source 的位置與參數來播放音效 ### 後處理 ( Post Processing ) ::: success 在 **畫面渲染完成** 之後,額外套用影像處理效果,類似相機拍完照片後,再加上濾鏡、亮度調整、模糊等,不會改變 3D 物件本身,只改變最終輸出的畫面 ::: * 在場景中加入 Global Volume ![image](https://hackmd.io/_uploads/S1hjF31jgg.png) * 幫這個 Volume 新增 Volume Profile ![image](https://hackmd.io/_uploads/Byt5chyogg.png) * 按 Add Override 新增濾鏡效果 ![image](https://hackmd.io/_uploads/HJ3Gjh1jge.png) * 選擇 Post-processing -> Vignette ![image](https://hackmd.io/_uploads/SysLshJigl.png) * 照下圖的參數調整 Vignette ( 暗角 ) 的效果 ![image](https://hackmd.io/_uploads/S1PJn31slx.png) * 記得開啟 CenterEyeAnchor 中的 Camera 的 Post Processing ![image](https://hackmd.io/_uploads/rk5fJpyslg.png) * 未來會透過腳本控制 Vignette 內的 Intensity 來表現受傷的視覺效果 ![image](https://hackmd.io/_uploads/B14B1pyjll.png =60%x) ### 粒子效果 ( Particle System ) :::success Particle System 是 Unity 提供的特效系統,用來生成並控制大量粒子 (Particles) 每個粒子可以是:點、貼圖、簡單幾何形狀,透過數量、速度、顏色、壽命的控制,能製造出自然現象或魔法效果 ::: * 新增 Particle System 到場景 ![image](https://hackmd.io/_uploads/HJP7ZaJsxl.png) * 我們要製作恢復的粒子效果,先將參數照下圖調整 ![image](https://hackmd.io/_uploads/Sy-RZayslx.png) * 接著我們要改變粒子的形狀,在 Effects 資料夾中有一個助教預先製做好的材質 ( Material ),使用 URP 用來繪製 Particles 的 Shader,把 Base Map 設定為星形的 Mask , Color Mode 設定為 Multiply 或 Overlay ![image](https://hackmd.io/_uploads/ryuH4TJoxl.png) * 將這個材質拉到 Particle system 的 Renderer 標籤中的 Material 欄位 ![image](https://hackmd.io/_uploads/HJRyraJoee.png) * Hierarchy 按到這個 particle system 物件就可以在 Scene / Game view 預覽粒子效果 ![image](https://hackmd.io/_uploads/rJRQUpkjex.png =60%x) * 把粒子效果的物件放到 CenterEyeAnchor 下,調整適當的位置與Scale,讓粒子以適當的大小環繞在 Camera 周遭 ![image](https://hackmd.io/_uploads/Byd1PpJoxx.png) ### LandingInjuryManager.cs 這個腳本負責檢測玩家落地瞬間的速度,判斷是否會造成傷害,以及造成多少傷害,如果受傷的話就播放受傷的音效以及受傷的視覺效果 * 在場景中新增一個空物件取名 Landing Injurt Manager,並加入 LandingInjurtManager.cs 到這個物件 * 照下圖把 Camera Rig 拉到 Locomotion 跟 Audio Source 欄位裡面,並放入 Stamina Manager ,並到 Audios 資料夾把 `body-fall-47877.mp3` 拉到 Injury Sound 中 ![image](https://hackmd.io/_uploads/rJefdA1jxe.png) ### oiiaio.cs ( overall injury ignore and immediately ok ) 受傷了該怎麼辦?對,擼貓,擼完貓就會好 * 把 oiiaio.cs 放到卯咪身上 * 照著下圖把恢復特效的 particle system , Stamina Manager 以及 Audios 資料夾的 ![image](https://hackmd.io/_uploads/Byfl5Aksxl.png) * 接著我們要使卯咪可以讓我們用 射線互動 ( Ray Interaction ),把 Ray Interaction 的 Block 拉到卯咪身上 ![image](https://hackmd.io/_uploads/B162hAkixg.png) * 此時卯咪下層會出現一個用來把射線互動的觸發視覺化的 DebugVisuals 可以先把他禁用 ![image](https://hackmd.io/_uploads/rkSF6Akjge.png) * 在卯咪身上再加一個 Interactable Unity Event Wrapper 元件,並把 Ray Interaction 的物件拉到 Interactable View ![image](https://hackmd.io/_uploads/rJksJkeolx.png) * 接著在 `when select()` 與 `when unselect()` event 各新增一個 監聽器 ( Listener ) ,並把卯咪放到 Object 的欄位,分別選擇 `oiiaio.Press` 與 `oiiaio.Release` function ![image](https://hackmd.io/_uploads/BkYEzJlixx.png) --- --- # 2025/09/18 Unity (Multiplayer) Peak ## 目標 學習 Photon 網路架構: * 何謂 Dedicated Sever、Client Hosted、Shared Mode * 何謂 Input Authority、State Authority、Network Input、RPCCalls * 如何同步 OVR Rig Character * 如何同步 場景中npc動畫 * 如何同步 生成物件、刪除物件 ## 所使用到的素材 (皆已放在課程素材包內) 1. [Photon Fusion2 SDK](https://doc.photonengine.com/fusion/current/getting-started/sdk-download) 2. [Photon Voice](https://doc.photonengine.com/voice/current/getting-started/voice-for-fusion) 3. [Bing Bong Model](https://sketchfab.com/3d-models/peak-bingbong-model-6d4f626e6c6946958fb80441ea7da27b) 4. [MRTK-Keyboard](https://github.com/Ayfel/MRTK-Keyboard/tree/main) 5. [Fusion2 Addon](https://doc.photonengine.com/fusion/current/addons/physics/download) 6. [Network Useful Script](https://drive.google.com/file/d/1j3_6UsuU4bRA7CuC8yzzSaGat_wLg6HG/view?usp=sharing) 7. [VR Useful Script](https://drive.google.com/drive/folders/1dXHGzB0lGAzTPaobOxKAfyPW5fohcnfX?usp=sharing) 8. [XRCamp Utilities](https://drive.google.com/file/d/1OTeWhSML3K-Zsl7W8okwgcY_IapPzrlr/view?usp=sharing) 9. [VR Hands](https://drive.google.com/file/d/1pOyr6dUa7k2hBwM1RMMaLQIMr-cWvuxH/view?usp=sharing) ## 上課重點紀錄 ### 1. 基本知識 ### 1.1. Input Authority VS State Authority * Input Authority: 有權限提供「輸入」、只會有一端有此權限 * State Authority: 有權限更改「狀態」、只會有一端有此權限 ### 1.2. Dedicated Server VS Client Hosted VS Shared Mode ![image](https://hackmd.io/_uploads/B1XY1LYogg.png) * Dedicated Server: 全部角色的 State Authority 在 Server端、進行同步與計算,玩家僅有個別角色的 Input Authority、提供輸入 - 好處:計算不用吃玩家效能 - 壞處:需要有伺服器維護遊戲進行 * Client Hosted: 一位玩家為房主、擁有全部角色的 State Authority 、負責進行同步與計算,其他玩家僅有Input Authority、提供輸入 - 好處:網路架構清楚、分工明確 - 壞處:遊戲效能看得Host端網路狀況、要撰寫Host Migration才能正常運行 * Shared Mode: 每位玩家都是clients、各自擁有State Authority、並計算自己的輸入與狀態,並將狀態的"結果"同步給其他玩家 (Input Authority則沒有很嚴格的限制)。 - 好處:快速開發、需要同步的資料較少、房主會自動繼承 - 壞處:遊戲效能需看自己的網路狀況、且玩家越多效果越差 ### 1.3. Network Input vs Network Event VS RPC Calls VS Networked Value * Network Input: - 使用者:擁有Input Authority的client端 - 時機:傳遞輸入給不同端點的玩家腳本、特別是在輸入腳本本身難以被禁用,例如: ovr manager - 特色:持續性輸入、有tick來做同步校正、需高準確性的同步數值 * Network Event: - 使用者:Client、Server(Host) 皆可 - 時機:需要即時性的動作、例如:開槍 - 特色:只發生一次的即時事件、傳輸安全 * RPC Calls: - 使用者:Client、Server(Host) 皆可 - 時機:限制呼叫來源、對象的同步方式 - 特色:可以依照Object Id來指定傳給特定玩家、適合需傳送參數的方法 * Networked Value - 使用者:有Network屬性的腳本 - 時機:持續同步的"狀態",包含位置、布林值等等 - 特色:會自動同步到其他Clients、十分方便 ### 1.4. 常見的 Network Component * Network Object:擁有網路性質的物件 ![image](https://hackmd.io/_uploads/HyQScUYixl.png) * Network Runner:代表玩家主機狀態,一台主機一個 ![image](https://hackmd.io/_uploads/S17dqLFslx.png) * Network Transform:同步位置、旋轉、大小 (有StateAuthority才能改變喔!) ![image](https://hackmd.io/_uploads/SJJwcUKoee.png) * Network RigidBody:同步Transform、速度、角速度 (有StateAuthority才能改變喔!) ![image](https://hackmd.io/_uploads/BJ7I98Kiee.png) * Network Mechanim Animator:同步動畫,包含Parameter、Root、Layers、Weight ![image](https://hackmd.io/_uploads/S1U4cIKole.png) ### 2. Login Menu ### 2.1. UI 1. 創建一個 World Space UI Canvas ![image](https://hackmd.io/_uploads/rkPyDR8hgl.png) 2. 於Canvas加入 "Pointable Canvas"、"Ray Interactable" ![image](https://hackmd.io/_uploads/Hyzy90L2gg.png) 3. 於Canvas底下(子物件)創建一個新的物件,命名為"Collider"、並添增"Box Collider"和"Collider Surface" >[!Tip]小提醒 >記得將Collider拉進Surface的腳本Reference中 ![image](https://hackmd.io/_uploads/SyMQjCUhex.png) ### 2.2. QuickStart >[!Tip]Network Runner >Network Runner用來維持每個機台運行多人系統、掌控目前本機玩家與伺服器資訊。 >因此,通常一個場景**只會有一個 Runner**,否則會出事!!! 我們可以先來看看 BasicSpawner.cs 這個腳本!([連結](https://drive.google.com/file/d/1j3_6UsuU4bRA7CuC8yzzSaGat_wLg6HG/view?usp=sharing)) **Connect to server and Start Game** ```C# public async Task Connect(int peerIndex = 0) { //runner = gameObject.AddComponent<NetworkRunner>(); runner = gameObject.GetComponent<NetworkRunner>(); runner.ProvideInput = false; runner.AddCallbacks(this); // Create the scene manager if it does not exist if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>(); // Create the NetworkSceneInfo from the current scene var scene = SceneRef.FromIndex(1); var sceneInfo = new NetworkSceneInfo(); if (scene.IsValid) { sceneInfo.AddSceneRef(scene, LoadSceneMode.Single); } // Start or join (depends on gamemode) a session with a specific name var args = new StartGameArgs() { GameMode = GameMode.Shared, Scene = scene, SceneManager = sceneManager, SessionName = RoomName.text }; await runner.StartGame(args); } ``` * ProvideInput:將Client端的input上傳至server、減少同步延遲 (適用於client mode) * runner.AddCallbacks(this):將runner的callback觸發到這個腳本的相關函式 like: ```c# public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) { } public void OnInput(NetworkRunner runner, NetworkInput input) { } public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { } public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) { } public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { } public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList) { } public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { } public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { } public void OnSceneLoadDone(NetworkRunner runner) { } public void OnSceneLoadStart(NetworkRunner runner) { } public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player){ } public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player){ } public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key, ArraySegment<byte> data){ } public void OnReliableDataProgress(NetworkRunner runner, PlayerRef player, ReliableKey key, float progress){ } ``` * sceneInfo.AddSceneRef(SceneRef.FromIndex(1), LoadSceneMode.Single):設定進入遊戲後的多人場景 * GameMode:有 **Shared**、AutoHostOrClient、Host、Client、Server * SessionName:**房間名稱** * PlayerCount:最大人數 * runner.StartGame(args):OnConnectedToServer -> OnPlayerJoined -> OnSceneLoadStart -> OnSceneLoadDone **Create Player Prefab when transfer scene** ```c# public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) { if (player == runner.LocalPlayer && _playerPrefab != null) { Debug.Log(runner.LocalPlayer + "," + player); // Create a unique position for the player Vector3 spawnPosition = new Vector3(0, 1, 0); //NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player); NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player, (runner, obj) => { }); // Keep track of the player avatars for easy access _spawnedCharacters.Add(player, networkPlayerObject); } } ``` * **runner.Spawn**:Instantiate gameobject with Network Object. You can also specify the authority here. * Record the spawned player for the objects authority transition . **Keep the Runner Alive (DontDestroyOnLoad)** ```c# public static BasicSpawner Instance { get; private set; } private void Awake() { if (Instance != null && Instance != this) { Destroy(this.gameObject); } else { Instance = this; DontDestroyOnLoad(this.gameObject); } } ``` ### 3. Game Scene ### 3.1. OVR Sync & Spawn Player >[!Tip]Network Object >很重要的一個觀念:**通常多人物件於每台裝置都會有一個在場景中、別人生成的物件你自己也會有一個!** >* 角色:每台裝置都會有n個角色、分別代表n個玩家。 >* 物件:每台裝置都會有該物件於場景、但權限歸屬於一個裝置。 通常在多人遊戲中,生成玩家的邏輯可視為生成物件,基本上把單人玩家加上多人元件即可。 但要注意,在VR中、若使用 **Oculus Platform**,玩家(Camera Rig)上都會有 **OVR Manager**,因此如果我們直接生成、他會跟另一位玩家的 OVR Manager 打架,導致兩邊的 Camera Rig 都被刪除😵 ##### 解決方法 * **本機端(Camera Rig)**:透過 **HardwareRig** 來紀錄並傳遞目前玩家、頭部、雙手的狀態 * **網路端(PlayerPrefab)**:另外生成出來的多人玩家物件。透過 **NetworkRig** 來接收玩家、頭部、雙手的狀態,並負責渲染、同步大家看到的角色動作 (因此玩家身體模型會放在這邊)。 ### 3.2. Camera Rig 即加上HardwareRig腳本即可。 ![image](https://hackmd.io/_uploads/ryNa7lw1bl.png) ### 3.3. HardwareRig (若透過 **NetworkTransform** 來同步玩家、Head、LeftHand、RightHand,則不需要使用下面的 **OnInput Callbacks + INetworkInput** 來同步,基本上兩者做的事情是一樣的!) ```c# public struct RigState : INetworkInput { public Vector3 PlayerPosition; public Quaternion PlayerRotation; public Vector3 HeadsetPosition; public Quaternion HeadsetRotation; public Vector3 LeftHandPosition; public Quaternion LeftHandRotation; public Vector3 RightHandPosition; public Quaternion RightHandRotation; } ``` ```c# #region INetworkRunnerCallbacks void INetworkRunnerCallbacks.OnInput(NetworkRunner runner, NetworkInput input) { RigState xrRigState = new RigState(); xrRigState.HeadsetPosition = headTransform.position; xrRigState.HeadsetRotation = headTransform.rotation; xrRigState.PlayerPosition = playerTransform.position; xrRigState.PlayerRotation = playerTransform.rotation; xrRigState.LeftHandPosition = leftHandTransform.position; xrRigState.LeftHandRotation = leftHandTransform.rotation; xrRigState.RightHandPosition = rightHandTransform.position; xrRigState.RightHandRotation = rightHandTransform.rotation; input.Set(xrRigState); } #endregion ``` ### 3.4. PlayerPrefab ![image](https://hackmd.io/_uploads/rJlbRnt1bx.png) * 可以建立一個Prefab,並於建立 Head、LeftHand、RightHand三個子物件 * 於最上層、Player物件(你也可以額外建立一個Player子物件)新增Network Object、NetworkRig的Component * 接著,於Player、Head、LeftHand、RightHand四個物件新增Network Transform來同步位置 (你也可以用OnInput Callbacks + INetworkInput方法來同步) ![image](https://hackmd.io/_uploads/BJEsNTF1Wg.png) * 設定好NetworkRig的Reference ![image](https://hackmd.io/_uploads/HJRlBTK1-e.png) ### 3.5. NetworkRig * 若自己在此裝置擁有StateAuthority -> 玩家自己的裝置端,則記錄場景上唯一的 HardwareRig ```c# public bool IsLocalNetworkRig => Object.HasStateAuthority; HardwareRig hardwareRig; public override void Spawned() { base.Spawned(); if (IsLocalNetworkRig) { hardwareRig = FindObjectOfType<HardwareRig>(); if (hardwareRig == null) Debug.LogError("Missing HardwareRig in the scene"); } // else it means that this is a client } ``` * 若**沒有透過 NetworkTransform** 來同步、則可以使用下方程式碼於 FixedUpdateNetwork() 來接收 **INetworkInput** 的資訊並改變玩家狀態 ```c# [Header("RigComponents")] [SerializeField] private NetworkTransform playerTransform; [SerializeField] private NetworkTransform headTransform; [SerializeField] private NetworkTransform leftHandTransform; [SerializeField] private NetworkTransform rightHandTransform; public override void FixedUpdateNetwork() { base.FixedUpdateNetwork(); if (GetInput<RigState>(out var input)) { playerTransform.transform.SetPositionAndRotation(input.PlayerPosition, input.PlayerRotation); headTransform.transform.SetPositionAndRotation(input.HeadsetPosition, input.HeadsetRotation); leftHandTransform.transform.SetPositionAndRotation(input.LeftHandPosition, input.LeftHandRotation); rightHandTransform.transform.SetPositionAndRotation(input.RightHandPosition, input.RightHandRotation); } } ``` * 於本機端則在Render()區塊、透過 HardwareRig 資訊直接更新玩家狀態 ```c# public override void Render() { base.Render(); if (IsLocalNetworkRig) { playerTransform.transform.SetPositionAndRotation(hardwareRig.playerTransform.position, hardwareRig.playerTransform.rotation); headTransform.transform.SetPositionAndRotation(hardwareRig.headTransform.position, hardwareRig.headTransform.rotation); leftHandTransform.transform.SetPositionAndRotation(hardwareRig.leftHandTransform.position, hardwareRig.leftHandTransform.rotation); rightHandTransform.transform.SetPositionAndRotation(hardwareRig.rightHandTransform.position, hardwareRig.rightHandTransform.rotation); } } ``` ### 3.6 Spawn NPC & Animation Sync 在Shared Mode中,可以與玩家互動的物件,基本上都會牽扯到「**權限轉移**」。 >[!Tip]StateAuthority >基本上大多的VR操作,包含Grab等等,都會需要操控者擁有該物件的StateAuthority! 在這一小節,我們將使用SpawnManager.cs (於多人場景中)於第一個玩家進入遊戲時生成所有應該一開始就在的多人物件,包含 **NPC**。 * **當玩家加入 (OnPlayerJoined)**:若是本地玩家+還沒生成過 -> SpawnAll ```c# [Networked] private NetworkBool NpcSpawned { get; set; } public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) { if (runner.LocalPlayer != player) return; if(!NpcSpawned) SpawnAll(runner); } ``` * **生成初始多人物件 (SpawnAll)** * StateAuthority:第一個加入的玩家 * InputAuthority:null (不限) * 透過 NPCBehavior、NPCRegistry 來記錄生成物件的狀態 ```c# private void SpawnAll(NetworkRunner runner) { NpcSpawned = true; int num = 0; foreach(NetworkPrefabRef obj in NPCPrefab) { var npc = runner.Spawn(obj, NPCPos[num].position, NPCPos[num].rotation, inputAuthority: null); var behavior = npc.GetComponent<NPCBehavior>(); if (behavior != null) NPCRegistry.Register(behavior); num++; } } ``` ```c# public static class NPCRegistry { private static readonly HashSet<NPCBehavior> npcs = new HashSet<NPCBehavior>(); public static void Register(NPCBehavior npc) => npcs.Add(npc); public static void Unregister(NPCBehavior npc) => npcs.Remove(npc); public static bool IsRegistered(NPCBehavior npc) => npcs.Contains(npc); public static IEnumerable<NPCBehavior> AllNPCsSnapshot() => npcs; } ``` ```c# public class NPCBehavior : NetworkBehaviour, IStateAuthorityChanged { private bool active; public override void Spawned() { if(!NPCRegistry.IsRegistered(this)) NPCRegistry.Register(this); active = Object.HasStateAuthority; } public override void Despawned(NetworkRunner runner, bool hasState) { NPCRegistry.Unregister(this); } public void StateAuthorityChanged() { active = Object.HasStateAuthority; } } ``` * **當初始玩家離開 (OnPlayerLeft)**:若還有剩餘玩家、則挑選經歷最久的玩家並轉移權限 ```c# public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) { // 剩餘玩家排序,挑最小當下一任 var remaining = runner.ActivePlayers.OrderBy(p => p.RawEncoded).ToList(); if (remaining.Count == 0) return; var nextOwner = remaining.First(); bool iAmNext = runner.LocalPlayer == nextOwner; // 當前沒有權限、且我被選中 → 主動請求接手 SpawnManager 與所有 NPC if (iAmNext) { Debug.Log("me!"); foreach (var npc in NPCRegistry.AllNPCsSnapshot()) { if (npc && npc.Object && !npc.Object.HasStateAuthority) npc.Object.RequestStateAuthority(); } } } ``` >[!Tip]AddCallbacks >記得如果程式中有包含到Network相關的Callback Functions,於腳本啟動時需執行 NetworkRunner.AddCallbacks(this),才能正常呼叫到喔! >`void Awake() { BasicSpawner.Instance.runner.AddCallbacks(this); }` * **Animation Prefab Setup** ![image](https://hackmd.io/_uploads/r1lefncyZx.png) * 新增 Network Object、Network Mecanim Animator、NPC Behavior這三個腳本 * 若你的NPC會在場景上"移動"、則需再新增 Network Transform * 設定 Network Object:**允許權限轉移**、不要在StateAuthority消失時摧毀自己 ![image](https://hackmd.io/_uploads/ByxufhcJWg.png) * 設定 Network Mecanim Animator:**設定Animator**、Sync選項設為Everything ![image](https://hackmd.io/_uploads/B1aTMhqkbe.png) ### 3.7. Sync Grab & RPC Calls * 基本上可以被Grab的物件、身上都會有下方圖片中的 **Collider、Rigidbody、Grabbable** 這三大組件 ![image](https://hackmd.io/_uploads/Bysumn5kZe.png) * 若使用的是Meta All in One,則子物件會有各種 **Grab Interactable** ![image](https://hackmd.io/_uploads/S1dpmh9kbx.png) * **注意1**:Grab影響的是此物件Rigidbody的數值,因此需使用 **Network Rigidbody 3D** 進行同步,且不得與 Network Transform 並用 ![image](https://hackmd.io/_uploads/rkhuN29ybl.png) * **注意2**:Grab會改變這個物件的狀態,因此需要擁有此物件的StateAuthority,才能夠成功Grab。可想而知,我們必須在 **Select(開始抓)**、**UnSelect(放開瞬間)** 的時候進行**權限轉移**與**Rigidbody的設置** * 偵測 Interactable 事件:**Interactable Unity Event Wrapper** (Meta SDK 提供腳本,不需額外下載) * 將你想偵測的 Interactable (Ray、Grab、HandGrab等等) 放到 Interactable View * 於 When Select() 處放置你自己的 Grab 函式 * 於 When Unselect() 處放置你自己的 UnGrab 函式 * 若有多個 Interactable 方式,則**加多個 Interactable Unity Event Wrapper** ![image](https://hackmd.io/_uploads/BkrYHnq1-e.png) ```c# //轉移權限 public void Grab() { if(!Object.HasStateAuthority) Object.RequestStateAuthority(); } //放開時設定並同步 isKinematic public void UnGrab() { RPC_UnGrab(); } ``` * 會發現,我們在UnGrab時,需要去將物件 Rigidbody 的 isKinematic 設回 false (允許受運動學影響),但外面的 Network Rigidbody 3D 並不會幫我們自動處理這部分的同步,因此我們需要使用 **Remote Procedure Call(RPC)** 來同步於各機台呼叫某個函式! ```c# [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_UnGrab(RpcInfo info = default) { GetComponent<Rigidbody>().isKinematic = false; } ``` * **RpcSources**:誰(哪個機台/Runner)可以主動呼叫這個RPC函式 * **RpcTargets**:誰可以被同步(被動)呼叫這個RPC函式 * 基本上在這邊,放開時的那個人因為先Grab過、權限在他身上,因此我們可以將 RpcSources 設為 **StateAuthority** 並允許同步到所有機台上 (**包含自己-RpcTargets.All**) **多人遊戲撰寫的難點就在於:** 1. 透過玩家與物件的 Authority 決定誰可以執行、可以呼叫 (**要時常切換到PlayerB的視點來檢驗!**) 2. RPC的 **同步對象(是否包含自己)**、**同步順序(本地先計算在同步?還是於同步時一起計算?)**、與**必要性(RPC過多將導致效能降低!)** ### 4. Advanced ### 4.1. Photon Voice 參考:https://www.youtube.com/watch?v=Jw0b6_R-m9c 1. 下載 [Photon Voice2](https://assetstore.unity.com/packages/tools/audio/photon-voice-2-130518?srsltid=AfmBOoqERUD6bnoWki5zBrMmpVvX1XWzE-o5Vo4UMRtbTYkKs2NrV05T) 2. 於 Photon Engine 的 Dashboard 新增 Voice 的 Application ![image](https://hackmd.io/_uploads/S1mkh3qkbg.png) 3. 複製你的 AppID 4. 於 Fusion>Resources>Photon App Settings ,設定好 **App Id Voice** (注意,跟你一起開發的朋友也要設定一樣的Id哦!) ![image](https://hackmd.io/_uploads/rJV5n2qkbx.png) 5. 在你擁有 Network Runner 的物件上,添增 **Fusion Voice Client** ![image](https://hackmd.io/_uploads/HJUWpn9yWe.png) 6. 並在其下方新增子物件,命名為"Recorder"、新增 **Recorder** 元件、並Reference到 Fusion Voice Client 的 Primary Recorder 上 ![image](https://hackmd.io/_uploads/H1mHpn51bg.png) 7. 在你的多人角色Prefab中,選擇一個會跟隨你玩家位置的物件(Player、Head、Body都可以),添增 **Speaker** ![image](https://hackmd.io/_uploads/HyaZRn5kZe.png) 8. 基本上這樣就設定好啦! 可以Build出去、試著在兩台頭盔上測試囉~~ (**記得開啟麥克風權限**) ### 4.2. Generate Lighting: GI & AO 參考影片: https://www.youtube.com/watch?v=NOv31HSqs6U (Baking) https://www.youtube.com/watch?v=mfU_qqTfJZ0 (AO) * 透過場景中光照的種類 (Realtime、Mixed、Baked),選擇你的 GI 種類 * 調整 Lightmapping Settings 的參數 (根據電腦效能、通常預設的參數都會跑很久xd) * 可以調整 Bounces 參數,將影響間接照明的計算(反彈)次數,越高光照效果越真實、但計算時間越久 * 勾選 AO (計算環境光遮蔽效果) * 開始 Generate Lighting! ![image](https://hackmd.io/_uploads/HJs11T9kWl.png) ### 4.3. FadeIn/Out 下載資源:[XRCamp Utilities](https://drive.google.com/file/d/1OTeWhSML3K-Zsl7W8okwgcY_IapPzrlr/view?usp=sharing) * 可以透過在眼前(CenterEyeAnchor)放置Quad、並動態改變其材質,來達成視野 FadIn/Out 的效果! * 此效果在VR專案中特別重要,因為在轉換場景時常會有卡頓現象,若先將視野 FadOut,則可以避免看到卡頓的畫面、開啟下個場景時 FadIn 也會有較好的視覺效果(類似睜眼) * XRCampUtilities>Prefab 中可以找到 FadeScreen 的 Prefab、將其拉到 CenterEyeAnchor 下方 ![image](https://hackmd.io/_uploads/HJI2bpq1Wx.png) * 將 FadeScreen z軸拉前面一點、讓鏡頭能看到這個 Quad 且被完全覆蓋 ![image](https://hackmd.io/_uploads/rJ84Xp9kbg.png) * 可以去更改腳本、材質樣式來製作其他 FadIn/out 的效果與時機! ![image](https://hackmd.io/_uploads/rkK4Mac1Wg.png) * 腳本內容參考: ```c# public class FadeScreen : MonoBehaviour { public bool fadeOnStart = true; public float fadeDuration = 2f; public Color fadeColor = Color.black; private Renderer rend; void Start() { rend = GetComponent<Renderer>(); if (fadeOnStart) { FadeIn(); } } public void FadeIn() { Fade(1, 0); } public void FadeOut() { Fade(0, 1); } public void Fade(float alphaIn, float alphaOut) { StartCoroutine(FadeScreenCoroutine(alphaIn, alphaOut)); } IEnumerator FadeScreenCoroutine(float alphaIn, float alphaOut) { float t = 0; while (t < fadeDuration) { t += Time.deltaTime; Color newColor = fadeColor; newColor.a = Mathf.Lerp(alphaIn, alphaOut, t / fadeDuration); rend.material.color = newColor; yield return null; } Color finalColor = fadeColor; finalColor.a = alphaOut; rend.material.color = finalColor; } } ``` ### 5. Let's Peak! **打包、開玩!** ![image](https://hackmd.io/_uploads/HkCn76ckWl.png) --- # 2025/10/30 VRIK and Hand Tracking in Photon Fusion ## 目標 * 學習如何在 Unity 中使用 Final IK (VRIK) * 如何在使用 Oculus Platform + Meta All in One 的情況下,同步玩家身體與手勢模型 ## 上課素材 [FinalIK Package (From IDVR)](https://www.dropbox.com/scl/fo/hvt3vmuju6xmbufl9lns7/AMexg822sFl3cOEZ6nmi8Xo?dl=0&e=1&preview=Final+IK+JiaJun.unitypackage&rlkey=qtk39lhtjyt6dbnwiuwtdh4ct) [課程素材 (包含腳本更新檔)](https://drive.google.com/drive/folders/1lrWIObjEC6vGJBZJ-jFVJWSXgRJFHg7M?usp=sharing) ## 相關影片 [Full Body VR with FINAL IK](https://www.youtube.com/watch?v=VxcX_x5cyho) ## 上課重點紀錄 ### Inverse Kinematic , IK ![image](https://hackmd.io/_uploads/HkRO4Sl1Wg.png) 基本上於Unity Engine中可以透過 **Animation Rigging** (要設定的細節、銜接到VR上較繁複)、**FinalIK** (已整合好的外部Package) 等等方式實作。 ### VRIK - Single Player * Import FinalIK、打開 Demo 場景 ![image](https://hackmd.io/_uploads/BJslz8gJ-e.png) * 將代表我們VR身體的角色模型、設定其骨架為 Humanoid(人型) 、並創建一個Avatar ![image](https://hackmd.io/_uploads/SytcN8gJbl.png) * 將設定好的模型放入場景中,加入VRIK (有Avatar即會自動reference) ![image](https://hackmd.io/_uploads/rJoeLLeJWl.png) * 將Head、Left Hand、Right Hand複製到OVR Rig對應的位置(如圖) ![image](https://hackmd.io/_uploads/BybyKLg1Wl.png) * 將OVR Rig中複製好的這些object的Position歸零、Scale也可以設為(1,1,1)、並命名為"HeadT"、"LeftHandT"、"RightHandT" * 對應好VRIK中各關鍵部位欲追蹤的Reference - 頭部 ![image](https://hackmd.io/_uploads/HJnVq8lkZe.png) - 左手 ![image](https://hackmd.io/_uploads/SyEY5IgkZx.png) - 右手 ![image](https://hackmd.io/_uploads/SyRiqLx1Wl.png) * 帶起頭盔測試看看吧! * **接著才是最重要的地方!!!** 透過畫面、我們可以在Runtime微調頭部、雙手的Position與Rotation、並記錄起來,在stop後paste上去。 可以參考 [相關影片 12:44 ](https://youtu.be/VxcX_x5cyho?si=pS50mTtdiCyZWqBb&t=764) * 最終 IK 結果 ![image](https://hackmd.io/_uploads/rkTtVT9k-e.png) ### VRIK - Multiple Player #### 設定 OVR Rig * (若要使用手部追蹤同步、請先更新[上課用Network相關腳本](https://drive.google.com/drive/folders/1lrWIObjEC6vGJBZJ-jFVJWSXgRJFHg7M?usp=sharing)!) * 將Hardware Rig的Head Transform、Left Hand Transform、Right Hand Transform reference給剛剛 VRIK 加入的"HeadT"、"LeftHandT"、"RightHandT" ![image](https://hackmd.io/_uploads/ryXM08gJWg.png) #### 設定 (Fusion)PlayerPrefab * 先將原先的頭、雙手的簡易**模型**關閉 ![image](https://hackmd.io/_uploads/H1G61dxyZl.png) * 接著、把剛剛做好的 VRIK 整個物件放入這個 PlayerPrefab 的子物件中、並調整到適當位置 ![image](https://hackmd.io/_uploads/By0bsalyWe.png) * **改為多人架構** >[!Tip]回顧 >IK追蹤:OVR Rig (anchor, IK Target) -> VRIK (body) >多人同步: OVR Rig (hardwareRig) -> PlayerPrefab (networkRig) 的 Player、Head、LeftHand、RightHand **將 VRIK 的 IK Target 更改為 PlayerPrefab 的 Player、Head、LeftHand、RightHand 即可!** ### Hand Tracking Model * 從 OVRInteractionComprehensive 物件中可以找到Hand Tracking的物件 (若沒有 Hand Tracking 功能、可以從Building Blocks拉近OVR Rig中) * 將左右兩手的手部模型物件複製給 PlayerPrefab 的相對Anchor位置 ![image](https://hackmd.io/_uploads/rylf-Cl1-e.png) * 將本地端的OVR Rig中的手部模型,之中的皮膚物件關閉 ![image](https://hackmd.io/_uploads/r1ciGAl1Wx.png) * 將連線端的PlayerPrefab中的手部模型,之中的皮膚物件材質改變成自己想要的材質球 (你可以複製一顆原本的、然後重新reference後更改其中的顏色、透明度、漸層等等) ![image](https://hackmd.io/_uploads/BklWXRg1Zg.png) * 最後,將PlayerPrefab中的身體模型,Rig中的hand關節物件,左右兩手的Scale都設為0 ![image](https://hackmd.io/_uploads/rkghZ4CxJZg.png) ### Sync Hand Bones * 設定 Hardware Rig,reference手部的父物件 ![image](https://hackmd.io/_uploads/ry_5NAgyZe.png) * 設定 Network Rig,一樣reference手部的父物件 ![image](https://hackmd.io/_uploads/SJ7CVCg1bx.png) * Hardware Rig將會偵測手部Bone節點、並上傳手部資訊 ![image](https://hackmd.io/_uploads/BJVUrRekWl.png) * Network Rig在Update端載入資訊、Render端同步效果 ![image](https://hackmd.io/_uploads/ByW5HCxybl.png) ![image](https://hackmd.io/_uploads/Sy2ur0e1be.png) ### Demo! --- # x. 其他 自學/課後: * [Quest Quick Build (Newest)](https://www.youtube.com/watch?v=paVX3Pm4Yq4) * [Oculus Gesture](https://www.youtube.com/watch?v=Lc1PuEatrCA) * Body Skeleton 若**還有時間**則可以去研究一下身體的部分,有身體的虛擬角色會更有帶入感! [Valem Tutorial](https://www.youtube.com/watch?v=tBYl-aSxUe0&list=PLrk7hDwk64-ZRB5lz-xJhgH7Lp6MIRcHJ&pp=iAQB) * Final IK 這個Package是包含手、手指以及身體等的終極Module,如果你是**高手的話**則可以嘗試一下! [Unity Asset Store](https://assetstore.unity.com/packages/tools/animation/final-ik-14290) * [Multiplayer](https://doc.photonengine.com/fusion/current/tutorials/host-mode-basics/1-getting-started) --- * Plastic SCM * 注意:在製作新進度前、或Push之前要先Pull!!! * 建議:Comment最好簡潔有力(寫重點),也可以再標記一次push的時間點 * 建議三人分別是兩位程式、一位美術來使用Plastic SCM * Q. 如果不只三個人想合作撰寫怎麼辦? A. 如果是美術或音樂組的話,程式組可以先生出一個prototype讓他們測試(例如模型大小、整體空間等等是可以先決定的),製作好模型、場景、動畫或音樂後則可包成Unity Package後匯入到有連結到Plastic SCM的專案中 * 功能(有學過Git概念的可以自行跳過) 1. Pull **注意:在製作新進度前、或Push之前要先Pull!!!** 自動Merge其他更變集的資料,如有衝突的話則可能使用到以下兩種情況: * choose from Origin Master : 選擇以本機的更變集為主 * choose from resources : 選擇以目的地的更變集為主 請自行與其他組員協調好再執行Pull的動作,否則更變集被覆蓋有時會很難救回來的! 2. Push * 建議每次上傳都打個簡短明瞭的註解 * Unity會先進行Check in的動作 * Push前如果想恢復某個物件或檔案修改前的版本,Undo即可 3. Branch * 假設今天非主要開發人員想進行測試或開發新功能,會建議在新的場景或另一個Branch製作 * 好處是別人不會Pull到你的Branch的東西,也就是沒有Merge或是Unity設定被覆蓋的問題 4. Switch to workspace 可以在Plastic SCM應用程式的更變集介面中右鍵某個更變集,並按下Switch to workspace則可將目前的專案還原到當時的版本 * 還原前必須先將當下的變更Push上去 * 若確定要還原到該變更,則需把該更變集之後的更變集**全部刪除**! * 創建 1. 下載Plastic SCM雲端版 [https://www.plasticscm.com/download](https://) 限制:三人、5Gb內免費 ![image](https://hackmd.io/_uploads/BkOeTpH6p.png) 2. 至Plastic SCM網頁版,使用Unity ID登入後,創建一個新的組織並創建Unity Repo * 先去Unity Dashboard新增Project [https://cloud.unity.com/](https://) ![image](https://hackmd.io/_uploads/B1FYAprTp.png) * 再到Plastic SCM Dashboard、進入Cloud [https://www.plasticscm.com/dashboard](https://) ![image](https://hackmd.io/_uploads/BksTppSaT.png) * 有出現專案畫面的話就代表專案建立成功囉! ![image](https://hackmd.io/_uploads/B186kASTa.png) * 現在就可以打開剛下載好的Plastic SCM,確認存儲庫是否有該專案啦 3. 開啟你想同步的Unity Project,並到Unity Version Control的介面 4. 填寫相關資訊,包含repo名稱,即步驟2.的專案名 5. 出現類似下面畫面後則代表你成功了! ![image](https://hackmd.io/_uploads/HyD86uZ06.png) 7. 最後一步啦! 寫下你的第一則Comment、Check in changes並Push就大功告成啦! * 邀請 1. 到Plastic SCM Cloud,點選組織的設定成員功能 ![image](https://hackmd.io/_uploads/B1IF1CBTT.png) 3. 利用Gmail邀請你的成員加入組織後,他們就可以在Plastic SCM的應用程式中選擇該組織並找到你的repo進行共編啦! # x2. 注意與建議 :::success 注意事項! 製作專案前有空看看吧!小心踩到這些地雷喔~ (是教授會注意的點^^) ::: * 避免**動暈症** (順移時角度變化過大、角色旋轉方式、急速移動) * 畫面過於炫彩(特效、顏色、燈光),導致**視覺疲乏** * 遊戲**元素過多**,無法呈現遊玩特色或故事主軸 (也會使專案loading變重) * 美術**風格相斥或過多**、與音樂不搭 * 隨時注意遊戲時的fps (最好能維持在**60Hz以上**) * 若角色有全身的話(除了頭以外),記得把**影子**拿掉! (mesh renderer>lightning * 有導引玩家的提示會更好! :::success 建議です! 學長姐們修課經驗談~ ::: * **遊戲的操作自由度越大越好、操作方式越少越好、越有創意越好** * 在VR世界中因場景很大的關係,有時候不會選擇切換整個scene,而是在不同地區使用不同模型、不同效果渲染+fadein/fadeout來轉場。(善用VR世界的環境) * 美術的部分,要注意素材不要選擇過度high poly的模型,會導致幀數下降,particle system在VR作品中也要慎用 * 一個專案的美術風格盡量維持一~兩種就好,且不要極端 (lowpoly+highpoly就會很怪) * 配音的部分,由於同學們並非專業配音員,容易因為聲音含糊或收錄到環境音,而導致玩家出戲、甚至聽不清內容。建議同學可以多加利用免費的 AI文字轉語音工具(TTS),如: [Elevenlabs](https://elevenlabs.io/)、[ttsmaker](https://ttsmaker.com/zh-hk)、[edge-tts(需使用python,可調性較大)](https://pypi.org/project/edge-tts/)等等。 * VR遊戲中,使用[空間音效(Audio Spatializer)](https://docs.unity3d.com/Manual/VRAudioSpatializer.html),可以讓玩家透過聲音,定位視野外的物體,為你的故事/玩法創造更多可能性! * 使用好音樂/音效,可以大幅提升玩家的沉浸感、改變故事的氛圍、甚至可以用來引導玩家以推進劇情! * 專案到後面script、prefab、package各類檔案會越來越多,記得隨時整理「需要」的檔案,並命名分類,分裝至不同資料夾中。 # 番外篇1. 連結VR頭盔與突發狀況解決方法 * Oculus Quest 2 / 3 1. Oculus Link (有線) 步驟1. 下載[Oculus](https://www.meta.com/zh-tw/help/quest/articles/headsets-and-accessories/oculus-rift-s/install-app-for-link/)應用程式並開啟 步驟2. 頭盔啟用Usb外部存取權(有出現的話) 步驟3. Enable Oculus Link (插上數據線的時候應該會出現,沒有的話去選單手動點選或切換有無線幾次) 步驟4. 到了白色的大廳就代表成功囉! ![image](https://hackmd.io/_uploads/S1Oxg4JRa.png) 2. Air Link(無線) 步驟1. 下載[Oculus](https://www.meta.com/zh-tw/help/quest/articles/headsets-and-accessories/oculus-rift-s/install-app-for-link/)應用程式並開啟 步驟2. 將電腦與頭盔連線至同樣網路(電腦分享也可) 步驟3. 到選單切換成Air Link模式、點選自己電腦的名稱並連線 步驟4. 連線成功後按下Enable Link 步驟5. 到了白色的大廳就代表成功囉! 3. 突發狀況 * 連線至白色大廳的途中一直轉圈圈進不去 **解決方法**:頭盔斷開重連,再不行就重新開機 * 線插進去了卻沒接到Oculus Link **解決方法**:頭盔重新開機,電腦將Unity關閉、去系統管理員把所有有關Oculus的程序關閉 (起碼比電腦重開來得好==) * 進入大廳後或是測試途中開始閃爍或卡卡的 **解決方式**:盡量使用有線的方式Link、或拔掉重連 # 番外篇2. 好用資源及連結 * 專案流程、組內分工/討論工具 * 約? [約時間網站](https://www.when2meet.com/) * 專案流程分工? [Trello](https://trello.com/zh-Hant) * 討論工具(圖像式)? 1. [Milanote](https://milanote.com/) 2. [Excalidraw](https://excalidraw.com/) * 資料與連結整理 * 程式組 1. [Unity 論壇](https://forum.unity.com/) 2. [Unity Document- Script](https://docs.unity3d.com/ScriptReference/) 3. [Unity 常用語法(較友善XD)](https://www.gameislearning.url.tw/article_content.php?getb=2&foog=9997) 4. [Steven Hu](https://steven-net.medium.com/) * 美術組 1. [Sketchfab](https://sketchfab.com/feed) 2. [SpeedTree](https://store.speedtree.com/) 3. [去背神器](https://www.remove.bg/zh-tw) 4. [UI Icon](https://www.flaticon.com/) 5. [Quadspinner](https://quadspinner.com/) 6. [Pinterest](https://www.pinterest.com/) 7. [ArtStation](https://www.artstation.com/?sort_by=community&dimension=all) 8. [Quixel](https://quixel.com/) * 音樂組 1. [小森平:常用音效](https://taira-komori.jpn.org/game01tw.html) 2. [甘茶音樂工坊:BGM](https://amachamusic.chagasi.com/music_uchiagehanabi.html) 3. [Text-to-Speech: Elevenlabs](https://elevenlabs.io/) 4. [音檔裁切](https://mp3cut.net/tw/) 5. [背景降躁](https://products.aspose.app/audio/zh-hant/remove-background-noise/mp3) * Youtube超好用教學 * Unity基礎 1. [Brackeys](https://www.youtube.com/@Brackeys) 2. [米飯教學室](https://www.youtube.com/watch?v=OZH7GSsLgaE) 3. [阿空](https://www.youtube.com/@RemptyGame/videos) * VR基礎 1. [Valem Tutorial](https://www.youtube.com/@ValemTutorials) 2. [Valem](https://www.youtube.com/@ValemVR)