前言
俯視角射擊遊戲(Top-down Shooter)是一類以俯視視角進行遊戲展示的射擊遊戲。在這種遊戲中,玩家控制著一個角色或載具,從俯視的角度上方觀察遊戲世界,並與敵人進行戰鬥。這種視角使玩家能夠有更好的全局觀察和策略性,同時也強調快速反應和精確射擊。
俯視角射擊遊戲的特色包括:
- 視角:俯視角度可以是固定的,也可以隨著玩家角色的移動而變化,但都允許玩家以全局視角觀察整個戰場,有利於制定策略和規劃行動。
- 射擊:玩家通常需要使用各種武器來與敵人進行戰鬥,包括槍械、爆炸物等。射擊動作依賴玩家的反應速度和準確性。
- 敵人:俯視角射擊遊戲通常有大量的敵人,它們可能以固定的路徑或隨機移動,玩家需要躲避敵人的攻擊並選擇最佳時機進行射擊。
- 可破壞元素:遊戲中的地圖通常會有各種可以破壞的元素,例如牆壁、箱子等,玩家可以利用這些元素來尋找掩護、改變戰術或發動攻擊。
- 升級與解鎖能力:許多俯視角射擊遊戲會提供升級系統,玩家可以透過消滅敵人或完成特定任務來獲取經驗值並提升角色的能力或解鎖新的武器和裝備。
一些比較熱門的俯視角射擊遊戲包括:
《元氣騎士》(Katana ZERO):是一款2D俯視角動作射擊遊戲。遊戲中,你扮演一名忍者刺客,透過使用刀劍和其他特殊技能,快速且脆弱地消滅敵人。遊戲以像素化的藝術風格呈現,結合了劇情、快速反應和策略,玩家需要利用時間的操作和環境的互動來完成任務。
《挺進地牢》(Enter the Gungeon):是一款像素風格的俯視角射擊遊戲。遊戲中,你將扮演一位勇敢的冒險者,進入一個充滿怪物和寶藏的地下城。你需要使用各種武器、道具和特殊技能來對抗敵人,並逐步深入地牢的層級。遊戲著重隨機生成的關卡和強大的敵人,同時也提供了多人合作模式。
《失落城堡》(Dead Cells):是一款像素風格的動作平台遊戲,也被歸類為俯視角射擊遊戲。玩家扮演的角色是一個不死的戰士,探索一個被怪物和陷阱充滿的廢墟。玩家需要透過戰鬥、探索和收集資源,不斷改進自己的能力,並逐漸深入廢墟。遊戲的關卡是隨機產生的,每次都有不同的挑戰和發現。
這些俯視角射擊遊戲取得了相當的成功,並且擁有龐大的玩家群。它們提供了豐富的內容和挑戰,讓玩家可以享受刺激的射擊體驗。這只是一些例子,市場上還有許多其他優秀的俯視角射擊遊戲可供選擇。
Unity 遊戲開發:角色移動與場景搭建
因為本期的重點放在多種射擊效果,角色移動和環境如何搭建等一些基礎的知識這裡就不細說了,節省大家的時間。
當然,之前我也寫過很多角色移動的方法和環境搭建的方法,有興趣的同學也可以先去了解一下:
【Unity 插件推薦】瓦片地圖工具高效打造 Unity2D 場景,TileMap 功能全面解析!
角色、環境與武器素材連結:
https://o-lobster.itch.io/simple-dungeon-crawler-16x16-pixel-pack
https://humanisred.itch.io/weapons-and-bullets-pixel-art -asset
Unity 遊戲開發:綁定槍械
1. 首先將各種槍械的素材加入人物作為子物體
首先要發射各式子彈就需要各式槍械作為載體,所以我們先給這個人物添加多個槍械並添加切換槍械的功能。
先將各種槍械的素材添加給人物作為子物體…
2. 給槍械也分別增加兩個子物體用作標記槍口和彈倉位置
3. 槍械動畫
為槍械添加動畫器,包括槍戒的待機動畫和發射動畫
透過一個 trigger 參數 shoot 開始播放發射動畫並在播放完動畫後切換回待機動畫。
4. 切換槍械
將所有子物體槍械都取消激活,這樣在激活的時候才會使用指定的槍械
在控制人物的腳本 PlayerMovement 中添加切換槍械的功能:
新建一個 GameObject 的陣列 guns 用來儲存所有槍械,一個 int 參數 gunNum 用作標記當前使用的槍械下標。
在 Start 函數中激活第一個槍械用作初始槍械,然後編寫一個函數 SwitchGun 並在Update函數中調用,在這個函數中將會偵測按鍵用來切換槍械。
private Animator animator;
private Rigidbody2D rigidbody;
public GameObject[] guns;
private int gunNum;
void Start()
{
animator = GetComponent<Animator>();
rigidbody = GetComponent<Rigidbody2D>();
guns[0].SetActive(true);
}
void Update()
{
SwitchGun();
}
void SwitchGun(){}
當按下 Q 鍵時將目前的槍械取消啟動,讓槍械編號減 1 並且如果編號小於 0 就將編號設為數組尾部,然後重新激活當前編號的槍械。
按下 E 鍵時基本一致,只是讓槍械下標加 1,如果下標超出數組邊界就重置為 0。
void SwitchGun()
{
if (Input.GetKeyDown(KeyCode.Q))
{
guns[gunNum].SetActive(false);
if (--gunNum < 0)
{
gunNum = guns.Length - 1;
}
guns[gunNum].SetActive(true);
}
if (Input.GetKeyDown(KeyCode.E))
{
guns[gunNum].SetActive(false);
if (++gunNum > guns.Length - 1)
{
gunNum = 0;
}
guns[gunNum].SetActive(true);
}
}
將所有槍械綁定給數組 guns。
然後運行遊戲,按下Q和E鍵,就可以自由切換槍械了。
Unity 遊戲開發:發射功能
現在依序為各種槍械添加發射功能:
手槍
(1) 槍械隨著滑鼠旋轉
給初始槍械手槍創建腳本 Pistol,程式碼已經加了詳細的註釋,這裡就不過多解釋了。
using UnityEngine;
public class Pistol : MonoBehaviour
{
//宣告一個 float 版本參數 interval 作為射擊間隔時間
public float interval;
//兩個 GameObject 參數輸入子彈和彈殼的預製體
public GameObject bulletPrefab;
public GameObject shellPrefab;
//兩個 Transform 參數用來標記槍口和彈倉位置
private Transform muzzlePos;
private Transform shellPos;
//兩個 Vector2 類型的參數用來記錄滑鼠位置和發射的方向
private Vector2 mousePos;
private Vector2 direction;
//一個 float 類型的參數 timer 用作計時器
private float timer;
//一個 Animator 參數取得動畫器
private Animator animator;
//然後在 Start 函數中取得動畫器和子物件位置方便後續使用
void Start()
{
animator = GetComponent<Animator>();
muzzlePos = transform.Find("Muzzle");
shellPos = transform.Find("BulletShell");
}
//接著在 Update 函數中持續取得滑鼠位置
void Update()
{
/*
* 我們可以輸入input.mousePosition取得滑鼠的目前位置
* 但這個位置是的座標位置,這個位置螢幕為原點建構的座標系統,需要的位置是在世界座標系統的實際座標
* 可以使用Camera.main.ScreenToWorldPoint來將像素座標轉換為世界座標
*/
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Shoot();
}
//這個函數中我們會讓槍口指向滑鼠方向
void Shoot()
{
/*
* 首先要取得滑鼠方向的向量
* 用目前位置減去槍械位置並進行標準化,即上市公司槍械所需的方向
* 然後更改槍械的局部座標,讓槍械的局部右方向總是等於這個方向
* 這樣就實現了槍械指示滑鼠位置的效果
*/
direction = (mousePos - new Vector2(transform.position.x, transform.position.y)).normalized;
transform.right = direction;
}
}
現在可以嘗試運行遊戲查看一下效果。
可以看到槍械在隨著滑鼠旋轉,但當滑鼠處於人物的左側時,槍械就會倒轉過來。
所以我們需要在滑鼠在人物左側時上下旋轉一下槍械,我們可以通過修改 localScale 屬性達到翻轉的效果。
private float flipY;
void Start()
{
//...
flipY = transform.localScale.y;
}
void Update()
{
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
if (mousePos.x < transform.position.x)
transform.localScale = new Vector3(flipY, -flipY, 1);
else
transform.localScale = new Vector3(flipY, flipY, 1);
Shoot();
}
(2) 射擊時間間隔
然後繼續編寫Shoot函數,當按住Fire鍵,也就是滑鼠左鍵時。
void Shoot()
{
direction = (mousePos - new Vector2(transform.position.x, transform.position.y)).normalized;
transform.right = direction;
if (timer != 0)
{
timer -= Time.deltaTime;
if (timer <= 0)
timer = 0;
}
if (Input.GetButton("Fire1"))
{
if (timer == 0)
{
timer = interval;
Fire();
}
}
}
void Fire() { //後面完善 }
(3) 創造好子彈、彈殼和爆炸特效
完成發射子彈的函數Fire之前我們需要創建好子彈、彈殼和爆炸特效。
給子彈添加剛體、碰撞器,並設置參數。
然後給彈殼添加剛體,稍微增大重力參數即可。
接著給爆炸特效添加動畫器。
(4) 為子彈添加圖層 Bullet 並使子彈之間不會相互碰撞(這很重要,不然子彈間會互相銷毀)。
(5) 編寫好子彈、彈殼和爆炸特效腳本
分別為子彈、彈殼和爆炸特效創建腳本 Bullet、BulletShell 和 Explosion,首先編寫 Bullet 腳本。
using UnityEngine;
public class Bullet : MonoBehaviour
{
//宣告一個 float 類型的參數 speed 設定子彈的速度
public float speed;
//一個 GameObject 參數傳入爆炸特效的預製體
public GameObject explosionPrefab;
//一個 Rigidbody2D 參數取得剛體
new private Rigidbody2D rigidbody;
//因為生成預製體後馬上就會使用這個參數,Start 函數來不及取得到剛體,所以在 Awake 函數中取得剛體
void Awake()
{
rigidbody = GetComponent<Rigidbody2D>();
}
//宣告一個公有的函數 SetSpeed 並傳入一個 Vector2 的參數設定子彈移動的方向
public void SetSpeed(Vector2 direction)
{
//將剛體的速度設定為方向乘以速度,讓子彈開始移動
rigidbody.velocity = direction * speed;
}
void Update()
{
}
//在子彈碰撞到物體時生成爆炸特效並銷毀子彈
private void OnTriggerEnter2D(Collider2D other)
{
Instantiate(explosionPrefab, transform.position, Quaternion.identity);
Destroy(gameObject);
}
}
繼續編寫彈殼腳本 BulletShell。
using System.Collections;
using UnityEngine;
public class BulletShell : MonoBehaviour
{
//聲明三個 flot 類型的參數用作彈殼被拋出的速度、停下的時間和彈殼消失的速度
public float speed;
public float stopTime = .5f;
public float fadeSpeed = .01f;
//一個 Riqidbody2D 參數和一個 SpriteRendera 參數取得剛體和精靈渲染器
new private Rigidbody2D rigidbody;
private SpriteRenderer sprite;
//同樣在 Awake 函數中取得到剛體和精靈渲染器方便後續使用
void Awake()
{
rigidbody = GetComponent<Rigidbody2D>();
sprite = GetComponent<SpriteRenderer>();
//給彈殼一個向上的速度實現拋出的效果
rigidbody.velocity = Vector3.up * speed;
//使用協程實現這個效果,新建一個協程 Stop
StartCoroutine(Stop());
}
//彈殼將在一段時間後停止模擬落地,落地後彈殼會逐漸淡出,直到完全透明後銷毀彈殼
IEnumerator Stop()
{
//在這個協程中先等待設定好的時間
yield return new WaitForSeconds(stopTime);
//然後將重力和速度都設為 0
rigidbody.velocity = Vector2.zero;
rigidbody.gravityScale = 0;
//然後開始一個 while 循環,當渲染器的 alpha 值大於 O 時每個畫面設定渲染器的顏色
while (sprite.color.a > 0)
{
//每次循環都讓 alpha 的值減少使其變的逐漸透明
sprite.color = new Color(sprite.color.r, sprite.color.g, sprite.color.g, sprite.color.a - fadeSpeed);
//然後等待一個 FixedUpdater 幀
yield return new WaitForFixedUpdate();
}
//在結束循環後銷毀掉彈殼
Destroy(gameObject);
}
}
然後編寫爆炸特效腳本 Explosion。
using UnityEngine;
public class Explosion : MonoBehaviour
{
//聲明一個 Animator 參數和一個 AnimatorStateInfo 參數取得動畫器和動畫進度
private Animator animator;
private AnimatorStateInfo info;
//在 Awake 函數中取得到動畫器方便後續使用
void Awake()
{
animator = GetComponent<Animator>();
}
void Update()
{
//持續的獲取動畫進度
info = animator.GetCurrentAnimatorStateInfo(0);
if (info.normalizedTime >= 1)
{
//播放完動畫後,銷毀特效
Destroy(gameObject);
}
}
}
(6)製作子彈、彈殼和爆炸特效預製體
將子彈、彈殼和爆炸特效都拖曳到資源視窗中製作成預製體,然後分別設定好對應的腳本參數就創建好預製體了。
(7) 發射子彈
現在我們可以繼續編寫手槍腳本中的Fire函數了
void Fire()
{
//首先觸發動畫器的參數 Shoot 播放發射動畫
animator.SetTrigger("Shoot");
//然後生成子彈的預體體,並將生成的子彈位置設為槍口的位置
GameObject bullet = Instantiate(bulletPrefab, muzzlePos.position, Quaternion.identity);
//接著取得 Bullet 腳本然後呼叫 SetSpeed 函數設定子彈發射的方向為槍口朝向的方向,也就是 direction 參數
bullet.GetComponent<Bullet>().SetSpeed(direction);
//最後生成彈殼的預製體並將位置設為彈倉位置,旋轉也設為彈倉的旋轉角度
Instantiate(shellPrefab, shellPos.position, shellPos.rotation);
}
回到 unity 運行遊戲,按下滑鼠左鍵,可以看到子彈可以朝著滑鼠方向發射了。
(8) 子彈和彈殼偏移
不過在很多遊戲中子彈都不那麼精準,會在一個小區間內產生偏移。
現在讓我們加上這個小功能,在發射子彈前使用Random.Range產生一個隨機的偏移角度。
void Fire()
{
animator.SetTrigger("Shoot");
GameObject bullet = Instantiate(bulletPrefab, muzzlePos.position, Quaternion.identity);
//我這裡就在 -5 度和 5 度之間產生了一個 10 度內的隨機偏移
float angel = Random.Range(-5f, 5f);
/*
* 然後在設定速度時對方向做一點更改,使用 Quaternion.AngleAxis 產生一個相對偏轉
* 傳入隨機出的角度並讓其繞著 z 軸旋轉
* 再乘以正常的方向就產生了以這個方向為基準的偏轉方向
*/
bullet.GetComponent<Bullet>().SetSpeed(Quaternion.AngleAxis(angel, Vector3.forward) * direction);
Instantiate(shellPrefab, shellPos.position, shellPos.rotation);
}
也可以用這個方法修改彈殼的程式碼,讓彈殼以一個隨機的角度拋出提升觀感。
進入 BulletShell 腳本,同樣在設定速度前隨機一個角度並讓拋出的方向偏轉這個角度。
void Awake()
{
rigidbody = GetComponent<Rigidbody2D>();
sprite = GetComponent<SpriteRenderer>();
//修改
float angel = Random.Range(-30f, 30f);
rigidbody.velocity = Quaternion.AngleAxis(angel, Vector3.forward) * Vector3.up * speed;
StartCoroutine(Stop());
}
再次運行遊戲,現在子彈發射時會在一個範圍內隨機射擊,彈殼也會以隨機的角度拋出了。
(9) 物件池優化
但現在存在一個問題,因為我們是透過不斷的實例化預製體來製造子彈或彈殼的,而這些生成的物體也會很快被銷毀。
現在數量比較少的情況下還好,一旦需要的物體數量達到一定程度,不斷的創造和銷毀物件會對遊戲性能造成很大的影響。
這時就需要用到物件池了,我們會將用完的物體取消激活並放回物件池中,在需要使用物體時再從對像激活池中物體使用。
只有在對像池裡的待分配物體不足時才會進行實例化操作,相對通常的創建和銷毀只是對物體進行啟動和取消啟動操作,節省了很多性能。
現在讓我們先來寫一個物件池腳本,新建一個腳本,取名為 ObjectPool。
這個腳本使用單例模式進行編寫,因為腳本不需要掛載在任何物件上,所以不需要繼承MonoBehavior
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool
{
private static ObjectPool instance; // 單例模式
// /**
// * 我們希望不同的物體可以被分開存儲,在這種情況下使用字典是最合適的
// * 所以宣告一個字典 objectPool 作為物件池主體,以字串型別的物件的名字作為key
// * 使用佇列儲存物件來作為 value ,這裡使用佇列只是因為入隊和出隊的操作較為方便,也可以換成其他集合方式
// * 然後實例化這個字典以便後續使用
// * /
private Dictionary<string, Queue<GameObject>> objectPool = new Dictionary<string, Queue<GameObject>>(); // 物件池字典
private GameObject pool; // 為了不讓視窗雜亂,宣告一個物件池父物體,作為所有生成物體的父物體
public static ObjectPool Instance // 單例模式
{
get
{
if (instance == null)
{
instance = new ObjectPool();
}
return instance;
}
}
public GameObject GetObject(GameObject prefab) // 從物件池中獲取物件
{
GameObject _object;
if (!objectPool.ContainsKey(prefab.name) || objectPool[prefab.name].Count == 0) // 如果物件池中沒有該物件,則實例化一個新的物件
{
_object = GameObject.Instantiate(prefab);
PushObject(_object); // 將新的物件加入物件池
if (pool == null)
pool = new GameObject("ObjectPool"); // 如果物件池父物體不存在,則建立一個新的物件池父物體
GameObject childPool = GameObject.Find(prefab.name + "Pool"); // 尋找該物件的子物件池
if (!childPool)
{
childPool = new GameObject(prefab.name + "Pool"); // 如果該物件的子物件池不存在,則建立一個新的子物件池
childPool.transform.SetParent(pool.transform); // 將該子物件池加入物件池父物件中
}
_object.transform.SetParent(childPool.transform); // 將新的物件加入該物件的子物件池中
}
_object = objectPool[prefab.name].Dequeue(); // 從物件池中取出一個物件
_object.SetActive(true); // 啟動該物件
return _object; // 傳回該物件
}
public void PushObject(GameObject prefab) // 將物件加入物件池中
{
//取得物件的名稱,因為實例化的物件名稱都會加上"(Clone)"的後綴,需要先去掉這個後綴才能使用名稱查找
string _name = prefab.name.Replace("(Clone)", string.Empty);
if (!objectPool.ContainsKey(_name))
objectPool.Add(_name, new Queue<GameObject>()); // 如果對像池中沒有該物件,則建立一個新的對像池
objectPool[_name].Enqueue(prefab); // 將物件加入物件池中
prefab.SetActive(false); // 將物件禁用
}
}
現在回到先前的腳本,將所有產生或銷毀的程式碼都使用物件池操作優化。
# Bullet 腳本
private void OnTriggerEnter2D(Collider2D other)
{
// Instantiate(explosionPrefab, transform.position, Quaternion.identity);
GameObject exp = ObjectPool.Instance.GetObject(explosionPrefab);
exp.transform.position = transform.position;
// Destroy(gameObject);
ObjectPool.Instance.PushObject(gameObject);
}
# BulletShell 腳本
IEnumerator Stop()
{
//...
// Destroy(gameObject);
ObjectPool.Instance.PushObject(gameObject);
}
# Explosion 腳本
void Update()
{
info = animator.GetCurrentAnimatorStateInfo(0);
if (info.normalizedTime >= 1)
{
// Destroy(gameObject);
ObjectPool.Instance.PushObject(gameObject);
}
}
# Gun 腳本
protected virtual void Fire()
{
animator.SetTrigger("Shoot");
// GameObject bullet = Instantiate(bulletPrefab, muzzlePos.position, Quaternion.identity);
GameObject bullet = ObjectPool.Instance.GetObject(bulletPrefab);
bullet.transform.position = muzzlePos.position;
float angel = Random.Range(-5f, 5f);
bullet.GetComponent<Bullet>().SetSpeed(Quaternion.AngleAxis(angel, Vector3.forward) * direction);
// Instantiate(shellPrefab, shellPos.position, shellPos.rotation);
GameObject shell = ObjectPool.Instance.GetObject(shellPrefab);
shell.transform.position = shellPos.position;
shell.transform.rotation = shellPos.rotation;
}
然後修改單殼的腳本 BulletShell。
因為之前拋出的操作是在 Awake 函數中進行的,而使用物件池重用彈殼不會再次呼叫 Awake 函數,所以我們將拋出部分的程式碼放在 OnEnable 函數中,這個函數將在物體被激活時調用。
private void OnEnable()
{
float angel = Random.Range(-30f, 30f);
rigidbody.velocity = Quaternion.AngleAxis(angel, Vector3.forward) * Vector3.up * speed;
//而由於彈殼的透明度和重力已經被修改過,所以每次重新設定彈殼的透明度和重力
sprite.color = new Color(sprite.color.r, sprite.color.g, sprite.color.b, 1);
rigidbody.gravityScale = 3;
StartCoroutine(Stop());
}
運行遊戲查看效果。
按下左鍵射擊後窗口中出現了對像池的父物體,展開就可以看到子彈、彈殼和爆炸特效的對像池和其中的物體了。
可以看到現在物體處於失活狀態,再次開始射擊後沒有再生成新物體,而是激活了這些物體使用,當物體該被銷毀時又會取消激活回到對像池中。
(10) 封裝槍械的父類
完成優化後我們就可以繼續製作其他種類的槍械了!
回到手槍腳本中仔細觀察其實可以發現槍械的行為大同小異,需要更改的只有一些變量或者發射的行為,
這種情況下可以將我們當前的腳本作為父類,讓其他槍械腳本繼承這個類別提高程式碼重複使用性。
首先我們新建一個類別 Gun 作為所有槍械的父類別。
將剛才手槍腳本的程式碼剪切到這個類別中實現最基礎的槍械功能然後讓手槍類別繼承承 Gun。
接著將所有 private 的變數和函數更改為 protected 讓子類別可以繼承這些基礎的變數和函數。
別類槍械的行為肯定會與手槍有些許不同,所以我們使用 virtual 關鍵子將所有函數設為虛函數,這樣類別中就可以透過重寫某些函數達到修改特定行為的效果。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Gun : MonoBehaviour
{
public float interval;
public GameObject bulletPrefab;
public GameObject shellPrefab;
protected Transform muzzlePos;
protected Transform shellPos;
protected Vector2 mousePos;
protected Vector2 direction;
protected float timer;
protected float flipY;
protected Animator animator;
protected virtual void Start()
{
animator = GetComponent<Animator>();
muzzlePos = transform.Find("Muzzle");
shellPos = transform.Find("BulletShell");
flipY = transform.localScale.y;
}
protected virtual void Update()
{
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
if (mousePos.x < transform.position.x)
transform.localScale = new Vector3(flipY, -flipY, 1);
else
transform.localScale = new Vector3(flipY, flipY, 1);
Shoot();
}
protected virtual void Shoot()
{
direction = (mousePos - new Vector2(transform.position.x, transform.position.y)).normalized;
transform.right = direction;
if (timer != 0)
{
timer -= Time.deltaTime;
if (timer <= 0)
timer = 0;
}
if (Input.GetButton("Fire1"))
{
if (timer == 0)
{
timer = interval;
Fire();
}
}
}
protected virtual void Fire()
{
animator.SetTrigger("Shoot");
// GameObject bullet = Instantiate(bulletPrefab, muzzlePos.position, Quaternion.identity);
GameObject bullet = ObjectPool.Instance.GetObject(bulletPrefab);
bullet.transform.position = muzzlePos.position;
float angel = Random.Range(-5f, 5f);
bullet.GetComponent<Bullet>().SetSpeed(Quaternion.AngleAxis(angel, Vector3.forward) * direction);
// Instantiate(shellPrefab, shellPos.position, shellPos.rotation);
GameObject shell = ObjectPool.Instance.GetObject(shellPrefab);
shell.transform.position = shellPos.position;
shell.transform.rotation = shellPos.rotation;
}
}
手槍程式碼直接基礎父類Gun即可,什麼也不需要做。
public class Pistol : Gun
{
}
散彈槍
現在來嘗試寫第一個子類槍械散彈槍:
(1) 建立一個新腳本起名為 Shotgun 並繼承父類別 Gun。
using UnityEngine;
public class Shotgun : Gun
{
//首先聲明個公有的 int 參數 bulletNum 表示一次開火射出多少發子彈
public int bulletNum = 3;
//一個公有的 float 變數 bulletAngle 表示每個子彈間的間隔角度
public float bulletAngle = 15;
//散彈槍與基礎槍械的區別是開槍時會均勻的射出多發子彈
//這個不同只涉及開火時,所以只需重寫Fire函數即可
//使用override關鍵字重寫函數,這樣這個函數就會覆寫繼承的函數
protected override void Fire()
{
//在重寫的函數中首先依舊是要觸發動畫器的 tigger 參數來播放射擊動畫
animator.SetTrigger("Shoot");
//然後算出子彈數的中間值用來計算每個子彈的偏轉角度
int median = bulletNum / 2;
//接著開始一個子彈次數的循環產生對應數量的子彈
for (int i = 0; i < bulletNum; i++)
{
//從物件池中取出一個子彈的預製體然後將子彈的位置設為槍口的位置
GameObject bullet = ObjectPool.Instance.GetObject(bulletPrefab);
bullet.transform.position = muzzlePos.position;
//根據子彈數量的奇偶來計算子彈應該偏轉的角度
if (bulletNum % 2 == 1)
{
}
else
{
}
}
}
}
(2) 散彈槍根據子彈數量的奇偶來計算子彈應該偏轉的角度
如果是奇數那麼只需要讓目前的循環次數,也就是第幾顆子彈減去法中間值。
這個值就代表這顆子彈是哪邊的第幾顆子彈,負數在左側,正數在右側,零就是中間的子彈,讓這個值乘以間隔角度就得到了當前子彈的偏轉角度。
而偶數子彈只會分佈在兩側,就需要在奇數的基礎上加上間隔角度的一半來得到偏轉角度。
(3) 完善程式碼
根據子彈數的奇偶計算出相應的偏轉角度後,就可以按照之前的方式設置子彈的偏轉量了。
生成完所有子彈後,同樣生成一個彈殼並設置位置和旋轉。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Shotgun : Gun
{
public int bulletNum = 3;
public float bulletAngle = 15;
protected override void Fire()
{
animator.SetTrigger("Shoot");
int median = bulletNum / 2;
for (int i = 0; i < bulletNum; i++)
{
GameObject bullet = ObjectPool.Instance.GetObject(bulletPrefab);
bullet.transform.position = muzzlePos.position;
if (bulletNum % 2 == 1)
{
bullet.GetComponent<Bullet>().SetSpeed(Quaternion.AngleAxis(bulletAngle * (i - median), Vector3.forward) * direction);
}
else
{
bullet.GetComponent<Bullet>().SetSpeed(Quaternion.AngleAxis(bulletAngle * (i - median) + bulletAngle / 2, Vector3.forward) * direction);
}
}
GameObject shell = ObjectPool.Instance.GetObject(shellPrefab);
shell.transform.position = shellPos.position;
shell.transform.rotation = shellPos.rotation;
}
}
(3) 散彈槍效果
回到 Unity,為散彈槍子物體添加腳本並設定好參數和預製體。
運行遊戲,現在子彈可以進行散射了!
也可以透過調整子彈總數和間隔角度來達到不同的發射效果。
火箭筒
除了常規的設定直線速度發射子彈,我們也可以透過不斷改變移動方向來達到曲線射擊的效果
(1) 編寫火箭筒腳本。
為火箭筒新建一個腳本 RocketLauncher 並進入,首先同樣也需要繼承父類 Gun,和散彈槍的思路類似,火箭筒也可以一次發射出多個火箭彈。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 火箭发射器类
public class RocketLauncher : Gun
{
// 火箭數量
public int rocketNum = 3;
// 火箭發射角度
public float rocketAngle = 15;
// 重寫父類的開火方法
protected override void Fire()
{
// 播放射擊動畫
animator.SetTrigger("Shoot");
// 延遲開火
StartCoroutine(DelayFire(.2f));
}
// 延遲開火協程
IEnumerator DelayFire(float delay)
{
yield return new WaitForSeconds(delay);
// 計算中間置
int median = rocketNum / 2;
// 循環發射火箭
for (int i = 0; i < rocketNum; i++)
{
// 從物件池中獲取火箭
GameObject bullet = ObjectPool.Instance.GetObject(bulletPrefab);
// 設定火箭位置
bullet.transform.position = muzzlePos.position;
// 根據火箭數量的奇偶性設定火箭發射角度
if (rocketNum % 2 == 1)
{
bullet.transform.right = Quaternion.AngleAxis(rocketAngle * (i - median), Vector3.forward) * direction;
}
else
{
bullet.transform.right = Quaternion.AngleAxis(rocketAngle * (i - median) + rocketAngle / 2, Vector3.forward) * direction;
}
// 設定火箭目標
bullet.GetComponent<Rocket>().SetTarget(mousePos);
}
}
}
(2) 創造火箭彈和新爆炸特效的預製體
因為和普通子彈基本上一致,只是添加了一個煙霧的粒子系統作為子物體,爆炸特效也是一樣,所以我提前準備好了預製體就不再演示了。
(3) 編寫火箭彈腳本
但腳本邏輯和子彈還是有很大不同的,所以給火箭新腳本 Rocket。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Rocket : MonoBehaviour
{
public float lerp; // 插值系数
public float speed = 15; // 移動速度
public GameObject explosionPrefab; // 爆炸特效癒治
new private Rigidbody2D rigidbody; // 剛體組件
private Vector3 targetPos; // 目標位置
private Vector3 direction; // 移動方向
private bool arrived; // 是否到達目標點
private void Awake()
{
rigidbody = GetComponent<Rigidbody2D>(); // 獲取剛體組件
}
public void SetTarget(Vector2 _target) // 設定目標點
{
arrived = false; // 重置的打標誌位
targetPos = _target; // 設定目標點
}
private void FixedUpdate()
{
direction = (targetPos - transform.position).normalized; // 計算移動方向
if (!arrived) // 如果還沒到達目標點
{
transform.right = Vector3.Slerp(transform.right, direction, lerp / Vector2.Distance(transform.position, targetPos)); // 差值旋轉
rigidbody.velocity = transform.right * speed; // 設定速度
}
if (Vector2.Distance(transform.position, targetPos) < 1f && !arrived) // 如果到達目標點
{
arrived = true; // 设置到達標誌位
}
}
private void OnTriggerEnter2D(Collider2D other) // 碰撞檢測
{
GameObject exp = ObjectPool.Instance.GetObject(explosionPrefab); // 從物件池中獲取爆炸特效
exp.transform.position = transform.position; // 設定特效位置
rigidbody.velocity = Vector2.zero; // 停止移动
StartCoroutine(Push(gameObject, .3f)); // 延遲回收
}
IEnumerator Push(GameObject _object, float time) // 延遲回收攜程
{
yield return new WaitForSeconds(time); // 等待一段時間
ObjectPool.Instance.PushObject(_object); // 回收物件
}
}
(4) 設定好火箭彈和火箭筒的腳本和參數
(5) 散彈槍效果
可以看到火箭筒的攻擊已經達到我們預期的效果。
雷射槍
接下來介紹兩種特殊的射擊方式。
一般的槍械都是透過生成子彈並讓子彈移動實現發射的功能,發射出的子彈有一個移動的過程,並且檢測擊中也是由子彈單獨完成。
不過有一些特殊的槍械也使用實體子彈,這種槍械”發射”的是一種瞬間到達的子彈。
在 Unity 中稱為“射線”,雷射就是一種典型的射線。
接下來我們就從雷射槍入手,通過射線檢測實現這種特殊的射擊方式。
(1) 編寫雷射槍腳本
為雷射槍創建腳本 Lasergun,同樣繼承槍械父類 Gun。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lasergun : Gun
{
private bool isShooting;
// 重寫父類的 Shoot 方法
protected override void Shoot()
{
// 計算槍口朝向
direction = (mousePos - new Vector2(transform.position.x, transform.position.y)).normalized;
transform.right = direction;
// 检测是否按下射击键
if (Input.GetButtonDown("Fire1"))
{
isShooting = true;
}
// 檢測是否放開射擊鍵
if (Input.GetButtonUp("Fire1"))
{
isShooting = false;
}
// 設定 Shoot 動畫狀態
animator.SetBool("Shoot", isShooting);
// 如果正在射擊,則發射子彈
if (isShooting)
{
Fire();
}
}
// 發射子彈
protected override void Fire()
{
// 檢測射線碰撞
RaycastHit2D hit2D = Physics2D.Raycast(muzzlePos.position, direction, 30);
// 繪製射線
Debug.DrawLine(muzzlePos.position, hit2D.point);
}
}
(2) 先運行遊戲,看看效果
回到 Unity 運行遊戲,按住滑鼠左鍵,可以看到一條射線從槍口向滑鼠方向射出了。
(3) 美化射線
但使用 Debug 繪製的線條太過簡陋,也只能在開發時看到,所以接下來使用其他方法繪製更美觀的射線。
我們需要像 DrawLine 方法一樣設定起始和結束位置,LineRenderer 就是一個不錯的選擇。
找到雷射槍的槍口子物體,添加上 LineRenderer 組件,LineRenderer 將根據可用點的位置繪製多段線條,我們只需要一條直線,所以將 size 設為 2。
將圖層順序調高防止被地面層遮擋,然後更改一下可用點的位置可以看到場景中已經出現了一段線條,適當調整寬度讓線條適配雷射槍的寬度 可以看到場景中已經出現了一段線條。
適當調整寬度讓線條適配雷射槍的寬度修改線段的顏色讓其符合雷射槍的配色,我在這裡就設定為紅色 LineRender 的顏色設定視窗和正常的略有不同,可以分別修改兩個邊界的顏色和透明度中間的值會自動進行漸變,因為需要純紅色所以兩端都設定為紅色,也不需要修改透明度你也可以根據自己的想法設定出更絢麗的顏色接著增加末端頂點的值讓線段的兩端更加圓滑,這樣一條用作顯示雷射的線段就完成了。
我們希望在開槍時才顯示線段,所以先將 LineRenderer 取消激活。
(4) 完善程式碼
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lasergun : Gun
{
private GameObject effect; // 特效
private LineRenderer laser; // 雷射線
private bool isShooting; // 是否正在射擊
protected override void Start()
{
base.Start();
laser = muzzlePos.GetComponent<LineRenderer>(); // 獲取雷射線組件
effect = transform.Find("Effect").gameObject; // 獲取特效物體
}
protected override void Shoot()
{
direction = (mousePos - new Vector2(transform.position.x, transform.position.y)).normalized; // 獲取射擊方向
transform.right = direction; // 轉向射擊方向
if (Input.GetButtonDown("Fire1")) // 按下射擊鍵
{
isShooting = true; // 正在射擊
laser.enabled = true; // 顯示雷射線
effect.SetActive(true); // 顯示特效
}
if (Input.GetButtonUp("Fire1")) // 鬆開射擊鍵
{
isShooting = false; // 停止射擊
laser.enabled = false; // 隱藏雷射線
effect.SetActive(false); // 隱藏特效
}
animator.SetBool("Shoot", isShooting); // 設定射擊動畫
if (isShooting) // 正在射擊
{
Fire(); // 發射子彈
}
}
protected override void Fire()
{
RaycastHit2D hit2D = Physics2D.Raycast(muzzlePos.position, direction, 30); // 發射射線
// Debug.DrawLine(muzzlePos.position, hit2D.point);
laser.SetPosition(0, muzzlePos.position); // 設定雷射起點
laser.SetPosition(1, hit2D.point); // 設定雷射終點
effect.transform.position = hit2D.point; // 設定特效位置
effect.transform.forward = -direction; // 設定特效方向
}
}
(5) 再次運行遊戲
回到 Unity 再次運行遊戲,按住滑鼠左鍵,現在就可以看到一條更美觀的射線了。
不過就算這樣這也只是一條線接下來需要讓這條線更像雷射,我們將使用後處理 Post-Processing 實現這一效果,你可以將後處理理解為一種濾鏡。
為了使用後處理,我們需要讓項目升級為 URP 通用渲染管線項目,你可以在創建項目時就選擇 URP 模板創建,不過這裡使用第二種方法升級。
(6) 升級URP項目
我們可以到 Windows ->Package Manager 中搜尋 universal-導入 URP 的包。
導入包後還需要一些步聚讓項自使用 URP,右鍵 Creat -> Rendering 創建一個 URP 的 Asset。
Unity 建立了兩個文件,我們選擇名字不帶 render 的文件並勾選 HDR 選項。
然後在 Project Setting -> Graphics 的 pipeline setting 選項中設置剛創建的 URP Asset。
最後勾選相機下的 poost processing 選項就設置完成了。
(7) 後處理
接下來就可以在層次視圖下右鍵建立一個 volume,選擇 global volume 全域生效。
可以看到場景中產生了一個名為 Global Volume 的物體,將由這個物體下的 volume 腳本給相機新增後處理特效。
首先在 Profile 屬性下點選新建建立一個新的 volume 設定檔,建立完成後下方將出現 Add Override 的按鈕,點擊按鈕可建立一個新特效。
選擇 Bloom 泛光效果,Bloom 會產生一個明亮的光暈效果,非常適合用在雷射上。
目前只需要勾選前兩個選項,Threshold 屬性將設定 Bloom 產生效果的閾值,Intensity 屬性則將影響 Bloom 產生的泛光效果的強度,試著將閾值調低,可以看到整個場景稍微亮了一些。
(8) 新建 Shader Graph
但我們需要更精確的調整某個物體的泛光值,這時就需要使用 HDR 了。
HDR 可以在設定顏色的同時設定強度,這個強度可用在 BIoom 的閾值上,也就是說,只要將 Bloom 的閾值提高到 1 以上,只有我們設定過強度的 HDR 顏色,Bloom 才會進行渲染。
要實現這個功能,我們需要自己創建一個 Shader 來創建材質,使用 URP 中的 Shader Graph 可以很方便的創建 Shader。
右鍵 Creat -> Shader Graph 建立一個 Shader Graph。
重命名為 ColorGraph 並雙擊開啟。
這個 Shader 需要顯示一個 HDR 的顏色,所以在左側的黑板建立一個顏色屬性,點擊加號建立一個 Color 屬性並選擇模式為 HDR,你可以將屬性理解為腳本中的公有變量。
要讓渲染的物體顯現出來還需要一個紋理,所以需要再創建一個 Texture 屬性。
你可以注意到主區域已經存在一個節點 master。
我們需要將對應數值輸出到這個節點來顯示相應的效果,首先右鍵創健一個 simple texture2d 節點來將紋理轉換為顏色輸出,將剛創建的紋理屬性拖曳到主區域中並連接到節點作為輸入。
然後創建一個 add 節點並讓 color屬性和節點輸出的顏色相加達到改變顏色的效果。
將這個改變後的顏色連接到 master 節點的 color 上,這樣就可以透過 color 屬性改變顏色了,不要忘記點擊左上角的 Save Asset 儲存後再退出 Shader Graph。
(9) 新建材質
右鍵 ColorGraphi 根據這個 shadert 創建一個材質起名為 BrightMaterial,首先創建一個正方形的 sprite 傳給 texture ,然後將顏色的強度提高到 1 以上。
將這個材質設置給顯示雷射的線段渲染器。
(10) 運行效果
運行遊戲,可以看到線段有一種明亮的感覺,看起來更像雷射了。
也可以在雷射末端添加一個粒子特效產生一個擊中目標的感覺提升觀感。
機槍
除了雷射還有一種射擊方式不需要實體子彈。
這種發射方式同樣使用射線檢測,透過模擬子彈的發射軌跡來實現效果,這種方式和實體子彈一樣,每次發射會產生一個物體作為彈道軌跡。
所以我們需要先創建彈道的預製體,這裡同樣使用 LineRenderer 模擬。
(1) 配置 LineRenderer 子彈
在場景中創建一個空物體起名為 BulletTracer 並添加線段渲染器。
同樣設定端點數為 2、末端頂點為 90 並提高圖層順序避免被遮擋。
調整線段的寬度到合適的感覺,也可以在線段的末端右鍵加入 key 來過渡線段的寬度,我這裡為了美觀略微增加了線段的末端寬度。
然後調整顏色,點擊下方的箭頭將軌跡的顏色設定為純灰色。
接著點擊上方的箭頭設定透明度,讓透明度隨著距離從 0 逐漸增加。
(2) 編寫腳本
然後編寫腳本 BulletTracer 並添加給這個物體。
這個腳本的功能和彈殼類似,讓軌跡生成後逐漸淡出,完全透明後銷毀物體。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletTracer : MonoBehaviour
{
public float fadeSpeed; //
漸隱速度
private LineRenderer line; //
線渲染器
private float alpha; // 透明度
private void Awake()
{
line = GetComponent<LineRenderer>(); // 取得線渲染器元件
alpha = line.endColor.a; // 取得線渲染器的結束顏色的透明度
}
private void OnEnable()
{
line.endColor = new Color(line.endColor.r, line.endColor.g, line.endColor.b, alpha); // 設定線渲染器的結束顏色的透明度
StartCoroutine(Fade()); // 開始漸隱協程
}
IEnumerator Fade()
{
while (line.endColor.a > 0) // 當線渲染器的結束顏色的透明度大於0時
{
line.endColor = new Color(line.endColor.r, line.endColor.g, line.endColor.b, line.endColor.a - fadeSpeed); // 透明度减少
yield return new WaitForFixedUpdate(); // 等待下一幀
}
ObjectPool.Instance.PushObject(gameObject); // 回收物件
}
}
回到 Unity,將 BulletTracer 物件拖曳到資源視窗中製作成預製體就製作完成了。
(3) 編寫機槍腳本
然後編寫機槍腳本 Rifle 並添加給機槍子物體。
這個槍械的功能與基礎槍械基本一致,只是要將生成的子彈替換為彈道預製體,這個步驟只涉及發射部分,所以重寫 Fire 函數即可。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Rifle : Gun
{
// 重寫父類別的 Fire 方法
protected override void Fire()
{
// 播放射擊動畫
animator.SetTrigger("Shoot");
// 發射射線偵測碰撞
RaycastHit2D hit2D = Physics2D.Raycast(muzzlePos.position, direction, 30);
// 取得物件池中的子彈並設定軌跡
GameObject bullet = ObjectPool.Instance.GetObject(bulletPrefab);
LineRenderer tracer = bullet.GetComponent<LineRenderer>();
tracer.SetPosition(0, muzzlePos.position);
tracer.SetPosition(1, hit2D.point);
// 取得物件池中的彈殼並設定位置和旋轉
GameObject shell = ObjectPool.Instance.GetObject(shellPrefab);
shell.transform.position = shellPos.position;
shell.transform.rotation = shellPos.rotation;
}
}
回到 Unity ,給 Rifle 腳本設定參數然後執行遊戲。
(4) 運行效果
切換到機槍開始射擊,現在可以看到發射的彈道軌跡效果了。
————————————————
本文原創(或整理)於亞洲電玩通,未經作者與本站同意不得隨意引用、轉載、改編或截錄。
特約作家簡介
支持贊助 / DONATE
亞洲電玩通只是很小的力量,但仍希望為復甦台灣遊戲研發貢獻一點動能,如果您喜歡亞洲電玩通的文章,或是覺得它們對您有幫助,歡迎給予一些支持鼓勵,不論是按讚追蹤或是贊助,讓亞洲電玩通持續產出,感謝。
BTC |
352Bw8r46rfXv6jno8qt9Bc3xx6ptTcPze |
|
ETH |
0x795442E321a953363a442C76d39f3fbf9b6bC666 |
|
TRON |
TCNcVmin18LbnXfdWZsY5pzcFvYe1MoD6f |