【unity实战】制作类元气骑士、挺进地牢——俯视角射击游戏多种射击效果(一)(附源码)

本期目标

近几年俯视角射击游戏随着《挺进地牢》等双摇杆射击游戏的火热再次出现在玩家的视野中,这类游戏通常都有种类繁多的武器射击方式,这也鼓励着玩家一次次的重开游戏来体验不同的枪械。

本期我们将在unity2d下实现各种不同的射击方式,并使用对像池优化内存开销。

前言

俯视角射击游戏Top-down Shooter)是一类以俯视视角进行游戏展示的射击游戏。在这种游戏中,玩家控制着一个角色或载具,从俯视的角度上方观察游戏世界,并与敌人进行战斗。这种视角使玩家能够有更好的全局观察和策略性,同时也强调快速反应和精确射击。

俯视角射击游戏的特点包括:

  • 视角:俯视角度可以是固定的,也可以随着玩家角色的移动而变化,但都允许玩家以全局视角观察整个战场,有利于制定战略和规划行动。
  • 射击:玩家通常需要使用各种武器来与敌人进行战斗,包括枪械、爆炸物等。射击动作依赖于玩家的反应速度和准确性。
    敌人:俯视角射击游戏通常有大量的敌人,它们可能以固定的路径或者随机移动,玩家需要躲避敌人的攻击并选择最佳时机进行射击。
  • 可破坏元素:游戏中的地图通常会有各种可以破坏的元素,例如墙壁、箱子等,玩家可以利用这些元素来寻找掩护、改变战术或者发起进攻。
  • 升级与解锁:许多俯视角射击游戏会提供升级系统,玩家可以通过消灭敌人或完成特定任务来获取经验值并提升角色的能力或解锁新的武器和装备。

一些比较火的俯视角射击游戏包括:

  1. 元气骑士》:(Katana ZERO)是一款2D俯视角动作射击游戏。游戏中,你扮演一名忍者刺客,通过使用刀剑和其他特殊技能,快速且脆弱地消灭敌人。游戏以像素化的艺术风格呈现,结合了剧情、快速反应和策略,玩家需要利用时间的操作和环境的互动来完成任务。

  2. 挺进地牢》:(Enter the Gungeon)是一款像素风格的俯视角射击游戏。游戏中,你将扮演一位勇敢的冒险者,进入一个充满怪物和宝藏的地下城。你需要使用各种武器、道具和特殊技能来对抗敌人,并逐步深入地牢的层级。游戏注重随机生成的关卡和强大的敌人,同时也提供了多人合作模式。

  3. 失落城堡》:(Dead Cells)是一款像素风格的动作平台游戏,也被归类为俯视角射击游戏。玩家扮演的角色是一个不死的战士,探索一个被怪物和陷阱充斥的废墟。玩家需要通过战斗、探索和收集资源,不断改进自己的能力,并逐渐深入废墟。游戏的关卡是随机生成的,每次都有不同的挑战和发现。

这些俯视角射击游戏取得了相当的成功,并且拥有庞大的玩家群体。它们提供了丰富的内容和挑战,让玩家可以享受到刺激的射击体验。这只是一些例子,市场上还有许多其他优秀的俯视角射击游戏可供选择。

欣赏

我在网上找了一些画面先给大家欣赏一下在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

开始

1. 角色移动和场景搭建

因为本期的重点放在多种射击效果,角色移动环境如何搭建等一些基础的知识这里就不细说了实,节省大家的时间
在这里插入图片描述

当然,之前我也写过很多角色移动的方法和环境搭建的方法,这里我贴出地址,感兴趣的同学也可以先去了解一下:
设置人物移动脚本、动画的切换和摄像机的跟随
绘制地图Tilemap的使用及一些技巧的使用

角色、环境和武器素材链接:
https://o-lobster.itch.io/simple-dungeon-crawler-16x16-pixel-pack
https://humanisred.itch.io/weapons-and-bullets-pixel-art-asset
在这里插入图片描述

2. 绑定枪械

2.1 首先将各种枪械的素材添加给人物作为子物体

在这里插入图片描述
在这里插入图片描述

2.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键,就可以自由切换枪械了
在这里插入图片描述

5. 发射功能

现在依次给各种枪械添加发射功能

5.1 手枪

(1) 枪械随着鼠标旋转

给初始枪械手枪创建脚本Pistol,代码已经加了详细的注释,这里就不过多解释了

using UnityEngine;

public class Pistol : MonoBehaviour
{
    
    
	//声明一个float类型的参数interval作为射击间隔时间
	public float interval;
	//两个GameObjecta参数分别传入子弹和弹壳的预制体
	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) 编写好子弹、弹壳和爆炸特效脚本

分别给子弹、弹壳和爆炸特效创建脚本BulletBulletShellExplosion

首先编写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运行游戏,按下鼠标左键,可以看到子弹可以朝着鼠标方向发射了
在这里插入图片描述

(7) 子弹和弹壳偏移

不过在很多游戏中子弹都不那么精准,会在一个小区间内产生偏移

现在让我们加上这个小功能,在发射子弹前使用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());
}

再次运行游戏,现在子弹发射时会在一个范围内随机射击,弹壳也会以随机的角度抛出了
在这里插入图片描述

(8) 对象池优化

但现在存在一个问题,因为我们是通过不断的实例化预制体来制造子弹或者弹壳的,而这些生成的物体也会很快被销毁,现在数量比较少的情况下还好,一旦需要的物体数量达到一定程度,不断的创建和销毁物体会对游戏性能造成很大的影响,这时就需要用到对象池了,我们会将用完的物体取消激活并放回对象池中,在需要使用物体时再从对象池中激活物体使用,只有在对象池里的待分配物体不足时才会进行实例化操作,相对通常的创建和销毁只是对物体进行激活和取消激活操作,节省了很多性能

现在让我们先来写一个对象池脚本,新建一个脚本,起名为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());
}

运行游戏查看一下效果
按下左键射击后窗口中出现了对象池的父物体
展开就可以看到子弹、弹壳和爆炸特效的对象池和其中的物体了
可以看到现在物体处于失活状态,再次开始射击后没有再生成新物体
而是激活了这些物体使用,当物体该被销毁时又会取消激活回到对象池中
在这里插入图片描述

5.2 封装枪械的父类

完成优化后我们就可以继续制作其他种类的枪械了
回到手枪脚术中仔细观察其实可以发现枪械的行为大同小异,需要更改的只有一些变量或者发射的行为
这种情况下可以将我们当前的脚本作为父类,让其他枪械脚本继承这个类提高代码复用性

首先我们新建一个类Gun作为所有枪械的父类
将刚才手枪脚本的代码剪切到这个类中实现最基础的枪械功能然后让手枪类继承承Gm
接着将所有private的变量和函数更改为protectedi让子类可以继承这些基础的变量和函数
类枪械的行为肯定会与手枪有些许不同
所以我们使用virtual关键子将所有函数设置为虚函数
这样Z类中就可以通过重写某些函数达到修改特定行为的效果

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
{
    
    

}

5.3 散弹枪

现在来尝试编写第一个子类枪械散弹枪

(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,给散弹枪子物体添加脚本并设置好参数和预制体
在这里插入图片描述
运行游戏,现在子弹可以进行散射了

在这里插入图片描述
也可以通过调整子弹总数和间隔角度来达到不同的发射效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

预告

到这里俯视角的直线射击效果与对像池优化就全部完成了
因为文章篇幅问题,在下期内容中我们将继续实现曲线射击与两种不需要实体子弹的射击方式
源码我也会一并放在下期内容,敬请期待
传送门:制作类元气骑士、挺进地牢——俯视角射击游戏多种射击效果(二)

参考

【视频】:https://www.bilibili.com/video/BV1xb4y1D7PZ/

完结

如果你有其他更好的方法也欢迎评论分享出来,当然如果发现文章中出现了什么问题或者疑问的话,也欢迎评论私信告诉我哦

好了,我是向宇,https://xiangyu.blog.csdn.net/

一位在小公司默默奋斗的开发者,出于兴趣爱好,于是开始自习unity。最近创建了一个新栏目【你问我答】,主要是想收集一下大家的问题,有时候一个问题可能几句话说不清楚,我就会以发布文章的形式来回答。 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~

猜你喜欢

转载自blog.csdn.net/qq_36303853/article/details/131520315