參考原視頻鏈接
【視頻】:https://space.bilibili.com/641773200注意
:本文為學(xué)習(xí)筆記記錄,推薦支持原作者,去看原視頻自己手敲代碼理解更加深入
本期目標(biāo)
近幾年俯視角射擊游戲
隨著《挺進(jìn)地牢》等雙搖桿射擊游戲的火熱再次出現(xiàn)在玩家的視野中,這類(lèi)游戲通常都有種類(lèi)繁多的武器
和射擊
方式,這也鼓勵(lì)著玩家一次次的重開(kāi)游戲來(lái)體驗(yàn)不同的槍械。
本期我們將在unity2d下實(shí)現(xiàn)各種不同的射擊方式,并使用對(duì)像池優(yōu)化內(nèi)存開(kāi)銷(xiāo)。
前言
俯視角射擊游戲
(Top-down Shooter
)是一類(lèi)以俯視視角進(jìn)行游戲展示的射擊游戲。在這種游戲中,玩家控制著一個(gè)角色或載具,從俯視的角度上方觀察游戲世界,并與敵人進(jìn)行戰(zhàn)斗。這種視角使玩家能夠有更好的全局觀察和策略性,同時(shí)也強(qiáng)調(diào)快速反應(yīng)和精確射擊。
俯視角射擊游戲的特點(diǎn)包括:
-
視角
:俯視角度可以是固定的,也可以隨著玩家角色的移動(dòng)而變化,但都允許玩家以全局視角觀察整個(gè)戰(zhàn)場(chǎng),有利于制定戰(zhàn)略和規(guī)劃行動(dòng)。 -
射擊
:玩家通常需要使用各種武器來(lái)與敵人進(jìn)行戰(zhàn)斗,包括槍械、爆炸物等。射擊動(dòng)作依賴于玩家的反應(yīng)速度和準(zhǔn)確性。
敵人:俯視角射擊游戲通常有大量的敵人,它們可能以固定的路徑或者隨機(jī)移動(dòng),玩家需要躲避敵人的攻擊并選擇最佳時(shí)機(jī)進(jìn)行射擊。 -
可破壞元素
:游戲中的地圖通常會(huì)有各種可以破壞的元素,例如墻壁、箱子等,玩家可以利用這些元素來(lái)尋找掩護(hù)、改變戰(zhàn)術(shù)或者發(fā)起進(jìn)攻。 -
升級(jí)與解鎖
:許多俯視角射擊游戲會(huì)提供升級(jí)系統(tǒng),玩家可以通過(guò)消滅敵人或完成特定任務(wù)來(lái)獲取經(jīng)驗(yàn)值并提升角色的能力或解鎖新的武器和裝備。
一些比較火的俯視角射擊游戲包括:
-
《
元?dú)怛T士
》:(Katana ZERO)是一款2D俯視角動(dòng)作射擊游戲。游戲中,你扮演一名忍者刺客,通過(guò)使用刀劍和其他特殊技能,快速且脆弱地消滅敵人。游戲以像素化的藝術(shù)風(fēng)格呈現(xiàn),結(jié)合了劇情、快速反應(yīng)和策略,玩家需要利用時(shí)間的操作和環(huán)境的互動(dòng)來(lái)完成任務(wù)。 -
《
挺進(jìn)地牢
》:(Enter the Gungeon)是一款像素風(fēng)格的俯視角射擊游戲。游戲中,你將扮演一位勇敢的冒險(xiǎn)者,進(jìn)入一個(gè)充滿怪物和寶藏的地下城。你需要使用各種武器、道具和特殊技能來(lái)對(duì)抗敵人,并逐步深入地牢的層級(jí)。游戲注重隨機(jī)生成的關(guān)卡和強(qiáng)大的敵人,同時(shí)也提供了多人合作模式。 -
《
失落城堡
》:(Dead Cells)是一款像素風(fēng)格的動(dòng)作平臺(tái)游戲,也被歸類(lèi)為俯視角射擊游戲。玩家扮演的角色是一個(gè)不死的戰(zhàn)士,探索一個(gè)被怪物和陷阱充斥的廢墟。玩家需要通過(guò)戰(zhàn)斗、探索和收集資源,不斷改進(jìn)自己的能力,并逐漸深入廢墟。游戲的關(guān)卡是隨機(jī)生成的,每次都有不同的挑戰(zhàn)和發(fā)現(xiàn)。
這些俯視角射擊游戲取得了相當(dāng)?shù)某晒?,并且擁有龐大的玩家群體。它們提供了豐富的內(nèi)容和挑戰(zhàn),讓玩家可以享受到刺激的射擊體驗(yàn)。這只是一些例子,市場(chǎng)上還有許多其他優(yōu)秀的俯視角射擊游戲可供選擇。
欣賞
我在網(wǎng)上找了一些畫(huà)面先給大家欣賞一下
開(kāi)始
1. 角色移動(dòng)和場(chǎng)景搭建
因?yàn)楸酒诘闹攸c(diǎn)放在多種射擊效果,角色移動(dòng)
和環(huán)境如何搭建
等一些基礎(chǔ)的知識(shí)這里就不細(xì)說(shuō)了實(shí),節(jié)省大家的時(shí)間
當(dāng)然,之前我也寫(xiě)過(guò)很多角色移動(dòng)的方法和環(huán)境搭建的方法,這里我貼出地址,感興趣的同學(xué)也可以先去了解一下:
設(shè)置人物移動(dòng)腳本、動(dòng)畫(huà)的切換和攝像機(jī)的跟隨
繪制地圖Tilemap的使用及一些技巧的使用
角色、環(huán)境和武器素材鏈接:
https://o-lobster.itch.io/simple-dungeon-crawler-16x16-pixel-pack
https://humanisred.itch.io/weapons-and-bullets-pixel-art-asset
2. 綁定槍械
2.1 首先將各種槍械的素材添加給人物作為子物體
首先要發(fā)射各式子彈就需要各式槍械作為載體
所以我們先給這個(gè)人物添加多個(gè)槍械并添加切換槍械的功能
首先將各種槍械的素材添加給人物作為子物體
2.2 給槍械也分別添加兩個(gè)子物體用作標(biāo)記槍口和彈倉(cāng)位置
3. 槍械動(dòng)畫(huà)
給槍械添加動(dòng)畫(huà)器,包括槍戒的待機(jī)動(dòng)畫(huà)和發(fā)射動(dòng)畫(huà)
通過(guò)一個(gè)trigger參數(shù)shoot開(kāi)始播放發(fā)射動(dòng)畫(huà)并在播放完動(dòng)畫(huà)后切換回待機(jī)動(dòng)畫(huà)
4. 切換槍械
將所有子物體槍械都取消激活,這樣在激活的時(shí)候才會(huì)使用指定的槍械
在控制人物的腳本PlayerMovement中添加切換槍械的功能
新建一個(gè)GameObject的數(shù)組guns用來(lái)儲(chǔ)存所有槍械
一個(gè)int參數(shù)gunNum用作標(biāo)記當(dāng)前使用的槍械下標(biāo)
在Start函數(shù)中激活第一個(gè)槍械用作初始槍械
然后編寫(xiě)一個(gè)函數(shù)SwitchGun:并在Update函數(shù)中調(diào)用
在這個(gè)函數(shù)中將會(huì)檢測(cè)按鍵用來(lái)切換槍械
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(){}
當(dāng)按下Q鍵時(shí)將當(dāng)前的槍械取消激活
讓槍械下標(biāo)減1并且如果下標(biāo)小于0就將下標(biāo)設(shè)為數(shù)組尾部
然后重新激活當(dāng)前下標(biāo)的槍械
按下E鍵時(shí)基本一致,只是讓槍械下標(biāo)加1
如果下標(biāo)超出數(shù)組邊界就重置為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);
}
}
將所有槍械綁定給數(shù)組guns
然后運(yùn)行游戲,按下Q和E鍵,就可以自由切換槍械了
5. 發(fā)射功能
現(xiàn)在依次給各種槍械添加發(fā)射功能
5.1 手槍
(1) 槍械隨著鼠標(biāo)旋轉(zhuǎn)
給初始槍械手槍創(chuàng)建腳本Pistol,代碼已經(jīng)加了詳細(xì)的注釋?zhuān)@里就不過(guò)多解釋了
using UnityEngine;
public class Pistol : MonoBehaviour
{
//聲明一個(gè)float類(lèi)型的參數(shù)interval作為射擊間隔時(shí)間
public float interval;
//兩個(gè)GameObjecta參數(shù)分別傳入子彈和彈殼的預(yù)制體
public GameObject bulletPrefab;
public GameObject shellPrefab;
//兩個(gè)Transform參數(shù)用來(lái)標(biāo)記槍口和彈倉(cāng)位置
private Transform muzzlePos;
private Transform shellPos;
//兩個(gè)Vector2類(lèi)型的參數(shù)用來(lái)記錄鼠標(biāo)位置和發(fā)射的方向
private Vector2 mousePos;
private Vector2 direction;
//一個(gè)float類(lèi)型的參數(shù)timer用作計(jì)時(shí)器
private float timer;
//一個(gè)Animator參數(shù)獲取動(dòng)畫(huà)器
private Animator animator;
//然后在Start函數(shù)中獲取到動(dòng)畫(huà)器和子物體位置方便后續(xù)使用
void Start()
{
animator = GetComponent<Animator>();
muzzlePos = transform.Find("Muzzle");
shellPos = transform.Find("BulletShell");
}
//接著在Update函數(shù)中持續(xù)的獲取鼠標(biāo)位置
void Update()
{
/*
* 我們可以使用Input.mousePosition獲取到鼠標(biāo)的當(dāng)前位置
* 但這個(gè)位置是鼠標(biāo)的像素坐標(biāo)位置,這個(gè)位置是以屏幕左下角為原點(diǎn)構(gòu)建的坐標(biāo)系,需要的鼠標(biāo)位置是在世界坐標(biāo)系的實(shí)際坐標(biāo)
* 可以使用Camera.main.ScreenToWorldPoint方法來(lái)將像素坐標(biāo)轉(zhuǎn)換為世界坐標(biāo)
*/
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Shoot();
}
//這個(gè)函數(shù)中我們會(huì)讓槍口指向鼠標(biāo)方向
void Shoot()
{
/*
* 首先要獲取到鼠標(biāo)方向的向量
* 用當(dāng)前鼠標(biāo)位置減去槍械位置并進(jìn)行標(biāo)準(zhǔn)化就獲得了槍械需要朝向的方向
* 然后更改槍械的局部坐標(biāo),讓槍械的局部右方向始終等于這個(gè)方向
* 這樣就實(shí)現(xiàn)了槍械始終指向鼠標(biāo)位置的效果
*/
direction = (mousePos - new Vector2(transform.position.x, transform.position.y)).normalized;
transform.right = direction;
}
}
現(xiàn)在可以嘗試運(yùn)行游戲查看一下效果
可以看到槍械在隨著鼠標(biāo)旋轉(zhuǎn),但當(dāng)鼠標(biāo)處于人物的左側(cè)時(shí),槍械就會(huì)倒轉(zhuǎn)過(guò)來(lái)
所以我們需要在鼠標(biāo)在人物左側(cè)時(shí)上下旋轉(zhuǎn)一下槍械
我們可以通過(guò)修改localScale屬性達(dá)到翻轉(zhuǎn)的效果
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) 射擊時(shí)間間隔
然后繼續(xù)編寫(xiě)Shoot函數(shù),當(dāng)按住Fire鍵,也就是鼠標(biāo)左鍵時(shí)
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) 創(chuàng)建好子彈、彈殼和爆炸特效
完成發(fā)射子彈的函數(shù)Fire之前我們需要?jiǎng)?chuàng)建好子彈、彈殼和爆炸特效
給子彈添加剛體、碰撞器,并設(shè)置參數(shù)
然后給彈殼添加剛體,稍微增大重力參數(shù)即可
接著給爆炸特效添加動(dòng)畫(huà)器
(4) 為子彈添加圖層Bullet并使子彈之間不會(huì)相互碰撞(這個(gè)很重要,子彈間會(huì)互相銷(xiāo)毀)
(5) 編寫(xiě)好子彈、彈殼和爆炸特效腳本
分別給子彈、彈殼和爆炸特效創(chuàng)建腳本
Bullet
、BulletShell
和Explosion
首先編寫(xiě)B(tài)ullet腳本
using UnityEngine;
public class Bullet : MonoBehaviour
{
//聲明一個(gè)float類(lèi)型的參數(shù)speed設(shè)置子彈的速度
public float speed;
//一個(gè)GameObject參數(shù)傳入爆炸特效的預(yù)制體
public GameObject explosionPrefab;
//一個(gè)Rigidbody2D參數(shù)獲取剛體
new private Rigidbody2D rigidbody;
//因?yàn)樯深A(yù)制體后馬上就會(huì)使用這個(gè)參數(shù),Start函數(shù)來(lái)不及獲取到剛體,所以在Awake函數(shù)中獲取剛體
void Awake()
{
rigidbody = GetComponent<Rigidbody2D>();
}
//聲明一個(gè)公有的函數(shù)SetSpeed并傳入一個(gè)Vector2的參數(shù)設(shè)置子彈移動(dòng)的方向
public void SetSpeed(Vector2 direction)
{
//將剛體的速度設(shè)置為方向乘以速度,讓子彈開(kāi)始運(yùn)動(dòng)
rigidbody.velocity = direction * speed;
}
void Update()
{
}
//在子彈碰撞到物體時(shí)生成爆炸特效并銷(xiāo)毀子彈
private void OnTriggerEnter2D(Collider2D other)
{
Instantiate(explosionPrefab, transform.position, Quaternion.identity);
Destroy(gameObject);
}
}
繼續(xù)編寫(xiě)彈殼腳本BulletShell
using System.Collections;
using UnityEngine;
public class BulletShell : MonoBehaviour
{
//聲明三個(gè)flot類(lèi)型的參數(shù)用作彈殼被拋出的速度、停下的時(shí)間和彈殼消失的速度
public float speed;
public float stopTime = .5f;
public float fadeSpeed = .01f;
//一個(gè)Riqidbody2D參數(shù)和一個(gè)SpriteRendera參數(shù)獲取剛體和精靈渲染器
new private Rigidbody2D rigidbody;
private SpriteRenderer sprite;
//同樣在Awake函數(shù)中獲取到剛體和精靈渲染器方便后續(xù)使用
void Awake()
{
rigidbody = GetComponent<Rigidbody2D>();
sprite = GetComponent<SpriteRenderer>();
//給彈殼一個(gè)向上的速度實(shí)現(xiàn)拋出的效果
rigidbody.velocity = Vector3.up * speed;
//使用協(xié)程實(shí)現(xiàn)這個(gè)效果,新建一個(gè)協(xié)程Stop
StartCoroutine(Stop());
}
//彈殼將在一段時(shí)間后停止模擬落地,落地后彈殼會(huì)逐漸淡出,直到完全透明后銷(xiāo)毀彈殼
IEnumerator Stop()
{
//在這個(gè)協(xié)程中首先等待設(shè)定好的時(shí)間
yield return new WaitForSeconds(stopTime);
//然后將重力和速度都設(shè)為0
rigidbody.velocity = Vector2.zero;
rigidbody.gravityScale = 0;
//然后開(kāi)始一個(gè)while循環(huán),當(dāng)渲染器的alpha值大于O時(shí)每幀設(shè)置渲染器的顏色
while (sprite.color.a > 0)
{
//每次循環(huán)都讓alpha的值減小使其變的逐漸透明
sprite.color = new Color(sprite.color.r, sprite.color.g, sprite.color.g, sprite.color.a - fadeSpeed);
//然后等待一個(gè)FixedUpdater幀
yield return new WaitForFixedUpdate();
}
//在結(jié)束循環(huán)后銷(xiāo)毀掉彈殼
Destroy(gameObject);
}
}
然后編寫(xiě)爆炸特效腳本Explosion
using UnityEngine;
public class Explosion : MonoBehaviour
{
//聲明一個(gè)Animator參數(shù)和一個(gè)AnimatorStateInfo參數(shù)獲取動(dòng)畫(huà)器和動(dòng)畫(huà)進(jìn)度
private Animator animator;
private AnimatorStateInfo info;
//在Awake函數(shù)中獲取到動(dòng)畫(huà)器方便后續(xù)使用
void Awake()
{
animator = GetComponent<Animator>();
}
void Update()
{
//持續(xù)的獲取動(dòng)畫(huà)進(jìn)度
info = animator.GetCurrentAnimatorStateInfo(0);
if (info.normalizedTime >= 1)
{
//當(dāng)播放完動(dòng)畫(huà)后,銷(xiāo)毀特效
Destroy(gameObject);
}
}
}
(6)制作子彈、彈殼和爆炸特效預(yù)制體
將子彈、彈殼和爆炸特效都拖動(dòng)到資源窗口中制作成預(yù)制體,然后分別設(shè)置好相應(yīng)的腳本參數(shù)就創(chuàng)建好預(yù)制體了
(7) 發(fā)射子彈
現(xiàn)在我們可以繼續(xù)編寫(xiě)手槍腳本中的Fire函數(shù)了
void Fire()
{
//首先觸發(fā)動(dòng)畫(huà)器的參數(shù)Shoot播放發(fā)射動(dòng)畫(huà)
animator.SetTrigger("Shoot");
//然后生成子彈的預(yù)體體,并將生成的子彈位置設(shè)為槍口的位置
GameObject bullet = Instantiate(bulletPrefab, muzzlePos.position, Quaternion.identity);
//接著獲取到Bullet腳本然后調(diào)用SetSpeed函數(shù)設(shè)置子彈發(fā)射的方向?yàn)闃尶诔虻姆较?,也就是direction參數(shù)
bullet.GetComponent<Bullet>().SetSpeed(direction);
//最后生成彈殼的預(yù)制體并將位置設(shè)為彈倉(cāng)位置,旋轉(zhuǎn)也設(shè)為彈倉(cāng)的旋轉(zhuǎn)角度
Instantiate(shellPrefab, shellPos.position, shellPos.rotation);
}
回到unity運(yùn)行游戲,按下鼠標(biāo)左鍵,可以看到子彈可以朝著鼠標(biāo)方向發(fā)射了
(7) 子彈和彈殼偏移
不過(guò)在很多游戲中子彈都不那么精準(zhǔn),會(huì)在一個(gè)小區(qū)間內(nèi)產(chǎn)生
偏移
現(xiàn)在讓我們加上這個(gè)小功能,在發(fā)射子彈前使用Random.Range
產(chǎn)生一個(gè)隨機(jī)的偏移角度
void Fire()
{
animator.SetTrigger("Shoot");
GameObject bullet = Instantiate(bulletPrefab, muzzlePos.position, Quaternion.identity);
//我這里就在-5度和5度之間產(chǎn)生了一個(gè)10度內(nèi)的隨機(jī)偏移
float angel = Random.Range(-5f, 5f);
/*
* 然后在設(shè)置速度時(shí)對(duì)方向做一點(diǎn)更改,使用Quaternion.AngleAxis產(chǎn)生一個(gè)相對(duì)偏轉(zhuǎn)
* 傳入隨機(jī)出的角度并讓其繞著z軸旋轉(zhuǎn)
* 再乘以正常的方向就產(chǎn)生了以這個(gè)方向?yàn)榛鶞?zhǔn)的偏轉(zhuǎn)方向
*/
bullet.GetComponent<Bullet>().SetSpeed(Quaternion.AngleAxis(angel, Vector3.forward) * direction);
Instantiate(shellPrefab, shellPos.position, shellPos.rotation);
}
也可以用這個(gè)方法修改彈殼的代碼,讓彈殼以一個(gè)隨機(jī)的角度拋出提升觀感
進(jìn)入BulletShell腳本,同樣在設(shè)置速度前隨機(jī)一個(gè)角度并讓拋出的方向偏轉(zhuǎn)這個(gè)角度
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());
}
再次運(yùn)行游戲,現(xiàn)在子彈發(fā)射時(shí)會(huì)在一個(gè)范圍內(nèi)隨機(jī)射擊,彈殼也會(huì)以隨機(jī)的角度拋出了
(8) 對(duì)象池優(yōu)化
但現(xiàn)在存在一個(gè)
問(wèn)題
,因?yàn)槲覀兪峭ㄟ^(guò)不斷的實(shí)例化預(yù)制體來(lái)制造子彈或者彈殼的,而這些生成的物體也會(huì)很快被銷(xiāo)毀,現(xiàn)在數(shù)量比較少的情況下還好,一旦需要的物體數(shù)量達(dá)到一定程度,不斷的創(chuàng)建和銷(xiāo)毀物體會(huì)對(duì)游戲性能
造成很大的影響,這時(shí)就需要用到對(duì)象池
了,我們會(huì)將用完的物體取消激活
并放回對(duì)象池中,在需要使用物體時(shí)再?gòu)膶?duì)象池中激活
物體使用,只有在對(duì)象池里的待分配物體不足時(shí)才會(huì)進(jìn)行實(shí)例化操作,相對(duì)通常的創(chuàng)建和銷(xiāo)毀只是對(duì)物體進(jìn)行激活和取消激活操作,節(jié)省了很多性能
現(xiàn)在讓我們先來(lái)寫(xiě)一個(gè)對(duì)象池腳本,新建一個(gè)腳本,起名為ObjectPool
這個(gè)腳本使用單例模式進(jìn)行編寫(xiě),因?yàn)槟_本不需要掛載在任何物體上,所以不需要繼承MonoBehavior
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool
{
private static ObjectPool instance; // 單例模式
// /**
// * 我們希望不同的物體可以被分開(kāi)存儲(chǔ),在這種情況下使用字典是最合適的
// * 所以聲明一個(gè)字典objectPool作為對(duì)象池主體,以字符串類(lèi)型的物體的名字作為key
// * 使用隊(duì)列存儲(chǔ)物體來(lái)作為value,這里使用隊(duì)列只是因?yàn)槿腙?duì)和出隊(duì)的操作較為方便,也可以換成其他集合方式
// * 然后實(shí)例化這個(gè)字典以備后續(xù)使用
// * /
private Dictionary<string, Queue<GameObject>> objectPool = new Dictionary<string, Queue<GameObject>>(); // 對(duì)象池字典
private GameObject pool; // 為了不讓窗口雜亂,聲明一個(gè)對(duì)象池父物體,作為所有生成物體的父物體
public static ObjectPool Instance // 單例模式
{
get
{
if (instance == null)
{
instance = new ObjectPool();
}
return instance;
}
}
public GameObject GetObject(GameObject prefab) // 從對(duì)象池中獲取對(duì)象
{
GameObject _object;
if (!objectPool.ContainsKey(prefab.name) || objectPool[prefab.name].Count == 0) // 如果對(duì)象池中沒(méi)有該對(duì)象,則實(shí)例化一個(gè)新的對(duì)象
{
_object = GameObject.Instantiate(prefab);
PushObject(_object); // 將新的對(duì)象加入對(duì)象池
if (pool == null)
pool = new GameObject("ObjectPool"); // 如果對(duì)象池父物體不存在,則創(chuàng)建一個(gè)新的對(duì)象池父物體
GameObject childPool = GameObject.Find(prefab.name + "Pool"); // 查找該對(duì)象的子對(duì)象池
if (!childPool)
{
childPool = new GameObject(prefab.name + "Pool"); // 如果該對(duì)象的子對(duì)象池不存在,則創(chuàng)建一個(gè)新的子對(duì)象池
childPool.transform.SetParent(pool.transform); // 將該子對(duì)象池加入對(duì)象池父物體中
}
_object.transform.SetParent(childPool.transform); // 將新的對(duì)象加入該對(duì)象的子對(duì)象池中
}
_object = objectPool[prefab.name].Dequeue(); // 從對(duì)象池中取出一個(gè)對(duì)象
_object.SetActive(true); // 激活該對(duì)象
return _object; // 返回該對(duì)象
}
public void PushObject(GameObject prefab) // 將對(duì)象加入對(duì)象池中
{
//獲取對(duì)象的名稱,因?yàn)閷?shí)例化的物體名都會(huì)加上"(Clone)"的后綴,需要先去掉這個(gè)后綴才能使用名稱查找
string _name = prefab.name.Replace("(Clone)", string.Empty);
if (!objectPool.ContainsKey(_name))
objectPool.Add(_name, new Queue<GameObject>()); // 如果對(duì)象池中沒(méi)有該對(duì)象,則創(chuàng)建一個(gè)新的對(duì)象池
objectPool[_name].Enqueue(prefab); // 將對(duì)象加入對(duì)象池中
prefab.SetActive(false); // 將對(duì)象禁用
}
}
現(xiàn)在回到之前的腳本,將所有生成或銷(xiāo)毀的代碼都使用對(duì)象池操作優(yōu)化
# 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
因?yàn)橹皰伋龅牟僮魇窃贏wake函數(shù)中進(jìn)行的,而使用對(duì)象池重用彈殼不會(huì)再次調(diào)用Awake函數(shù),所以我們將拋出部分的代碼放在OnEnable函數(shù)中,這個(gè)函數(shù)將在物體被激活時(shí)調(diào)用
private void OnEnable()
{
float angel = Random.Range(-30f, 30f);
rigidbody.velocity = Quaternion.AngleAxis(angel, Vector3.forward) * Vector3.up * speed;
//并且由于彈殼的透明度和重力已經(jīng)被修改過(guò),所以要每次重新設(shè)置彈殼的透明度和重力
sprite.color = new Color(sprite.color.r, sprite.color.g, sprite.color.b, 1);
rigidbody.gravityScale = 3;
StartCoroutine(Stop());
}
運(yùn)行游戲查看一下效果
按下左鍵射擊后窗口中出現(xiàn)了對(duì)象池的父物體
展開(kāi)就可以看到子彈、彈殼和爆炸特效的對(duì)象池和其中的物體了
可以看到現(xiàn)在物體處于失活狀態(tài),再次開(kāi)始射擊后沒(méi)有再生成新物體
而是激活了這些物體使用,當(dāng)物體該被銷(xiāo)毀時(shí)又會(huì)取消激活回到對(duì)象池中
5.2 封裝槍械的父類(lèi)
完成優(yōu)化后我們就可以繼續(xù)制作其他種類(lèi)的槍械了
回到手槍腳術(shù)中仔細(xì)觀察其實(shí)可以發(fā)現(xiàn)槍械的行為大同小異,需要更改的只有一些變量或者發(fā)射的行為
這種情況下可以將我們當(dāng)前的腳本作為父類(lèi),讓其他槍械腳本繼承這個(gè)類(lèi)提高代碼復(fù)用性
首先我們新建一個(gè)類(lèi)Gun作為所有槍械的父類(lèi)
將剛才手槍腳本的代碼剪切到這個(gè)類(lèi)中實(shí)現(xiàn)最基礎(chǔ)的槍械功能然后讓手槍類(lèi)繼承承Gm
接著將所有private的變量和函數(shù)更改為protectedi讓子類(lèi)可以繼承這些基礎(chǔ)的變量和函數(shù)
類(lèi)槍械的行為肯定會(huì)與手槍有些許不同
所以我們使用virtual關(guān)鍵子將所有函數(shù)設(shè)置為虛函數(shù)
這樣Z類(lèi)中就可以通過(guò)重寫(xiě)某些函數(shù)達(dá)到修改特定行為的效果
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;
}
}
手槍代碼直接基礎(chǔ)父類(lèi)Gun即可,什么也不需要做
public class Pistol : Gun
{
}
5.3 散彈槍
現(xiàn)在來(lái)嘗試編寫(xiě)第一個(gè)子類(lèi)槍械散彈槍
(1) 創(chuàng)建一個(gè)新腳本起名為Shotgun并繼承父類(lèi)Gun
using UnityEngine;
public class Shotgun : Gun
{
//首先聲明個(gè)公有的int參數(shù)bulletNum表示一次開(kāi)火射出多少發(fā)子彈
public int bulletNum = 3;
//一個(gè)公有的float變量bulletAngle表示每個(gè)子彈間的間隔角度
public float bulletAngle = 15;
//散彈槍與基礎(chǔ)槍械的區(qū)別是開(kāi)槍時(shí)會(huì)均勻的射出多發(fā)子彈
//這個(gè)不同只涉及開(kāi)火時(shí),所以只需重寫(xiě)Fire函數(shù)即可
//使用override關(guān)鍵字重寫(xiě)函數(shù),這樣這個(gè)函數(shù)就會(huì)覆蓋掉繼承的函數(shù)
protected override void Fire()
{
//在重寫(xiě)的函數(shù)中首先依舊是要觸發(fā)動(dòng)畫(huà)器的tigger參數(shù)來(lái)播放射擊動(dòng)畫(huà)
animator.SetTrigger("Shoot");
//然后算出子彈數(shù)的中間值用來(lái)計(jì)算每個(gè)子彈的偏轉(zhuǎn)角度
int median = bulletNum / 2;
//接著開(kāi)始一個(gè)子彈次數(shù)的循環(huán)生成對(duì)應(yīng)數(shù)量的子彈
for (int i = 0; i < bulletNum; i++)
{
//從對(duì)象池中取出一個(gè)子彈的預(yù)制體然后將子彈的位置設(shè)置為槍口的位置
GameObject bullet = ObjectPool.Instance.GetObject(bulletPrefab);
bullet.transform.position = muzzlePos.position;
//根據(jù)子彈數(shù)量的奇偶來(lái)計(jì)算子彈應(yīng)該偏轉(zhuǎn)的角度
if (bulletNum % 2 == 1)
{
}
else
{
}
}
}
}
(2) 散彈槍根據(jù)子彈數(shù)量的奇偶來(lái)計(jì)算子彈應(yīng)該偏轉(zhuǎn)的角度
如果是奇數(shù)那么只需要讓當(dāng)前的循環(huán)次數(shù),也就是第幾顆子彈減去法中間值
這個(gè)值就代表這顆子彈是哪邊的第幾顆子彈,負(fù)數(shù)在左側(cè),正數(shù)在右側(cè),零就是中間的子彈
讓這個(gè)值乘以間隔角度就得到了當(dāng)前子彈的偏轉(zhuǎn)角度
而偶數(shù)子彈只會(huì)分布在兩側(cè),就需要在奇數(shù)的基礎(chǔ)上加上間隔角度的一半來(lái)得到偏轉(zhuǎn)角度
(3) 完善代碼
根據(jù)子彈數(shù)的奇偶計(jì)算出相應(yīng)的偏轉(zhuǎn)角度后,就可以按照之前的方法設(shè)置子彈的偏轉(zhuǎn)向量了
生成完所有子彈后,一同樣生成一個(gè)彈殼并設(shè)置位置和旋轉(zhuǎn)
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,給散彈槍子物體添加腳本并設(shè)置好參數(shù)和預(yù)制體
運(yùn)行游戲,現(xiàn)在子彈可以進(jìn)行散射了
也可以通過(guò)調(diào)整子彈總數(shù)和間隔角度來(lái)達(dá)到不同的發(fā)射效果
預(yù)告
到這里俯視角的直線射擊效果與對(duì)像池優(yōu)化就全部完成了
因?yàn)槲恼缕鶈?wèn)題,在下期內(nèi)容中我們將繼續(xù)實(shí)現(xiàn)曲線射擊與兩種不需要實(shí)體子彈的射擊方式源碼
我也會(huì)一并放在下期內(nèi)容,敬請(qǐng)期待
傳送門(mén):制作類(lèi)元?dú)怛T士、挺進(jìn)地牢——俯視角射擊游戲多種射擊效果(二)
完結(jié)
如果你有其他更好的方法也歡迎評(píng)論分享出來(lái),當(dāng)然如果發(fā)現(xiàn)文章中出現(xiàn)了什么問(wèn)題或者疑問(wèn)的話,也歡迎評(píng)論私信告訴我哦
好了,我是向宇,https://xiangyu.blog.csdn.net/文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-654573.html
一位在小公司默默奮斗的開(kāi)發(fā)者,出于興趣愛(ài)好,于是開(kāi)始自習(xí)unity。最近創(chuàng)建了一個(gè)新欄目【你問(wèn)我答】,主要是想收集一下大家的問(wèn)題,有時(shí)候一個(gè)問(wèn)題可能幾句話說(shuō)不清楚,我就會(huì)以發(fā)布文章的形式來(lái)回答。 雖然有些問(wèn)題我可能也不一定會(huì),但是我會(huì)查閱各方資料,爭(zhēng)取給出最好的建議,希望可以幫助更多想學(xué)編程的人,共勉~文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-654573.html
到了這里,關(guān)于【用unity實(shí)現(xiàn)100個(gè)游戲之1】制作類(lèi)元?dú)怛T士、挺進(jìn)地牢——俯視角射擊游戲多種射擊效果(一)(附源碼)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!