FPSMicroGame 武器系统

FPSMicroGame 武器系统

射击

  • 散射

散射的计算WeaponController.cs脚本里。在GetShotDirectionWithinSpread函数里,主要是通过Vector3.Slerp,将 枪口的前向向量 和 一个随机的从球心到球面的单位向量,做插值。插值比例就是BulletSpreadAngle / 180f。可以想象一下, 两个三维向量角度之差最多是180°,所以BulletSpreadAngle 是最大散射角。

 void HandleShoot()
 {      
     //...
     // spawn all bullets with random direction
     for (int i = 0; i < bulletsPerShotFinal; i++)
     {
         Vector3 shotDirection = GetShotDirectionWithinSpread(WeaponMuzzle);
         ProjectileBase newProjectile = Instantiate(ProjectilePrefab, WeaponMuzzle.position,
                                                    Quaternion.LookRotation(shotDirection));
         newProjectile.Shoot(this);
     }
     //...
 }
 
 
 public Vector3 GetShotDirectionWithinSpread(Transform shootTransform)
 {
     float spreadAngleRatio = BulletSpreadAngle / 180f;
     Vector3 spreadWorldDirection = Vector3.Slerp(shootTransform.forward, UnityEngine.Random.insideUnitSphere,
                                                  spreadAngleRatio);
 
     return spreadWorldDirection;
 }
  • 后坐

    • 射击游戏中,为了模拟真实的枪械,后坐力又分为水平后坐力和竖直后坐力。由于这两种后坐力的影响,枪械在连发时,弹道会形成一个T字型,俗称T型弹道。具体原理是:竖直后坐会一直把枪口上抬,抬升到一个最大角度后保持。水平后坐会给弹道增加随机的左右偏移。所以在枪械连发时,可以看到由于竖直后坐的作用,弹道快速上抬,之后高度不变,左右摇摆,得到T型弹道。
    • 完善的游戏中,后坐力的手感表现需要,模型的运动表现 + 弹道的表现 + 镜头动画的表现。当前的FPSMicroGame中,只有模型的运动表现。我们先来看看。在PlayerWeaponsManager.cs里。
      • 在update函数里叠加了每帧的后坐力,m_AccumulatedRecoil其实代表后坐力累加后,期望枪械所在的位置。
      • 在lateUpdate函数里 调用UpdateWeaponRecoil();计算了后坐力的动画,并最终修改了武器的位置WeaponParentSocket.localPosition。这里要注意,后坐力是朝Z轴负方向。
        • UpdateWeaponRecoil函数里,先判断期望的后坐力位置,和当前后坐力位置差异。如果差异过大,则往期望后坐力位置移动。否者说明后坐力已经达到,就开始恢复。
    void Update()
    {
        //...
        // Handle accumulating recoil
        if (hasFired)
        {
            m_AccumulatedRecoil += Vector3.back * activeWeapon.RecoilForce;
            m_AccumulatedRecoil = Vector3.ClampMagnitude(m_AccumulatedRecoil, MaxRecoilDistance);
        }
        //...
    }
    
    
    void LateUpdate()
    {
        UpdateWeaponAiming();
        UpdateWeaponBob();
        UpdateWeaponRecoil();
        UpdateWeaponSwitching();
    
        // Set final weapon socket position based on all the combined animation influences
        WeaponParentSocket.localPosition =
            m_WeaponMainLocalPosition + m_WeaponBobLocalPosition + m_WeaponRecoilLocalPosition;
    }
    
    
    // Updates the weapon recoil animation
    void UpdateWeaponRecoil()
    {
        // if the accumulated recoil is further away from the current position, make the current position move towards the recoil target
        if (m_WeaponRecoilLocalPosition.z >= m_AccumulatedRecoil.z * 0.99f)
        {
            m_WeaponRecoilLocalPosition = Vector3.Lerp(m_WeaponRecoilLocalPosition, m_AccumulatedRecoil,
                                                       RecoilSharpness * Time.deltaTime);
        }
        // otherwise, move recoil position to make it recover towards its resting pose
        else
        {
            m_WeaponRecoilLocalPosition = Vector3.Lerp(m_WeaponRecoilLocalPosition, Vector3.zero,
                                                       RecoilRestitutionSharpness * Time.deltaTime);
            m_AccumulatedRecoil = m_WeaponRecoilLocalPosition;
        }
    }
    
  • 开火间隔

    • 控制枪械射速的变量,在程序实现上比较简单。在接收到输入时,调用TryShoot。只有子弹余量大于1,并且距离上次触发射击超过DelayBetweenShots时间后,才可以调用HandleShoot();

      bool TryShoot()
      {
          if (m_CurrentAmmo >= 1f
              && m_LastTimeShot + DelayBetweenShots < Time.time)
          {
              HandleShoot();
              m_CurrentAmmo -= 1f;
      
              return true;
          }
      
          return false;
      }
      
  • 开镜 Aiming

    • 看到这里其实很简单,开镜和关镜,就是移动一下枪的位置,同时修改一下FOV。对于装有倍镜的武器,还要叠加一层开镜UI。(Unity中的Fov指的是视锥体垂直方向两个对边的夹角)。开镜会应用一个更小的FOV,相机只观察更小的一个角度,但这部分的景物,还要填充满屏幕,所以就实现了放大。
    • 这里注意一下,游戏内存在两个相机,一个是主相机PlayerCamera,一个是只拍摄player武器模型的WeaponCamera。
    // Updates weapon position and camera FoV for the aiming transition
    void UpdateWeaponAiming()
    {
        if (m_WeaponSwitchState == WeaponSwitchState.Up)
        {
            WeaponController activeWeapon = GetActiveWeapon();
            if (IsAiming && activeWeapon)
            {
                m_WeaponMainLocalPosition = Vector3.Lerp(m_WeaponMainLocalPosition, 			  			   		AimingWeaponPosition.localPosition + activeWeapon.AimOffset,AimingAnimationSpeed * Time.deltaTime);
                SetFov(Mathf.Lerp(m_PlayerCharacterController.PlayerCamera.fieldOfView, 	 		  				activeWeapon.AimZoomRatio * DefaultFov, AimingAnimationSpeed * Time.deltaTime));
            }
            else
            {
                m_WeaponMainLocalPosition = Vector3.Lerp(m_WeaponMainLocalPosition, 			    				DefaultWeaponPosition.localPosition, AimingAnimationSpeed * Time.deltaTime);
                SetFov(Mathf.Lerp(m_PlayerCharacterController.PlayerCamera.fieldOfView, DefaultFov, 				AimingAnimationSpeed * Time.deltaTime));
            }
        }
    }
    
    
    
    // Sets the FOV of the main camera and the weapon camera simultaneously
    public void SetFov(float fov)
    {
        m_PlayerCharacterController.PlayerCamera.fieldOfView = fov;
        WeaponCamera.fieldOfView = fov * WeaponFovMultiplier;
    }
    
  • 武器运动摇摆 Weapon Bob

    • Weapon Bob 是指角色运动时武器的摇摆动作。demo中摇摆是通过lateupdate中UpdateWeaponBob函数来做的。函数主要做了两件事,通过角色的速度,计算出摇摆因子。通过摇摆因子,乘上正玄函数,得到枪在本帧的位置。注意到函数分别计算了水平方向和竖直方向上的摇摆位置:hBobValue、vBobValue,竖直方向上摇摆的频率是水平的两倍。最后枪械摇摆会画出一个横着的8字型。

      // Updates the weapon bob animation based on character speed
      void UpdateWeaponBob()
      {
          if (Time.deltaTime > 0f)
          {
              Vector3 playerCharacterVelocity =
                  (m_PlayerCharacterController.transform.position - m_LastCharacterPosition) / Time.deltaTime;
      
              // calculate a smoothed weapon bob amount based on how close to our max grounded movement velocity we are
              float characterMovementFactor = 0f;
              if (m_PlayerCharacterController.IsGrounded)
              {
                  characterMovementFactor =
                      Mathf.Clamp01(playerCharacterVelocity.magnitude /
                                    (m_PlayerCharacterController.MaxSpeedOnGround *
                                     m_PlayerCharacterController.SprintSpeedModifier));
              }
      
              m_WeaponBobFactor =
                  Mathf.Lerp(m_WeaponBobFactor, characterMovementFactor, BobSharpness * Time.deltaTime);
      
              // Calculate vertical and horizontal weapon bob values based on a sine function
              float bobAmount = IsAiming ? AimingBobAmount : DefaultBobAmount;
              float frequency = BobFrequency;
              float hBobValue = Mathf.Sin(Time.time * frequency) * bobAmount * m_WeaponBobFactor;
              float vBobValue = ((Mathf.Sin(Time.time * frequency * 2f) * 0.5f) + 0.5f) * bobAmount *
                  m_WeaponBobFactor;
      
              // Apply weapon bob
              m_WeaponBobLocalPosition.x = hBobValue;
              m_WeaponBobLocalPosition.y = Mathf.Abs(vBobValue);
      
              m_LastCharacterPosition = m_PlayerCharacterController.transform.position;
          }
      }
      
  • 武器瞄准摇摆 Weapon Sway

    • Weapon Sway 是指武器在瞄准时,武器自己的晃动。这个在demo中没有实现。
  • 开火特效、音效

    • 开火特效

      • 这里可以看到在开火时,枪口有绿色的粒子特效,射出子弹的弹道,枪身过热导致的红色烟雾。我们分别看下这些是如何实现的。

FPSMicroGame 武器系统
1. 枪口开火特效:在射击时,在枪口位置,实例化了一个粒子特效 prefabMuzzleFlashPrefab。并且在2s后销毁。
2. 子弹弹道,也是实例化。

void HandleShoot()
{
    //生成子弹弹道 spawn all bullets with random direction
    for (int i = 0; i < bulletsPerShotFinal; i++)
    {
        Vector3 shotDirection = GetShotDirectionWithinSpread(WeaponMuzzle);
        ProjectileBase newProjectile = Instantiate(ProjectilePrefab, WeaponMuzzle.position,
                                                   Quaternion.LookRotation(shotDirection));
        newProjectile.Shoot(this);
    }
    
    //枪口特效 muzzle flash
    if (MuzzleFlashPrefab != null)
    {
        GameObject muzzleFlashInstance = Instantiate(MuzzleFlashPrefab, WeaponMuzzle.position,
                                                     WeaponMuzzle.rotation, WeaponMuzzle.transform);
        // Unparent the muzzleFlashInstance
        if (UnparentMuzzleFlash)
        {
            muzzleFlashInstance.transform.SetParent(null);
        }

        Destroy(muzzleFlashInstance, 2f);
    }
    
}
  3. 过热导致的红色烟雾:这个在另一个脚本OverheatBehavior.cs里面。这里可以看到烟雾的控制是通过调整粒子系统的rateOverTimeMultiplier参数。它会调整粒子曲线的幅度。同时枪械在过热时的颜色也会发生改变。
   void Update()
   {
       // visual smoke shooting out of the gun 枪械在过热时的颜色。
       float currentAmmoRatio = m_Weapon.CurrentAmmoRatio;
       if (currentAmmoRatio != m_LastAmmoRatio)
       {
           m_OverheatMaterialPropertyBlock.SetColor("_EmissionColor",
                                                    OverheatGradient.Evaluate(1f - currentAmmoRatio));
   
           foreach (var data in m_OverheatingRenderersData)
           {
               data.Renderer.SetPropertyBlock(m_OverheatMaterialPropertyBlock, data.MaterialIndex);
           }
   
           //过热烟雾
           m_SteamVfxEmissionModule.rateOverTimeMultiplier = SteamVfxEmissionRateMax * (1f - currentAmmoRatio);
   }
- 音效

    - 开火音效分为两种,连续开火音效 和 单发开火音效。具体看WeaponController文件,这个根据UseContinuousShootSound变量来区分。一个AudioSource可以通过 PlayOneShot 函数,播放多个声音片段。
 	//连续射击音效    
	void UpdateContinuousShootSound()
       {
           if (UseContinuousShootSound)
           {
               if (m_WantsToShoot && m_CurrentAmmo >= 1f)
               {
                   if (!m_ContinuousShootAudioSource.isPlaying)
                   {
                       m_ShootAudioSource.PlayOneShot(ShootSfx);
                       m_ShootAudioSource.PlayOneShot(ContinuousShootStartSfx);
                       m_ContinuousShootAudioSource.Play();
                   }
               }
               else if (m_ContinuousShootAudioSource.isPlaying)
               {
                   m_ShootAudioSource.PlayOneShot(ContinuousShootEndSfx);
                   m_ContinuousShootAudioSource.Stop();
               }
           }
       }
   
   //单发开火音效
       void HandleShoot()
       {
           // play shoot SFX
           if (ShootSfx && !UseContinuousShootSound)
           {
               m_ShootAudioSource.PlayOneShot(ShootSfx);
           }
       }
  • 命中特效、音效

    • 在demo中命中没有播放音效,只有有命中特效,但是代码中是有音效部分的。我们观察一下子弹的prefab:Projectile_Blaster。其中impact Vfx里引用了一个prefab,VFX_LazerSparksGreen。再看一下脚本ProjectileStandard的OnHit函数中做了四件事,计算了伤害、命中特效,音效,和子弹的销毁。
    void OnHit(Vector3 point, Vector3 normal, Collider collider)
            {
                // damage
                if (AreaOfDamage)
                {
                    // area damage
                    AreaOfDamage.InflictDamageInArea(Damage, point, HittableLayers, k_TriggerInteraction,
                        m_ProjectileBase.Owner);
                }
                else
                {
                    // point damage
                    Damageable damageable = collider.GetComponent<Damageable>();
                    if (damageable)
                    {
                        damageable.InflictDamage(Damage, false, m_ProjectileBase.Owner);
                    }
                }
    
                // impact vfx
                if (ImpactVfx)
                {
                    GameObject impactVfxInstance = Instantiate(ImpactVfx, point + (normal * ImpactVfxSpawnOffset),
                        Quaternion.LookRotation(normal));
                    if (ImpactVfxLifetime > 0)
                    {
                        Destroy(impactVfxInstance.gameObject, ImpactVfxLifetime);
                    }
                }
    
                // impact sfx
                if (ImpactSfxClip)
                {
                    AudioUtility.CreateSFX(ImpactSfxClip, point, AudioUtility.AudioGroups.Impact, 1f, 3f);
                }
    
                // Self Destruct
                Destroy(this.gameObject);
    
    
  • 射程、伤害衰减和穿墙

    • 这部分的逻辑demo中都没有,但是可以看一下ProjectileStandard中的Update函数,这里会有子弹的运动逻辑和命中判定逻辑。
    • 子弹的运动逻辑里有一部分是做TrajectoryCorrection弹道修正的。游戏中子弹原本是在武器的枪口处生成,这里是将子弹在限定距离TrajectoryCorrectionDistance内,逐步将子弹的弹道修正为从准心射出,更符合FPS的游戏的玩法:从表现上子弹应该是从枪口射出,从玩法上 子弹应该从准心处射出。读者们直接在 Projectile_Blaster.prefab 里把 TrajectoryCorrectionDistance 改成0就能看到,游戏中子弹是直接从准心处射出。
    • demo中没有穿墙,而是在 Update 中每帧进行SphereCastAll,进行命中检测
上一篇:Unity 鼠标旋转物体360展示


下一篇:NGUI实现滑动屏幕的时候,进行环形旋转