# Unity 2D 彈幕遊戲開發 --- # 目錄 ---- ### 素材 ### 遊戲玩法 ### 物件導向概念 ### 列舉(Enum) ### 動畫Animator ---- ### Dotween動畫 ### 人物操作 ### 彈幕發射 ### 關卡控制器 ### 敵人AI ---- ### 道具、額外功能 ### 導出為網頁 ### 原始專案 --- ## 素材 ---- ### 一、下載 #### [點擊下載](https://drive.google.com/file/d/1G1JzjG-V0LZT6Vvlx0An-LkDXIODDyoa/view?usp=sharing) ---- ### 二、匯入 ---- #### 步驟1: #### 在Project視窗按右鍵選擇Show in Explorer ![](https://i.imgur.com/pgI96EC.png) ---- #### 步驟2: #### 將檔案全部解壓縮到Asset文件夾裡面 ![](https://i.imgur.com/gkXEUCF.png) ---- #### 即可在Project視窗中使用這些素材 ![](https://i.imgur.com/v1kcwWT.png) ---- ### 三、切片 ---- #### 步驟1: #### 選中Project視窗未經過切片之圖片 ![](https://i.imgur.com/sRXSivM.png) ---- #### 步驟2: #### 在Inspector視窗將Sprite Mode變更為Multiple #### 並按下Sprite Editor這個按鈕進入裁切 ![](https://i.imgur.com/Kr0ZJxz.png) ---- #### 步驟3: #### 先按下右上角按鈕將其轉為黑白模式 #### 再來按下左上角的Slice按鈕進行切片 #### 確認模式為自動並按下Slice按鈕進行自動切片 ![](https://i.imgur.com/5DgImPs.png) ---- #### 步驟4: #### 自動切片後可能還是會有些多餘的切片 #### 直接選中按下Deltet鍵將其刪除 #### 重疊部分則對其邊界用滑鼠拖移調整 #### 或者也可以自己按住滑鼠左鍵拖移出新的切片 ![](https://i.imgur.com/SU9f6pG.png) ---- #### 步驟5: #### 此遊戲中克萊因是可以玩家揮舞的物件 #### 所以將原本為中心的軸心 #### 調整至腳下 #### 這樣旋轉時就是以腳為中心去旋轉 ![](https://i.imgur.com/Yo6k4Uk.png) ![](https://i.imgur.com/efevACK.png) ---- #### 切片完後即可自由使用這些切片完的素材 #### 並可將其拖曳至場景中 ![](https://i.imgur.com/GARNX1D.png) ---- ### Asset Store(資源商店) ---- ### 可在這裡下載一些免費/付費素材 ### 並應用於你的遊戲中 ---- #### 步驟1: #### 在Unity左上角找到Window #### 並選擇Asset Store即可進入 ![](https://i.imgur.com/Vuvjx0E.png) ---- #### 如果Unity版本為2020以上的 #### 由於Unity已禁止直接在編輯器中打開AssetStore #### 所以必須直接登入網站安裝 ### [AssetStore連結](https://assetstore.unity.com/) ---- #### 步驟1: #### 搜尋名稱 #### 點擊要的資源 ![](https://i.imgur.com/0vGRf8K.png) ---- #### 步驟2: #### 點擊下載並等待好後按下Import導入到專案中 ![](https://i.imgur.com/w0kvMuW.png) ---- ### 依照上述流程安裝需要的兩個資源 #### 1.Cartoon Fx Free #### 2.Dotween ---- #### Dotween動畫在導入後會跳出視窗 #### 請在視窗中按下Setup將其導入至專案中 ![](https://i.imgur.com/77jppS8.png) ---- ### 資源說明 ---- ### 1.Cartoon Fx Free #### 粒子動畫特效包 #### 用參數定義並製作的特效 #### 匯入專案後可根據需求 #### 延伸出各式特效 ---- ### 2.Dotween #### 提供函式庫可用程式 #### 可用其直接對物件位置、大小、旋轉進行動畫 #### 具體後續會在介紹如何使用 --- ## 遊戲玩法 ---- ## [SAOTSJ](https://allforwife.itch.io/saotsj) ### 操作 #### WSAD控制移動 #### 角色會根據滑鼠方向旋轉 #### 滑鼠左鍵發射子彈攻擊 #### 滑鼠右鍵揮刀攻擊並可彈開子彈 --- ## 物件導向概念 ---- ### 程式的運作就是由不同物件來運行的 ### 每個物件都由類別來產生 ---- ### 在Project視窗右鍵創建兩個script ### 分別命名為Biological以及Player ![](https://i.imgur.com/mmeEaQV.png) ---- ### 封裝 ---- #### 就是將所有屬性變數在類別(class)裡面 #### 都定義為private(私有的) #### 這樣其他腳本就無法存取到這個變數 #### 所以要在設計公共方法set來賦予值 #### 以及公共方法get來得到值 #### 用意是在於保護code #### ㄅ過廢物如我都懶得這樣寫 ㄅ歉 ---- ### 程式碼(封裝): ``` csharp= public class Biological : MonoBehaviour { private int hp; private int atk; public void setHp(int hp) { this.hp = hp; //this.hp是指這個腳本本身的hp(第3行) //程式碼中呼叫某個變數時是先看括號{}setHp的hp明顯最近 //所以直接寫hp是使用setHp(int hp)的參數 } public int getHp() { return hp; } } ``` 之後其他腳本想存取這些屬性都要利用set、get這些公開方法 ---- ### 繼承 ---- #### 玩家跟敵人都是生物 #### 他們的共同點是都有血量 #### 也會發射子彈進行攻擊 #### 那麼就定義一個生物的Class #### 讓玩家和敵人都繼承它 #### 就可以不用再寫一次 ---- #### 然後不一樣的地方 #### 像是玩家還會揮刀之類的 #### 再在玩家這邊的Class定義 #### 敵人也是同理 ---- ### 程式碼(繼承): ``` csharp= public class Biological : MonoBehaviour { protected float hp; //protected被保護的型別 只可自己使用以及繼承自自己的腳本去做使用 } ``` 打開命名為生物的腳本加入變數hp ```csharp= public class Player : Biological { } ``` 將原本Player繼承的MonoBehaviour 改為繼承自生物 繼承後即可直接在Player使用hp這個變數 ---- ### 多型 ---- #### class中可做多個同名的方法 #### 並將參數設置成不同型別 #### 或者不同數量 #### 之後使用此方法時 #### 系統就會依照你輸入的參數去調用方法 ---- ### 程式碼(多型): ``` csharp= public class Biological : MonoBehaviour { public void move() {} public void move(int x) {} public void move(float x) {} } ``` ---- ### 覆寫 ---- #### 用於修改繼承來的方法 #### 玩家繼承生物後 #### 獲得了發射子彈這項方法 #### 但玩家的彈幕發射可以升級來加強 #### 敵人彈幕則是會依據不同種類使用不同彈幕 #### 這些不同點必須使用覆寫來修改原本的方法 #### 才能實現 ---- ### 程式碼(覆寫): ``` csharp= public class Biological : MonoBehaviour { public virtual void Fire(float time) { //程式敘述... } } ``` 原腳本方法須加上關鍵字virtual ``` csharp= public class Player : Biological { public override void Fire(float time) { //改寫程式敘述 } } ``` 使用關鍵字override進行方法覆寫 這樣即可修改在Player時的Fire方法 ---- ### 呼叫原腳本屬性、方法 ---- #### 有時候可能會需要原繼承腳本的屬性/方法 ---- ### 程式碼: ``` csharp= public class Biological : MonoBehaviour { public virtual void Fire(float time) { //程式敘述... } } ``` ``` csharp= public class Player : Biological { public override void Fire(float time) { base.Fire(time); } } ``` 使用base可使用原腳本屬性變數、方法 --- ## 列舉(Enum) ---- ##### 列舉其實不是必要的東西 ##### 但是他可以讓你比較好閱讀 ---- ``` csharp= // 列舉遊戲狀態 enum GameStatus { GAME_READY, // =0 遊戲準備 GAME_START, // =1 遊戲開始 GAME_OVER // =2 遊戲結束 //不論定義幾個狀態 這個列舉都只視為一個變數的大小 } // 宣告你的列舉變數 遊戲狀態 GameStatus gameStatus; // Use this for initialization void Start () { // 用switch來判斷你的列舉狀態 switch (gameStatus) { case GameStatus.GAME_READY: //等同於 case 0 Debug.Log("GAME_READY"); break; case GameStatus.GAME_START: Debug.Log("GAME_START"); break; case GameStatus.GAME_OVER: Debug.Log("GAME_OVER"); break; } } ``` --- ## 動畫Animation與Animator ---- ### Animation ---- #### 除了可以直接逐幀把圖片替換製作成逐幀動畫 #### 也可以進行形變、大小變換 ---- #### 步驟1: #### 如果預設沒有找到Animation視窗 #### 就點左上角選單的Window並選擇Animation #### 將其叫出 ![](https://i.imgur.com/JvMDw7o.png) ---- #### 步驟2: #### 在Inspector視窗加入元件 ![](https://i.imgur.com/gHlvFst.png) #### 搜尋並選擇Animator ![](https://i.imgur.com/d0JejKs.png) ---- #### 步驟3 #### 在Animation視窗創建相關動畫 ![](https://i.imgur.com/T4NXnaL.png) ---- #### 步驟4: #### 按下左上角的小紅點即可開始錄製 ![](https://i.imgur.com/Djgw4s9.png) ---- #### 步驟5: #### 按下錄製後 #### 將時間軸移到0:10左右的位置 ![](https://i.imgur.com/HDFsj65.png) ---- #### 在Inspector視窗調整z軸的旋轉值至131.92 #### 這是動畫的起始點 ![](https://i.imgur.com/VdTx22j.png) ---- #### 步驟6: #### 將時間軸移到0:40左右的位置 ![](https://i.imgur.com/RiY539D.png) ---- #### 在Inspector視窗調整z軸的旋轉值至-135.3 #### 這是動畫的結束點 ![](https://i.imgur.com/q0gFwRp.png) ---- #### 有了起始點和結束點 #### 動畫就會自己漸變出中間的過程 #### 如此就完成了一個揮舞的動畫 ---- #### 點此可新增動畫 #### 並請依照上述流程 #### 做一個反向揮舞(從右至左)的動畫 ![](https://i.imgur.com/bGGa0Zb.png) ---- ## Animator ---- #### 動畫的狀態機用來控管底下的Animation #### 比如說人物可以會移動、跳、攻擊等等 #### 那麼Animator可以實現動畫邏輯 #### 判斷每個狀態應該用甚麼動畫 #### 這裡要來實作亞絲娜的克萊因連擊 ---- #### 步驟1 #### 從Window/Animation/Animator叫出Animator視窗 ![](https://i.imgur.com/8VULwZ8.png) ---- ![](https://i.imgur.com/xHlG8rr.png) ---- #### 步驟2 #### 在左邊選擇Parameters(參數) #### 按下+新增整數變數Atk ![](https://i.imgur.com/HAIep9m.png) ---- #### 步驟3 #### 在右邊區塊右鍵創建一個新的狀態 ![](https://i.imgur.com/XyDSAcn.png) ---- #### 命名為none #### 對Entry右鍵選第二個 #### 將起始線連到none ![](https://i.imgur.com/yJpEQZP.png) ---- ![](https://i.imgur.com/C7V2Wrv.png) ---- #### 步驟4 #### 對none右鍵選第一個Make Transition #### 將線連到我們做的兩個動畫(Animation)狀態 ![](https://i.imgur.com/3OzNKpE.png) ---- #### 步驟5 #### 同樣動畫部分也要回傳回來所以連成這樣 ![](https://i.imgur.com/bwW0005.png) ---- #### 步驟6 #### 點擊線段將值設定成如下 #### 其他幾個同理 ![](https://i.imgur.com/bRuJLQj.png) ---- #### 步驟7 #### 加入判斷如果Atk等於1的話 #### 則進入這個狀態 ![](https://i.imgur.com/y3Wmm10.png) ---- #### 反之 #### 不等於1的話則退出狀態 ![](https://i.imgur.com/ySJe2hd.png) ---- #### 另一個攻擊動畫也同理 #### 只是Atk應設置為等於2時判斷 ---- ## 再來要寫程式碼控制 ## 狀態機的參數Atk ---- ## 來到Player ---- ``` csharp= Animator ani; public int nextAtk = 0;//紀錄揮刀的連擊 void Start() { ani = GetComponent<Animator>(); //當然這個屬性變數也可以寫在生物裡面 再在Player呼叫 //總之讓它獲取Animator的元件 } void Attack() { AnimatorStateInfo stateinfo = ani.GetCurrentAnimatorStateInfo(0);//獲得狀態機 if (nextAtk > 0 && stateinfo.normalizedTime >= 1f) { nextAtk = 0; ani.SetInteger("Atk", nextAtk); //這個是判定如果動畫撥放完的話 就將其歸0 回到none狀態 } if (Input.GetKey(KeyCode.Mouse1)) { if (nextAtk == 0) nextAtk++;//按下右鍵時當下處於none狀態的動畫 則nextAtk=1進入攻擊狀態 else if(nextAtk > 0 && stateinfo.normalizedTime >= 0.75f) { //如果已經處於攻擊狀態的話 //就將判定放得在寬鬆一點 //撥放到7成左右就可以揮第二刀 //進行連擊動畫 if (nextAtk >= 2) nextAtk = 1; else nextAtk++; } } ani.SetInteger("Atk", nextAtk); //將變數設置到狀態機參數上 } ``` --- ## Dotween動畫 ---- ### 簡單來說就是用程式碼 ### 直接動態插入動畫 ### 接下來進行打擊感的製作 ---- ## 來到Biological腳本 ---- #### 在使用之前需在腳本上方引入此函式庫 ``` csharp= using DG.Tweening; ``` ---- ## 程式碼(Dotween): ``` csharp= Tween shake_Do;//用來存取晃動的動畫 Tween color_Do;//用來存取變色的動畫 public void Shake() { if (shake_Do == null)//當晃動動畫不存在時 { shake_Do = transform.DOShakePosition(0.5f, 0.2f); //賦址給一個新的晃動動畫 //第一個參數為晃動大小 第二個參數為執行秒數 //此方法為用來晃動位置 transform.DOShakeScale(0.5f, 0.2f); //同理 晃動大小 color_Do = transform.GetComponent<SpriteRenderer>().DOColor(new Color32(255, 200, 200, 255), 0.075f); //獲取圖片元件做顏色漸變 //將顏色設置為紅色 //此RGBA顏色參數請自行去查顏色表 } else { if (!shake_Do.IsPlaying()) { //當動畫沒被執行的話 做跟上面同樣的效果 //這樣寫的理由 是因為如果shake_Do為空的話 //直接使用方法會報錯 或許也可改寫成try catch shake_Do = transform.DOShakePosition(0.2f, 0.2f); transform.DOShakeScale(0.2f, 0.2f); color_Do = transform.GetComponent<SpriteRenderer>().DOColor(new Color32(255, 200, 200, 255), 0.075f); } } }//將此動畫放置在碰撞處理中 即可做出受擊效果 public void Re_Do() { if (color_Do != null) { if (!color_Do.IsPlaying()) { color_Do = transform.GetComponent<SpriteRenderer>().DOColor(Color.white, 0.1f); } }//將此放置在Update裡面 將顏色漸變動畫結束後可以還原原本顏色 } ``` --- ## 人物操作 ---- ### 必要元件 ---- ## Rigibody 2D ![](https://i.imgur.com/7K5Js2c.png) ---- ## Box Collider 2D ![](https://i.imgur.com/lO8eMrn.png) ---- ### 新增元件的方式 #### 於Inspector視窗點此按鈕新增元件 ![](https://i.imgur.com/KaL9BTI.png) ---- #### 其中碰撞盒的大小可從直接從碰撞盒元件調整 #### 或者按下調整按鈕在Scene中編輯它的大小 ![](https://i.imgur.com/smX6gis.png) ---- ## 人物移動 ---- ## Class:Player ``` csharp= void Move() { float hor = Input.GetAxis("Horizontal") * speed; //左右移動 float ver = Input.GetAxis("Vertical") * speed; //上下移動 rig.velocity = new Vector2(hor, ver); //通過變更剛體的速率來進行移動 } ``` ---- ## 根據滑鼠位置轉向 ---- ## Class:Player ``` csharp= void RotateMouse(float speed) { // 獲取目標物件當前的世界座標系位置,並將其轉換為螢幕座標系的點 Vector3 Pos = Camera.main.WorldToScreenPoint(transform.position); // 設定滑鼠的螢幕座標向量,用上面獲得的Pos的z軸資料作為滑鼠的z軸資料,使滑鼠座標與目標物件座標處於同一層面上 Vector3 mousePos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, Pos.z); Vector3 target = Camera.main.ScreenToWorldPoint(mousePos); //將人物朝向(滑鼠位置-自身位置) transform.up = target - transform.position; } ``` ---- ## 定義一個class Allfun ## 裡面放入static方法Create ``` csharp= public static Transform Create(string path, Transform Par, Vector3 Size, Vector3 Pos, Quaternion Rota) { var obj = Instantiate(Resources.Load<GameObject>(path)); obj.transform.parent = Par; obj.transform.localScale = Size; obj.transform.position = Pos; obj.transform.rotation = Rota; return obj.transform; }//從檔名利用Resources獲取預置物並創建 public static Transform Create(GameObject path, Transform par, Vector3 size, Vector3 pos, Quaternion rota) { var tmpObj = Instantiate(path); tmpObj.transform.parent = par; tmpObj.transform.localScale = size; tmpObj.transform.position = pos; tmpObj.transform.rotation = rota; return tmpObj.transform; }//直接創建預置物 ``` ---- ## 血條控制 ---- ### 步驟1 #### 於Hierarchy視窗找到主角 #### 右鍵新增Canvas(畫布)作為他的子物件 ![](https://i.imgur.com/TWJZJye.png) ---- ### 步驟2 #### 點擊創建出來的畫布 #### 修改Inspector中的Canvas元件 #### 將渲染模式改為WorldSpace #### 事件相機改成MainCamera #### 然後把位置(PosX、Y、Z)都設置為0 #### 最後調整一下大小 ![](https://i.imgur.com/cwEC2bi.png) ---- ### 步驟3 #### 在Canvas下右鍵創建Image #### 作為血條的底框 ![](https://i.imgur.com/ycwimn1.png) ---- ### 步驟4 #### 在Inspector(屬性)視窗中的Image元件中 #### 拖入UI這張全白的圖片 ![](https://i.imgur.com/WPtUMV8.png) ---- ### 步驟5 #### 回到Hierarchy視窗 #### 再創建一個Image #### 作為主要控制的血條 ![](https://i.imgur.com/k5gTWc6.png) ---- ### 步驟6 #### 來到血條的Inspector(屬性)視窗 #### 將Image元件設置如下 ![](https://i.imgur.com/nIi5kKl.png) ---- #### 藉由變更Fill Amount的值控制目前血條的比例 #### 當然還需要調整下血條的長寬 ![](https://i.imgur.com/hXQUexW.png) ---- ### 程式碼 #### 再來需要程式碼控制FillAmount來扣血 ---- ### 寫在Class:Biological #### 令繼承於生物的都可以調用此方法來控制血量 ``` csharp = public float hp; public float maxHp; public float setHpUi(Transform hpCanvas, float hp, float maxHp) { var hpImage = hpCanvas.GetChild(0).GetChild(0).GetComponent<Image>(); if (hp > maxHp) hp = maxHp; else if (hp < 0) hp = 0; hpImage.fillAmount = hp / maxHp; //fillAmount等於血量/血量最大值的比例 return hp; } ``` ---- ## 受傷 ---- ### 於Player調用 #### ``` csharp = void Start() { hp = maxHp; //初始化血量上限 } private void OnTriggerEnter2D(Collider2D col) {// if(col.CompareTag("enemyBullet")) { var bullet = col.gameObject.GetComponent<Bullet>(); //獲得碰撞物件也就子彈 然後獲得其script hp = setHpUi(hp_Ui, hp - bullet.atk, hp_Max); if(hp <= 0 && !GameControl.inst.isGameover) {//如果血量歸零 var eff = GameControl.inst.effectList; var obj = eff[(int)GameControl.effect.blueExplo]; //創建死亡特效 Instantiate(obj, transform.position, transform.rotation); Destroy(gameObject); //刪除自己 GameControl.inst.isGameover = true; //Gameover } Shake(); //晃動 bullet.damage(); //消滅碰撞到的子彈 } } ``` --- ## 彈幕控制 ---- ## 子彈設置 ---- #### 將子彈圖片拖入場景 #### 並設置子彈碰撞盒及剛體 #### 設置如下 ![](https://i.imgur.com/j0ja6w0.png) ---- ### 程式碼 #### 創建Bullet的script,並拖曳給彈幕做成預置物。 ``` csharp= public type bulletType; public enum type { shot, around, } public void setAround(Transform trans) { AroundTrans = trans; bulletType = type.around; } void FixedUpdate() { if(bulletType == type.shot)//一般子彈發射 { transform.Translate(Vector3.up * speed * Time.deltaTime); //Vector3.up是往上發射 //而它會因為物件旋轉z軸而發生變動 //所以只要生成時改變transform.rotation.z //就可以發射至其他方向 } else if(bulletType == type.around)//子彈環繞敵人旋轉 { if(AroundTrans != null) transform.RotateAround(AroundTrans.position, Vector3.forward, speed * Time.deltaTime); //這個是讓子彈以某物為中心進行旋轉 else { Destroy(gameObject); //某物也就是敵人死了的話 就一起移除 } } } public void block() { var effect = GameControl.inst.effectList; var obj = effect[(int)GameControl.effect.block]; Instantiate(obj, transform.position, transform.rotation); Destroy(gameObject); } private void OnTriggerEnter2D(Collider2D col) { if (col.CompareTag("boundary") && bulletType != type.around) { Destroy(gameObject); } else if(isEnemy && (col.CompareTag("Klein") || col.CompareTag("starburst"))) { block(); } } ``` ---- ## 玩家發射子彈 ---- ## Class:Player ``` csharp= public float atkTime; void Start() { StartCoroutine(Fire(atkTime));//開啟協程 } public override IEnumerator Fire(float time) { while(true) { yield return new WaitForSeconds(time);//每time秒發射子彈 if (Input.GetKey(KeyCode.Mouse0)) {//按下左鍵 var control = GameControl.inst; var path = control.bulletList[(int)GameControl.bullet.ball]; //獲得列舉中的球狀子彈 var par = control.Garbage; Vector3 size = new Vector3(1, 1, 1); Vector3 pos = transform.position; var offsetY = 0.4f; Vector3 localPos = transform.localPosition; //獲得本地座標 var rotaZ = transform.rotation.eulerAngles.z; //根據當前的彈幕等級 變更發射方式 if (barrageLv == 0) { AllFun.Create(path, par, size, pos, Quaternion.Euler(0, 0, rotaZ + 0)); //旋轉值必須用Quaternion.Euler(向量)才能修改 } else if (barrageLv == 1) { path = control.bulletList[(int)GameControl.bullet.slash]; for (int i = -1; i <= 1; i += 2) { pos = new Vector3(localPos.x + i, localPos.y + offsetY, localPos.z); //生成左右各一邊的斬擊型子彈 AllFun.Create(path, par, size, pos, Quaternion.Euler(0, 0, rotaZ + 0)); } } else if (barrageLv == 2) { for (int i = -25; i <= 25; i += 25) { AllFun.Create(path, par, size, pos, Quaternion.Euler(0, 0, rotaZ + i)); //正前方-25 0 25 \|/的方向發射子彈 } } else if (barrageLv == 3) { //這個就是將前面兩種一起發射 path = control.bulletList[(int)GameControl.bullet.ball]; for (int i = -1; i <= 1; i += 2) { pos = new Vector3(localPos.x + i, localPos.y + offsetY, localPos.z); AllFun.Create(path, par, size, pos, Quaternion.Euler(0, 0, rotaZ + 0)); } pos = new Vector3(localPos.x, localPos.y + offsetY, localPos.z); path = control.bulletList[(int)GameControl.bullet.slash]; for (int i = -45; i <= 45; i += 45) { AllFun.Create(path, par, size, pos, Quaternion.Euler(0, 0, rotaZ + i)); } } } } } ``` --- ## 關卡控制器 ---- ### 創建一個GameControl的腳本 #### 用來管理關卡以及管理預製物 #### 拖給Hierarchy視窗中的MainCamera ---- ### 預製物管理 ``` csharp = [HeaderAttribute("Asset")] public List<GameObject> bulletList = new List<GameObject>(); public List<GameObject> enemyList = new List<GameObject>(); public List<GameObject> itemList = new List<GameObject>(); public List<GameObject> enemyBulletList = new List<GameObject>(); public List<GameObject> effectList = new List<GameObject>(); public enum bullet { ball = 0, slash = 1 } public enum enemyBullet { ball = 0, slash = 1, super = 2, square } public enum effect { slash, blood, hit, block, blueExplo, } ``` ---- ### 回到編輯器 #### 將預製物從Asset視窗照順序 #### 拖曳到剛剛創建好的欄位 #### 即可使用 ![](https://i.imgur.com/OpzIKuv.png) ---- ### Stage程式碼 ``` csharp= [System.Serializable]//序列化 public class Stage { public Sprite backGround; public List<EnemyList> enemyList; } [System.Serializable] public class EnemyList { public Enemy.type_enum enemyTeam;//敵人隊伍 有時候可能會一次生成3~5隻 public Transform pos;//生成位置 直接在場景中指定為某物件的位置 public float enemyTime;//每批生成速度 } public class GameControl : MonoBehaviour { [HeaderAttribute("Stage")] public List<Stage> stage = new List<Stage>(); //序列化後的class 宣告後就可以顯示在屬性視窗了 } ``` ---- ### 創建完成 #### 即可在屬性視窗自行定義每關敵人的配置 ![](https://i.imgur.com/rAWsb0W.png) ---- ### 生成敵人 #### 根據定義好的敵人配置來生成關卡的敵人 ``` csharp= //這邊不用協程是 //因為玩家太快擊退一批敵人 //中間空檔不是有點無聊嗎 //所以必須在擊殺敵人後 縮短下次生成的時間 盡可能不間斷的生成 public void CreateEnemy_Update(Stage stage) { if (!isGameover && enemyCount < enemyCount_Max) { EnemyList eny; eny = stage.enemyList[enemyCount]; createEnyTime += Time.deltaTime; if ( createEnyTime >= eny.enemyTime) { Instantiate(enemyList[(int)eny.enemyTeam], eny.pos.position, Quaternion.Euler(0, 0, 0)); createEnyTime = 0; enemyCount++; } } } ``` ---- ### 遊戲結束和通關的事件 #### 都是預先在畫布中做好 #### 然後需要再讓物件顯示 ``` csharp= IEnumerator GameOver(float time) { while(true) { if(isGameover && !isClear) { yield return new WaitForSeconds(time); gameover_Obj.SetActive(true); } yield return new WaitForSeconds(0.5f); } } IEnumerator Clear(float time) { while (true) { if (isClear) { yield return new WaitForSeconds(time); clear_Obj.SetActive(true); } yield return new WaitForSeconds(0.5f); } } ``` --- ## 敵人AI ---- ### 發射子彈 ``` csharp= public override IEnumerator Fire(float time) { var enyBullet = GameControl.inst.enemyBulletList; GameObject obj = enyBullet[(int)GameControl.enemyBullet.ball]; var trans = transform; if (type == type_enum.tsj_x5) { var pos = new Vector2(transform.position.x + 1f, transform.position.y); for (int i = 0; i < 6; i++) { var bullet = AllFun.Create(obj, transform, new Vector3(1,1,1), pos, Quaternion.Euler(0, 0, 0)); bullet.GetComponent<Bullet>().setAround(transform); bullet.GetComponent<Bullet>().setSpeed(200); yield return new WaitForSeconds(0.5f); } } while(true) { yield return new WaitForSeconds(time); if(!dontShot) { if (type == type_enum.tsj_face_x3 || type == type_enum.tsj_face_x5) { Instantiate(obj, transform.position , Quaternion.Euler(0,0,180)); } else if(type == type_enum.tsj_rabbit) { for (int i = -45; i <= 45; i += 45) //-45 0 45 { obj = enyBullet[(int)GameControl.enemyBullet.slash]; var rotaZ = transform.eulerAngles.z; var bullet = AllFun.Create(obj, null, new Vector3(1,1,1), trans.position, Quaternion.Euler(0, 0, rotaZ + i + 180)); bullet.GetComponent<Bullet>().setSpeed(5); } } else if(type == type_enum.tsj_lizard) { obj = enyBullet[(int)GameControl.enemyBullet.slash]; var rotaZ = 90; var bullet = AllFun.Create(obj, null, new Vector3(1, 1, 1), trans.position, Quaternion.Euler(0, 0, rotaZ)); bullet.GetComponent<Bullet>().setSpeed(5); rotaZ = -90; bullet = AllFun.Create(obj, null, new Vector3(1, 1, 1), trans.position, Quaternion.Euler(0, 0, rotaZ)); bullet.GetComponent<Bullet>().setSpeed(5); } else if(type == type_enum.tsj_slime_stool) { //Boss的話 //有很多不同技能和彈幕 //在Animation中直接錄製bool dontShot //決定甚麼時候進行發射 for(int i=-1; i<=1;i++) { obj = enyBullet[(int)GameControl.enemyBullet.super]; var pos = new Vector2(trans.position.x+i, trans.position.y); var bullet = AllFun.Create(obj, null, new Vector3(1.25f, 1.25f, 1.25f), pos, Quaternion.Euler(0, 0, 0)); bullet.GetComponent<Bullet>().setTrack(); bullet.GetComponent<Bullet>().setSpeed(5); } } else if(type == type_enum.tsj_kobold) { if (skill == 1) { for (int i = 0; i < 360; i += 45) { obj = enyBullet[(int)GameControl.enemyBullet.slash]; var rotaZ = transform.eulerAngles.z; var bullet = AllFun.Create(obj, null, new Vector3(1, 1, 1), trans.position, Quaternion.Euler(0, 0, i + 180)); bullet.GetComponent<Bullet>().setSpeed(5); } } else if(skill == 2) { for (int i=-1; i<=1; i++) { obj = enyBullet[(int)GameControl.enemyBullet.ball]; var pos = new Vector2(trans.position.x + i, trans.position.y); var bullet = AllFun.Create(obj, null, new Vector3(1, 1, 1), pos, Quaternion.Euler(0, 0, 180)); bullet.GetComponent<Bullet>().setSpeed(10); } } else if(skill == 3) { obj = enyBullet[(int)GameControl.enemyBullet.slash]; var rota = Random.Range(-100.0f, -140.0f); var bullet = AllFun.Create(obj, null, new Vector3(1.5f, 1.5f, 1.5f), trans.position, Quaternion.Euler(0, 0, rota)); bullet.GetComponent<Bullet>().setSpeed(15); } else if(skill == 4) { obj = enyBullet[(int)GameControl.enemyBullet.ball]; for (int i = 0; i < 5; i++) { var bullet = AllFun.Create(obj, null, new Vector3(1.5f, 1.5f, 1.5f), trans.position, Quaternion.Euler(0, 0, Random.Range(-25,25))); } } else if(skill == 5) { for (int i = -100; i <= 100; i+=20) { obj = enyBullet[(int)GameControl.enemyBullet.slash]; var rotaZ = transform.eulerAngles.z; var bullet = AllFun.Create(obj, null, new Vector3(1.2f, 1.2f, 1.2f), trans.position, Quaternion.Euler(0, 0, i + 180)); bullet.GetComponent<Bullet>().setSpeed(10); } } else if(skill == 6) { for (int i = -4; i <= 4; i+=2) { obj = enyBullet[(int)GameControl.enemyBullet.ball]; var pos = new Vector2(trans.position.x, trans.position.y + i); var bullet = AllFun.Create(obj, null, new Vector3(1.3f, 1.3f, 1.3f), pos, Quaternion.Euler(0, 0, 90)); bullet.GetComponent<Bullet>().setSpeed(10); } } } else if (type == type_enum.tsj_gleameyes) { if (skill == 1) { for(float i=-0.75f*2; i<=0.75f*2; i+=0.75f) { obj = enyBullet[(int)GameControl.enemyBullet.super]; var pos = new Vector2(trans.position.x + i, trans.position.y); var bullet = AllFun.Create(obj, null, new Vector3(1.25f, 1.25f, 1.25f), pos, Quaternion.Euler(0, 0, 0)); bullet.GetComponent<Bullet>().setTrack(); bullet.GetComponent<Bullet>().setSpeed(5); } } else if(skill == 2) { var eny = GameControl.inst.enemyList[0]; Instantiate(eny, trans.position, Quaternion.Euler(0, 0, 0)); } else if(skill == 3) { for (int i = 90; i < 360; i+=Random.Range(60,80)) { obj = enyBullet[(int)GameControl.enemyBullet.slash]; var rotaZ = transform.eulerAngles.z; var bullet = AllFun.Create(obj, null, new Vector3(1.5f, 1.5f, 1.5f), trans.position, Quaternion.Euler(0, 0, i + 180)); bullet.GetComponent<Bullet>().setSpeed(5); } } else if(skill == 4) { for (int i = 0; i < 360; i+=20) { obj = enyBullet[(int)GameControl.enemyBullet.slash]; var rotaZ = transform.eulerAngles.z; var bullet = AllFun.Create(obj, null, new Vector3(1, 1, 1), trans.position, Quaternion.Euler(0, 0, i + 180)); bullet.GetComponent<Bullet>().setSpeed(5); } } } } } } ``` ---- ## 行動 ``` csharp= IEnumerator Act()//一般行動 移動、變更動畫等 { if (type == type_enum.tsj_x5 || isBoss) { yield return new WaitForSeconds(3); } while(true) { if (type == type_enum.tsj_x5) { transform.position = Vector3.MoveTowards(transform.position, GameControl.inst.player.position, 4 * Time.deltaTime); //移動到目標點的方法 //這邊是移動到玩家的位置 } else if(type == type_enum.tsj_rabbit) { transform.Rotate(0, 0, -100 * Time.deltaTime); rig.velocity = Vector3.down / 2; hp_Ui.rotation = Quaternion.Euler(0, 0, transform.eulerAngles.z); } else if(type == type_enum.tsj_kobold) { AnimatorStateInfo stateinfo = ani.GetCurrentAnimatorStateInfo(0); if(skill == 0) { skill = Random.Range(1, 8); //隨機亂數選擇下招技能 if (skill >= 7) skill = 1; ani.SetInteger("skill", skill); //控制狀態機令他動畫 } else { if(stateinfo.normalizedTime >= 1.0f) { skill = 0; ani.SetInteger("skill", skill); //撥放完則歸0 } } } else if(type == type_enum.tsj_gleameyes) { AnimatorStateInfo stateinfo = ani.GetCurrentAnimatorStateInfo(0); if (skill == 0) { skill = Random.Range(1, 6); ani.SetInteger("skill", skill); } else { if (stateinfo.normalizedTime >= 1.0f) { skill = 0; ani.SetInteger("skill", skill); } } } yield return new WaitForSeconds(0.01f); } } ``` --- ## 道具、額外功能 ---- #### 遊戲中還有三種道具 #### 分別為藥水、雜燴兔肉以及甘蔗 #### 效果是恢復hp、增強彈幕、增強近戰等 ---- ### 創建Item腳本 ``` csharp= public class Item : MonoBehaviour { public type itemType; public GameObject effect; public enum type { potion, meat, cane } private void OnTriggerEnter2D(Collider2D col) { if(col.CompareTag("Player")) { if(itemType == type.potion) { var player = col.GetComponent<Player>(); player.hp = player.setHpUi(player.hp_Ui, player.hp + 50, player.hp_Max); //增加玩家血量 } else if(itemType == type.meat) { var player = col.GetComponent<Player>(); if(player.barrageLv < 3) player.barrageLv++; //增加子彈等級 } else if(itemType == type.cane) { var player = col.GetComponent<Player>(); player.KleinLvUp(); //強化近戰能力 } Instantiate(effect, transform.position, transform.rotation); Destroy(gameObject); } } } ``` ---- #### Player升級近戰方法 ``` csharp= public void KleinLvUp() { kleinLv++; if (kleinLv == 1) { kleinObj.transform.GetChild(0).gameObject.SetActive(true);//顯示放置在克萊因底下的克萊因 klAtk += 5; } else if(kleinLv == 2) { ani.speed += 0.1f;//增加動畫速度 klAtk += 5;//增加攻擊力 } else if(kleinLv == 3) { kleinObj.transform.GetChild(0).gameObject.SetActive(true); kleinObj.transform.GetChild(1).gameObject.SetActive(true); klAtk += 5; } else if(kleinLv == 4) { ani.speed += 0.15f; klAtk += 5; } } ``` ---- ## Switch系統 ### 畫面底下會生成桐人 ### 讓他撐過10秒就可以施放星爆 ---- ``` csharp= using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using DG.Tweening; public class Kirito : Biological { SpriteRenderer sprite; public Sprite starburst; public Sprite ready; bool readyBool; public Text timeUi; public GameObject readyUi; public float moveSpeed; float atkTime_Max; // Start is called before the first frame update void Start() { hp_Max = hp; atkTime_Max = atkTime; atkTime = 0; sprite = GetComponent<SpriteRenderer>(); rig = GetComponent<Rigidbody2D>(); transform.DOMoveY(-4,0.75f); //生成之後讓他往上彈 而不要直接顯示 } // Update is called once per frame void FixedUpdate() { Re_Do(); //還原受到打擊時變更的顏色 if(!readyBool) { atkTime += Time.deltaTime; timeUi.text = "" + (int)atkTime; //如果處於一般狀態 //則每秒增加時間 //並且變更文字令其顯示還要幾秒 } if (atkTime >= atkTime_Max && !readyBool) { //當時間超過10秒 //將其進入ready狀態 sprite.sprite = ready; //變更圖片 Destroy(hp_Ui.GetChild(0).gameObject); //刪除 // //因為ready狀態好了後就不會受傷 Destroy(timeUi.transform.parent.gameObject); //刪除時間的秒數UI介面 readyBool = true; readyUi.SetActive(true); //顯示switch的字樣 Destroy(gameObject.GetComponent<PolygonCollider2D>()); //這邊可做可不做 //只是變更成不同碰撞盒而已 GetComponent<BoxCollider2D>().enabled = true; } if(sprite.sprite == starburst) { //星爆狀態 if (readyUi.activeSelf) { readyUi.SetActive(false); //取消顯示switch字樣 transform.localScale = new Vector3(1, 1, 1); //變更大小 GetComponent<BoxCollider2D>().size = new Vector2(3, 4); //變更碰撞盒範圍 讓星爆可以砍更多敵人 } rig.velocity = transform.up * moveSpeed * Time.deltaTime; } } private void OnTriggerEnter2D(Collider2D col) { if(readyBool) { if(col.CompareTag("Player") && sprite.sprite != starburst) { //就是在桐人準備好的時候 //玩家主動觸碰他 //就可以switch //然後進入星爆狀態 sprite.sprite = starburst; //變更圖片 gameObject.tag = "starburst"; //變更標籤 Destroy(gameObject, 3); //3秒後刪除 GameControl.inst.kiritoCount--; } } else { if (col.CompareTag("enemyBullet")) { //就受傷阿 //這邊忘記弄成方法 //放在生物類別的話就不用重寫那麼多次 var bullet = col.gameObject.GetComponent<Bullet>(); hp = setHpUi(hp_Ui, hp - bullet.atk, hp_Max); if (hp <= 0 && !GameControl.inst.isGameover) { var eff = GameControl.inst.effectList; var obj = eff[(int)GameControl.effect.blueExplo]; Instantiate(obj, transform.position, transform.rotation); Destroy(gameObject); GameControl.inst.kiritoCount--; } Shake(); bullet.damage(); } } } } ``` ---- ## 在GameControl進行生成 ``` csharp= IEnumerator CreateKirito(float time) { while (true) { yield return new WaitForSeconds(time);//每time秒執行一次 if(kiritoCount < kiritoCount_Max)//少於數量上限的話 就生成 { var pos = new Vector2(Random.Range(posX_MinMax.x, posX_MinMax.y), -6); Instantiate(kiritoObj, pos, Quaternion.Euler(0, 0, 0)); kiritoCount++; } } } ``` --- ## 導出網頁 ---- ### 下載itch.io的模板 ## [載點 ](https://drive.google.com/file/d/117N3SFkAf6cj0OWrYM1NrJ-rVnSJazyR/view?usp=sharing) ---- ![](https://i.imgur.com/0EqFnYe.png) ---- ![](https://i.imgur.com/n1maL9x.png) ---- ![](https://i.imgur.com/HZt8wBb.png) ---- #### 然後搜尋itch.io註冊 #### 選擇html上傳 #### 上傳遊戲的zip檔 #### 調整解析度為1120*630即可 --- ## 專案檔案 ---- ## [載點](https://drive.google.com/file/d/1I3koGR9lbf82554U6ogvv2eQJMyGB_sP/view?usp=sharing) #### 直接匯入到unity專案中即可使用 #### unity版本為2019.4以上
{"metaMigratedAt":"2023-06-15T16:18:46.176Z","metaMigratedFrom":"Content","title":"Unity 2D 彈幕遊戲開發","breaks":true,"contributors":"[{\"id\":\"faca3329-cc98-475b-ac42-c0a846116bf8\",\"add\":38431,\"del\":2413}]"}
    323 views