# 玩轉 Unity ## 什麼是遊戲引擎? 所謂遊戲引擎是將製作遊戲所需要的許多功能,打包成一個工具,省略許多開發步驟,讓開發者更加專注在遊戲設計上。 ### Unity Untiy 有幾個優點: 1.網路上學習資源多。 2.Unity 官方有持續維護更新自家引擎。 3.Unity 支援多平台開發遊戲,例如桌機、手機、家用主機等。 ## Unity 介面教學 - Hierarchy 用於顯示場上所有物件的階層關係 - Scene 場景,編輯、排列物品。 - Game 是測試、操作遊戲用。 - Project 顯示專案用到的資源。 - Inspector 偵錯工具。 ## 父物件、子物件及 Prefab ### 父物件、子物件 - component SpriteRenderer 為 2D 物件的渲染器,能針對圖案做許多設定 - SpriteRenderer 中有一個 Order in Layer 選項,可以調節圖層 - Ctrl 鍵壓住,可以在 Hierarchy 視窗中多選務件,一口氣做 Component 變更 - 件立物件的父子關係,讓其他物件附著在指定物件上。在 Hierarchy 中,選取子物件並拖曳到父物件上面,即可件立父子關係 ### Prefab - Prefab 定義為預先製作好的物體 - 只要將已經在 Hierarchy 中做好的新物件組拉回 project 內的 Asset 資料夾中,就會成為 Prefab - 即使刪除了場上的物件組,你也可以在拉出 Prefab 來代替 - 物件重複使用度高的時候,建議製成 Prefab ## 建立 Script & C# Script ```csharp= public class ClassTest : MonoBehaviour { // Use this for initialization void Start () // 只有在遊戲開始時執行一次 { Debug.Log('Hello Game'); } // Update is called once per frame void Update () //Update 內的內容,每一次遊戲回圈都會運行一次(前提是該腳本的附著物與腳本都是啟用的) { Debug.Log('Update'); } } ``` ## 類別、屬性與方法簡介 ### 腳本 - 每一個創造出來的腳本,都是一個 Class (類別) ### Class 類別 最主要的構成元素: - 屬性:靜態、可倍數值化符號化的 (我有些什麼)=> 在類別內會使用到的物件 - 方法:動態、該物體可執行的事項 (我能夠做些什麼)=> 將我們想讓此類別做的事情在方法內描述 Example: Class 路人甲 - 屬性:身高、體重、年齡、智商 - 方法:睡眠、行走、閱讀、減肥 ### 屬性會在方法內做調用 Example: 當使用"投彈"(方法)時,會使"機上飛彈數量"(屬性)減少、"重量"(屬性)減少。 ### 方法內可以包含其他方法 Example: "出任務"(方法)包含了"起飛"(方法)+"飛行"(方法)+"投彈"(方法)+"降落"(方法) ### 撰寫腳本前 - 想好腳本能做些什麼(方法) - 想好腳本會用到什麼(屬性) ## 變數宣告 變數需要經過宣告才能使用,包含最基礎的變數型別(種類)和識別字(名稱)。 浮點數:變數型別之一,小數。 ### 常用變數種類 System 內建 - int 整數範圍(-2147483648~2147483647),long 長整數 - float 浮點數(double倍精確數) - string 字串 - bool 布林 Unity Engine 內建 - GameObject 遊戲物件 ### 宣告的格式 - 存取修飾詞(決定誰可以對此變數做修改) - 資料型別(此變數的類型) - 變數名稱(此變數的名稱) #### 存取修飾詞 共有五種,從 C# 7.2 版起有六種,這邊只提兩種: - public 所有腳本皆可使用 - private 只有此腳本可以使用 #### 宣告位置 在 class 內宣告,或是方法內。 #### Box Script 製作 ```csharp= public class Box : MonoBehaviour { //高度 寬度 float public float Height; public float Width; //名字 public string BoxName; //盒子的開盒狀態 public bool IsOpen; } ``` ## 變數賦值 float 賦值: float B = 3.14f; (需要加 f 從 double 轉型到 float,C#預設沒打 f 就是 double,double可以到小數點後非常多位) ### Box 賦值範例 ```csharp= public class Box : MonoBehaviour { //高度 寬度 float public float Height; public float Width; //名字 public string BoxName; //盒子的開盒狀態 public bool IsOpen; private void Start() { Height = 100; Width = 50; BoxNam = '公事包'; IsOpen = true; } } ``` ## 迴圈 遞增遞減運算子 ```csharp= // 先執行 X=a之後執行 a = a+1 X=a++; // 先執行 a = a+1 之後執行 X=a X=++a; ``` for 迴圈 ```csharp= fot(int i =0; i<2; i++) { Debug.Log(i); } ``` ### While 條件達成時就執行 ```csharp= while (Weight < 100) { Debug.Log( "我的體重是" + Weight); Weight++; } Debug.Log("太胖啦"); ``` ## 方法 Example: ```csharp= public class spaceShip : MonoBehaviour { public float height; public float bottom; public float area; // Start is called before the first frame update void Start() { area = triangle(height, bottom); } public float triangle (float height, float bottom) { float tempArea; tempArea = bottom * height / 2; return tempArea; } } ``` ## 青蛙射擊(遊戲流程規劃+架構解說) 玩家操作一隻坐在噴射板上的青蛙,青蛙會受地心引力牽扯向下,點擊滑鼠左鍵會消耗一點能量使其噴射向上,點擊滑鼠右鍵會消耗兩點能量射出美乃滋攻擊阻礙物,失敗三次後遊戲結束。 補充能量: 1.吃場地上的美乃滋+1點。 2.擊落阻礙物補滿能量。 失敗條件: 1.撞擊到阻礙物。 2.墜地。 ### 遊戲流程 ### 程式架構 GameManager 遊戲主流程: 1.物件生成 - BackgroundController(背景) - SpawnMonster(怪物) - SpawnEnergy(能量球) 2.物件控制 - PlayerController(角色) - MonsterController(怪物) - BulletController(子彈) 3.其他 - DelayDestroy DeadSpace(物件銷毀相關) - MoveObject(移動物件) - TimerMachine(計時器) - AudioPlayer_Monster(怪物音效) ### 靜態方法 不需要宣告,也不需要賦值即可使用的方法 靜態方法的使用方式是 類別名.靜態方法名(); ex: Debug.Log(); ```csharp= //在 Update 內使用 if(Input.GetMouseButtonDown(0))//0左鍵 1右鍵 2中鍵 { //如果按下滑鼠按鍵瞬間要做的事情 } //在 Update 內使用 if(Input.GetMouseButton(0))//0左鍵 1右鍵 2中鍵 { //只要按著滑鼠左鍵就會反覆觸發的事情 } ``` ## Collider 2D(碰撞器)、Rigidbody 2D(剛體) - Collider 2D 控制碰撞的範圍及效果 - Rigidbody 2D 賦予物件物理效果模擬,且能做多項調整 ## OnTrigger function - OnTriggerEnter 2D 撞入的一瞬間 - OnTriggerStay 2D 重疊時期 - OnTriggerExit 2D 離開的一瞬間 ```csharp= public void OnTriggerEnter2D(Collider2D collision) { if(collision.tag == "floor") { Debug.Log("撞到地板"); } } ``` ### Player function 1.Jump ```csharp= public Rigidbody2D RB; // Update is called once per frame void Update() { } //Jump public void Jump() { //x為0(水平0),y增加為8(向上8)。 RB.velocity = new Vector2(0, 8); } ``` 2.Prepare ```csharp= public Rigidbody2D RB; public Vector2 StartPosition; // Update is called once per frame void Update() { } //Prepare public void Prepare() { //速度為0 RB.velocity = Vector2.zero; //調整為運動學控制 RB.bodyType = RigidbodyType2D.Kinematic; //回到初始的位置 gameObject.transform.position = StartPosition; } ``` 3.Go ```csharp= public void Go() { //回歸受重力影響 RB.bodyType = RigidbodyType2D.Dynamic; } ``` ## 動畫 - 動畫 單獨動作的動態 - 動畫機 整合多個動畫,決定其觸發時機點以及播放邏輯 青蛙方面要製作 4 個動畫 1.待機 2.跳躍 3.掉落中 4.受傷 ### 動畫機開啟 - 新建動畫機流程: project 區域右鍵 create => Animation Controller,然後將動畫機黏至父物件 - 新建動畫流程: project 區域右驗 create => Animation - 錄製動畫: window => Animation => Animator ```csharp= public class PlayerController : MonoBehaviour { public Rigidbody2D RB; public Vector2 StartPosition; public Animator Anim; //建立一個 Anim 變數,型別為 Animator // Update is called once per frame void Update() { if(Input.GetMouseButtonDown(0)) { Jump(); //Prepare(); //Go(); } } //Go public void Go() { //回歸受到重力影響 RB.bodyType = RigidbodyType2D.Dynamic; //開始遊戲設定為"Fall" Anim.SetTrigger("Fall"); //呼叫 Anim,抓取裡面的trigger。 } } ``` ## 粒子系統 選擇指定的 gameObject,右鍵 Effects,選particle system。 - Duration 噴發多久 - Loop 重複播放 - Start LifeTime 代表每一顆粒子生成後的存活時間 Emission: - Rate over time 粒子的噴發量 Size over lifetime: - 每一顆粒子從出現到消失期間,大小的變化。 Play On Awake: 一開始就會先噴一次。 ### Material Create => Material 自己建立粒子: - Shader => Standard => albedo => 選擇想要的圖片=> Sprites => Default ## 創建怪獸 Create 區域右鍵按 Create empty 建立 Game Object ### 讓怪獸以自訂速度水平移動 創建怪獸後,建立一個 script,這個 class 要做的事情是讓怪獸水平移動。 完成怪獸的 prefab 後,記得按 apply 更新。Prefab 完成後,就可以將場上原本製作的 object 移除。 ```csharp= public class MoveObject : MonoBehaviour { //想移動的物體 public GameObject Obj; //自訂速度 public float Speed; // Update is called once per frame void Update() { //(Speed * Time.deltaTime, 0, 0) 中,前面第一組數據(Speed * Time.deltaTime)代表 X軸 //,後面兩組0表示YZ兩軸,這邊我們只需要怪物往左移動,故只移動X軸。 //Time.deltaTime 是每一幀所花的時間,每一個畫面所花的時間。由於每個人的電腦效能不同,每秒能跑的幀數可能也不同。 //Time.deltaTime 可以確保在不同裝置上的速度一樣。 Obj.transform.position += new Vector3(Speed * Time.deltaTime, 0, 0); } } ``` ### 怪物生成點 ![](https://i.imgur.com/Hyi8bYB.png) 先創建一個新 gameobject,命名為 SpawnMonster,並且 reset。 ![](https://i.imgur.com/0ImiUZV.png) 在 SpawnMonster 中創建5個 object,並定義為怪物生成點。 創建一個 SpawnMonster script。 ```csharp= //產生位置 public Transform[] SpawnPosition; // monster prefab dog and cat public GameObject MonsterPop; public GameObject MonsterUnya; // Start is called before the first frame update void Start() { InvokeRepeating("Spawn", 0, 1.5f);//InvokeRepeating("Spawn", 0, 3) 呼叫 Spawn 方法,從第 0 秒開始,每 3 秒呼叫一次。 } public void Spawn() { Vector3 tempSpawnPos; //此次決定要生成怪物的位置 tempSpawnPos = SpawnPosition[ Random.Range(0, 5) ].position; int temp = Random.Range(1, 100); GameObject newMonster; if(temp >= 80) { //20% create cat //Instantiate(物件 位置 旋轉) 物件:GameObject 位置:Vector3 旋轉量:Quaternion newMonster = Instantiate(MonsterUnya, tempSpawnPos, Quaternion.identity); } else { //80% create dog newMonster = Instantiate(MonsterPop, tempSpawnPos, Quaternion.identity); } } ``` ### 遊戲空間設定 ![](https://i.imgur.com/KSzEzpy.png) ## 計時器製作 功能: - 歸零 - 開始計時 - 暫停 建立一個計時器腳本 ```csharp= public class TimerMachine : MonoBehaviour { //當前秒數 public float Timer; //控制 Timer 開始暫停 public bool AllowRun; void Update() { if (Input.GetMouseButtonDown(0)) { Run(); } else if(Input.GetMouseButtonDown(1)) { Init(); } if (AllowRun) { Timer += Time.deltaTime; } } //初始化 歸零 public void Init() { Timer = 0; AllowRun = false; } //讓碼表開始計時 public void Run() { AllowRun = true; } //暫停 public void Pause() { AllowRun = false; } } ``` ## 回合流程管理 ![](https://i.imgur.com/G8fSyB8.jpg) ### Enum 枚舉 當一個東西有特定的多種狀態時,可以考慮用 Enum 做列舉,ex: 角狀狀態:健康/中毒/麻痺/無敵 ```csharp= public Enum status(){ 健康, 中毒, 麻痺, 無敵 } ``` 寫成 Enum 在 Unity 上會變成下拉選單,可以搭配 Switch 使用。 首先創建一個 GameManager 腳本,然後在場上建立一個 GameManager 遊戲物件,並將 GameManager 腳本放到上面 ```csharp= //只有 Enum 可以宣告在 Class 外面,方便其他腳本取用。 public enum GameState { Prepare, InGame, Die, Score } public class GameManager : MonoBehaviour { //用來記錄當前的回合流程是哪一個 public GameState State; //玩家腳本 public PlayerController Player; public SpawnMonster Spawner_Monster; //青蛙的次數 public int Chance; void Update() { if (Input.GetMouseButtonDown(0)) { switch (State) { case GameState.Prepare: //讓玩家開始掉落 Player.Go(); //前往下一個遊戲狀態 SetGameState(GameState.InGame); break; case GameState.InGame: Player.Jump(); break; case GameState.Die: Debug.Log("已經死亡不能動"); break; case GameState.Score: break; default: break; } } } //設定流程 public void SetGameState(GameState state) { State = state; switch (State) { case GameState.Prepare: //角色回到準備狀態 Player.Prepare(); //停止生成怪物 Spawner_Monster.StopGenerate(); break; case GameState.InGame: //開始生成怪物 Spawner_Monster.StartGenerate(); break; case GameState.Die: Chance--; Spawner_Monster.StopGenerate(); Invoke("GameCheck", 1); //延遲一秒執行指定方法 break; case GameState.Score: break; default: break; } } //確認是否還有遊玩機會 public void GameCheck() { if (Chance > 0) { //如果還有機會 SetGameState(GameState.Prepare); } else { //機會用完 SetGameState(GameState.Score); } } } ``` 另外也要修改 playerController 的 script 來對應 GameManager script。 ## 能量系統製作(美乃滋) ![](https://i.imgur.com/13UKtr7.png) - 撰寫一個方法用來對能量做變動 - PolygonCollider 2D ,可以調整多邊形碰撞範圍。 - 製作美乃滋動畫。 - 參考建立怪物生成點的方式設置美乃滋生成點 - 建立生成美乃滋的 script。 ```csharp= //產生位置 public Transform[] SpawnPosition; public GameObject Prefab_Energy; //生成頻率 public float Delay; //開始生成能量球 public void StartGenerate() { InvokeRepeating("Spawn", 0, Delay); } //停止生成能量球 public void StopGenerate() { CancelInvoke(); } //生成單顆能量球 public void Spawn() { Vector3 tempSpawnPos; //此次決定要生成能量球的位置 tempSpawnPos = SpawnPosition[Random.Range(0, SpawnPosition.Length)].position; Instantiate(Prefab_Energy, tempSpawnPos, Quaternion.identity); //生成的物件、生成位置,不需要旋轉 } ``` ## 讓背景移動 原理: - 每 1-3 秒生成一棵樹,大小不固定。 - 建立樹的預製物 - 建立樹的腳本 ```csharp= //樹的預製物 public GameObject Prefab_Tree; //倒數計時器 public float Timer; //最長生成間隔 public float DelayMax; //最短生成間隔 public float DelayMin; //每一次的生成會從兩個數字間取值 //樹最大體積 public float TreeSizeMax; //樹最小體積 public float TreeSizeMin; private void Start() { //初始化第一次的倒數值 Timer = Random.Range(DelayMin, DelayMax); } void Update() { Timer -= Time.deltaTime; if(Timer <= 0) { //生成一顆樹 SpawnTree(); //重新倒數 Timer = Random.Range(DelayMin, DelayMax); } } public void SpawnTree() { GameObject tempTree = Instantiate(Prefab_Tree, new Vector3(12, -4.6f, 1), Quaternion.identity); float tempSize = Random.Range(TreeSizeMin, TreeSizeMax); tempTree.transform.localScale = new Vector3(tempSize, tempSize, 1); } ``` ## 銷毀物件 建立 Deadspace 腳本,在銷毀的 GameObject 掛上 Deasspace。 要產生碰撞至少要有其中一方掛上 Rigidbody 2D。 在需要銷毀物件的死亡區域掛上 DeadSpace 腳本。 ```csharp= private void OnTriggerEnter2D(Collider2D collision) { Destroy(collision.gameObject); } ``` ## 製作子彈 - 製作子彈物件(box collider2d, Rigidbody2d),完成後做成預製物。 - 製作子彈動畫 - 如果要對 Prefab 物件的欄位賦值,不能拉入場景上的物件,只能拉 Asset 資料夾內的物件。 ```csharp= ``` ### 銷毀子彈 ```csharp= public GameObject Bullet; private PlayerController Player; void Start() { //如果要對 Prefab 物件的欄位賦值,不能拉入場景上的物件,只能拉 Asset 資料夾內的物件。 //在 Start 內執行,找到 Player 物件,並且抓取其上的 PlayerController 腳本 //GameObject.Find("物件名"),會找尋場地上的物件中有對應名稱的 //找尋物件上的component,GetComponent<類別名稱>() Player = GameObject.Find("Player").GetComponent<PlayerController>(); } private void OnTriggerEnter2D(Collider2D collision) { if (collision.tag == "Monster") { //銷毀怪物 Destroy(collision.gameObject); //回復能量 Player.EnergyFull(); //銷毀子彈 Destroy(Bullet); } } ``` ## UI製作 UGUI:Unity的UI系統 注意: ![](https://i.imgur.com/vPFSrz2.png) ### 常用UI Components - Canvas - Image - Text - Button ### 邀要製作的 UI - 左上方分數 - 右上方暫停/青蛙機會顯示 - 左下角能量條 - 正下方的操作提示 - 結算頁面 - 暫停面板 ### UI 製作 Hierachy 視窗點右鍵選 UI,點選要創建的物件。 - Canvas 的 Render Mode 中有三個選項,overlay 是所有的 ui 物件一定在畫面最上方。Camera 要指定攝影機。 - Canvas Scaler 可以依照不同的螢幕大小,幫畫面大小作調整。 - React Transform 是 UI 專用的 Transform, - EventSystem 偵測事件。 ## UI 套用功能套用 能量 UI 套用:在 PlayerContorller 腳本裡新增一個陣列用來擺放所有能量點物件。 ```csharp= public GameObject[] Obj_EnergyPoint;//能量點 UI 顯示物件 //計算能量 public void SetEnergy(int add) { //在計算開始前先把能量 ui 全部關閉 for (int i = 0; i < Obj_EnergyPoint.Length; i++) { Obj_EnergyPoint[i].SetActive(false);//GameObject物件.SetActive(bool是否開啟)開關物件用的方法 } if(Energy + add > 6) { Energy = 6; } else if(Energy + add < 0) { Energy = 0; } else { Energy += add; } //依序打開能量點 UI for (int i = 0; i < Energy; i++) { Obj_EnergyPoint[i].SetActive(true); } } //補滿能量 public void EnergyFull() { Energy = 6; //打開每一個能量點 for (int i = 0; i < Obj_EnergyPoint.Length; i++) { Obj_EnergyPoint[i].SetActive(true); } } ``` ### 暫停面板 Button UI 中會附帶一個 Button Script,當中有一個 On Click 選項,可以將點擊時希望發生的事情寫在裡面。 將暫停面板的 GameObject 拉到 On Click 中。 先在 GameManager 腳本中寫一個暫停方法。 ```csharp= //暫停 public void Pause() { //暫停時間 Time.timeScale = 0;//Time.timeScale = float; 時間流速,設定時間速度,0為暫停。 } //回到遊戲 public void BackToGame() { //回復時間 Time.timeScale = 1; } //離開遊戲 public void ExitGame() { //離開遊戲 Application.Quit(); } ``` ### 結算面板 ```csharp= //顯示結算內容 public void ShowGameDetail() { //當前分數 分數 = 當前的時間*2 + 怪物擊殺*5 Text_Score.text = (int)((Timer.Timer * 2) + (MonsterKill * 5)) + "分"; //歷史高分 //生存時間 Text_Time.text = (int)(Timer.Timer) + "秒"; //擊殺怪物 Text_Kill.text = MonsterKill + "隻"; } ``` ### 重來 ```csharp= //遊戲初始化 public void InitGame() { //將機會調整成3 Chance = 3; //顯示青蛙硬幣 for (int i = 0; i < Coins.Length; i++) { if (i < Chance) { Coins[i].SetActive(true); } else { Coins[i].SetActive(false); } } //回到準備狀態 SetGameState(GameState.Prepare); //關閉結算 ScorePanel.SetActive(false); //碼表歸零 Timer.Init(); //怪物擊殺數歸零 MonsterKill = 0; } ``` ## 添加音效 在要添加音效的 gameobject 上面新增 Audio Source Component。 在 PlayerController script 中新增 Audio Object 和播放方法。 ```csharp= //AudioClip 表示我們要用的音源檔 public AudioClip Clip_Die; public AudioClip Clip_Jump; public AudioClip Clip_Eat; //AudioSource 播放器,AudioClip CD public AudioSource Audio; public void Jump() { if( Energy > 0) { RB.velocity = new Vector2(0, 8); //跳躍設定為"Jump" Anim.SetTrigger("Jump"); Particle.Play(); //消耗一點能量 SetEnergy(-1); //跳躍音效 PlayEfect(Clip_Jump); } else { Debug.Log("能量不足。"); } } ``` ## 存檔系統 Unity 存檔系統主要依靠 PlayerPrefs ![](https://i.imgur.com/ChB94U2.png) ```csharp= //歷史高分 public int HighestScore; //顯示結算內容 public void ShowGameDetail() { int Score = (int)((Timer.Timer * 2) + (MonsterKill * 5)); //當前分數 分數 = 當前的時間*2 + 怪物擊殺*5 Text_Score.text = Score + "分"; //歷史高分 if (Score > HighestScore) { HighestScore = Score; PlayerPrefs.SetInt("HighestScore", HighestScore); } Text_Top.text = HighestScore + "分"; //生存時間 Text_Time.text = (int)(Timer.Timer) + "秒"; //擊殺怪物 Text_Kill.text = MonsterKill + "隻"; } ``` ## 細節處理 製作 DelayDestroy 腳本,用來刪除 flash, smoke。 ```csharp= public float Delay;//要在幾秒之後銷毀 void Start() { Destroy(gameObject, Delay);//多載寫法,延遲刪除 } ``` ## AssetStore 資源引用~自適屏(螢幕自動調適) 自適屏種類 - 硬性保持比例,其餘部分以黑色取代 - 依照螢幕改變物體及畫面比例 先用 Windows 內的 AssetStore 功能導入資源包 ## 物件池 物件池是一種優化手段 ![](https://i.imgur.com/1YwZejJ.png) ![](https://i.imgur.com/dWOkMcI.png) ![](https://i.imgur.com/Fhj1fUD.png)