一、前言
我与另外两个组员合作进行了FPS游戏(第一人称射击游戏)的开发,这个游戏对应于我们在项目开始设想的PVP玩家对战游戏。玩家之间的之间对战会让游戏变得更加紧张刺激,还能够增强玩家之间的感情。
我在这个游戏中主要负责的工作是枪械特效、枪械声音、射击反馈震屏、子弹散射、枪械射击弹孔、ui子弹数量、pun控制输入同步、remote开枪效果同步。
二、实现枪械射击
首先,我们是在weapon文件夹下完成对枪械的相关脚本的创建和编写的。
创建IWeapon脚本用来管理接口,命名空间归类到Scripts.Weapon下,防止出现冲突:
namespace Scripts.Weapon
{
public interface IWeapon
{
void DoAttack();
}
}
然后编写Firearms脚本,这是枪械整体共用的部分。其中把Firearms枪械类抽象掉,因为这不是具体的枪械,并且可以挂在game object上:
获取枪口的位置,还要知道子弹抛出的位置。抛出弹壳使用粒子变量,避免太多的实例化过程。
需要一个弹夹AmmoInMag,设置容纳的子弹数。并且设置每把枪最大的子弹携带数。此外开枪需要设置动画。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Scripts.Weapon
{
public abstract class Firearms : MonoBehaviour, IWeapon
{
public GameObject BulletPrefab;
public Camera EyeCamera;
public Camera GunCamera;
public Transform MuzzlePoint;
public Transform CasingPoint;
public ParticleSystem MuzzleParticle;
public ParticleSystem CasingParticle;
public AudioSource FirearmsShootingAudioSource;
public AudioSource FirearmsReloadAudioSource;
public FirearmsAudioData FirearmsAudioData;
public ImpactAudioData ImpactAudioData;
public GameObject BulletImpactPrefab;
public float FireRate;
public int AmmoInMag = 30;
public int MaxAmmoCarried = 120;
public float SpreadAngle;
[SerializeField] internal Animator GunAnimator;
public List<ScopeInfo> ScopeInfos;
public ScopeInfo BaseIronSight;
protected ScopeInfo rigoutScopeInfo;
public int GetCurrentAmmo => CurrentAmmo;
public int GetCurrentMaxAmmoCarried => CurrentMaxAmmoCarried;
protected int CurrentAmmo;
protected int CurrentMaxAmmoCarried;
protected float LastFireTime;
protected AnimatorStateInfo GunStateInfo;
protected float EyeOriginFOV;
protected float GunOriginFOV;
protected bool IsAiming;
protected bool IsHoldingTrigger;
private IEnumerator doAimCoroutine;
private Vector3 originalEyePosition;
protected Transform gunCameraTransform;
protected virtual void Awake()
{
CurrentAmmo = AmmoInMag;
CurrentMaxAmmoCarried = MaxAmmoCarried;
GunAnimator = GetComponent<Animator>();
EyeOriginFOV = EyeCamera.fieldOfView;
GunOriginFOV = GunCamera.fieldOfView;
doAimCoroutine = DoAim();
gunCameraTransform = GunCamera.transform;
originalEyePosition = gunCameraTransform.localPosition;
rigoutScopeInfo = BaseIronSight;
}
public void DoAttack()
{
Shooting();
}
protected abstract void Shooting();
protected abstract void Reload();
//protected abstract void Aim();
protected bool IsAllowShooting()
{
return Time.time - LastFireTime > 1 / FireRate;
}
protected Vector3 CalculateSpreadOffset()
{
float tmp_SpreadPercent = SpreadAngle / EyeCamera.fieldOfView;
return tmp_SpreadPercent * UnityEngine.Random.insideUnitCircle;
}
protected IEnumerator CheckReloadAmmoAnimationEnd()
{
while (true)
{
yield return null;
GunStateInfo = GunAnimator.GetCurrentAnimatorStateInfo(2);
if (GunStateInfo.IsTag("ReloadAmmo"))
{
if (GunStateInfo.normalizedTime >= 0.9f)
{
int tmp_NeedAmmoCount = AmmoInMag - CurrentAmmo;
int tmp_RemainingAmmo = CurrentMaxAmmoCarried - tmp_NeedAmmoCount;
if (tmp_RemainingAmmo <= 0)
{
CurrentAmmo += CurrentMaxAmmoCarried;
}
else
{
CurrentAmmo = AmmoInMag;
}
CurrentMaxAmmoCarried = tmp_RemainingAmmo <= 0 ? 0 : tmp_RemainingAmmo;
yield break;
}
}
}
}
protected IEnumerator DoAim()
{
while (true)
{
yield return null;
float tmp_EyeCurrentFOV = 0;
EyeCamera.fieldOfView =
Mathf.SmoothDamp(EyeCamera.fieldOfView,
IsAiming ? rigoutScopeInfo.EyeFov : EyeOriginFOV,
ref tmp_EyeCurrentFOV,
Time.deltaTime * 2);
float tmp_GunCurrentFOV = 0;
GunCamera.fieldOfView =
Mathf.SmoothDamp(GunCamera.fieldOfView,
IsAiming ? rigoutScopeInfo.GunFov : GunOriginFOV,
ref tmp_GunCurrentFOV,
Time.deltaTime * 2);
Vector3 tmp_RefPosition = Vector3.zero;
gunCameraTransform.localPosition = Vector3.SmoothDamp(gunCameraTransform.localPosition,
IsAiming ? rigoutScopeInfo.GunCameraPosition : originalEyePosition,
ref tmp_RefPosition,
Time.deltaTime * 2);
}
}
internal void Aiming(bool _isAiming)
{
IsAiming = _isAiming;
GunAnimator.SetBool("Aim", IsAiming);
if (doAimCoroutine == null)
{
doAimCoroutine = DoAim();
StartCoroutine(doAimCoroutine);
}
else
{
StopCoroutine(doAimCoroutine);
doAimCoroutine = null;
doAimCoroutine = DoAim();
StartCoroutine(doAimCoroutine);
}
}
internal void SetupCarriedScope(ScopeInfo _scopeInfo)
{
rigoutScopeInfo = _scopeInfo;
}
internal void HoldTrigger()
{
DoAttack();
IsHoldingTrigger = true;
}
internal void ReleaseTrigger()
{
IsHoldingTrigger = false;
}
internal void ReloadAmmo()
{
Reload();
}
}
[System.Serializable]
public class ScopeInfo
{
public string ScopeName;
public GameObject ScopeGameObject;
public float EyeFov;
public float GunFov;
public Vector3 GunCameraPosition;
}
}
三、枪械特效配置
首先在Assets里面找到之前以及导入的资源包,将其中的Bullet_GoldFire_Small_MuzzleFlare预制体拖拽到场景中。这个预制体是开火的特效实现。然后将它移动到枪械的Components物体里面作为子物体。
随后将其拖拽到枪口的位置,点击restart,即可看到开火的效果。
此外还需要将它的layer设置为Gun。
在Hierachy面板打开枪,也就是Assault_Rifle_01_FPSController,然后找到Bullet_GoldFire_Small_MuzzleFlare:
在旁边的inspector面板可以对这个开火的特效进行一些设置。
点击AK47,将枪焰效果物体拖到相应的位置,之前编写好的脚本就可以按照程序执行这个开火效果了,这个效果的播放次数和播放时间也将由脚本进行控制。
对于子弹射出的效果,需要将Particle System物体移动到出弹口的位置。然后就会有弹壳弹出的效果。
同样的,这个效果的layer也要设置为gun。其属性也可以在inspector面板进行更改。
后来发现弹壳的朝向是错误的,于是将rotation进行了修改,这样就没有问题了。
四、枪械声音
首先找到AssultRife的节点,然后打开AssultRife脚本,跳转到Fireams枪械脚本,在其中加入两个和枪械声音相关的变量。
public AudioSource FirearmsShootingAudioSource;//射击
public AudioSource FirearmsReloadAudioSource;//换弹夹
新建一个脚本,命名为FirearmsAudioData枪械声音。然后将audio都编写进来。
namespace Scripts.Weapon
{
[CreateAssetMenu(menuName = "FPS/Firearms Audio Data")]
public class FirearmsAudioData : ScriptableObject
{
public AudioClip ShootingAudio;
public AudioClip ReloadLeft;//换子弹的声音
public AudioClip ReloadOutOf;//弹夹打空,换一个全新的弹夹
}
}
[CreateAssetMenu(menuName = “FPS/Firearms Audio Data”)]
这个类继承ScriptableObject。
会发现在create菜单的最上方出现了刚才新建的选项。
新建后,就得到了一个可以播放的声音对象,可以在旁边进行音频的赋值。
然后在Firearms里面新增一个公开的变量。把枪械音源写到里面去,使用public进行修饰,这样才能给它进行赋值。
public FirearmsAudioData FirearmsAudioData;
找到枪械,就可以为它赋上Firearms Audio Data值。
在AssultRife脚本里完成播放开枪音源的逻辑的编写。
FirearmsShootingAudioSource.clip = FirearmsAudioData.ShootingAudio;
FirearmsShootingAudioSource.Play();
同样地,实现换弹夹的声音播放。需要注意的是,要防止换弹夹的声音把开枪的声音掐断。由于是有两种音源,所以要进行一下判断。
protected override void Reload()
{
GunAnimator.SetLayerWeight(2, 1);
GunAnimator.SetTrigger(CurrentAmmo > 0 ? "ReloadLeft" : "ReloadOutOf");
FirearmsReloadAudioSource.clip =
CurrentAmmo > 0
? FirearmsAudioData.ReloadLeft
: FirearmsAudioData.ReloadOutOf;
FirearmsReloadAudioSource.Play();