前言
在第一人称射击(FPS)游戏中,控制枪械的后坐力是为了增加游戏的真实性和挑战性。开枪后的后坐力通常会导致玩家的视角和枪口偏移,影响接下来的射击精准度。以下是一些常见的方法来模拟和控制后坐力:
- 视角偏移:当玩家开枪时,可以通过编程让玩家的视角向上或者侧向偏移,模拟因后坐力导致的枪口跳动。这种偏移通常是瞬间的,并且随后会有一个恢复过程,视角逐渐回到原位。实现步骤:
- 监听射击事件。
- 在射击时,立即调整玩家的摄像机旋转角度,向上偏移一定的角度(模拟垂直后坐力)。
- 根据需要,也可以添加水平方向的随机偏移(模拟水平后坐力)。
- 通过插值函数(如Lerp或Slerp)逐渐将摄像机旋转角度恢复到射击前的状态。
- 枪械动画:可以为枪械创建后坐力动画,在射击时播放这个动画。动画可以在3D建模软件中预先制作好,也可以通过程序动态生成。实现步骤:
- 设计并制作后坐力动画。
- 射击时触发动画的播放。
- 动画结束后,可以让枪械逐渐回到初始位置,或者在多次连续射击中保持一定的偏移量。
- 枪械物理模拟:使用物理引擎来模拟枪械的后坐力效果。这种方法可以使得后坐力更加自然和可预测。实现步骤:
- 在枪械模型上附加一个物理组件(如刚体)。
- 射击时,对枪械施加一个向后的力或冲量。
- 利用物理引擎来处理这个力的效果,使得枪械产生后退和上抬的动作。
结合这些方法,开发者可以创造出符合游戏设计需求的后坐力效果,使得射击体验既具有挑战性又富有乐趣。在实际应用中,通常会根据不同武器的特性来调整后坐力的大小和表现方式,以便玩家可以通过技能和习惯来掌握每种武器的射击特点。
本文主要是探究第一种办法,因为枪械的射击通常我们会选择射线投射的方式实现,要实现后座力效果和第一种最匹配。
不加后座力效果
射击的具体实现可以看我之前的文章:一个通用的FPS枪支不同武器射击控制脚本
简单添加后座力
我们先做摄像机给摄像机添加一个父类,实现后座力的原理就是控制摄像机这个父类的xy旋转偏移值
public class GunRecoil : MonoBehaviour { private Vector2 recoil; // 后坐力向量,存储X和Y方向上的偏移量 public float addSpeed = 0.1f;// 后坐力增加速度 public float subSpeed = 10f;// 后坐力减少速度 void Update() { recoil.x = Mathf.MoveTowards(recoil.x, 0, subSpeed * Time.deltaTime); recoil.y = Mathf.MoveTowards(recoil.y, 0, subSpeed * Time.deltaTime); // 将recoil.y应用到物体的X轴旋转角度,将recoil.x应用到物体的Y轴旋转角度 transform.localEulerAngles = new Vector3(-recoil.y, recoil.x, 0); //测试 if (Input.GetKey(KeyCode.Mouse0)) { AddRecoil(); } } // 添加后坐力 public void AddRecoil() { recoil.x += Random.Range(-1, 1) * addSpeed; recoil.y += addSpeed; } }
效果
限制后座力
我们想要更加精准,可以修改addSpeed和subSpeed为Vector2值的,当然我们不希望后座力导致视角一直往上抬,需要限制一个值
private Vector2 recoil; // 后坐力向量,存储X和Y方向上的偏移量 public Vector2 addSpeed = new Vector2(0.5f, 0.75f); // 后坐力增加速度 public Vector2 subSpeed = new Vector2(3f, 5f); // 后坐力减少速度 public Vector2 maxRecoil = new Vector2(1, 5); // 后坐力的最大值 void Update() { // 使用Mathf.MoveTowards方法逐渐将recoil.x和recoil.y减少到0 // subSpeed.x和subSpeed.y分别表示在每秒内减少的量,乘以Time.deltaTime可以使速度与帧率无关 recoil.x = Mathf.MoveTowards(recoil.x, 0, subSpeed.x * Time.deltaTime); recoil.y = Mathf.MoveTowards(recoil.y, 0, subSpeed.y * Time.deltaTime); // 将recoil.y应用到物体的X轴旋转角度,将recoil.x应用到物体的Y轴旋转角度 transform.localEulerAngles = new Vector3(-recoil.y, recoil.x, 0); //测试 if (Input.GetKey(KeyCode.Mouse0)) { AddRecoil(); } } // 添加后坐力 public void AddRecoil() { // // 通过随机取值范围[-1, 1]来增加recoil.x的值,并限制在-maxRecoil.x和maxRecoil.x之间 recoil.x = Mathf.Clamp(recoil.x + Random.Range(-1, 1) * addSpeed.x, -maxRecoil.x, maxRecoil.x); // // 增加recoil.y的值,并限制在0和maxRecoil.y之间 recoil.y = Mathf.Clamp(recoil.y + addSpeed.y, 0, maxRecoil.y); }
效果
子弹落点控制
修改GunRecoil
public static GunRecoil Instance; private void Awake() { Instance = this; } public void AddRecoil(int xDir) { // // 通过随机取值范围[-1, 1]来增加recoil.x的值,并限制在-maxRecoil.x和maxRecoil.x之间 recoil.x = Mathf.Clamp(recoil.x + xDir * addSpeed.x, -maxRecoil.x, maxRecoil.x); // // 增加recoil.y的值,并限制在0和maxRecoil.y之间,纵向后座力一般只有往上的 recoil.y = Mathf.Clamp(recoil.y + addSpeed.y, 0, maxRecoil.y); } //。。。 [System.Serializable] public struct AmmoPosData { public Data[] datas; // 子弹数量对应的数据 // 根据子弹数量获取子弹偏移方向 public int GetDir(int ammoCount) { int maxId = datas.Length - 1; int nextId = 0; Data dt; // 在数据列表中查找匹配的数据 do { dt = datas[nextId]; nextId++; } while (nextId <= maxId && ammoCount > dt.ammo); // 根据随机数确定偏移方向 float random = Random.Range(0, 1f); if (random < dt.left) { return -1; // 左偏移 } else if (random < dt.left + dt.right) { return 1; // 右偏移 } return 0; // 不偏移 } [System.Serializable] public struct Data { public int ammo; // 子弹数量 public float left; // 左偏移概率 public float right; // 右偏移概率 } }
修改GunSystem调用,及发射子弹脚本
private int shootAmmo;//射击第几颗子弹 public AmmoPosData ammoData; private void Update(){ // 射击 if (readyToShoot && shooting && !reloading && bulletsLeft > 0) { bulletsShot = bulletsPerTap; Shoot();//射击脚本 } if (!shooting) shootAmmo = 0; } private void Shoot() { shootAmmo++; GunRecoil.Instance.AddRecoil(ammoData.GetDir(shootAmmo)); //。。。 }
1. 如果我们想要一个七字型
的弹道,那就先让前四发子弹往右,后面有十发子弹往左,最后的区间就五五开
效果
2. 如果要T形的话可以直接修改配置即可
效果
镜像弹道
修改GunRecoil
//。。。 [System.Serializable] public struct AmmoPosData { public float mirrorRate;//镜像概率 public Data[] datas; // 子弹数量对应的数据 // 根据子弹数量获取子弹偏移方向,和是否镜像 public int GetDir(int ammoCount, bool mirror) { int maxId = datas.Length - 1; int nextId = 0; Data dt; // 在数据列表中查找匹配的数据 do { dt = datas[nextId]; nextId++; } while (nextId <= maxId && ammoCount > dt.ammo); // 根据随机数确定偏移方向 float random = Random.Range(0, 1f); if (random < dt.left) { return mirror ? 1 : -1; // 左偏移 } else if (random < dt.left + dt.right) { return mirror ? -1 : 1; // 右偏移 } return 0; // 不偏移 } //按概率判断是否镜像 public bool GetMirror() { if(Random.Range(0, 1f) < mirrorRate) return true; return false; } [System.Serializable] public struct Data { public int ammo; // 子弹数量 public float left; // 左偏移概率 public float right; // 右偏移概率 } }
修改GunSystem调用,及发射子弹脚本
bool isMirror; private void Update(){ // 射击 if (readyToShoot && shooting && !reloading && bulletsLeft > 0) { bulletsShot = bulletsPerTap; Shoot();//射击脚本 } if (!shooting) shootAmmo = 0; } private void Shoot() { shootAmmo++; //一般在第一发子弹决定弹道的镜像 if(shootAmmo == 1)isMirror = ammoData.GetMirror(); GunRecoil.Instance.AddRecoil(ammoData.GetDir(shootAmmo, isMirror)); //。。。 }
配置镜像概率为0.5
测试可以看到弹道有一半的概率变成镜像
添加散射
现在的弹道太规律了,子弹是指哪里就打哪里,然而实际游戏中子弹可能会根据落点区间的大小基于瞄准射线进行一定的偏移,准星的大小就侧面体现了落点区间的大小
其实就是定义射击时的散布度
[Tooltip("射击时的散布度")] public float spread; private void Shoot() { shootAmmo++; //一般在第一发子弹决定弹道的镜像 if(shootAmmo == 1)isMirror = ammoData.GetMirror(); GunRecoil.Instance.AddRecoil(ammoData.GetDir(shootAmmo, isMirror)); // 散布 float x = Random.Range(-spread, spread); float y = Random.Range(-spread, spread); // 计算带有散布的射击方向 Vector3 direction = fpsCam.transform.forward + new Vector3(x, y, 0); // 射线检测 if (Physics.Raycast(fpsCam.transform.position, direction, out RaycastHit rayHit, range)) { if (rayHit.collider.CompareTag("Wall")) { Debug.Log("击中墙壁"); // 击中敌人特效 var res = Instantiate(hitSpecialEffectsWall, rayHit.point, Quaternion.Euler(0, 90, 0)); res.transform.parent = rayHit.transform;//设置父类 } } }
完整代码
//后座力 public class GunRecoil : MonoBehaviour { public static GunRecoil Instance; private void Awake() { Instance = this; } private Vector2 recoil; // 后坐力向量,存储X和Y方向上的偏移量 public Vector2 addSpeed = new Vector2(0.5f, 0.75f); // 后坐力增加速度 public Vector2 subSpeed = new Vector2(3f, 5f); // 后坐力减少速度 public Vector2 maxRecoil = new Vector2(1, 5); // 后坐力的最大值 void Update() { // 使用Mathf.MoveTowards方法逐渐将recoil.x和recoil.y减少到0 // subSpeed.x和subSpeed.y分别表示在每秒内减少的量,乘以Time.deltaTime可以使速度与帧率无关 recoil.x = Mathf.MoveTowards(recoil.x, 0, subSpeed.x * Time.deltaTime); recoil.y = Mathf.MoveTowards(recoil.y, 0, subSpeed.y * Time.deltaTime); // 将recoil.y应用到物体的X轴旋转角度,将recoil.x应用到物体的Y轴旋转角度 transform.localEulerAngles = new Vector3(-recoil.y, recoil.x, 0); } // 添加后坐力 public void AddRecoil(int xDir) { // // 通过随机取值范围[-1, 1]来增加recoil.x的值,并限制在-maxRecoil.x和maxRecoil.x之间 recoil.x = Mathf.Clamp(recoil.x + xDir * addSpeed.x, -maxRecoil.x, maxRecoil.x); // // 增加recoil.y的值,并限制在0和maxRecoil.y之间,纵向后座力一般只有往上的 recoil.y = Mathf.Clamp(recoil.y + addSpeed.y, 0, maxRecoil.y); } } [System.Serializable] public struct AmmoPosData { public float mirrorRate;//镜像概率 public Data[] datas; // 子弹数量对应的数据 // 根据子弹数量获取子弹偏移方向,和是否镜像 public int GetDir(int ammoCount, bool mirror) { int maxId = datas.Length - 1; int nextId = 0; Data dt; // 在数据列表中查找匹配的数据 do { dt = datas[nextId]; nextId++; } while (nextId <= maxId && ammoCount > dt.ammo); // 根据随机数确定偏移方向 float random = Random.Range(0, 1f); if (random < dt.left) { return mirror ? 1 : -1; // 左偏移 } else if (random < dt.left + dt.right) { return mirror ? -1 : 1; // 右偏移 } return 0; // 不偏移 } //按概率判断是否镜像 public bool GetMirror() { if(Random.Range(0, 1f) < mirrorRate) return true; return false; } [System.Serializable] public struct Data { public int ammo; // 子弹数量 public float left; // 左偏移概率 public float right; // 右偏移概率 } }