枪械切换(1)——Unity随手记(2021.2.16)

今天实现的内容:

脚本结构的优化

为接下来的切换枪械做铺垫,脚本需要再进行一次大改,目的是再次将枪械的操作用一个脚本统一管理,方便接下来对不同枪械能使用同一套代码进行操作。将之前在AssaultRifle脚本中实现的控制操作放到新脚本中进行,再进行封装以及将一些写在AssaultRifle脚本中的枪械共有的属性再搬到Firearms中。

以下是新脚本WeaponManager ,用来管理所有武器的操作逻辑,以及主副武器的切换。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Scripts.Weapon;

// 用于武器的控制 切换
public class WeaponManager : MonoBehaviour
{
    
    
    // 主武器
    public Firearms mainWeapon;
    // 副武器
    public Firearms secondaryWeapon;


    // 当前手上拿着的武器
    private Firearms currentWeapon;
    // FPCharacterControllerMovement的引用 用于传递Animator
    private FPCharacterControllerMovement controller;

    // 切换武器
    private void SwapWeapon()
    {
    
    
        if (Input.GetAxis("Mouse ScrollWheel") != 0) //当使用滚轮时
        {
    
    
            currentWeapon.gameObject.SetActive(false); //隐藏现在的武器
            currentWeapon = (currentWeapon == mainWeapon) ? secondaryWeapon : mainWeapon; //切换武器
            currentWeapon.gameObject.SetActive(true); //显示切换后的武器
            controller.SetupAnimator(currentWeapon.gunAnimator); //切换Animator
        }
        else if (Input.GetKeyDown(KeyCode.Alpha1)) //当按下键盘1键时
        {
    
    
            currentWeapon.gameObject.SetActive(false);
            currentWeapon = mainWeapon;
            currentWeapon.gameObject.SetActive(true);
            controller.SetupAnimator(currentWeapon.gunAnimator);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2)) //当按下键盘2键时
        {
    
    
            currentWeapon.gameObject.SetActive(false);
            currentWeapon = secondaryWeapon;
            currentWeapon.gameObject.SetActive(true);
            controller.SetupAnimator(currentWeapon.gunAnimator);
        }

    }

    private void Start()
    {
    
    
        if(currentWeapon == null)
        {
    
    
            Debug.Log("current weapon is null");
        }

        currentWeapon = mainWeapon;
        currentWeapon.gameObject.SetActive(true);
        secondaryWeapon.gameObject.SetActive(false);

        controller = GetComponent<FPCharacterControllerMovement>();
        controller.SetupAnimator(currentWeapon.gunAnimator);
    }

    private void Update()
    {
    
    
        // 如果当前没有武器 什么都不执行
        if (!currentWeapon) return;

        // 换弹
        if (Input.GetKeyDown(KeyCode.R))
        {
    
    
            currentWeapon.ReloadAmmo();
        }

        //按住扳机
        if (Input.GetMouseButton(0))
        {
    
    
            currentWeapon.HoldTrigger();
        }

        //松开扳机
        if(Input.GetMouseButtonUp(0))
        {
    
    
            currentWeapon.ReleaseTrigger();

        }

        // 瞄准 按下就会瞄准
        if (Input.GetMouseButtonDown(1))
        {
    
    
            currentWeapon.Aiming(true);
        }

        // 松开按键退出瞄准
        if (Input.GetMouseButtonUp(1))
        {
    
    

            currentWeapon.Aiming(false);
        }

        SwapWeapon();
    }
}

优化后的AssaultRifle类,将瞄准放到Firearms中进行实现,同时将isAllowShooting放到AssaultRifle中实现。isAllowShooting我认为除了自动武器的射速控制,还可以用来实现武器半自动,栓动。Shooting也略有不同,判断是否还有子弹被放到了Firearms中实现。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace Scripts.Weapon
{
    
    
    public class AssaultRifle : Firearms
    {
    
    
        // 是否正在装填 装填时不能打断 也不能再执行装填
        private bool m_isReloading;
        // 弹仓里是否有子弹 关系到能发射的子弹是否多一颗
        private bool m_isBulletLeft;



        protected override void Start()
        {
    
    
            base.Start();
            m_isReloading = false;
            m_isAiming = false;
            m_isAimingIn = false;
            m_startAimInTime = 0;

        }

        // 发射子弹 需要派生类来具体实现
        protected override void Shooting()
        {
    
    
            // 如果能够继续射击 这里是射速限制
            if (!isAllowShooting()) return;
            // 是否正在举枪 为了规避BUG 举枪动画播放时不能播放Fire动画
            if (!m_isAimingIn)
            {
    
    
                // 播放开火动画 注意我们会根据是否处于瞄准播放不同层的Fire
                gunAnimator.Play("Fire", m_isAiming ? 2 : 0, 0);
            }

            // 弹药减一
            currentAmmoInMag -= 1;

            // 播放开火音效
            firearmsShootingAudioSource.clip = firearmsShootingAudioData.ShootingAudio;
            firearmsShootingAudioSource.Play();
            // 运用后坐力
            cameraLook.DoRecoil();
            // 创建子弹轨迹
            CreateBullet();
            // 播放枪口粒子特效
            muzzleParticle.Play();
            // 播放抛壳粒子特效
            casingParticle.Play();
            // 重置上一次发射时间
            lastFireTime = Time.time;
        }

        // 装填 
        protected override void Reload()
        {
    
    
            // 首先要有子弹可以换
            if (currentAmmoCarried > 0)
            {
    
    
                // 其次是确实需要换弹并且没有在换弹
                if(currentAmmoInMag <= ammoInMag && !m_isReloading)
                {
    
    
                    // 将换弹layer的权重设置为1
                    // 接下来Reload动画层的权重会一直为1
                    gunAnimator.SetLayerWeight(1, 1);
                    // 判断当前弹仓里是否有子弹
                    m_isBulletLeft = (currentAmmoInMag > 0);
                    // 当前需要播放哪个动画?
                    gunAnimator.SetTrigger(m_isBulletLeft ? "ReloadLeft" : "ReloadOutOfAmmo");
                    // 当前需要播放哪个音效?
                    firearmsReloadingAudioSource.clip = (m_isBulletLeft ? 
                        firearmsShootingAudioData.ReloadLeft : firearmsShootingAudioData.ReloadOutOfAmmo);
                    firearmsReloadingAudioSource.Play();
                    // 设置状态
                    m_isReloading = true;
                    // 将所有的子弹放到currentAmmoCarried
                    currentAmmoCarried += currentAmmoInMag;
                    // 将currentAmmoInMag设置为0 一是为了方便接下来的计算 二是为了让换弹时无法射击
                    currentAmmoInMag = 0;
                    // 开始执行换弹协程 只有动画快要播放完成时才真正换弹
                    StartCoroutine(CheckReloadAmmoAnimationEnd()); 
                }
                else
                {
    
    
                    // 不需要换弹
                    #if UNITY_EDITOR
                    Debug.Log("Dont need to reload!");
                    #endif
                }
            }
            else
            {
    
    
                // 备用子弹打光了
                #if UNITY_EDITOR
                Debug.Log("Out of Ammo!");
                #endif
                return;
            }
        }

        // 瞄准 已在Firearms中实现所需要的逻辑


        // 创建子弹
        protected override void CreateBullet()
        {
    
    
            // 实例化子弹对象
            GameObject temp_bullet = Instantiate(bulletPrefab, muzzlePoint.position, muzzlePoint.rotation);
            // 引用子弹脚本
            Bullet temp_bulletScript = temp_bullet.GetComponent<Bullet>();
            temp_bulletScript.bulletSpeed = bulletVelocity;
            // 给子弹随机散射角度
            temp_bullet.transform.eulerAngles += CalculateBulletSpreadOffset();
        }

        // 是否能够射击 用于武器的射速限制
        protected override bool isAllowShooting()
        {
    
    
            return (Time.time - lastFireTime > 1 / fireRate);
        }

        // 检查换弹动画是否播放完成
        // 如果播放完成了就可以从逻辑上换弹了
        private IEnumerator CheckReloadAmmoAnimationEnd()
        {
    
    
            while (true)
            {
    
    
                yield return null;
                // 一定要在每帧赋值 才能得到current state
                gunStateInfo = gunAnimator.GetCurrentAnimatorStateInfo(1);
                if (gunStateInfo.IsTag("ReloadAmmo")) //这个基本上肯定是true 因为我们先设置了播放换弹动画
                {
    
    
                    if (gunStateInfo.normalizedTime >= 0.9f) //当换弹动画快要播完时
                    {
    
         
                        if(m_isBulletLeft) //如果我们装填时弹仓中有子弹
                        {
    
    
                            // 剩下的子弹能不能填满完整个弹匣?
                            currentAmmoInMag = (currentAmmoCarried > ammoInMag + 1) ? ammoInMag + 1 : currentAmmoCarried;
                            // 装了多少就减多少
                            currentAmmoCarried -= currentAmmoInMag;
                        }
                        else //如果我们装填时弹仓中没子弹
                        {
    
    
                            // 剩下的子弹能不能填满完整个弹匣?
                            currentAmmoInMag = (currentAmmoCarried > ammoInMag) ? ammoInMag : currentAmmoCarried;
                            // 装了多少就减多少
                            currentAmmoCarried -= currentAmmoInMag;
                        }

                        m_isReloading = false;
                        yield break;
                    }
                }
            }
        }

        // 瞄准时要干什么 已在Firearms中实现所需要的逻辑
    }
}

Firearms 类除了承接原来写在AssaultRifle中的参数,方法,还要再定义一套方法用于作为WeaponManager中使用的接口。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace Scripts.Weapon
{
    
    
    // 枪械类
    public abstract class Firearms : MonoBehaviour,IWeapon
    {
    
    
        // 子弹初速
        public float bulletVelocity = 100f;

        // 枪口位置 用于生成子弹
        public Transform muzzlePoint;
        // 抛出蛋壳的位置
        //public Transform casingPoint;

        // 摄像机 用于瞄准时修改FOV
        public Camera eyeCamera;
        // 摄像机FOV的原数值
        protected float originFOV;

        // 枪焰粒子效果
        public ParticleSystem muzzleParticle; //粒子材质有自发光效果 所以不再需要灯效
         枪焰火光效果
        //public Light muzzleLight;
        // 抛壳粒子效果
        public ParticleSystem casingParticle;

        // 音效
        public FirearmsAudioData firearmsShootingAudioData; //开火音效数据
        public AudioSource firearmsShootingAudioSource; //开火AudioSource
        public AudioSource firearmsReloadingAudioSource; //装填AudioSource

        // 弹匣弹药量
        public int ammoInMag = 30;
        // 能携带的最大弹药数
        public int maxAmmoCarried = 120;
        // 子弹预制体
        public GameObject bulletPrefab;

        // 子弹散射最大角
        public float spreadAngle;

        // 射速速率 一秒钟能打几发
        public float fireRate;
        // 上一次开火的时间
        protected float lastFireTime;
        // 是否正在开火
        protected bool isShooting = false;

        // 当前弹匣弹药量
        protected int currentAmmoInMag = 30;
        // 当前携带的弹药数
        protected int currentAmmoCarried = 120;

        // 枪械动画
        [HideInInspector]
        public Animator gunAnimator;
        // 枪械动画机的信息
        protected AnimatorStateInfo gunStateInfo;

        // 是否正在瞄准
        protected bool m_isAiming;
        // 是否正在举枪 用于规避BUG
        protected bool m_isAimingIn;
        // 举枪动画开始时间 用于规避BUG
        protected float m_startAimInTime;
        // 瞄准时的目标FOV
        protected float m_targetFOV = 48f;
        // 处理瞄准协程方法的协程变量
        protected IEnumerator doAimCoroutine;

        // 摄像机控制脚本 用于枪械后坐力功能
        protected FPCameraLook cameraLook;



// --------------------------------------------------- 方法 ------------------------------------------------------- //
        // 执行攻击
        public void DoAttack()
        {
    
    
            // 对于枪械来说 发动攻击的方式就是发射子弹
            Shooting();
        }

        // 瞄准 每种枪的瞄准会有不同(也许吧) 画面放大会有不同(机瞄,二倍镜,三倍镜) 需要派生类来具体实现
        internal void Aiming(bool isAiming)
        {
    
    
            m_isAiming = isAiming;
            // 如果还没有进入瞄准
            if (m_isAiming)
            {
    
    
                m_isAimingIn = true;
                m_startAimInTime = Time.time;
            }

            // 设置参数
            gunAnimator.SetBool("Aim", m_isAiming);
            // 开启DoAim协程 防止协程冲突
            if (doAimCoroutine == null) // 说明协程还没有打开
            {
    
    
                doAimCoroutine = DoAim();
                StartCoroutine(doAimCoroutine);
            }
            else // 不为空说明协程已经打开了
            {
    
    
                // 首先停下之前的协程
                StopCoroutine(doAimCoroutine);
                doAimCoroutine = null;
                // 赋值以后再执行起来
                doAimCoroutine = DoAim();
                StartCoroutine(doAimCoroutine);
            }
        }

        // 按下扳机 作为外部接口 定义当按住枪械扳机时会发生什么
        internal void HoldTrigger()
        {
    
    
            // 当枪里没子弹时 无法射击 或者也许可以发出一个音效?
            if (currentAmmoInMag <= 0)
            {
    
    
                isShooting = false;    
                return; 
            }

            // 发动攻击
            isShooting = true;
            DoAttack();
        }

        // 松开扳机 作为外部接口 定义当松开枪械扳机时会发生什么
        internal void ReleaseTrigger()
        {
    
    
            isShooting = false;
        }


        // 松开扳机 作为外部接口 定义当装填弹药时会发生什么
        internal void ReloadAmmo()
        {
    
    
            // 当装填弹药时 会发生装填弹药这件事
            Reload();
        }

        // 发射子弹的逻辑 每种枪的子弹发射是不同的(也许吧) 需要派生类来具体实现
        protected abstract void Shooting();

        // 装填的逻辑 每种枪的换弹逻辑是不同的 需要派生类来具体实现
        protected abstract void Reload();

        // 实例化子弹的逻辑 需要派生类来具体实现
        protected abstract void CreateBullet();

        // 是否能够射击 用于武器的射速限制 或者半自动/栓动武器的实现
        protected abstract bool isAllowShooting();

        // 计算子弹散射量
        protected Vector3 CalculateBulletSpreadOffset()
        {
    
    
            // 子弹的散射会根据摄像机fov大小做调整
            float temp_spreadPercent = spreadAngle / eyeCamera.fieldOfView;
            // 使用随机数
            return Random.insideUnitCircle* temp_spreadPercent;
        }

        // 定义瞄准时需要做的 枪械瞄准时摄像机FOV都会放大
        protected IEnumerator DoAim()
        {
    
    
            float temp_currentFOV = 0;
            while (true)
            {
    
    
                yield return null;

                // 处理瞄准时视野放大功能
                eyeCamera.fieldOfView =
                       Mathf.SmoothDamp(eyeCamera.fieldOfView,
                       m_isAiming ? m_targetFOV : originFOV,
                       ref temp_currentFOV,
                       Time.deltaTime * 10f);
                if (Mathf.Abs(eyeCamera.fieldOfView - m_targetFOV) < 0.001f)
                {
    
    
                    eyeCamera.fieldOfView = m_targetFOV;
                    yield break;
                }
            }
        }

        protected virtual void Awake()
        {
    
    
            gunAnimator = GetComponent<Animator>();
        }

        // virtual修饰符方便子类修改
        protected virtual void Start()
        {
    
    
            currentAmmoInMag = ammoInMag;
            currentAmmoCarried = maxAmmoCarried;
            originFOV = eyeCamera.fieldOfView;
            cameraLook = FindObjectOfType<FPCameraLook>();
            // 现在doAimCoroutine是对DoAim协程方法的引用了
            doAimCoroutine = DoAim();
        }

        // 武器的主要控制由WeaponManager来进行
        // 这里只进行一些辅助控制
        protected void Update()
        {
    
    
            if ((Time.time - m_startAimInTime) > 0.3f) //是否正在举枪 举枪时不同时播放开火动画
            {
    
    
                m_isAimingIn = false;
            }

            if (!isShooting)
            {
    
    
                cameraLook.RecoverRecoil();
            }
        }
    }
}

切换武器逻辑的初步实现

如果要我来写的话,切换武器的逻辑就是,我拿原本资源里有的手枪预制件放到和AK预制件的相同层,切换就是预制件的开关,到时候专门做一个脚本,名字都想好了就叫WeaponSwitcher,来专门管这两个预制件就行了。不过逻辑就是这样的逻辑。关键是看在Unity中GameObject要如何制作。

    // 切换武器
    private void SwapWeapon()
    {
    
    
        if (Input.GetAxis("Mouse ScrollWheel") != 0) //当使用滚轮时
        {
    
    
            currentWeapon.gameObject.SetActive(false); //隐藏现在的武器
            currentWeapon = (currentWeapon == mainWeapon) ? secondaryWeapon : mainWeapon; //切换武器
            currentWeapon.gameObject.SetActive(true); //显示切换后的武器
        }
        else if (Input.GetKeyDown(KeyCode.Alpha1)) //当按下键盘1键时
        {
    
    
            currentWeapon.gameObject.SetActive(false);
            currentWeapon = mainWeapon;
            currentWeapon.gameObject.SetActive(true);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2)) //当按下键盘2键时
        {
    
    
            currentWeapon.gameObject.SetActive(false);
            currentWeapon = secondaryWeapon;
            currentWeapon.gameObject.SetActive(true);
        }
    }

切换武器的GameObject实现

在这里插入图片描述
可以看到,我们将资源当中的手枪和手臂的部分与原本的步枪放到同一级,接下来要做的就是配置手枪相应的动画,参数。以及在换枪时将新枪的动画传递过去,其实要传递的我觉得不止是动画,还有新枪的后坐力参数,振屏参数等,这些以后再做。
在这里插入图片描述
至于手枪的动画,我是直接将步枪动画机复制了一份,将其中的动画片段都改为手枪的。有素材就是任性。

最后,在切枪的逻辑代码中,在枪械切换后为controller脚本修改Animator。

    // 切换武器
    private void SwapWeapon()
    {
    
    
        if (Input.GetAxis("Mouse ScrollWheel") != 0) //当使用滚轮时
        {
    
    
            currentWeapon.gameObject.SetActive(false); //隐藏现在的武器
            currentWeapon = (currentWeapon == mainWeapon) ? secondaryWeapon : mainWeapon; //切换武器
            currentWeapon.gameObject.SetActive(true); //显示切换后的武器
            controller.SetupAnimator(currentWeapon.gunAnimator); //切换Animator
        }
        else if (Input.GetKeyDown(KeyCode.Alpha1)) //当按下键盘1键时
        {
    
    
            currentWeapon.gameObject.SetActive(false);
            currentWeapon = mainWeapon;
            currentWeapon.gameObject.SetActive(true);
            controller.SetupAnimator(currentWeapon.gunAnimator);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2)) //当按下键盘2键时
        {
    
    
            currentWeapon.gameObject.SetActive(false);
            currentWeapon = secondaryWeapon;
            currentWeapon.gameObject.SetActive(true);
            controller.SetupAnimator(currentWeapon.gunAnimator);
        }
	}

BUG以及缺陷:

Firearms 类中的接口有时会过度封装。

        // 松开扳机 作为外部接口 定义当装填弹药时会发生什么
        internal void ReloadAmmo()
        {
    
    
            // 当装填弹药时 会发生装填弹药这件事
            Reload();
        }

第一次切换武器时,传递的Animator为空,这个BUG其实原因很简单,手枪的脚本在一开始的Active为false,不会执行脚本中的Start,也就是说一开始我们并没有获取手枪中的Animator,当我们换枪时,SwapWeapon传递的currentWeapon.gunAnimator自然就是null。要解决这个问题可以在编辑器中手动添加,或者让脚本在Awake中获取Animator。

        protected virtual void Awake()
        {
    
    
            gunAnimator = GetComponent<Animator>();
        }

开枪会打到自己,没错,原因是子弹的射线并没有屏蔽掉玩家自己。我们要做的就是给射线一个LayerMask。

        // mask用来屏蔽掉玩家自己 防止开枪时的子弹打到自己
        public LayerMask bulletMask;
		
		// 在Raycast参数中运用mask
		if (Physics.Raycast(bulletTransform.position, 
                (prevPosition - bulletTransform.position).normalized,//射线发射方向为从上一帧的位置发射到这一帧的位置 
                out RaycastHit temp_hit,
                (bulletTransform.position - prevPosition).magnitude
                , bulletMask.value))//射线最大长度为这两个位置之间的长度
                {
    
    ......}

每次切换到步枪时,都会播放开火声,经过排查是不知何时勾选了AudioSource组件的Play On Wake。
在这里插入图片描述

最后还有一个问题,手枪开枪时能看到子弹轨迹“戳”到枪里去了,我觉得造成的原因一部分是因为子弹的TrailRenderer的设置问题,可能还有一部分是因为GunCamera的问题。


值得注意的:

我们的枪械切换功能还没做完。


猜你喜欢

转载自blog.csdn.net/qq_37856544/article/details/113825592