Unity开发一个FPS游戏之五

news2024/9/20 1:20:55

这个系列的前几篇文章介绍了如何从头开始用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)和手榴弹,以及一个包含了各种道具和交互效果的演示场景。

演示效果可见视频:

如果想了解详细的,欢迎下载并点赞好评,多多支持,感谢! ^_^

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2130287.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

828华为云征文|华为云Flexus X服务器centos挂载新增的磁盘教程

华为云Flexus X服务器推荐公司、企事业单位及生产环境使用&#xff0c;特别注重安全和稳定性 &#x1f680;【828华为云盛典&#xff0c;Flexus X引领算力新纪元&#xff01;】&#x1f680; 在数字化转型的征途中&#xff0c;数据安全是企业最坚实的后盾。华为云Flexus X实例…

中关村科金推出得助音视频鸿蒙SDK,助力金融业务系统鸿蒙化提速

鸿蒙生态大势所趋&#xff0c;各种应用适配加速 近日&#xff0c;华为纯血鸿蒙系统&#xff08;HarmonyOS NEXT&#xff09;再度引发市场高度关注。据媒体消息&#xff0c;鸿蒙NEXT Beta版将在9月24日对Mate 60系列、X5系列、Pura70系列等16款旗舰机型进行推送&#xff0c;这已…

春招审核流程优化:Spring Boot系统设计

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理大学生入学审核系统的相关信息成为必然。开…

Arcgis实现面空间位置从东至西从南至北排序

效果 背景 工作项目中经常会遇到需要对网格进行编号,而编号是有一定原则的,比如空间位置从上到下从左到右,或者其它原则,那么都可以通过下面的方式来实现 1、准备数据 点shp文件,查看初始FID字段标注,目前是一个无序的状态 2、排序 字段选择空间字段,空间排序方法…

业务流程建模(BPM)的重要性及其应用

什么是业务流程建模&#xff08;BPM&#xff09;&#xff1f; 业务流程建模&#xff08;BPM&#xff09;是对企业内各项业务流程进行图形化描述的一种方法。它旨在通过可视化的方式帮助企业理解和分析现有的业务流程&#xff0c;从而发现潜在的问题并进行改进。BPM通常采用流程…

Linux学习-Docker文件系统

Overlayfs Overlayfs 是一种类似 aufs 的一种堆叠文件系统&#xff0c;于 2014 年正式合入 Linux-3.18 主线内核&#xff0c;目前其功能已经基本稳定&#xff08;虽然还存在一些特性尚未实现&#xff09;且被逐渐推广。 Overlayfs 是一种堆叠文件系统&#xff0c;它依赖并建立…

stm32 IIC总线busy解决方法

参考博客&#xff1a; https://blog.csdn.net/PP_hui/article/details/112229696 1------这是IIC的初始化代码&#xff1a; #include "i2c.h"/* USER CODE BEGIN 0 */ // #define __HAL_AFIO_REMAP_I2C1_ENABLE() AFIO_REMAP_ENABLE(AFIO_MAPR_I2C1_REMAP) /* USE…

最新Open-vocabulary方法个人学习小结

Open-vocabulary方法总结 Yolo-world CVPR2024 预训练公式&#xff1a;区域-文本对 传统的目标检测方法&#xff0c;包括yolo系列都是用实例注释进行训练的&#xff0c;它由边界框和类别标签组成。 在本文中&#xff0c;我们将实例注释重新表述为区域-文本对。具体来说&#…

2024 年 GitLab Global DevSecOps 报告解读

近日 GitLab 正式发布了 2024 年 GitLab Global DevSecOps 报告&#xff0c;报告主题为 What’s next in DevSecOps。在全球有超 5000 位 IT 人员参与了该报告的调研&#xff0c;超 70% 为企业管理者&#xff0c;50% 以上的受访者所在企业规模超过 500人。该报告深刻揭示了在 A…

Qt进程通信,不推荐使用QSharedMemory和QLocalSocket,而是推荐ZMQ

一、据一位资深的网友说QLocalSocket有问题&#xff0c;共享内存QSharedMemory也有&#xff0c;比如存在多线程问题&#xff0c;不灵活&#xff0c;丢数据等问题都有&#xff0c;而且还占资源。血的教训。后来换成了zmqprotobuf。ZMQ进程内&#xff0c;进程间&#xff0c;机器间…

重塑科普展厅魅力,以用户体验为核心的策略性规划新探索!

如今&#xff0c;尽管数字技术已在全国范围内得到广泛应用&#xff0c;努力缩小地域间的信息鸿沟&#xff0c;但地域信息差依然是一个不容忽视的存在&#xff0c;他们由于文化背景、教育水平、生活习惯等方面的差异&#xff0c;对科普知识的需求和接受程度也各不相同&#xff0…

深入解析全连接层:PyTorch 中的 nn.Linear、nn.Parameter 及矩阵运算

文章目录 数学概念&#xff08;全连接层&#xff0c;线性层&#xff09;nn.Linear()nn.Parameter()Q1. 为什么 self.weight 的权重矩阵 shape 使用 ( out_features , in_features ) (\text{out\_features}, \text{in\_features}) (out_features,in_features)而不是 ( in_featur…

复现OpenVLA:开源的视觉-语言-动作模型及原理详解

复现OpenVLA&#xff1a;开源的视觉-语言-动作模型及原理详解 1. 摘要2. 引言3. 相关工作4. 模型结构4.1 模视觉-语言模型VLM4.2 训练流程4.3 图像分辨率4.4 微调视觉编码器4.5 训练轮数4.6 学习率4.7 训练细节4.8 参数高效微调 5. 复现5.1 下拉代码5.2 安装环境依赖5.2.1 创建…

什么是科技与艺术相结合的异形创意圆形(饼/盘)LED显示屏

在当今数字化与创意并重的时代&#xff0c;科技与艺术的融合已成为推动社会进步与文化创新的重要力量。其中&#xff0c;晶锐创显异形创意圆形LED显示屏作为这一趋势下的杰出代表&#xff0c;不仅打破了传统显示设备的形态束缚&#xff0c;更以其独特的造型、卓越的显示效果和广…

浏览器百科:网页存储篇-IndexedDB介绍(十)

1.引言 在现代网页开发中&#xff0c;数据存储需求日益增多和复杂&#xff0c;传统的客户端存储技术如localStorage和sessionStorage已难以满足大型数据的存储和管理需求。为了解决这一问题&#xff0c;HTML5 引入了 IndexedDB&#xff0c;在本篇《浏览器百科&#xff1a;网页…

SiC,GaN驱动优选驱动方案SiLM5350系列SiLM5350MDDCM-DG 带米勒钳位Clamp保护功能 单通道隔离栅极驱动器

SiLM5350MDDCM-DG是一款适用于IGBT、MOSFET的单通道 隔离门极驱动器&#xff0c;具有10A拉电流和10A灌电流驱动能 力。提供内部钳位功能&#xff0c;可单独控制 上升时间和下降时间。 在 SOP8 封 装 中 具 有 3000VRMS 隔 离 耐 压 &#xff08; 符 合 UL1577&#xff09;。 与…

Vue-Route4 ts

小满学习视频 Vue-Route 官网 项目的目录结构&#xff1a; 1. Vue-Router的使用 安装Vue-route pnpm add vue-router4创建router文件 /route/index.vue import { createRouter } from "vue-router"; import {createMemoryHistory,createWebHashHistory,create…

C语言 | Leetcode C语言题解之第395题至少有K个重复字符的最长子串

题目&#xff1a; 题解&#xff1a; int longestSubstring(char* s, int k) {int ret 0;int n strlen(s);for (int t 1; t < 26; t) {int l 0, r 0;int cnt[26];memset(cnt, 0, sizeof(cnt));int tot 0;int less 0;while (r < n) {cnt[s[r] - a];if (cnt[s[r] - …

Framework | 在Android中运行时获取顶层Activity并处理业务逻辑

Framework | 在Android中运行时获取顶层Activity并处理业务逻辑 在Android应用的开发中,有时需要获取当前正在运行的顶层Activity,尤其是当应用需要监控特定的页面或执行特殊的业务处理时,例如在截图界面进行操作或在特定的活动页面展示特定的功能。本文将详细介绍如何通过…

中电金信:金融级数字底座“源启”:打造新型数字基础设施 筑牢千行百业数字化转型发展基石

近期&#xff0c;金融级数字底座“源启”登录中国电子《最轻大国重器》融媒体报道。从数字底座到数智底座&#xff0c;从金融行业到千行百业&#xff0c;“源启”用数智化转型的中国电子解决方案&#xff0c;为全球企业转型及安全发展提供强大动能。 立足中国电子科技创新成果&…