今天实现的内容:
枪械功能的继续实现:
在这个项目中弹药装填被设计为当弹仓中还有子弹时换弹会多一发子弹的情况。其实我很想设计成换弹就直接将整个弹匣扔掉,这样显得硬核一点。但是还是设计成大多数竞技射击游戏那样,弹匣少了多少弹药就补充多少弹药的形式。
// 装填
protected override void Reload()
{
// 首先要有子弹可以换
if (currentAmmoCarried > 0)
{
// 其次是确实需要换弹并且没有在换弹
if(currentAmmoInMag <= ammoInMag && !m_isReloading)
{
// 将换弹layer的权重设置为1
gunAnimator.SetLayerWeight(1, 1);
// 判断当前弹仓里是否有子弹
m_isBulletLeft = (currentAmmoInMag > 0);
// 当前需要播放哪个动画?
gunAnimator.SetTrigger(m_isBulletLeft ? "ReloadLeft" : "ReloadOutOfAmmo");
// 设置状态
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;
}
}
首先,在没有子弹可以换的时候,在确实需要换弹,也就是子弹没有装满的时候,在正在换弹的时候,我们是不会执行换弹的。Reload函数仅仅是播放动画,设置状态m_isReloading,将所有子弹都放到currentAmmoCarried,将currentAmmoInMag清零,以及开启真正的换弹方法,该方法是一个协程。
// 检查换弹动画是否播放完成
// 如果播放完成了就可以从逻辑上换弹了
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;
}
}
}
}
这是实际的换弹方法,之所以使用协程是因为装填动画需要时间,一定要等装填动画快完成了我们才能执行换弹的真正逻辑。具体实现思路看注释吧,不想写了。
弹匣里有弹药才能开火,并且还要看满不满足射速的条件,在开火执行时,弹匣中的子弹数减一,直接播放开火动画,创建子弹(目前只有轨迹),播放枪口粒子特效,播放抛壳粒子特效,最后重置上一次发射的时间来用于下次射击的射速条件判断。
// 发射子弹 需要派生类来具体实现
protected override void Shooting()
{
// 如果弹匣里有弹药
if (currentAmmoInMag <= 0) return;
// 并且能够继续射击
if (!isAllowShooting()) return;
// 弹药减一
currentAmmoInMag -= 1;
// 播放开火动画
gunAnimator.Play("Fire", 0, 0);
// 创建子弹轨迹
CreateBullet();
// 播放枪口粒子特效
muzzleParticle.Play();
// 播放抛壳粒子特效
casingParticle.Play();
// 重置上一次发射时间
lastFireTime = Time.time;
}
看看就行了
// 实例化子弹
protected override void CreateBullet()
{
GameObject temp_bullet = Instantiate(bulletPrefab, muzzlePoint.position, muzzlePoint.rotation);
temp_bullet.AddComponent<Rigidbody>().velocity = temp_bullet.transform.forward* 100f;
}
为了方便子类修改Shooting代码以实现不同的攻击方式,将原本写在DoAttack中的代码写到AssaultRifle中的Shooting中。本来我们在之前写的就是突击步枪的攻击方式。这样做没有问题。可以这么理解,武器的功能是发动攻击(DoAttack),枪械类发动攻击的方式是射击(Shooting),不同的枪械有不同的射击方式,所以需要在具体的枪械子类中来实现具体的Shooting,以及CreateBullet。
下面是到今天为止的AssaultRifle类和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;
}
// 发射子弹 需要派生类来具体实现
protected override void Shooting()
{
// 如果弹匣里有弹药
if (currentAmmoInMag <= 0) return;
// 并且能够继续射击
if (!isAllowShooting()) return;
// 弹药减一
currentAmmoInMag -= 1;
// 播放开火动画
gunAnimator.Play("Fire", 0, 0);
// 创建子弹轨迹
CreateBullet();
// 播放枪口粒子特效
muzzleParticle.Play();
// 播放抛壳粒子特效
casingParticle.Play();
// 重置上一次发射时间
lastFireTime = Time.time;
}
// 装填
protected override void Reload()
{
// 首先要有子弹可以换
if (currentAmmoCarried > 0)
{
// 其次是确实需要换弹并且没有在换弹
if(currentAmmoInMag <= ammoInMag && !m_isReloading)
{
// 将换弹layer的权重设置为1
gunAnimator.SetLayerWeight(1, 1);
// 判断当前弹仓里是否有子弹
m_isBulletLeft = (currentAmmoInMag > 0);
// 当前需要播放哪个动画?
gunAnimator.SetTrigger(m_isBulletLeft ? "ReloadLeft" : "ReloadOutOfAmmo");
// 设置状态
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;
}
}
// 实例化子弹
protected override void CreateBullet()
{
GameObject temp_bullet = Instantiate(bulletPrefab, muzzlePoint.position, muzzlePoint.rotation);
temp_bullet.AddComponent<Rigidbody>().velocity = temp_bullet.transform.forward* 100f;
}
// 检查换弹动画是否播放完成
// 如果播放完成了就可以从逻辑上换弹了
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;
}
}
}
}
protected void Update()
{
// 开火
if (Input.GetMouseButton(0))
{
DoAttack();
}
// 换弹
if(Input.GetKeyDown(KeyCode.R))
{
Reload();
}
}
}
}
子类AssaultRifle来具体定义Shooting,Reload,以及CreateBullet是干嘛
using UnityEngine;
namespace Scripts.Weapon
{
// 枪械类
public abstract class Firearms : MonoBehaviour,IWeapon
{
// 枪口位置 用于生成子弹
public Transform muzzlePoint;
// 抛出蛋壳的位置
public Transform casingPoint;
// 枪焰粒子效果
public ParticleSystem muzzleParticle;
枪焰火光效果
//public Light muzzleLight;
// 抛壳粒子效果
public ParticleSystem casingParticle;
// 弹匣弹药量
public int ammoInMag = 30;
// 能携带的最大弹药数
public int maxAmmoCarried = 120;
// 子弹预制体
public GameObject bulletPrefab;
// 射速速率 一秒钟能打几发
public float fireRate;
// 上一次开火的时间
protected float lastFireTime;
// 当前弹匣弹药量
protected int currentAmmoInMag = 30;
// 当前携带的弹药数
protected int currentAmmoCarried = 120;
// 枪械动画
protected Animator gunAnimator;
// 枪械动画机的信息
protected AnimatorStateInfo gunStateInfo;
// virtual修饰符方便子类修改
protected virtual void Start()
{
currentAmmoInMag = ammoInMag;
currentAmmoCarried = maxAmmoCarried;
gunAnimator = GetComponent<Animator>();
}
// 执行攻击
public void DoAttack()
{
// 对于枪械来说 发动攻击的方式就是发射子弹
Shooting();
}
// 发射子弹 需要派生类来具体实现
protected abstract void Shooting();
// 装填
protected abstract void Reload();
// 是否能够射击 用于武器的射速限制
protected bool isAllowShooting()
{
return (Time.time - lastFireTime > 1 / fireRate);
}
// 如何实例化一个子弹 交给子类来具体实现
protected abstract void CreateBullet();
}
}
基类Firearms中的所有成员都是public或者protected的。至于射速限制,还是交给基类来做,在我们的世界里,枪械都是有射速限制的。
新素材的导入:
使用粒子效果实现的抛壳效果已经有了,换了新的枪焰粒子效果。我看之前用的素材是基于脚本的抛壳来着?粒子的具体制作过程嘛?没有,我直接用的别人的。本项目无任何商业用途。
新动画:
这个有点意思
在动画机里面没有进入方式,一开始我还在想Up是不是搞错了(这个项目是照着别人的视频做的),后来发现我在脚本里,直接Animator.Play(“Fire”)就能直接播放动画了,播放完之后自动转换。看来我真是学傻了,想着一定要从某个状态转换过来啊。
这个故事告诉我们,动画机中的动画可以跳过动画机参数直接用脚本播放,以前一直没注意。
// 执行攻击
public void DoAttack()
{
// 如果弹匣里有弹药
if (currentAmmoInMag <= 0) return;
// 并且能够继续射击
if (!isAllowShooting()) return;
// 弹药减一
currentAmmoInMag -= 1;
// 播放开火动画
GunAnimator.Play("Fire", 0, 0);
// 发射子弹
Shooting();
// 重置上一次发射时间
lastFireTime = Time.time;
}
换弹动画有两种,一种是枪膛里还有子弹(ReloadLeft),另一种是枪膛里已经没有子弹了,子弹打空了(ReloadOutOfAmmo)。
设置一个新的layer,layer的weight越高动作越受到这个层的影响,用两个Trigger类型参数来控制动画变换,动画机如下。
注意,这个Idle在这里只是一个空的动画节点,设计目的是为了在换弹之后能继续执行其他layer的动画(目前Reload层的权重一直是1)。
这两个换弹动画都要打上Tag,方便之后在脚本中找到。
BUG以及缺陷:
UP讲的这个完全没搞懂
子弹抛壳的粒子在玩家转动时会跟着一起转,很诡异。
已经解决了,改一改粒子组件的这个参数
值得注意的:
gunStateInfo要记得赋值,而且一定要在每帧赋值,才能得到当前的动画状态。
gunStateInfo = gunAnimator.GetCurrentAnimatorStateInfo(1);
行了,就这样了,肝不动了。今天这个换弹算是写出自己的特色了哈哈。