今天实现的内容:
脚本结构的优化
为接下来的切换枪械做铺垫,脚本需要再进行一次大改,目的是再次将枪械的操作用一个脚本统一管理,方便接下来对不同枪械能使用同一套代码进行操作。将之前在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的问题。
值得注意的:
我们的枪械切换功能还没做完。