当前位置: 首页 > article >正文

Unity开发FPS游戏之完结篇

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

相关文章:

  • Jmeter进阶篇(28)结合AI做性能测试:开启性能测试自动化新篇章
  • 宏海科技募资额有所缩减,最大销售和采购都重度依赖美的集团
  • 云服务器和物理服务器租用哪个好?
  • 第四话:JS中的eval函数
  • Linux 进程管理详解
  • [Linux] 信号(singal)详解(一)
  • RDIFramework.NET CS敏捷开发框架 SOA服务三种访问(直连、WCF、WebAPI)方式
  • Java程序员最新场景面试题总结
  • Brain.js(二):项目集成方式详解——npm、cdn、下载、源码构建
  • 电子电气架构 --- 车载网关GW连接外部IP Tester
  • springboot371高校实习管理系统(论文+源码)_kaic
  • 鸿蒙Next星河版基础用例
  • Leetcode 第425场周赛分析总结
  • 力扣1382:将二叉搜索树便平衡
  • Scala的模式匹配变量类型
  • 方寸 i560 安全存储加密芯片 SoC 存储安全芯片技术手册
  • Ubuntu24.04配置DINO-Tracker
  • php+Mysql单页支持不同数据结构不同查询条件查搜多表实例
  • 006 MATLAB编程基础
  • ansible自动化运维(一)配置主机清单
  • 在html页面显示一个变量,而这个变量中有xss脚本,如何安全的把这个变量原样展示出来
  • 360笔试题之LINUX和UNIX篇
  • 数据结构——排序第三幕(深究快排(非递归实现)、快排的优化、内省排序,排序总结)超详细!!!!
  • 【Debug】hexo-github令牌认证 Support for password authentication was removed
  • Node.js-Mongodb数据库
  • 电脑显示器拔插DVI线后副屏不显示