# Unity 2D 彈幕遊戲開發
---
# 目錄
----
### 素材
### 遊戲玩法
### 物件導向概念
### 列舉(Enum)
### 動畫Animator
----
### Dotween動畫
### 人物操作
### 彈幕發射
### 關卡控制器
### 敵人AI
----
### 道具、額外功能
### 導出為網頁
### 原始專案
---
## 素材
----
### 一、下載
#### [點擊下載](https://drive.google.com/file/d/1G1JzjG-V0LZT6Vvlx0An-LkDXIODDyoa/view?usp=sharing)
----
### 二、匯入
----
#### 步驟1:
#### 在Project視窗按右鍵選擇Show in Explorer

----
#### 步驟2:
#### 將檔案全部解壓縮到Asset文件夾裡面

----
#### 即可在Project視窗中使用這些素材

----
### 三、切片
----
#### 步驟1:
#### 選中Project視窗未經過切片之圖片

----
#### 步驟2:
#### 在Inspector視窗將Sprite Mode變更為Multiple
#### 並按下Sprite Editor這個按鈕進入裁切

----
#### 步驟3:
#### 先按下右上角按鈕將其轉為黑白模式
#### 再來按下左上角的Slice按鈕進行切片
#### 確認模式為自動並按下Slice按鈕進行自動切片

----
#### 步驟4:
#### 自動切片後可能還是會有些多餘的切片
#### 直接選中按下Deltet鍵將其刪除
#### 重疊部分則對其邊界用滑鼠拖移調整
#### 或者也可以自己按住滑鼠左鍵拖移出新的切片

----
#### 步驟5:
#### 此遊戲中克萊因是可以玩家揮舞的物件
#### 所以將原本為中心的軸心
#### 調整至腳下
#### 這樣旋轉時就是以腳為中心去旋轉


----
#### 切片完後即可自由使用這些切片完的素材
#### 並可將其拖曳至場景中

----
### Asset Store(資源商店)
----
### 可在這裡下載一些免費/付費素材
### 並應用於你的遊戲中
----
#### 步驟1:
#### 在Unity左上角找到Window
#### 並選擇Asset Store即可進入

----
#### 如果Unity版本為2020以上的
#### 由於Unity已禁止直接在編輯器中打開AssetStore
#### 所以必須直接登入網站安裝
### [AssetStore連結](https://assetstore.unity.com/)
----
#### 步驟1:
#### 搜尋名稱
#### 點擊要的資源

----
#### 步驟2:
#### 點擊下載並等待好後按下Import導入到專案中

----
### 依照上述流程安裝需要的兩個資源
#### 1.Cartoon Fx Free
#### 2.Dotween
----
#### Dotween動畫在導入後會跳出視窗
#### 請在視窗中按下Setup將其導入至專案中

----
### 資源說明
----
### 1.Cartoon Fx Free
#### 粒子動畫特效包
#### 用參數定義並製作的特效
#### 匯入專案後可根據需求
#### 延伸出各式特效
----
### 2.Dotween
#### 提供函式庫可用程式
#### 可用其直接對物件位置、大小、旋轉進行動畫
#### 具體後續會在介紹如何使用
---
## 遊戲玩法
----
## [SAOTSJ](https://allforwife.itch.io/saotsj)
### 操作
#### WSAD控制移動
#### 角色會根據滑鼠方向旋轉
#### 滑鼠左鍵發射子彈攻擊
#### 滑鼠右鍵揮刀攻擊並可彈開子彈
---
## 物件導向概念
----
### 程式的運作就是由不同物件來運行的
### 每個物件都由類別來產生
----
### 在Project視窗右鍵創建兩個script
### 分別命名為Biological以及Player

----
### 封裝
----
#### 就是將所有屬性變數在類別(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
#### 將其叫出

----
#### 步驟2:
#### 在Inspector視窗加入元件

#### 搜尋並選擇Animator

----
#### 步驟3
#### 在Animation視窗創建相關動畫

----
#### 步驟4:
#### 按下左上角的小紅點即可開始錄製

----
#### 步驟5:
#### 按下錄製後
#### 將時間軸移到0:10左右的位置

----
#### 在Inspector視窗調整z軸的旋轉值至131.92
#### 這是動畫的起始點

----
#### 步驟6:
#### 將時間軸移到0:40左右的位置

----
#### 在Inspector視窗調整z軸的旋轉值至-135.3
#### 這是動畫的結束點

----
#### 有了起始點和結束點
#### 動畫就會自己漸變出中間的過程
#### 如此就完成了一個揮舞的動畫
----
#### 點此可新增動畫
#### 並請依照上述流程
#### 做一個反向揮舞(從右至左)的動畫

----
## Animator
----
#### 動畫的狀態機用來控管底下的Animation
#### 比如說人物可以會移動、跳、攻擊等等
#### 那麼Animator可以實現動畫邏輯
#### 判斷每個狀態應該用甚麼動畫
#### 這裡要來實作亞絲娜的克萊因連擊
----
#### 步驟1
#### 從Window/Animation/Animator叫出Animator視窗

----

----
#### 步驟2
#### 在左邊選擇Parameters(參數)
#### 按下+新增整數變數Atk

----
#### 步驟3
#### 在右邊區塊右鍵創建一個新的狀態

----
#### 命名為none
#### 對Entry右鍵選第二個
#### 將起始線連到none

----

----
#### 步驟4
#### 對none右鍵選第一個Make Transition
#### 將線連到我們做的兩個動畫(Animation)狀態

----
#### 步驟5
#### 同樣動畫部分也要回傳回來所以連成這樣

----
#### 步驟6
#### 點擊線段將值設定成如下
#### 其他幾個同理

----
#### 步驟7
#### 加入判斷如果Atk等於1的話
#### 則進入這個狀態

----
#### 反之
#### 不等於1的話則退出狀態

----
#### 另一個攻擊動畫也同理
#### 只是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

----
## Box Collider 2D

----
### 新增元件的方式
#### 於Inspector視窗點此按鈕新增元件

----
#### 其中碰撞盒的大小可從直接從碰撞盒元件調整
#### 或者按下調整按鈕在Scene中編輯它的大小

----
## 人物移動
----
## 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(畫布)作為他的子物件

----
### 步驟2
#### 點擊創建出來的畫布
#### 修改Inspector中的Canvas元件
#### 將渲染模式改為WorldSpace
#### 事件相機改成MainCamera
#### 然後把位置(PosX、Y、Z)都設置為0
#### 最後調整一下大小

----
### 步驟3
#### 在Canvas下右鍵創建Image
#### 作為血條的底框

----
### 步驟4
#### 在Inspector(屬性)視窗中的Image元件中
#### 拖入UI這張全白的圖片

----
### 步驟5
#### 回到Hierarchy視窗
#### 再創建一個Image
#### 作為主要控制的血條

----
### 步驟6
#### 來到血條的Inspector(屬性)視窗
#### 將Image元件設置如下

----
#### 藉由變更Fill Amount的值控制目前血條的比例
#### 當然還需要調整下血條的長寬

----
### 程式碼
#### 再來需要程式碼控制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();
//消滅碰撞到的子彈
}
}
```
---
## 彈幕控制
----
## 子彈設置
----
#### 將子彈圖片拖入場景
#### 並設置子彈碰撞盒及剛體
#### 設置如下

----
### 程式碼
#### 創建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視窗照順序
#### 拖曳到剛剛創建好的欄位
#### 即可使用

----
### 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 宣告後就可以顯示在屬性視窗了
}
```
----
### 創建完成
#### 即可在屬性視窗自行定義每關敵人的配置

----
### 生成敵人
#### 根據定義好的敵人配置來生成關卡的敵人
``` 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)
----

----

----

----
#### 然後搜尋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}]"}