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

news2024/11/15 12:38:40

文章目录

  • 本期目标
  • 前言
  • 欣赏
  • 开始
    • 1. 角色移动和场景搭建
    • 2. 绑定枪械
      • 2.1 首先将各种枪械的素材添加给人物作为子物体
      • 2.2 给枪械也分别添加两个子物体用作标记枪口和弹仓位置
    • 3. 枪械动画
    • 4. 切换枪械
    • 5. 发射功能
      • 5.1 手枪
        • (1) 枪械随着鼠标旋转
        • (2) 射击时间间隔
        • (3) 创建好子弹、弹壳和爆炸特效
        • (4) 为子弹添加图层Bullet并使子弹之间不会相互碰撞
        • (5) 编写好子弹、弹壳和爆炸特效脚本
        • (6)制作子弹、弹壳和爆炸特效预制体
        • (7) 发射子弹
        • (7) 子弹和弹壳偏移
        • (8) 对象池优化
      • 5.2 封装枪械的父类
      • 5.3 散弹枪
        • (1) 创建一个新脚本起名为Shotgun并继承父类Gun
        • (2) 散弹枪根据子弹数量的奇偶来计算子弹应该偏转的角度
        • (3) 完善代码
        • (3) 效果
  • 预告
  • 参考
  • 完结

本期目标

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

本期我们将在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。最近创建了一个新栏目【你问我答】,主要是想收集一下大家的问题,有时候一个问题可能几句话说不清楚,我就会以发布文章的形式来回答。 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/735646.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

xilinx zc706 10/100/100M以太网完整性测试说明

一&#xff1a;概述 最近接触FPGA比较多&#xff0c;本次是一款zc706芯片的FPGA&#xff0c;开发版是长这样的&#xff0c;基本上开发也是安装开发版的模块组合进行开发。 开发版上以太网芯片使用的是Marvell 881116R。 本次只是验证下控制以太网进行模式测试的说明&#xf…

子集 (力扣)数学推理 JAVA

给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[],[1],[2],[1,2],[3],[…

Windows10上Docker和Kubernetes的安装

官网&#xff1a;https://www.docker.com/products/docker-desktop/ 历史版本&#xff1a;https://download.docker.com/linux/static/stable/ 1、去Docker官网下载Docker Desktop&#xff0c;并一键安装 2、安装k8s所需的镜像 下载k8s-for-docker-desktop包 git clone http…

WAIC2023记录

汤晓鸥 学生与工作&#xff1a; 微软布局大模型方向

Vue3+Vite 项目配置 vue-router,并完成路由模块化

前言 我的技术栈&#xff1a;Vue3 Vite TypeScirpt我的包管理工具&#xff1a;pnpm&#xff08;v8.6.6&#xff09;我的 node.js 版本&#xff1a;v16.14.0 一、安装vue-router pnpm install vue-router二、创建页面 在 /src/views 文件夹下创建 home、login、test文件夹…

Chat2DB-开源AI智能数据库客户端工具 能够将自然语言转换为SQL

一、Chat2DB是什么 Chat2DB 是一款有开源免费的多数据库客户端工具&#xff0c;支持windows、mac本地安装&#xff0c;也支持服务器端部署&#xff0c;web网页访问。和传统的数据库客户端软件Navicat、DBeaver 相比Chat2DB集成了AIGC的能力&#xff0c;能够将自然语言转换为SQ…

论文笔记--SentEval: An Evaluation Toolkit for Universal Sentence Representations

论文笔记--SentEval: An Evaluation Toolkit for Universal Sentence Representations 1. 文章简介2. 文章概括3 文章重点技术3.1 evaluation pipeline3.2 使用 4. 代码4.1 数据下载4.2 句子嵌入4.3 句子嵌入评估 5. 文章亮点6. 原文传送门7. References 1. 文章简介 标题&…

windows PE 指南(基础部分)(一)

windows PE 指南&#xff08;基础部分&#xff09;&#xff08;一&#xff09; 查找软件注册码64位汇编和编译连接PE和COFF文件简介PE和COFF文件布局简介源代码的作用obj的作用PE的作用 COFF格式文件布局概览PE格式文件布局概览COFF格式和PE格式的布局总结 COFF头COFF File Hea…

linux的磁盘分区管理思路

1、基本分区管理 1.1、磁盘划分思路 进入分区 新建分区 fdisk 更新分区表&#xff08;刷新&#xff09;partprobe /dev/sda 格式化 &#xff08;文件系统&#xff09; 挂载使用 - -> mount(开机自动挂载||autofs自动挂载) 1.2、使用fdisk分区 n创建新分区&#xff08;…

一、VMware虚拟机安装以及centos7镜像安装

目录 一、安装VMware虚拟机 二、centos7安装 (模板机&#xff09; 2.1 虚拟机安装 2.2 虚拟机配置 三、配置服务器 IP 地址 1. VMware中配置 2. window服务器 3. 在虚拟机中&#xff08;centos0&#xff09; 资源包网盘链接&#xff1a;链接&#xff1a;https://pan.bai…

ActiveMQ消息中间件应用场景

一、ActiveMQ简介 ActiveMQ是Apache出品&#xff0c;最流行的&#xff0c;能力强劲的开源消息总线。ActiveMQ是一个完全支持JMS1.1和J2EE1.4规范的JMS Provide实现。尽管JMS规范出台已经是很久的事情了&#xff0c;但是JMS在当今的J2EE应用中仍然扮演这特殊的地位。 二、Active…

速成版-带您一天学完vue2框架

vue是一个前端js框架&#xff0c;可以简化Dom操作&#xff0c;实现响应式数据驱动。前面全是废话&#xff0c;哈哈&#xff0c;接下来一起学习吧。 目录 一、vue基础 1.1、vue简介 1.2、第一个Vue程序 1.3、vue基础-el挂载点 1.4、data数据对象 二、本地应用-指令篇 2.…

DESCN:用于个体治疗效果估计的深度全空间交叉网络

英文题目&#xff1a;DESCN: Deep Entire Space Cross Networks for Individual Treatment Effect Estimation 翻译&#xff1a;用于个体治疗效果估计的深度全空间交叉网络 单位&#xff1a;阿里 论文链接&#xff1a; 代码&#xff1a;https://github.com/kailiang-zhong/…

由中序及后序遍历序列构建二叉树的函数参数解析

【二叉树构建函数的参数确立示意图】 ile&#xff1a;中序遍历左端点位置&#xff0c;iri&#xff1a;中序遍历右端点位置 ple&#xff1a;后序遍历左端点位置&#xff0c;pri&#xff1a;后序遍历右端点位置 【函数代码】 int build(int ile,int iri,int ple,int pri){int ro…

(文章复现)考虑微网新能源经济消纳的共享储能优化配置matlab代码

参考文献&#xff1a; [1]谢雨龙,罗逸飏,李智威等.考虑微网新能源经济消纳的共享储能优化配置[J].高电压技术,2022,48(11):4403-4413. 1.基本原理 双层规划是具有两个层次的优化问题&#xff0c;具有外层和内层两个优化目标&#xff0c;本研究的问题结构如图2所示。两层问题相…

打印机不打印故障检查步骤

第一步&#xff1a;检查打印机电源是否接通、打印机电源开关是否打开、打印机数据线的连接是否正确。 第二步&#xff1a;检查打印机进纸盒中是否有纸&#xff0c;打印机内是否卡纸&#xff0c;感光鼓组件是否有问题。 第三步&#xff1a;检查应用程序是否有问题或存在病毒。 第…

C#,数值计算——分数阶的贝塞尔函数(Bessel functions of fractional order)源代码

分数阶微积分这一重要的数学分支&#xff0c;其诞生在1695年&#xff0c;几乎和经典微积分同时出现。那一年&#xff0c;德国数学家Leibniz 和法国数学家LHopital 通信&#xff0c;探讨当导数的阶变为1/2时&#xff0c;其意义是什么&#xff1f;当时Leibniz也不知道定义与意义&…

强化学习-理解及应用:解决迷宫问题

什么是强化学习&#xff1f; 强化学习&#xff08;Reinforcement Learning, RL&#xff09;是一种机器学习方法&#xff0c;旨在让智能体&#xff08;agent&#xff09;通过与环境的交互学习如何做出最优的行动选择以获得最大的累积奖励。 7个基本概念 强化学习主要由智能体…

YApi-高效、易用、功能强大的可视化接口管理平台——(三)YApi 项目管理

YApi 项目管理 新建项目修改项目图标项目迁移项目拷贝删除项目配置环境请求配置请求参数示例返回数据示例storage工具函数异步处理&#xff08;v1.3.13支持&#xff09; token全局mock 新建项目 点击右上角的 新建项目&#xff0c;进入新建项目页面&#xff1a; 完善项目信息…

JVM理论(三)运行时数据区--PC寄存器/虚拟机栈/本地方法栈

运行时数据区(JVM内存结构) JVM内存结构 内存是非常重要的资源,是硬盘和CPU的中间桥梁,承载操作系统和应用程序的实时运行.JVM内存布局规定java在运行过程中内存申请、分配、管理的策略&#xff0c;保证JVM高效稳定运行。不同的JVM对于内存划分和管理机制存在部分差异(如J9和JR…