Unity开发一个单人FPS游戏的教程总结

news2025/1/27 23:19:41

 这个系列的前几篇文章介绍了如何从头开始用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/2283353.html

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

相关文章

论文速读|Is Cosine-Similarity of Embeddings Really About Similarity?WWW24

论文地址&#xff1a; https://arxiv.org/abs/2403.05440 https://dl.acm.org/doi/abs/10.1145/3589335.3651526 bib引用&#xff1a; inproceedings{Steck_2024, series{WWW ’24},title{Is Cosine-Similarity of Embeddings Really About Similarity?},url{http://dx.doi.o…

71.在 Vue 3 中使用 OpenLayers 实现按住 Shift 拖拽、旋转和缩放效果

前言 在前端开发中&#xff0c;地图功能是一个常见的需求。OpenLayers 是一个强大的开源地图库&#xff0c;支持多种地图源和交互操作。本文将介绍如何在 Vue 3 中集成 OpenLayers&#xff0c;并实现按住 Shift 键拖拽、旋转和缩放地图的效果。 实现效果 按住 Shift 键&#…

PyQt6医疗多模态大语言模型(MLLM)实用系统框架构建初探(上.文章部分)

一、引言 1.1 研究背景与意义 在数字化时代,医疗行业正经历着深刻的变革,智能化技术的应用为其带来了前所未有的发展机遇。随着医疗数据的指数级增长,传统的医疗诊断和治疗方式逐渐难以满足现代医疗的需求。据统计,全球医疗数据量预计每年以 48% 的速度增长,到 2025 年将…

250125-package

1. 定义 包就是文件夹&#xff0c;作用是在大型项目中&#xff0c;避免不同人的编写的java文件出现同名进而导致报错&#xff1b;想象一个场景&#xff0c;在一个根目录中&#xff0c;每一个人都有自己的一个java文件夹&#xff0c;他可以将自己编写的文件放在该文件夹里&…

FastExcel的使用

前言 FastExcel 是一款基于 Java 的开源库&#xff0c;旨在提供快速、简洁且能解决大文件内存溢出问题的 Excel 处理工具。它兼容 EasyExcel&#xff0c;提供性能优化、bug 修复&#xff0c;并新增了如读取指定行数和将 Excel 转换为 PDF 的功能。 FastExcel 的主要功能 高性…

Redis实战(黑马点评)——关于缓存(缓存更新策略、缓存穿透、缓存雪崩、缓存击穿、Redis工具)

redis实现查询缓存的业务逻辑 service层实现 Overridepublic Result queryById(Long id) {String key CACHE_SHOP_KEY id;// 现查询redis内有没有数据String shopJson (String) redisTemplate.opsForValue().get(key);if(StrUtil.isNotBlank(shopJson)){ // 如果redis的数…

python3+TensorFlow 2.x(三)手写数字识别

目录 代码实现 模型解析&#xff1a; 1、加载 MNIST 数据集&#xff1a; 2、数据预处理&#xff1a; 3、构建神经网络模型&#xff1a; 4、编译模型&#xff1a; 5、训练模型&#xff1a; 6、评估模型&#xff1a; 7、预测和可视化结果&#xff1a; 输出结果&#xff…

基础项目——扫雷(c++)

目录 前言一、环境配置二、基础框架三、关闭事件四、资源加载五、初始地图六、常量定义七、地图随机八、点击排雷九、格子类化十、 地图类化十一、 接口优化十二、 文件拆分十三、游戏重开 前言 各位小伙伴们&#xff0c;这期我们一起学习出贪吃蛇以外另一个基础的项目——扫雷…

[操作系统] 深入进程地址空间

程序地址空间回顾 在C语言学习的时&#xff0c;对程序的函数、变量、代码等数据的存储有一个大致的轮廓。在语言层面上存储的地方叫做程序地址空间&#xff0c;不同类型的数据有着不同的存储地址。 下图为程序地址空间的存储分布和和特性&#xff1a; 使用以下代码来验证一下…

OpenCV:图像处理中的低通滤波

目录 简述 什么是低通滤波&#xff1f; 各种滤波器简介与实现 方盒滤波 均值滤波 中值滤波 高斯滤波 双边滤波 各种滤波的对比与应用场景 相关阅读 OpenCV基础&#xff1a;图像变换-CSDN博客 OpenCV&#xff1a;图像滤波、卷积与卷积核-CSDN博客 简述 低通滤波是一…

32、【OS】【Nuttx】OSTest分析(1):stdio测试(二)

背景 接上篇wiki 31、【OS】【Nuttx】OSTest分析&#xff08;1&#xff09;&#xff1a;stdio测试&#xff08;一&#xff09; 继续stdio测试的分析&#xff0c;上篇讲到标准IO端口初始化&#xff0c;单从测试内容来说其实很简单&#xff0c;没啥可分析的&#xff0c;但这几篇…

OpenAI掀桌子!免费版ChatGPT,提供o3-mini模型!

逆天免费用 今天凌晨&#xff0c;OpenAI联合创始人兼首席执行官Sam Altman宣布了一个大消息——免费版ChatGPT&#xff0c;将提供o3-mini模型&#xff01; 网页们纷纷不淡定了 看来OpenAI&#xff0c;这o3-mini还没正式上线呢&#xff0c;就免费开放使用了。 不过还是要感谢…

redis离线安装部署详解(包括一键启动)

像上文一样 因为在学习的过程中没有查到一个详细的离线部署方案 所以在自己学习之后想要自己写一个文章 希望可以帮助后续学习redis离线部署的朋友少走一线弯路 首先就是下载安装包 可以自己在本地下载再传到机器上&#xff08;通过xftp或lrzsz都可&#xff09; http://d…

图论汇总1

1.图论理论基础 图的基本概念 二维坐标中&#xff0c;两点可以连成线&#xff0c;多个点连成的线就构成了图。 当然图也可以就一个节点&#xff0c;甚至没有节点&#xff08;空图&#xff09; 图的种类 整体上一般分为 有向图 和 无向图。 有向图是指 图中边是有方向的&a…

小利特惠源码/生活缴费/电话费/油卡燃气/等充值业务类源码附带承兑系统

全新首发小利特惠/生活缴费/电话费/油卡燃气/等充值业务类源码附带U商承兑系统 安装教程如下 图片:

ESMC-600M蛋白质语言模型本地部署攻略

前言 之前介绍了ESMC-6B模型的网络接口调用方法&#xff0c;但申请token比较慢&#xff0c;有网友问能不能出一个本地部署ESMC小模型的攻略&#xff0c;遂有本文。 其实本地部署并不复杂&#xff0c;官方github上面也比较清楚了。 操作过程 环境配置&#xff1a;CUDA 12.1、…

Java 实现Excel转HTML、或HTML转Excel

Excel是一种电子表格格式&#xff0c;广泛用于数据处理和分析&#xff0c;而HTM则是一种用于创建网页的标记语言。虽然两者在用途上存在差异&#xff0c;但有时我们需要将数据从一种格式转换为另一种格式&#xff0c;以便更好地利用和展示数据。本文将介绍如何通过 Java 实现 E…

Ubuntu20.04 运行 PL-VIO

文章目录 运行后不知为何没有线特征 运行后不知为何没有线特征

centos操作系统上以service形式运行blackbox_exporter监控网页端口

文章目录 前言一、blackbox_exporter是什么二、使用步骤1.获取二进制文件2.准备部署脚本3.执行命令&#xff0c;进行部署4.prometheus中增加需要监控页面的job信息 三、查看部署结果四、配置到grafana中总结 前言 记录一下centos操作系统上以简单的service形式运行blackbox_ex…

Linux内核编程(二十一)USB驱动开发-键盘驱动

一、驱动类型 USB 驱动开发主要分为两种&#xff1a;主机侧的驱动程序和设备侧的驱动程序。一般我们编写的都是主机侧的USB驱动程序。 主机侧驱动程序用于控制插入到主机中的 USB 设备&#xff0c;而设备侧驱动程序则负责控制 USB 设备如何与主机通信。由于设备侧驱动程序通常与…