概述
之前做过一个小型的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)对象池管理
生成的时候如果池子里有,就从池子里拿,这里用到了队列,代码如下:
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)开镜
主要用于狙击枪,狙击枪射程远、伤害高,一般是右键开镜之后获得一个放大的视野进行瞄准再射击。主要流程如下:
(2)添加关键帧事件
游戏中有很多的动画,我们可以在动画的关键帧上添加脚本。
找到相应的动画片段,选择合适的位置,例如动画开始或者结束的时候,点击Events左侧的图标进行事件添加。然后在动画所属的物体上添加脚本撰写同名方法,就会在动画播放到这一帧的时候启动这个事件了。
武器交互
(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);
}
}