概述

【Unity项目实践】FPS项目复盘及关键技术要点_FPS

之前做过一个小型的FPS项目,基本实现了以下这些功能点:

  • 人物移动:前进后退、鼠标调整视角、右键瞄准;模拟后坐力、初步处理武器穿模的问题。
  • 怪物生成:在地图上放置怪物生成点,实现怪物随机从这些生成点上生成;并通过对象池实现怪物的回收利用。
  • 武器系统:实现了武器切换、换弹,目前的武器包括手枪、狙击枪、刀等;运用了继承,通过继承武器基类提高了开发效率。
  • 武器交互:实现了伤害判定和UI的更新。


人物移动

Unity自带的FirstPersonController可以实现基本的人物移动功能,我们自己撰写脚本的时候只需要补充一些细节。

(1)后坐力

后坐力 = 镜头抖动 + 弹道偏移

这里我们先简单模拟一下镜头抖动,镜头抖动是指开枪之后先左右小幅度抖动+向上移位一段时间,再缓慢复位,代码如下:

IEnumerator ShootRecoil_Camera(float recoil)
{
float xOffset = Random.Range(0.3f, 0.6f) * recoil;//开枪之后会有一个向上抬的效果
float yOffset = Random.Range(-0.15f, 0.15f) * recoil;
firstPersonController.xRotOffset = xOffset;
firstPersonController.yRotOffset = yOffset;
//模拟停留一下之后又往下偏
for (int i=0; i<6; i++)
{
yield return null;
}
firstPersonController.xRotOffset = 0;
firstPersonController.yRotOffset = 0;
}

(2)处理对象穿模

常见的有以下三种处理方式:

  • 添加碰撞体
  • 碰到墙的时候把枪竖起来或者背起来
  • 利用第二个摄像机制造一种没有穿墙的假象

这个项目中使用了第三种,这种方式主要是利用摄像机的层级选择。我们将武器设置成一个单独的层级,在主摄像机中取消勾选这个武器的层级,同时我们在主摄像机的位置再创建第二个摄像机,使其只拍摄武器这个层级,并且将第二个摄像机的层级设置在主摄像机之上。这样就算穿模了,武器的图像还是显示在别的物体之上,视觉上没有穿模。


怪物生成

(1)随机生成

创建一个GameController脚本,用来管理场景中的怪物生成点,生成怪物的时候在这些生成点中随机选择位置。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
public static GameManager Instance;
public Transform[] Points;
void Start()
{
Instance = this;
}

public Vector3 GetPoints()
{
return Points[Random.Range(0, Points.Length)].position;
}
}

(2)对象池管理

生成的时候如果池子里有,就从池子里拿,这里用到了队列,代码如下:

  public List<ZombieController> zombies;//当前场景中的僵尸  
private Queue<ZombieController> zombiePool = new Queue<ZombieController>();//备用僵尸,这里用到了对象池
public Transform Pool;

void Start()
{
StartCoroutine(CheckZombie());
}

// 检查僵尸
IEnumerator CheckZombie()
{
while (true)
{
yield return new WaitForSeconds(1);
// 僵尸数量不够,产生僵尸
if (zombies.Count<3)
{
// 池子里面有,从池子拿
if (zombiePool.Count>0)
{
ZombieController zb = zombiePool.Dequeue();//出队
zb.transform.SetParent(transform);
zb.transform.position = GameManager.Instance.GetPoints();
zombies.Add(zb);//添加到当前场景中的僵尸中
zb.gameObject.SetActive(true);
zb.Init();
yield return new WaitForSeconds(2);
}
// 池子没有,就实例化
else
{
GameObject zb = Instantiate(prefab_Zombie, GameManager.Instance.GetPoints(), Quaternion.identity, transform);
zombies.Add(zb.GetComponent<ZombieController>());
}
}
}
}

怪物死亡的时候就加入到对象池中:

public void ZombieDead(ZombieController zombie)
{
zombies.Remove(zombie);
zombiePool.Enqueue(zombie);//入队
zombie.gameObject.SetActive(false);
zombie.transform.SetParent(Pool);
}


武器系统

武器系统的核心点在于:为了提升开发效率,我们使用一个武器基类来实现所有的武器共性的逻辑。

比如我们会有不同的枪存在,那么枪的基类中主要包括如下部分:

  • 数值:包括最大子弹数、当前子弹数、备用子弹数、伤害数值、后坐力数值等
  • 初始化:进入游戏的时候需要先给武器初始赋值
  • 拿到武器:初始化数值、播放拿起武器的动画、播放音效等
  • 退出武器:播放退出动画、音效等
  • 效果:包含音效和视觉特效的播放时间点
  • 开枪效果:一些共性的开枪效果,比如弹坑、音效、射线检测等

下面详细记录一下一些细节:开镜+关键帧事件。

(1)开镜

主要用于狙击枪,狙击枪射程远、伤害高,一般是右键开镜之后获得一个放大的视野进行瞄准再射击。主要流程如下:

【Unity项目实践】FPS项目复盘及关键技术要点_对象池_02

(2)添加关键帧事件

游戏中有很多的动画,我们可以在动画的关键帧上添加脚本。

找到相应的动画片段,选择合适的位置,例如动画开始或者结束的时候,点击Events左侧的图标进行事件添加。然后在动画所属的物体上添加脚本撰写同名方法,就会在动画播放到这一帧的时候启动这个事件了。

【Unity项目实践】FPS项目复盘及关键技术要点_FPS_03


武器交互

(1)射线检测

运用了Unity中的Raycast这个API,文档内容如下:

​https://docs.unity3d.com/ScriptReference/Physics.Raycast.html​

使用射线检测的步骤:

  • 从摄像机发射一条射线
  • 如果可以穿墙,返回一个数组;如果不能穿墙,返回碰到的第一个对象
  • 判断打到的物体是不是可以打的怪物
  • 如果是,那么实例化一个命中怪物的效果,并且做数值更新;如果不是,实例化一个普通的弹坑

第一段代码:射线检测

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);//从主摄像机发射出一条射线

if (canThroughWall)
{
//Physics.RaycastNonAlloc(ray, hitInfos, 1500f);
RaycastHit[] raycastHits = Physics.RaycastAll(ray, 1500f);
for (int i = 0; i < raycastHits.Length; i++)
{
HitGameObject(raycastHits[i]);
}
}
else
{
if (Physics.Raycast(ray, out RaycastHit hitInfo, 1500f))
{
HitGameObject(hitInfo);
}
}

第二段代码:伤害判定

private void HitGameObject(RaycastHit hitInfo)
{
//判断是不是打到了僵尸
if(hitInfo.collider.gameObject.CompareTag("Zombie"))
{
//实例化一个命中效果
GameObject go = Instantiate(prefab_BulletEF[1], hitInfo.point, Quaternion.identity);
go.transform.LookAt(Camera.main.transform);
//僵尸的逻辑
ZombieController zombie = hitInfo.collider.gameObject.GetComponent<ZombieController>();
if (zombie == null) zombie = hitInfo.collider.gameObject.GetComponent<ZombieController>();
zombie.Hurt(attackValue);
}
else if (hitInfo.collider.gameObject!= player.gameObject)
{
GameObject go = Instantiate(prefab_BulletEF[0], hitInfo.point, Quaternion.identity);
go.transform.LookAt(Camera.main.transform);
}
}