这个系列的前几篇文章介绍了如何从头开始用Unity开发一个FPS游戏,感兴趣的朋友可以回顾一下。这个系列的文章如下:
Unity开发一个FPS游戏_unity 模仿开发fps 游戏-CSDN博客
Unity开发一个FPS游戏之二_unity 模仿开发fps 游戏-CSDN博客
Unity开发一个FPS游戏之三-CSDN博客
Unity开发一个FPS游戏之四_unity fps-CSDN博客
在这篇文章中,我将计划完善以下几方面:
1. 增加一把狙击枪,实现通过瞄准镜来进行放大瞄准。
2. 新增手榴弹,实现投掷手榴弹的爆炸效果。
3. 重新调整瞄准准星
4. 对模型的材质进行优化。
1. 增加狙击枪武器
模型与动画
在Sketchfab网站上有很多制作精美的狙击枪,这里我选择的是https://skfb.ly/onBJQ,这是英国出品的著名的AWP狙击步枪。下载之后导入到我们之前的Blender文件中,把其中的枪本体,弹匣,枪机分别绑定到对应的武器骨骼,然后调整手臂骨骼来适配武器,如下图
这里需要注意的是,因为AWP是栓动步枪,即每打一枪需要旋转并拉动枪机,因此和之前其他自动步枪相比,还需增加多一个旋转的动作,我把对应的枪机分成两部分,为此要增加多一个枪机的骨骼。
对应狙击步枪的动画制作,和其他枪械的制作过程类似,这里就不再重复,可以查看我之前的文章。
最后把制作好的狙击枪的模型和动画导出为FBX文件,导入到Unity项目中。
瞄准镜效果
下面我们要制作瞄准镜的瞄准效果,当从瞄准镜瞄准时,应能看到放大的瞄准图像。最简单的一个思路时直接调整主相机的FOV,把整个画面拉近,但是这样做不符合实际情况,因为我们只希望瞄准镜内的图像放大。另一个思路是增加多一个相机,把这个相机放在主相机前面,调整其FOV,把放大后的图像投到瞄准镜内。这种方式更好,但是性能上会消耗较大。下面是这种思路的实现方式。
首先在weapon.cs脚本里面增加一个属性hasScope,表示这个武器是否带瞄准镜,在Awake方法中增加以下代码:
[Header("Scope")]
public bool hasScope = false;
public Material ScopeMat;
void Awake() {
...
if (hasScope) {
_renderTex = new RenderTexture(1024, 1024, 16, RenderTextureFormat.ARGB32);
_scopeCamObj = new GameObject("ScopeCamera");
_scopeCamObj.transform.SetParent(transform.Find("pose_controller/weapon"));
_scopeCamObj.transform.localPosition = transform.Find("pose_controller/weapon/Aimpoint").localPosition;
_scopeCamObj.transform.localRotation = transform.Find("pose_controller/weapon/Aimpoint").localRotation;
_scopeCam = _scopeCamObj.AddComponent<Camera>();
_scopeCam.fieldOfView = 10;
_scopeCam.targetTexture = _renderTex;
ScopeMat.SetTexture("_MainTex", _renderTex);
}
}
在代码中,如果判断武器是带瞄准镜的,我们将新建一个渲染材质和一个相机,把相机挂载到武器的相关位置,然后设置这个相机的FOV,使得其能获得放大后的图像,然后把相机拍摄的图像赋予给这个材质。最后把这个材质设置为ScopeMat这个材质的主材质。ScopeMat这个材质将作为瞄准镜镜片的材质。
下面我们新建一个材质,例如命名为ScopeMergeMat,定义一个新的Shader,把瞄准镜的十字图像和相机拍摄的图像材质融合起来。这里我是采用URP的Shader Graph Shader来定义一个新的Shader,如下图所示:
可以看到这个Shader定义了两个输入,MainTex和SecondTex,把他们简单的相乘即可。 在SecondTex里面,我找了一张带有瞄准镜十字线的图片作为输入。然后MainTex就用以上提到的相机拍摄的相片作为输入。
新建一个名为ScopeMergeMat的材质,其设置如上图的右半部分所示。
最后就是在我们导入的狙击枪的模型中,新建一个Cylinder的GameObject,调整其大小使得能跟瞄准镜的镜片相匹配,然后把刚才创建的ScopeMergeMat材质赋予给这个GameObject即可。这样我们就能把放大后的图像投射到瞄准镜上了。
子弹抛壳效果
当狙击枪射击后,需要玩家手动拉枪栓,这时子弹会抛出。为此我们可以在动画中增加一个事件,当播放到拉动枪栓时,生成一个子弹并抛出,对weapon.cs脚本做如以下改动:
[SerializeField] GameObject casingBoltPrefab;
public int fireCasingEventFrame = 0;
void Awake() {
...
foreach (AnimationClip clip in _animator.runtimeAnimatorController.animationClips) {
if (clip.name.Contains("Shoot") && fireCasingEventFrame != 0 ) {
AnimationEvent animationFireCasingEvent = new AnimationEvent
{
time = fireCasingEventFrame/clip.frameRate,
functionName = "CasingHandler"
};
clip.AddEvent(animationFireCasingEvent);
}
}
}
private void CasingHandler() {
_casing = Instantiate(casingBoltPrefab, _eject.position, _eject.rotation);
_casing.transform.localScale = new Vector3(2, 2, 2);
}
以下视频是狙击枪的效果:
2. 增加手榴弹
模型与动画
同样是在Sketchfab上找到一个模型,https://skfb.ly/6Rxtp。导入Blender之后进行相应的骨骼绑定,并制作对应的动画效果。投掷手榴弹的动画比开枪的动画要简单一些,不用制作最复杂的重新装弹的动画。在原模型中没有把手榴弹的拉环单独出来,为此需要在Blender的编辑模式里面编辑,选择拉环相关的面,按P进行分离后保存为一个单独的物体。最后和手臂进行结合,效果如下图:
最后把模型导出为FBX文件,再导入到Unity项目即可。
投掷效果
和之前做的枪支开枪的效果不同,手榴弹是整个扔出去的。因此也是要采用之前的给动画增加事件的方式,当投弹的动画播放到手榴弹要出手时,需要新建一个手榴弹的GameObject,然后根据测量到的目标的距离,给新建的手榴弹GameObject施加一个作用力。
改造一下Weapon.cs文件,增加以下代码:
[SerializeField] GameObject grenadePrefab;
public bool gunType = false;
public int throwGrenadeEventFrame = 0;
public Vector3 targetPos;
private Transform _grenadeTrans;
private bool _registerOnce = false;
void Awake() {
...
foreach (AnimationClip clip in _animator.runtimeAnimatorController.animationClips) {
...
if (clip.name.Contains("Shoot") && !gunType && throwGrenadeEventFrame != 0 && !_registerOnce) {
AnimationEvent animationGrenadeEvent = new AnimationEvent
{
time = throwGrenadeEventFrame/clip.frameRate,
functionName = "GrenadeHandler"
};
clip.AddEvent(animationGrenadeEvent);
AnimationEvent animationEndEvent = new AnimationEvent
{
time = clip.length - 0.1f,
functionName = "EndAnimationHandler",
stringParameter = clip.name
};
clip.AddEvent(animationEndEvent);
_registerOnce = true;
}
}
if (!gunType) {
_grenadeTrans = transform.Find("pose_controller/weapon/base/Gran_base");
}
}
private void GrenadeHandler() {
_grenade = Instantiate(grenadePrefab, _grenadeTrans.position, _grenadeTrans.rotation);
_grenade.transform.localScale = new Vector3(30, 30, 30);
_grenadeTrans.GameObject().SetActive(false);
// Base on the target position to calculate the force.
Vector3 direction = (Vector3.up + (targetPos - _grenadeTrans.position).normalized).normalized;
float distance = (targetPos - _grenadeTrans.position).magnitude;
float force = distance * 0.6f;
if (force > 50f) {
force = 50f;
}
_grenade.GetComponent<Rigidbody>().AddForce(direction * force, ForceMode.Impulse);
}
在以上代码中,当播放投掷手榴弹的Shoot动画时,如果播放到手榴弹要出手的那一帧,就会触发注册的GrenadeHandler函数,这时将新建一个手榴弹GameObject,然后设置其位置。之后就要根据目标的位置来计算要施加的力的大小和方向。这里我简单的设置方向为手榴弹指向目标方向的往Y轴倾斜45度,可以理解为水平方向往上倾斜45度,这样手榴弹就会以一个抛物线来飞行。然后力量的大小根据与目标的距离来决定。最后给这个手榴弹的刚体添加一个即时类型的力即可。多说一句,ForceMode.Impulse代表一个即时类型的力,例如我施加一个10牛顿的力,在这种类型下,F=MV,假设物体质量为1Kg,那么可知物体的速度为10米/秒。Unity的物理引擎会帮我们处理手榴弹之后的飞行路线。
修改以下PlayerController.cs脚本文件,当玩家按下鼠标左键,触发开火时,我们需要获取当前瞄准对象的位置,传递给Weapon.cs,以下是相关改动:
private void Update() {
...
if (_input.shoot) {
...
if (_weaponType == WeaponType.Grenade) {
Ray ray = new Ray(_mainCamera.transform.position, _mainCamera.transform.forward);
RaycastHit hit;
Vector3 targetPosition;
if (Physics.SphereCast(ray, 0.5f, out hit, maxThrowGrenadeDistance)) {
targetPosition = hit.collider.gameObject.transform.position;
} else {
targetPosition = _mainCamera.transform.position + _mainCamera.transform.forward * maxThrowGrenadeDistance;
}
StartCoroutine(ThrowGrenade(targetPosition));
}
}
}
private IEnumerator ThrowGrenade(Vector3 targetPos) {
_weaponBehavior.targetPos = targetPos;
_playerAnimator.SetTrigger("Shoot");
_weaponAnimator.SetTrigger("Shoot");
yield return new WaitUntil(()=>_weaponBehavior.ClipFinishedName.Contains("Shoot"));
_weaponType = WeaponType.Gun;
// Restore the previous weapon
if (_weaponIndex == 0) {
_weaponIndex = weaponPrefabs.Count - 1;
} else {
_weaponIndex -= 1;
}
StartCoroutine(SwitchWeapon());
}
以上代码中,当判断开火触发,并且当前的武器类型是手榴弹时,通过从当前摄像机发出的射线检测来获取目标的位置,并传递给weapon.cs对象,当手榴弹投掷完成后,切换回之前的枪支武器。
爆炸效果
通常手榴弹都是延时爆炸,当拉开拉环后,过3-4秒即爆炸。在之前的系列中,我已实现了当子弹射击到油桶时触发油桶爆炸,我们按照这个思路稍加修改即可。
首先是制作手榴弹爆炸后分裂为几个部分的模型,这个同样可以利用Blender的Cell Fracture插件来完成,与之前油桶模型的制作类似。
修改之前的debris代码,改动如下:
public float destroyAfter = 2f;
void Start()
{
if (destroyAfter > 0) {
StartCoroutine (DestroyAfter ());
}
}
private IEnumerator DestroyAfter ()
{
//Wait for set amount of time
yield return new WaitForSeconds (destroyAfter);
OnDestroy ();
}
public void OnDestroy()
{
GameObject o = Instantiate(Explosive_debris, transform.position, transform.rotation);
GameObject explode = Instantiate(explodeEffect, transform.position, transform.rotation);
Destroy(gameObject);
}
把这个脚本添加到投掷手榴弹新创建的GameObject上。
以下视频是投掷手榴弹的效果演示:
3. 调整瞄准准星
在之前的文章中,我是把瞄准的准星直接放置在模型中的,但是这会有一些问题,由于模型在游戏中不是放置在正中间,因此准星和实际瞄准的物体会有偏差,无法实现精准的瞄准。另外在其他一些流行的FPS游戏,例如使命召唤中,我们可以看到瞄准准星是会随着玩家的动作而发生变化的,例如在静止时准星会更精准,当玩家运动时,准星的十字会扩大,导致准度下降。当玩家重装子弹时准星会消失等等。因此我也仿照使命召唤的设计来重新调整瞄准准星。
具体做法是,在2D UI的GameScreen这个Prefab里面,新建一个名为Crosshair的Empty GameObject,然后在其下新建四个GameObject,分别对应准星十字的上下左右4个部分,每个部分都是一个白色的小方块,如下图所示:
这个Crosshair准星放置在屏幕的正中间。
给这个GameScreen增加一个名为Crosshair.cs的脚本文件,代码如下:
public class Crosshair : MonoBehaviour
{
[SerializeField] GameObject crosshair;
public float smoothness = 10f;
private RectTransform _rectCrosshair;
private Vector2 _rectCrosshairSize;
private PlayerController.PlayerStatus _status;
// Start is called before the first frame update
void Start()
{
_rectCrosshair = crosshair.GetComponent<RectTransform>();
_rectCrosshairSize = _rectCrosshair.sizeDelta;
}
// Update is called once per frame
void Update()
{
switch (_status) {
case PlayerController.PlayerStatus.Idle:
crosshair.SetActive(true);
ExpandCrossUpdate(0.5f);
break;
case PlayerController.PlayerStatus.Walk:
crosshair.SetActive(true);
ExpandCrossUpdate(1.0f);
break;
case PlayerController.PlayerStatus.Shoot:
crosshair.SetActive(true);
ExpandCrossUpdate(0.5f);
break;
case PlayerController.PlayerStatus.Switch:
crosshair.SetActive(true);
ExpandCrossUpdate(0.5f);
break;
default:
crosshair.SetActive(false);
break;
}
}
public void SetStatus(PlayerController.PlayerStatus status) {
_status = status;
}
private void ExpandCrossUpdate(float expandDegree)
{
Vector2 targetSize = _rectCrosshairSize * expandDegree;
_rectCrosshair.sizeDelta = Vector2.Lerp(_rectCrosshair.sizeDelta, targetSize, Time.deltaTime * smoothness);
}
}
这个代码的作用是,根据当前玩家的状态,来调整准星的大小以及是否可见。
4. 代码及资源
最后我把所有的代码和资源都发布到Unity商店了FPS basic package | Systems | Unity Asset Store,这些资源包括了5种不同的现代武器枪械(Adaptive combat rifle, AR 15, HK 416, AK74, AWP)和手榴弹,以及一个包含了各种道具和交互效果的演示场景。
演示效果可见视频:
如果想了解详细的,欢迎下载并点赞好评,多多支持,感谢! ^_^