---
# System prepended metadata

title: 113-1 Metaverse 課程講義
tags: [NYCU TA]

---

# 113-1 Metaverse 課程講義

# 雲端資料夾

[課程素材包](https://drive.google.com/drive/folders/1HzvexBlurS1gE15yS91XBQlEzpq2Qfa9)
[Google Drive: FruitNinja_VR_Tutorial](https://drive.google.com/file/d/1_mJaNeCo7-2mSqYG3ZbRdaBy_uXAWsRd/view?usp=sharing)

# 目錄
1. [創建URP專案](#1-創建URP專案)
2. [匯入場景、道具](#2-匯入場景、道具)
3. [產生UI與中文字幕](#3-產生UI與中文字幕)
4. [VR遊戲 vs 3D遊戲](#4-VR遊戲-vs-3D遊戲)
5. [Unity VR設定](#5-Unity-VR設定)
6. **VR角色基礎建置**
    6.1. [角色控制 (Camera、Hands、Move、Turn)](#61-角色控制)
    6.2. [場景互動 (Hover、Grab)](#62-場景互動)
    6.3. [UI](#63-UI)
    6.4. [VR Raycast](#64-Raycast)
    6.5. [Feedback (Vision、Haptic)](#65-Feedback)
7. [整理 Heirachy 視窗](#7-整理-Heirachy-視窗)
8. [互動與遊戲流程](#8-互動與遊戲流程)
    [8.0. XRCamp_Utilities部分功能介紹](#80-XRCamp_Utilities部分功能介紹-請先將-XRCamp_Utilities-匯入專案中)
    [8.1. [補充] Unity Event vs. Game Event](#81-補充-Unity-Event-vs-Game-Event)
9. [場景(Scene)切換](#9-場景Scene切換)
10. [為遊戲加分 (負責美術、音樂的同學注意！！！)](#10-為遊戲加分-美術、音樂組同學注意！！！)
    10.1. [為遊戲添加音樂吧！](#101-為遊戲添加音樂吧！) 
    10.2. [為場景增加一個會動的NPC吧！ (內含Sketchfab及Mixamo教學)](#102-為場景增加一個會動的NPC吧！)
    10.3. [Particle System (粒子系統)–某名人：「逆轟高灰！」](#103-Particle-System-粒子系統–「逆轟高灰」！)
    10.4  [Post Processing 提升畫面氛圍](#104-Post-Processing-提升畫面氛圍)
---
X. [其他 (內有Plastic SCM)](#x-其他)
X2. [注意與建議](#x2-注意與建議)


# 2024/09/12 Unity (VR) Fruit Ninja

## 目標

從頭開始，完整製作出一個 XR project (可銜接多人)。

預設已經先行完成Unity Learn - Misson 1，對Unity有基礎的認知。

TARGET： [Fruit Ninja VR - Gameplay Trailer | PS VR](https://www.youtube.com/watch?v=hPY4TRRHwZc)


## 所使用到的素材 (皆已放在課程素材包內)
1. [XR Camp Utilities](https://drive.google.com/drive/folders/1cN4LKXDMaVtaXzZxrDOUkjezCA6RT8QD?usp=sharing) -- 其中包含助教寫好，常用到的Script和工具
2. [Low-Poly Simple Nature Pack](https://assetstore.unity.com/packages/3d/environments/landscapes/low-poly-simple-nature-pack-162153)
3. [Low Poly 3D Medieval Weapon Pack](https://assetstore.unity.com/packages/3d/props/weapons/low-poly-3d-medieval-weapon-pack-186935)
4. [Fruits LowPoly Pack Lite](https://assetstore.unity.com/packages/3d/props/fruits-lowpoly-pack-lite-273980)
5. [super Mario warp pipe](https://sketchfab.com/3d-models/super-mario-warp-pipe-463aa0650b0b4ced9401f14ada0f22c6)
6. [Bomb](https://sketchfab.com/3d-models/bomb-1a9395dac5bc4742bdd780b3cce93416)
7. [Low-poly Ninja](https://sketchfab.com/3d-models/low-poly-ninja-54783c019894430bb163e02fff915fe9)
8. [FREE Skybox Extended Shader](https://assetstore.unity.com/packages/vfx/shaders/free-skybox-extended-shader-107400)
9. [Sketchfab SDK](https://assetstore.unity.com/packages/tools/input-management/sketchfab-for-unity-14302) -- 可以將從Sketchfab上下載之GLTF檔直接匯入Unity的工具


## 上課重點紀錄

:::success 
請善用瀏覽器的關鍵字搜尋功能：```Ctrl+F```
:::

[完整Unity專案檔](https://drive.google.com/file/d/1KG7SyJWF9JT5kpWhiUQRjZg92ZnOhjuv/view?usp=sharing)
  
### 1. 創建URP專案

* **創建專案**
    打開Unity Hub，建立一個空的Universal 3D Unity專案，版本請選擇2022.3.x以上的LTS版，並按下Create Project：
    ![image](https://hackmd.io/_uploads/r12fdzepA.png)

    如欲使用版本控制，請勾選右下角的"Use Unity Version Control"。


### 2. 匯入場景、道具
    
* 課程資源、網站介紹
    * 課程Discord頻道中的"綜合資源"欄位底下，有這門課[過去所購買的諸多素材](https://drive.google.com/drive/folders/1HzvexBlurS1gE15yS91XBQlEzpq2Qfa9)供同學免費使用，同時也提供了許多免費資源連結，請多加利用。
    * 這堂課所使用到的所有素材，都已列在[#所使用到的素材](#所使用到的素材-皆已放在課程素材包內)底下
* 將材質(Material)轉換成URP格式
    1. 當匯入的物件呈現粉紅色(無法正確顯示材質)，
    2. 請前往 Window -> Rendering -> Render Pipeline Converter
    3. 全部勾選，並點擊 Initialize And Convert
    
    ![image](https://hackmd.io/_uploads/BJFcPpbTp.png)
    

### 3. 產生UI與中文字幕

1. 創建一個UI->Text
![image](https://hackmd.io/_uploads/BkxmRmjp6.png)
2. 自行上網下載字體 (Windows預設字體在C:\Windows\Fonts)
3. 前往 Window -> Text Mesh Pro -> Font Asset Creator 產生客製化的字形圖檔。
![image](https://hackmd.io/_uploads/B1xdUTbap.png)
4. 將生成的檔案，丟入Text的"Font Asset"欄位中
![image](https://hackmd.io/_uploads/ryFcAQspT.png)

### 4. VR遊戲 vs 3D遊戲
* **沉浸感**—360全景、120FOV，遊玩時有「空間感」
* **人機互動**、互動**反饋**
* **不同視角**的體驗 (沙盒、上帝視角等等)
* 更多想像空間 (魔法、劍技、溶解等等)

### 5. Unity VR設定
* 下載Oculus並link至電腦

![image](https://hackmd.io/_uploads/S1Oxg4JRa.png)

* 下載XR Plugin Management

![image](https://hackmd.io/_uploads/r1ThcTHp6.png)

* 使用OpenXR (支援oculus&steamVR)

![image](https://hackmd.io/_uploads/SJ1fjar6T.png)

* Fix/Edit All warning rules

![image](https://hackmd.io/_uploads/HkCFiaBpT.png)

* 選擇使用的裝置Profiles (基本上這堂課是HTC vive/Oculus Touch)

![image](https://hackmd.io/_uploads/HJxKl4JRp.png)

* Package Manager>Unity Registry> **XR Interaction Toolkit** >Install，順便也Import **starter asset**


![image](https://hackmd.io/_uploads/r18mh6Hpa.png)

![image](https://hackmd.io/_uploads/ryiInTHTT.png)



### 6.1 角色控制

1. Camera
    * 右鍵Create-> XR-> XR Origin

    ![image](https://hackmd.io/_uploads/SkLA1PWRp.png)
    * Camera tracking mode: "floor"

      ![image](https://hackmd.io/_uploads/S15jV0B6a.png)

2. Hands(Controller)
    * 設置Controller Profile

    ![image](https://hackmd.io/_uploads/SySBevW0p.png)

    * 把跟Raycast相關的Components從Controller上拔掉

    ![image](https://hackmd.io/_uploads/HJ2jlDb0T.png)

    * 將Oculus Hands(已製作好捏、握的手勢動作)放到Controller底下，使玩家的Controller有模型

    ![image](https://hackmd.io/_uploads/HJI8-vbRp.png)

    * 上述步驟請在Left、Right Controller各做一次
3. Move
    * 在XR Origin的Inspector中新增：
        1. Locomotion System
        2. Character Controller(記得去調整角色的Height喔)
        3. Continuous Move Provider(**Action-based**)
           * 將**Use Gravity**打勾
           * Mode選用**Immediately**
           * Move Speed調至2~4左右即可(視人物、場景大小)
           * 在想給予移動功能的Hand上勾選Use Reference，並在下方貼入**XRI/Hand/Locomotion/Move**之Reference

    ![image](https://hackmd.io/_uploads/BkhsfwZRp.png)
4. Turn
    * 分為連續轉動(Continuous turn)與間斷轉動(Snap turn)，因現實世界的玩家都是使用連續轉動的關係，在遊戲中若使用Continuous turn的話容易導致動暈症的產生(轉動速度不一致)，因此會建議大家以Snap turn為主
    * 與新增Move的方法相似，新增**Snap Turn Provider(Action-based)**
    * 在想給予移動功能的Hand上勾選Use Reference，並在下方貼入**XRI/Hand/Locomotion/Snap turn**之Reference即可

    ![image](https://hackmd.io/_uploads/H1-s4w-06.png)
5. Jump
    * 在VR遊戲中不是那麼建議讓玩家有太過劇烈的動作(避免動暈)，因此Jump的功能在一般**VR劇情遊戲的專案內不會推薦**給大家使用。

### 6.2. 場景互動
1. 先介紹一下，在VR遊戲中，互動功能需要兩個角色--
    * Interactor：互動方，在XR Origin中便是兩個Controller(手)
    * Interactable：可被互動方，通常有XR Simple Interactable(**Hover、Select、Activate**)、以及XR **Grab** Interactable
2. 互動時，**會動的一方需要有Rigidbody**(剛體)，**雙方都要有Collider** (碰撞箱/偵測箱)
3. 先新增XR Direct Interactor、Sphere Collider至我們兩個Controller上，並**把Sphere Collider的Is trigger打開**(變成觸發箱而非碰撞箱)。另外，注意Sphere的Collider大小，大概調至0.1就可以了。

    ![image](https://hackmd.io/_uploads/BJLFkXgT0.png)


4. 將XR Grab Interactable新增到我們場景中的武器上
*     要記得把Knife的Rigidbody-Use Gravity打開喔，不然拿起放開後會飄來飄去XD
*     此時武器需要跟環境做"碰撞"，因此碰撞箱要打開(IsTrigger關閉)
*     XR Grab Interactable-Attach Transform可以設定為武器握柄的部分(另創一個Gameobject)，並調整角度到正確地拿武器姿勢即可。

    ![image](https://hackmd.io/_uploads/rJVNeQlTC.png)
    
5. [Movement type](https://fistfullofshrimp.com/unity-vr-grab-interactables/)
    * Instantaneous(較常用)：**無視物體的Rigidbody**，且**會穿透任何物體**，會一直黏在手上直到放開為止。
    * Kinematic ：會**有些許的延遲**(不會完全貼在手上)，且會被手的物理性質影響，**會穿透只要是沒有Rigidbody的物體**
    * Velocity Tracking(較真實)：**模擬真實**在拿物體的現象，且會與沒有Rigidbody的物體進行碰撞(也就是說**除了自己額外設定的物體以外都會進行碰撞**)、**不會穿透過去**。

6. 好啦！到目前為止，目前VR場景就有一個可以移動的玩家，以及一個可以拿起來丟來丟去的武器囉~ 
   如果還有時間的話，可以試試看把場景中的其他物件也變成可以互動的物件喔~ (**記得把static關掉**^^)

7. Q. 在互動中如何觸發event?
   A. 
   第一種方式，即是在Interactable Component底下的Interactable Events中，有Hover(碰觸)、Select(捏)、Activate(握)、Focus等等放Event的地方，也就是在物件上觸發。
   
   第二種方式，即是在Controller的Interactor上觸發，那就多了Haptic Event(震動反饋)可以設定。
   
   第三種方式(給那些Coding比較強，想自由一點的同學)，你當然可以使用OntriggerEnter、OnCollisionEnter等方式，利用Hand Contrller的Sphere Collider與物件的Collider碰撞時觸發Script自訂的程式。

    ![image](https://hackmd.io/_uploads/SJ1H3DW0p.png)

    ![image](https://hackmd.io/_uploads/r1PkkO-0a.png)

    ![image](https://hackmd.io/_uploads/HJHopPZRp.png)
### 6.3. UI
1. UI在VR專案中，基本上我們**不會將其放在頭盔眼前顯示**(像是3D遊戲，UI為Screen Space)，一方面是因為控制字幕、圖片在頭盔眼上的清晰度這件事是非常麻煩，每個人的感覺也差很多。
2. 另一方面也是因為使用者在遊玩的時候，頭盔會一直跟著玩家移動、旋轉，若視野底下一直有字幕等UI出現，會使**動暈症**的發生更加容易，體感變差。
3. 因此助教這邊推薦大家使用的是**World Space**的UI，透過有背景顏色的Panel、Image或是特效，上面附有你想要呈現的字幕，在VR頭盔中呈現的感覺會好很多 (記得Canvas的大小要重新設定，通常會小很多)

    ![image](https://hackmd.io/_uploads/HJc3eOZCa.png)

4. 取消原有的Graphic Raycaster，新增**Tracked Device Graphic Raycaster**，使UI在VR空間中能被使用者互動到(例如Button)

    ![image](https://hackmd.io/_uploads/HyezEbdb0T.png)

5. **VR專案中透過World Space的UI去提示玩家目前該怎麼做、要去哪裡，是很重要的元素喔！**

    ![image](https://hackmd.io/_uploads/HyIa-7eTC.png)
    ![image](https://hackmd.io/_uploads/H1PxfXgaR.png)


### 6.4. Raycast
1. XR > **Ray Interactor(Action-based)**，新增兩個(左、右)並取名

    ![image](https://hackmd.io/_uploads/H1WhbuWA6.png)

2. 一樣要設定兩隻手分別的Preset
3. 設定射線起點位置
![image](https://hackmd.io/_uploads/SyKBG7gT0.png)

5. 目前兩隻手的Ray便可顯示在我們的VR頭盔中，隨著我們的Controller移動，並也可以使用此Ray interactor來與場景得interactable物件互動喔！
6. Q. 如何讓我們的Raycast在想顯示的時候顯示呢? (加分項目)
   比方說按下Activate(捏，食指按紐)來打開Raycast，其他時候隱藏，使遊戲更完整
   A.
   * 增加Ray Able Controller至XR Origin中
   * 並把XRI/Hand/Activate(兩手)分別放到兩個Action Reference中
   * 將剛剛Create的兩個Ray Interactor放入Left Ray及Right Ray中即可

    ![image](https://hackmd.io/_uploads/B1ADzuWR6.png)

### 6.5. Feedback
1. Vision
    * 使用至於Camera前方的Block來改變視野，是在VR專案中常常使用在轉場動畫上的一個技巧
    * 將Prefab/FadeScreen加在XR Origin>Main Camera底下就好，然後要記得把FadeScreen的Shader改成**URP/Lit**喔！

    ![image](https://hackmd.io/_uploads/rygTqNubRT.png)

2. Haptic
    * 助教有提供Haptic Interactable的腳本，方便同學使用震動反饋來添增VR遊戲的互動性！
    * 使用方式：添增Script至兩個Controller上，貼好Controller Reference後便可在Game Event或是Unity Event使用此腳本的**HapticActiveOnce**函式來觸發震動
    * 若同學好奇在程式中是怎麼觸發的話，都可以點進去C#檔案查看腳本內容喔~
      助教提供的都是很基礎、常用的C#、Unity語法，特別是程式組的同學可以參考一下！

    ![image](https://hackmd.io/_uploads/H1QdtObAp.png)

    ![image](https://hackmd.io/_uploads/Sk7s77gTC.png)

   
### 7. 整理 Heirachy 視窗

* 雜亂的Heirachy不利於開發。
* 請善用Empty GameObject為其命名當作「抽屜」，將不同功能的GameObject分類以方便管理。
![image](https://hackmd.io/_uploads/rJHCX7ga0.png)


   
### 8. 互動與遊戲流程

我們的目標是實現以下動作：
1. 武器碰到水果時，手把震動並加分
2. 遊戲前、中、後，UI有所變化
3. Slice：https://www.youtube.com/watch?v=GQzW6ZJFQ94

### 8.0. XRCamp_Utilities部分功能介紹 (請先將 XRCamp_Utilities 匯入專案中)

> **XRCamp_Utilities中，包含了三種讓玩家與物體互動的方式：**
> * **TriggerOnClick**
    點擊滑鼠左鍵時，朝玩家畫面(MainCamera)面向方向射出一道射線，射線接觸到物體時觸發動作。
> * **TriggerOnCollision**
    物體與對應Layer的其他物體碰撞時觸發動作。
> * **TriggerOnKeyPress**
    玩家按下特定按鍵時觸發動作。
> 
> 以上互動的方式皆透過 Unity Event/Game Event觸發動作。


### 8.1. [補充] Unity Event vs. Game Event

#### Unity Event 使用方法：

1. 點擊Unity Event()底下的+號，即可添加要執行的動作即可。
![image](https://hackmd.io/_uploads/SJSD_Miaa.png)


#### Game Event 使用方法：

1. 先於Project視窗底下點擊右鍵，創建一個GameEvent
![image](https://hackmd.io/_uploads/ryNTtzsaa.png)
2. 在Heirachy中創建一個空的GameObject，為其添加"GameEventListener.cs"的Script。
![image](https://hackmd.io/_uploads/HkUq2fsTT.png)
3. 將創建的GameEvent拖曳至"On Event"欄位中，並於底下的Response添加想觸發的動作。 GameEventListener會監聽對應的GameEvent，並於GameEvent觸發時執行底下的動作。
![image](https://hackmd.io/_uploads/SJH1sfipT.png)


### 9. 場景(Scene)切換

1. 創建一個空物件"SceneManager"，並為其加上Script "Scene Transition"
![image](https://hackmd.io/_uploads/ryb_DQs6T.png)
2. 創建一個空物件"MoveToNextScene"，為其添加"Box Collider"、"Trigger On Collision"
    * Box Collider裡面的"Is Trigger"需要打勾
3. 使用"Trigger On Collision"，觸發SceneManager的轉場指令。
    * "GoToSceneAsync"所輸入的數字，為Scene在"File"->"Build Settings"中，Scenes in Build底下的數字編號。
    ![image](https://hackmd.io/_uploads/rJfHumoTT.png)
4. 為玩家人物"PlayerCapsule"創建一個新的Layer名為"Player"，以此作為Target Layer Mask。
5. 將MoveToNextScene移動到門的後方，使玩家走出門後觸發轉場。


### 10. 為遊戲加分 (美術、音樂組同學注意！！！)

自學/課前：
* [Mixamo](https://www.mixamo.com/#/)
* [AI: Text to Speech](https://elevenlabs.io/)
* [Particle System](https://www.youtube.com/watch?v=SrWrUN56UWU)

### 10.1.  為遊戲添加音樂吧！
1. 音效在Unity中主要是兩個角色在作用
    * Audio Listener：聆聽者，通常會自動加在Main Camera中
    * Audio Source：音源，可放入Audio Clip並調整類似音樂軟體的相關設定，也有空間音效(3D)的多項設定可以設置
2. 注意！！！ Unity基本上只接受**Mp3**以及**Wav**喔
3. 添加BGM (2D音效)
    * 在場景Manager物件中新增Audio Source的Component，並將你的Mp3/Wav放到Audio Clip裡面
    * 將Play On Awake、Loop打勾，遊戲中就有開啟時自動播放且循環播放的背景音啦！

    ![image](https://hackmd.io/_uploads/rkPR6_Z0T.png)
4. 添增音效 (3D音效)
    * 在想要產生聲音的物件(例如麥克風)上加上Audio Source，一樣將音檔放入Audio Clip中
    * 將Spatial Blend設為1，此時這個音源便是一個3D空間音效
    * 3D空間音效在Unity有幾個常用的設定：
        * Volume Rolloff：距離與聲音大小的關係式，預設是對數下降，其他也有像是線性下降的關係是可以選擇
        * Min Distance & Max Distance：Min~Max中間的空間則是可以聽到聲音的範圍，要注意這些數值會跟物體大小有關，因此物件若要有3D音效的話都要親自去做調整

    ![image](https://hackmd.io/_uploads/rymilFZ0p.png)

    * 在Scene視窗中也可以看到Min/Max Distance的範圍，如下圖即為(0.41 ~ 2)

    ![image](https://hackmd.io/_uploads/BJgfpQlpC.png)

    * Q. 如何在Unity Event中觸發音效播放?
      A.
      * 在Unity Event加入Animator.Play()的函示即可，若想在Script裡面控制播放Audio Clip的話，同樣也是使用Animator.Play()喔

      ![image](https://hackmd.io/_uploads/HkpH0Kb06.png)

         
>             public AudioClip[] ballClip;
>             public AudioSource ballSource;

>             ballSource.clip = ballClip[1];
>             ballSource.Play();

### 10.2.  為場景增加一個會動的NPC吧！
1. Sketchfab in Unity
    * 先去下載 [Sketchfab for Unity](https://assetstore.unity.com/packages/tools/input-management/sketchfab-for-unity-14302)
    * 在Unity即可直接載入GLTF模型檔 (若跳出Plugin Update就不用理他，直接關掉該提醒視窗就好)

    ![image](https://hackmd.io/_uploads/r1LnmYbAa.png)

2. Mixamo
    * 找到GLTF匯入Unity後檔案中的模型fbx檔，或是直接在Sketchfab上下載fbx也可(要人形完整的)
    * 匯入[Mixamo](https://www.mixamo.com/#/)

    ![image](https://hackmd.io/_uploads/Hyb8NYbAT.png)

    * 製作完動畫後，匯出成fbx，直接丟進去Unity就大功告成！(可以在匯入的model底下找到剛剛的Animation Clip)

    ![image](https://hackmd.io/_uploads/ryvTNYWCp.png)

3. Model
    * 大小-- 匯入的模型檔太大的話，可以改Scale Factor(先改這個為主，不要直接改場景的Scale，不然Scale數值會太大或太小)

    ![image](https://hackmd.io/_uploads/rJKrHtW0T.png)

    * 材質-- 若發現材質消失的話，可以試試看Use External Material或是Use Embedded Material，再不行的話就直接在Unity內創幾個Material然後重貼進來(適用在模型Material算少的時候)

    ![image](https://hackmd.io/_uploads/SJ1gUt-0a.png)

    * 動畫-- 通常fbx附加的動畫都不是Write able，所以要編輯Animation的一些性質，例如looping、Curve等等簡單的設定可以直接從這邊設定

    ![image](https://hackmd.io/_uploads/SkfYIKZ0a.png)

4. Animation (若同學是在Blender中製作後再匯入Unity的就可以先跳過^^)
    * 在Unity中，產生Animation主要也有兩個角色(怎麼都是兩個XD)--
        * Animation：包含整個動畫內容，時間軸上每個物件的屬性變化、時間軸上的事件觸發等等

        ![image](https://hackmd.io/_uploads/H1PX_YW0T.png)

        * Animator：包含Layer(同時執行不同的動畫流程，一心二用的感覺)、Parameter(控制流程的變數)、流程圖(包含Animation、Transition、Condition)等等

        ![image](https://hackmd.io/_uploads/Byv2dKWCT.png)

    * 把剛剛fbx底下的Animation Clip放入場景中的人物模型上，Unity就會自動生成Animator，並設計好遊戲開始變自動播放該Animation囉！

    ![image](https://hackmd.io/_uploads/SJdaDFW0p.png)
    

    * Q. 如何在Unity Event中觸發動畫播放?
      A.
      * 跟Audio source很像，加入Animator.Play()的函示即可，Play的名稱即為想播放的Animation名稱(在流程圖上的，非Clip名稱)

        ![image](https://hackmd.io/_uploads/rJxIKtZA6.png)

      * 設計簡單的流程圖，讓角色觸發完後回到原本的行為動畫

        ![image](https://hackmd.io/_uploads/BJue9Y-Aa.png)

     * **更多有關Animation、Animator的製作，在之後上課外聘講師也會介紹喔！**
### 10.3. Particle System (粒子系統)--「逆轟高灰」！
* 透過Light、PostProcessing、Model、UI的場景創建下，我們可以讓場景看起來更有氣氛

    ![image](https://hackmd.io/_uploads/S1e2pQl60.png)
    
* 在管子中加上向上的(Cone-Shape) Particle System，使水果拋上時有上升氣流的感覺
    
    ![image](https://hackmd.io/_uploads/rJZNJ4gpR.png)

* 在參數中可多使用Random between constants/curves使效果更自然

    ![image](https://hackmd.io/_uploads/BkIFJ4l6C.png)

* 適時增加點天氣效果，例如下雨、迷霧、打雷等等的特效，會使作品更加分！
    
    ![image](https://hackmd.io/_uploads/HJbHA7gpR.png)

* 配合自己的場景大小以及所需的雨量，可以分別在Emission以及Shape的Radius中調整喔

    ![image](https://hackmd.io/_uploads/HkZaitZCT.png)

* **更多有關Particle System的功能介紹，在3/23的Unity工作坊時講師也會介紹喔！**


### 10.4. Post Processing 提升畫面氛圍

* Global Volume
    1. 創建一個空的GameObject
    2. 在Inspector介面中，點選"Add Component" -> "Volume"
    3. 創建一個新的profile
    4. 點擊"Add Override"，為其添加各種濾鏡
    5. 使用Weight來設定權重，也可以利用此參數設計動態效果(例如受傷)

    ![image](https://hackmd.io/_uploads/B1jzlElaC.png)

* Local Volume
    1. 為這個GameObject添加一個Box Collider，限定濾鏡作用範圍
    2. 將"Volume"底下的Mode欄位，改為"Local"即可
    ![image](https://hackmd.io/_uploads/H1LCy4l6C.png)
    ![image](https://hackmd.io/_uploads/SksyeNxTR.png)

---

# 2024/09/19 Multiplayer Fruit Ninja VR

## 目標

承續上週的Fruit Ninja VR，教學如何使用Photon Pun快速轉換成多人版本，並介紹如何在Unity中多人合作與製作專案須注意的要素。

課前/自學：[How to Make a VR Multiplayer Game by Valem](https://www.youtube.com/watch?v=KHWuTBmT1oI&t=1698s)

## 上課重點紀錄

:::success 
請善用瀏覽器的關鍵字搜尋功能：```Ctrl+F```
:::

### 1. PUN 2
* 設定Photon Cloud Apps
    1. 登入(註冊)Photon帳號，進入設定畫面
    ![image](https://hackmd.io/_uploads/SJTO7eLaA.png)
    2. 建立新應用程式
    ![image](https://hackmd.io/_uploads/B1gfNlIpA.png)
    3. 應用類型「多人遊戲 -> SDK「Pun」 -> 應用名稱「自訂」
    ![image](https://hackmd.io/_uploads/rydCNlUpR.png)
* 加入PUN 2到Unity專案
    1. Asset store 搜尋 ["PUN 2"](https://assetstore.unity.com/packages/tools/network/pun-2-free-119922)，點擊「Open in Unity」 
    ![image](https://hackmd.io/_uploads/Hk1XxxUT0.png)
    2. 回到Unity Package Manager將PUN2 import
    ![image](https://hackmd.io/_uploads/BJgxblL6C.png)
    ![image](https://hackmd.io/_uploads/rJFUbeU6A.png)
    3. 匯入後自動跳出Pun Wizard或是可以在window標籤中找到
    ![image](https://hackmd.io/_uploads/BkwN8g8aR.png)
    ![image](https://hackmd.io/_uploads/r1GxPe8TR.png)
    4. 將先前建好的PUN App ID貼上後Setup Project，將自動找到Photon Server Settings並填入App ID
    ![image](https://hackmd.io/_uploads/B1cOOgIaR.png)
    ![image](https://hackmd.io/_uploads/H1b6uxITC.png)
* [PUN 2 API reference](https://doc-api.photonengine.com/en/pun/current/index.html)
    善用官方的API reference可以快速知道每個class、function的使用方法、層級關係
    ![image](https://hackmd.io/_uploads/rJ6wXZUaC.png)

    

### 2. Connect to Server
1. 新增一個C# script "connectToServer.cs"
    ```c#
        using UnityEngine;
        using Photon.Pun;
        using Photon.Realtime;

        public class ConnectToServer : MonoBehaviourPunCallbacks
        {
            void Start()
            {
                Debug.Log("Connecting...");
                PhotonNetwork.GameVersion = "0.0.1";
                PhotonNetwork.ConnectUsingSettings();
            }
            public override void OnConnectedToMaster()
            {
                Debug.Log("Connected to Server");
                PhotonNetwork.JoinLobby();
            }
            public override void OnDisconnected(DisconnectCause cause)
            {
                Debug.Log("Disconnected from Server for reason " + cause.ToString());
            }
        }

    ```
2. 在場景中新增一個Empty object，將"connectToServer.cs"拉到component中
    ![image](https://hackmd.io/_uploads/SJvDZZITA.png)
3. 如果目前為止的設定都正確，Play之後可以在console看到以下Debug log
    ![image](https://hackmd.io/_uploads/HkB6bbU6C.png)
### 3.1. 創建房間 - XRI Keyboard
* XRI keyboard
    1. import
    ![image](https://hackmd.io/_uploads/ByCUEGIaR.png)
    2. 將XRI Global Keyboard Manager加到場景，並且把XR Origin拉到player root
    ![image](https://hackmd.io/_uploads/B1aBrz86C.png)
    3. 使用Samples -> XR Interaction Toolkit -> 版本號 -> Spatial Keyboard -> Prefabs中的Input Field Global Keyboard來新增Input Field
    ![image](https://hackmd.io/_uploads/SJ-wIzUTA.png)
### 3.2. 創建房間 - 列出、加入房間
* **RoomListing.cs** - 綁在顯示房間名稱的按鈕prefab上，將會由"RoomListingMenu.cs"根據房間的列表生成與房間個數相同的prefab

    ```c#
    using UnityEngine;
    using Photon.Realtime;
    using Photon.Pun;
    using TMPro;

    public class RoomListing : MonoBehaviour
    {
        [SerializeField]
        private TMP_Text _text;
        public RoomInfo RoomInfo { get; private set; }
        public void SetRoomInfo(RoomInfo info)
        {
            RoomInfo = info;
            _text.text = info.Name;
        }

        public void OnClick_JoinRoom()
        {
            Debug.Log("Joining Room: " + RoomInfo.Name);
            PhotonNetwork.JoinRoom(RoomInfo.Name);
        }
    }

    ```
    * **public void SetRoomInfo(RoomInfo info)** - 由"RoomListingMenu.cs"傳入RoomInfo來更改顯示房名
    * **public void OnClick_JoinRoom()** - 當JoinRoom按鈕被處時調用
![image](https://hackmd.io/_uploads/HJa3GEwa0.png)
* RoomListingMenu.cs - 繼承MonoBehaviourPunCallbacks類，覆寫Photon提供的Callback function，用來控制UI的房間清單中RoomList Prefab的生成與消滅
    ```c#
    using System.Collections.Generic;
    using Photon.Pun;
    using UnityEngine;

    public class RoomListingMenu : MonoBehaviourPunCallbacks
    {
        [SerializeField]
        private RoomListing _roomListingPrefab;
        [SerializeField]
        private Transform _content;

        private List<RoomListing> _listings = new List<RoomListing>();

        public override void OnRoomListUpdate(List<Photon.Realtime.RoomInfo> roomList)
        {
            foreach (Photon.Realtime.RoomInfo info in roomList)
            {
                if (info.RemovedFromList)
                {
                    int index = _listings.FindIndex(x => x.RoomInfo.Name == info.Name);
                    if (index != -1)
                    {
                        Destroy(_listings[index].gameObject);
                        _listings.RemoveAt(index);
                    }
                }
                else
                {
                    int index = _listings.FindIndex(x => x.RoomInfo.Name == info.Name);
                    if (index == -1){
                        RoomListing listing = Instantiate(_roomListingPrefab, _content);
                        if (listing != null)
                        {
                            listing.SetRoomInfo(info);
                            _listings.Add(listing);
                        }
                    }
                }
            }
        }
    }

    ```
    >  標記為 removeFromList 的房間不會立即從伺服器上刪除，避免瞬間的改變在其他客戶端出現混亂或不一致的狀態，以及性能考量
    >  
    * OnRoomListUpdate(List<Photon.Realtime.RoomInfo> roomList) : 當RoomList的內容變化時被調用，回傳有房間資訊的List
    
    
    ![image](https://hackmd.io/_uploads/BkLHI4Pa0.png)

### 3.3. 創建房間 - 載入新場景
* 當加入或創建房間之後，顯示當前房間的面板
    1. 創建房間 - CreateRoom.cs / OnCreatedRoom()
    ```c#
    using UnityEngine;
    using Photon.Pun;
    using Photon.Realtime;
    using TMPro;

    public class CreateRoom : MonoBehaviourPunCallbacks
    {
        [SerializeField]
        private TMP_Text _roomName;
        public void OnClick_CreateRoom()
        {
            if (!PhotonNetwork.IsConnected)
                return;
            RoomOptions roomOptions = new RoomOptions();
            roomOptions.MaxPlayers = 2;
            PhotonNetwork.JoinOrCreateRoom(_roomName.text, roomOptions, TypedLobby.Default);
        }

        public override void OnCreatedRoom()
        {
            Debug.Log("Room Created Successfully", this);
            PhotonNetwork.LoadLevel(1);
        }

        public override void OnCreateRoomFailed(short returnCode, string message)
        {
            Debug.Log("Room Creation Failed: " + message, this);
        }
    }

    ```
### 3.4. 房間 - 玩家同步至host場景
* PhotonNetwork.AutomaticallySyncScene = true - 可以使client自動轉移到與host相同的場景

### 4.1. 網路物件
* Photon View
    > * 唯一識別
    > * 物件同步
    > * ownership
    > * 監聽網路事件
    ![image](https://hackmd.io/_uploads/SJ994wKaC.png)

     
    
    * Photon Transform View (Classic)
        > * 同步位置、旋轉、大小
        > * 處理網路同步中的不連續 (interpolation)
        > * 預測物體位置 (Extrapolation)
        ![image](https://hackmd.io/_uploads/H1lswvYpA.png)


    * Photon Rigidbody View
        > * 同步速度和角速度，使運動符合物理規則。
        > * 預測、插值
    * Photon Animation View
        > * 同步動畫**參數** (trigger容易丟失)
        > * 平滑過度
### 4.2. 網路物件 - 玩家
* 生成玩家要解決的問題:
> 1. 一個場景只能有一個Camera
> 2. 只能有一個Audio Listener
> 3. 只能操控自己的player prefab
> 4. photon rigidbody view必須綁一個rigidbody，但是player controller組件也有rigidbody效果，所以必須拔掉
* 解決方法
> 1. 在Player prefab中先把Camera、Audio Listener、Controller除model外的東西、Locomotion相關scripts、轉場用的FadeScreen預設關掉
> 2. 在載入場景的時候將自己的player prefab中這些東西打開
![image](https://hackmd.io/_uploads/HJ1mAwYa0.png)
* **LocalPlayerSetup.cs**
    ```c#
        using System.Collections.Generic;
        using Photon.Pun;
        using UnityEngine;

        public class LocalPlayerSetup : MonoBehaviourPunCallbacks  
        {
            public List<Component> componentsToEnable; 
            public List<GameObject> gameObjectsToEnable;

            public void Setup()
            {
                if (photonView.IsMine)
                {
                    foreach (Component component in componentsToEnable)
                    {
                        if(component != null)
                        {
                            Behaviour behaviour = component as Behaviour;
                            if (behaviour != null)
                            {
                                behaviour.enabled = true;
                            }
                            else
                            {
                                Renderer renderer = component as Renderer;
                                if (renderer != null)
                                {
                                    renderer.enabled = true;
                                }
                            }
                        }
                    }

                    foreach (GameObject gameObject in gameObjectsToEnable)
                    {
                        if(gameObject != null)
                        {
                            gameObject.SetActive(true);
                        }
                    }
                }
            }
        }
    ```
### 4.3. 網路物件 - 生成
* 修改ConnectToServer.cs
    ```c#
    using UnityEngine;
    using Photon.Pun;
    using Photon.Realtime;
    using UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard;
    using UnityEngine.SceneManagement;

    public class ConnectToServer : MonoBehaviourPunCallbacks
    {
        private GameObject singlePlayer;
        public GameObject playerPrefab;
        public GameObject singlePlayerPrefab;
        public Transform spawnPoint;
        private GameObject _player;
        public GlobalNonNativeKeyboard keyboardManager;
        private int sceneIndex;
        private Scene currentScene;

        void Start()
        {
            PhotonNetwork.AutomaticallySyncScene = true;
            PhotonNetwork.AddCallbackTarget(this);

            currentScene = SceneManager.GetActiveScene();
            sceneIndex = currentScene.buildIndex;
            Debug.Log("Scene Index: " + sceneIndex);
            if(sceneIndex == 0)
            {
                Debug.Log("Connecting...");
                PhotonNetwork.GameVersion = "0.0.1";
                PhotonNetwork.ConnectUsingSettings();
                InstantiateSinglePlayer();
            }else if(sceneIndex == 1)
            {
                Debug.Log("Joining Room...");
                _player = PhotonNetwork.Instantiate(playerPrefab.name, spawnPoint.position, spawnPoint.rotation);
                _player.GetComponent<LocalPlayerSetup>().Setup();
                PhotonNetwork.LocalPlayer.NickName = "Player " + PhotonNetwork.CurrentRoom.PlayerCount;
            }
        }
        public override void OnConnectedToMaster()
        {
            Debug.Log("Connected to Server");
            if(!PhotonNetwork.InLobby)PhotonNetwork.JoinLobby();
        }
        public override void OnDisconnected(DisconnectCause cause)
        {
            Debug.Log("Disconnected from Server for reason " + cause.ToString());
        }

        public void InstantiateSinglePlayer()
        {
            singlePlayer = Instantiate(singlePlayerPrefab, spawnPoint.position, spawnPoint.rotation);
            keyboardManager.playerRoot = singlePlayer.transform;
            keyboardManager.cameraTransform = singlePlayer.GetComponentInChildren<Camera>().transform;
        }
    }
    ```
> * 生成網路物件 (使用prefab name，prefab必須含Photon View並放在Assets/Resources)
>    * PhotonNetwork.Instantiate(playerPrefab.name, spawnPoint.position, spawnPoint.rotation);
    
> * 生成一般物件
>    * Instantiate(singlePlayerPrefab, spawnPoint.position, spawnPoint.rotation);
### 5. RPC (Remote Procedure Calls)
* 使用時機
    1. 玩家間狀態同步的主要手段之一(血量、分數等)
    2. 某個行為觸發的事件需要立即通知多位玩家
    3. 需要由伺服器或MasterClient決策的邏輯處理
    4. 針對特定玩家的通訊(私訊、交易等)
* 程式範例(加分)
    ```c#
    public void AddScore(string playerName)
        {
            Debug.Log("AddScore by fruit");
            GetComponent<PhotonView>().RPC("RPC_AddScore", RpcTarget.AllBuffered, playerName);
        }

        [PunRPC]
        public void RPC_AddScore(string playerName)
        {
            Debug.Log("RPC_AddScore by fruit, playerName: " + playerName);
            NetworkGameManager networkGameManager = GameObject.Find("NetworkGameManager").GetComponent<NetworkGameManager>();
            networkGameManager.AddScore(playerName, fruitScore);
        }
    ```
    > * 當AddScore(string playerName)被調用，透過掛載此腳本的物件上的PhotonView來調用RPC，其他client的相同view ID物件上的RPC_AddScore(string playerName)將會被調用
    > * RPC function 需要在前面加上[PunRPC]標籤
* 使用方式
    * RPC(string methodName, **RpcTarget target**, params object[] parameters)
        > target指定傳送RPC的方式與對象，詳見下一部分RPC Target說明
        > parameters的數量為要傳入所調用的RPC function的參數數量
    * RPC(string methodName, **Player targetPlayer**, params object[] parameters )	
        > 此方法允許你在指定玩家的客戶端上進行 RPC 調用。當然，調用會受到本客戶端和遠端客戶端的延遲影響。
        > targetPlayer傳入指定玩家的Player類，可以透過Player Prefab上的PhotonView中的Controller或Owner兩種Properties來取得


* RPC Target
    1. All
    > 將 RPC 發送給所有其他玩家，並立即在本客戶端執行。後來加入的玩家將不會執行此 RPC。
    2. Others

    > 將 RPC 發送給所有其他玩家。本客戶端不執行該 RPC。後來加入的玩家將不會執行此 RPC。
    3. MasterClient
    > 將 RPC 僅發送給 MasterClient。請注意：MasterClient 可能會在執行該 RPC 之前斷開連接，這可能會導致 RPC 丟失。
    4. AllBuffered
    > 將 RPC 發送給所有其他玩家，並立即在本客戶端執行。新玩家加入時會收到此 RPC，因為它被緩存（直到本客戶端離開）。
    5. OthersBuffered
    > 將 RPC 發送給所有其他玩家。本客戶端不執行該 RPC。新玩家加入時會收到此 RPC，因為它被緩存（直到本客戶端離開）。
    6. AllViaServer
    > 通過伺服器將 RPC 發送給所有玩家（包括本客戶端）。本客戶端像其他客戶端一樣在從伺服器接收到該 RPC 時執行它。優點：伺服器發送 RPC 的順序在所有客戶端上是相同的。
    7. AllBufferedViaServer
    > 通過伺服器將 RPC 發送給所有玩家（包括本客戶端），並為後來加入的玩家緩存它。本客戶端像其他客戶端一樣在從伺服器接收到該 RPC 時執行它。優點：伺服器發送 RPC 的順序在所有客戶端上是相同的。


### 6. RaiseEvent
* 使用時機
    1. 類似於RPC，為玩家間狀態同步的主要手段之一(分數、金錢等)
    2. 可以使用在未綁定PhotonView的對象上(較RPC更靈活)
    3. 非特定對象的事件，例如:天氣變化、難度調整
* 程式範例(Fruit Ninja VR沒有使用到RaiseEvent，以官方文件範例說明)
    ```c#
    using ExitGames.Client.Photon;
    using Photon.Realtime;
    using Photon.Pun;

    public class SendEventExample
    {
        // If you have multiple custom events, it is recommended to define them in the used class
        public const byte MoveUnitsToTargetPositionEventCode = 1;

        private void SendMoveUnitsToTargetPositionEvent()
        {
            object[] content = new object[] { new Vector3(10.0f, 2.0f, 5.0f), 1, 2, 5, 10 }; // Array contains the target position and the IDs of the selected units
            RaiseEventOptions raiseEventOptions = new RaiseEventOptions { Receivers = ReceiverGroup.All }; // You would have to set the Receivers to All in order to receive this event on the local client as well
            PhotonNetwork.RaiseEvent(MoveUnitsToTargetPositionEventCode, content, raiseEventOptions, SendOptions.SendReliable);
        }
    }
    ```
    > 此程式將MoveUnitsToTargetPosition這個事件的EventCode設為1, 並傳送了Vector3(10.0f, 2.0f, 5.0f), 1, 2, 5, 10以上五項Objects，ReceiverGroup是All，使用Reliable方式傳送

* 使用方式
    * 發起事件
        * RaiseEvent(byte eventCode, object eventContent, RaiseEventOptions raiseEventOptions, SendOptions sendOptions)
            > Eventcode須介於0-199間，但避免使用0較好，大約有200自定義事件可以使用
            > eventContent須為可序列化的對象，例如string, byte, integer, float等，以及他們的array，或者這些類型的數組。使用byte作為key的Hashtable也可以
            > [raiseEventOption](https://doc-api.photonengine.com/en/pun/current/class_photon_1_1_realtime_1_1_raise_event_options.html#a1e93c8a0af49774c4da13c1ca93f04d5)可以做一些客製化設定例如能夠接收此事件的群組
            > sendOptions用於設置可靠性、加密等選項的傳送選項。
    * 接收事件
        * 方法1 - 實作IOnEventCallback的OnEvent()
            ```c#
            using ExitGames.Client.Photon;
            using Photon.Realtime;
            using Photon.Pun;

            public class ReceiveEventExample : MonoBehaviour, IOnEventCallback
            {
                private void OnEnable()
                {
                    PhotonNetwork.AddCallbackTarget(this);
                }

                private void OnDisable()
                {
                    PhotonNetwork.RemoveCallbackTarget(this);
                }

                public void OnEvent(EventData photonEvent)
                {
                    byte eventCode = photonEvent.Code;
                    if (eventCode == MoveUnitsToTargetPositionEvent)
                    {
                        object[] data = (object[])photonEvent.CustomData;
                        Vector3 targetPosition = (Vector3)data[0];
                        for (int index = 1; index < data.Length; ++index)
                        {
                            int unitId = (int)data[index];
                            UnitList[unitId].TargetPosition = targetPosition;
                        }
                    }
                }
            }
            ```
            > 1. 需要PhotonNetwork.AddCallbackTarget(this)
            > 2. 需要繼承IOnEventCallback
            > 3. 必須實作OnEvent()
        * 方法2 - 註冊function到PhotonNetwork.NetworkingClient.EventReceived
            ```c#
            using ExitGames.Client.Photon;
            using Photon.Realtime;
            using Photon.Pun;

            public class ReceiveEventExample : MonoBehaviour
            {
                private void OnEnable()
                {
                    PhotonNetwork.NetworkingClient.EventReceived += OnEvent;
                }

                private void OnDisable()
                {
                    PhotonNetwork.NetworkingClient.EventReceived -= OnEvent;
                }

                private void OnEvent(EventData photonEvent)
                {
                    byte eventCode = photonEvent.Code;
                    if (eventCode == MoveUnitsToTargetPositionEvent)
                    {
                        object[] data = (object[])photonEvent.CustomData;
                        Vector3 targetPosition = (Vector3)data[0];
                        for (int index = 1; index < data.Length; ++index)
                        {
                            int unitId = (int)data[index];
                            UnitList[unitId].TargetPosition = targetPosition;
                        }
                    }
                }
            }
            ```
            > 1. 必須註冊接收事件的function

    




---

# 2024/11/07 Multi-Final IK

## 目標

OpenXR + Final IK + PhotonStream
製作多人系統中的Full Body Avatar


## 所使用到的素材 (皆已放在課程素材包內)
1. [Final IK](https://www.dropbox.com/scl/fo/hvt3vmuju6xmbufl9lns7/AMexg822sFl3cOEZ6nmi8Xo?e=1&preview=Final+IK+JiaJun.unitypackage&rlkey=qtk39lhtjyt6dbnwiuwtdh4ct&st=zcdlxvhp&dl=0)


## 上課重點紀錄


---

# 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)










