Unity开发一个FPS游戏之四

news2024/11/24 15:47:54

在前面的系列中,我已介绍了如何实现一个基本的FPS游戏,这里将继续进行完善,主要是增加更换武器以及更多动作动画的功能。

之前我是采用了网上一个免费的3D模型来构建角色,这个模型自带了一把AR自动步枪,并且自带了一些动作的动画,例如更换弹药,射击,瞄准等。我准备在这个模型的基础上进行扩展,增加AK47这个武器,以及重新制作并增加更多的动画,例如奔跑,走路等动画。

动画制作

设置关联骨骼

在Blender中导入网上下载的3D模型。导入后需要重新设置手的IK反向运动学骨骼,在姿态模式下,选择hand_IK的骨骼,然后按shift选择hand骨骼,然后shift-I来进行关联,设置IK骨骼约束的链长为3,即根控制骨骼是到upper_arm骨骼。现在点击hand_IK骨骼,按G键调整位置,可以看到整个手臂的骨骼会自动按照hand_IK的变化来运动。

调整模型大小

调整缩放比例,对于这个模型,我是把缩放比例调到0.6,可以通过Blender的测量工具进行测量。调整后的模型高度大致在2米。另外就是调整模型的朝向,因为Blender和Unity采用不同的坐标系,模型的朝向是相反的。

设置模型姿态

为了调整姿态,导入一个AK的模型,和枪械的相关骨骼进行关联。调整模型的骨骼,设置模型的多个相关动画。

稍微介绍一下在Blender中制作动画,需要进入姿态模式,然后移动骨骼,调整到合适的位置,然后按A全选,然后按I插入关键帧即可。

武器切换

要实现武器切换,有两个思路,一个是分别制作玩家和每一种武器对应的模型和动画,然后分别导出为不同的FBX文件。另一个思路是把玩家和武器分开来导出。可以看到第2种思路更合理,因为玩家的模型只需保留一个就可以,然后武器保存为不同的模型文件。但需要切换时就加载相应的武器模型即可。

当然这两种思路在制作模型动画时还是需要一起制作的,不然没法把武器的动作和玩家的动作很好的吻合在一起,我们只需要在导出时,把不需要的物体隐藏即可。

把制作好的玩家和武器的模型导入到Unity,并保存为Prefab。然后就可以进行代码改造了。

Animator定义

每个武器有自己的动画,然后玩家对应不同的武器也有不同的动画,要如何根据武器来进行动画的管理切换呢,我们可以用Unity的Animator override来进行处理。定义两个通用的Animator,分别对应Player和Weapon,在里面定义好状态的转换,例如以下是Player的Animator定义:

在以上的状态切换,我都是通过Trigger来触发的,没有用到Bool。因为我是在PlayerController脚本里面来维护状态。对于需要循环播放的动画,例如Sprint, Walk, Aim这些动画剪辑,我们需要在模型的Animation里面勾选Loop Time。

然后对于每一种武器,我们都定义一个Animator override,继承这个通用的Animator,只是覆盖相应状态的动画即可。对于Player也是需要定义多个Animator override来对应不同的武器,覆盖相应的动画。

之后在每一种武器的Prefab里面,增加一个Animator的组件,里面选择对应的Animator override。

代码重构

在之前的代码里面,武器的属性管理都是放在PlayerController里面的。现在既然要把玩家和武器分离,那么需要重构之前的代码,新增一个Weapon.cs的脚本文件,挂在武器上面。这个脚本除了管理相应的动作动画之外,其他和武器相关的属性也都移到这个脚本管理,不再放在原来的PlayerController脚本管理。代码如下:

public class Weapon : MonoBehaviour
{
    [Header("Bullet")]
    [SerializeField] GameObject bulletPrefab;
    [SerializeField] GameObject casingPrefab;
    public int bulletVolume = 30;
    public float bulletSpeed = 800f;

    [Header("Property")]
    [Tooltip("The shoot speed per minute")]
    public int shootSpeed = 400;
    public Vector3 aimOffset = Vector3.zero;

    [Tooltip("Audio clip played when reload.")]
    [SerializeField] AudioClip audioClipReload;
    [Tooltip("Audio clip played when fire.")]
    [SerializeField] AudioClip audioClipFire;
    public AudioSource audioSource;

    public string WeaponName;
    public AnimatorOverrideController PlayerAnimatorController;
    public string ClipFinishedName;

    private Transform _muzzle;
    private Transform _eject;
    public MuzzleEffect _muzzleEffect;
    private GameObject _bullet;
    private GameObject _casing;
    private int _currentBulletVolume;

    private Animator _animator;
    // Start is called before the first frame update
    void Awake()
    {
        _animator = GetComponent<Animator>();
        foreach (AnimationClip clip in _animator.runtimeAnimatorController.animationClips) {
            if (clip.name.Contains("Reload") || clip.name.Contains("Hide")) {
                AnimationEvent animationEndEvent = new AnimationEvent
                {
                    time = clip.length - 0.1f,
                    functionName = "EndAnimationHandler",
                    stringParameter = clip.name
                };
                clip.AddEvent(animationEndEvent);
            }
        }

        _muzzle = transform.Find("pose_controller/weapon/Muzzle");
        _eject = transform.Find("pose_controller/weapon/Eject");
        _muzzleEffect = GetComponent<MuzzleEffect>();
        _currentBulletVolume = bulletVolume;

        audioSource.clip = audioClipFire;
    }

    public void Shoot() {
        _bullet = Instantiate(bulletPrefab, _muzzle.position, _muzzle.rotation);
        _bullet.GetComponent<Rigidbody>().velocity = _muzzle.forward * bulletSpeed;
        _casing = Instantiate(casingPrefab, _eject.position, _eject.rotation);
        _muzzleEffect.Effect(_muzzle.position);
        audioSource.Play();
        _currentBulletVolume--;
    }

    public int GetCurrentBulletVolume() {
        return _currentBulletVolume;
    }

    public void Reload() {
        _currentBulletVolume = bulletVolume;
    }

    private void EndAnimationHandler(string name) {
        ClipFinishedName = name;
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

以上代码里面定义了武器对应的玩家的Animator override。另外就是添加了动画播放结束的事件处理,例如当武器切换时,需要等待旧武器的隐藏动画播放完毕后才能播放新武器的动画,因此需要通过事件来获知动画是否已播放完成。

之后就是修改PlayerController的脚本文件,代码改动如下:

public class PlayerController : MonoBehaviour
{
    ...
    [Header("Weapon")]
    [SerializeField] List<GameObject> weaponPrefabs;
    [SerializeField] GameObject playerPrefab;

    private int _weaponIndex = 0;
    private Animator _playerAnimator;
    private Animator _weaponAnimator;
    private GameObject _player;
    private GameObject _weapon;

    [Flags]
    private enum PlayerStatus {
        Idle,
        Walk,
        Sprint,
        Reload,
        Shoot,
        Aim,
        Switch
    }
    private PlayerStatus _playerStatus;

    private void Start()
    {
        ...
        InstantiateWeapon();
        _playerAnimator.SetTrigger("Idle");
        _weaponAnimator.SetTrigger("Idle");
        _playerStatus = PlayerStatus.Idle;
    }

    private void InstantiateWeapon() {
        _weapon = Instantiate(weaponPrefabs[_weaponIndex]);
        _weapon.transform.SetParent(transform.GetChild(0));
        _weapon.transform.localRotation = Quaternion.Euler(-90, 0, 0);
        _weaponAnimator = _weapon.GetComponent<Animator>();
        _weaponBehavior = _weapon.GetComponent<Weapon>();

        _player = Instantiate(playerPrefab);
        _player.transform.SetParent(transform.GetChild(0));
        _player.transform.localPosition = new Vector3(0.1f, -1.15f, 0f);
        _player.transform.localRotation = Quaternion.Euler(0, 0, 0);
        _playerAnimator = _player.GetComponent<Animator>();
        
        _playerAnimator.runtimeAnimatorController = _weaponBehavior.PlayerAnimatorController;  
        ExecuteEvents.Execute<IGameMessage>(_gameManager, null, (x,y)=>x.ArmoMessage(_weaponBehavior.GetCurrentBulletVolume()));
        ExecuteEvents.Execute<IGameMessage>(_gameManager, null, (x,y)=>x.TotalArmoMessage(_weaponBehavior.bulletVolume));
    }

    private void PlayAnimation() {
        ...
        if (_input.switchWeapon) {
            StartCoroutine(SwitchWeapon());
        }
    }

    private IEnumerator SwitchWeapon() {
        _weaponBehavior.ClipFinishedName = "";
        _weaponAnimator.SetTrigger("Hide");
        _playerAnimator.SetTrigger("Hide");
        _input.switchWeapon = false;
        _playerStatus = PlayerStatus.Switch;
        yield return new WaitUntil(()=>_weaponBehavior.ClipFinishedName.Contains("Hide"));
        Destroy(_weapon);
        Destroy(_player);
        if (_weaponIndex >= weaponPrefabs.Count -1) {
            _weaponIndex = 0;
        } else {
            _weaponIndex += 1;
        }
        InstantiateWeapon();
        _weaponAnimator.SetTrigger("Take");
        _playerAnimator.SetTrigger("Take");
    }

    

以上的代码定义了一个Enum类型的状态值来管理当前的状态,定义了一个Weapon prefab列表来保存所有可以切换的武器。定义了一个根据不同的武器来初始化对应的武器和玩家Gameobject的InstantiateWeapon函数。还有就是当收到切换武器的键盘输入时,调用SwitchWeapon进行切换。切换的时候需要先播放现有武器和玩家的隐藏动画,然后等动画播放完毕后,销毁现有的武器和玩家对象,然后重新调用InstantiateWeapon来初始化,并且播放Take动画。

武器切换的效果如下:

switch_weapon


瞄准模式

现在流行的FPS游戏,提供了机械瞄准的功能,比一般的抵腰射击瞄准姿势能提高准确度。要实现瞄准功能,有两个思路,一个是在机械瞄准的位置设置另一个虚拟摄像机,然后当要切换为瞄准模式的时候进行摄像机切换。另一种是调整玩家和武器的位置到瞄准位置。可以看到第2个思路更符合实际情况。

首先是打开Assets/Input的PlayerInputAsset文件,增加一个名为Aim的Action,类型为Button。在这个Action下面增加一个绑定到鼠标右键。

编辑Scripts目录的Weapon.cs文件,增加一个Vector3 aimOffset的属性,因为对于不同的武器,其瞄准的位置时有差异的。

编辑Scripts目录里面的PlayerInputAsset.cs文件,增加以下代码:

public class PlayerInputAsset : MonoBehaviour
{
    ...
    public bool aim;

    public void OnAim(InputValue value) {
        aim = true;
    }
}

编辑Scripts目录的PlayerController文件,增加以下代码:

public class PlayerController : MonoBehaviour
{
    ...
    private CinemachineVirtualCamera _camera;
    private Vector3 _aimOffset = new Vector3(-0.131f, 0.066f, -0.3f);
    private Vector3 _weaponVelocity = Vector3.zero;
    private Vector3 _playerVelocity = Vector3.zero;
    private float _cameraVelocity = 0f;
    private Vector3 _weaponPosition;
    private Vector3 _weaponAimPosition;
    private Vector3 _playerPosition;
    private Vector3 _playerAimPosition;
    private bool _currentAimStatus = false;

    private void InstantiateWeapon() 
    {
        ...
        _weaponPosition = _weapon.transform.localPosition;
        _weaponAimPosition = _weaponPosition + _weaponBehavior.aimOffset;
        _playerPosition = _player.transform.localPosition;
        _playerAimPosition = _playerPosition + _weaponBehavior.aimOffset;
    }


    private void Aim() {
        if (_weapon.transform.localPosition == _weaponPosition) {
            _weapon.transform.Find("pose_controller/crosshair").gameObject.SetActive(false);
            _playerAnimator.SetTrigger("Aim");
            _weaponAnimator.SetTrigger("Aim");
        }
        if (_weapon.transform.localPosition == _weaponAimPosition) {
            _input.aim = false;
            _playerStatus = PlayerStatus.Aim;
        } else {
            _weapon.transform.localPosition = Vector3.SmoothDamp(_weapon.transform.localPosition, _weaponAimPosition, ref _weaponVelocity, 0.05f);
            _player.transform.localPosition = Vector3.SmoothDamp(_player.transform.localPosition, _playerAimPosition, ref _playerVelocity, 0.05f);
            _camera.m_Lens.FieldOfView = Mathf.SmoothDamp(_camera.m_Lens.FieldOfView, 35, ref _cameraVelocity, 0.05f);
        }
    }

    private void ExitAim() {
        if (_weapon.transform.localPosition == _weaponAimPosition) {
            _weapon.transform.Find("pose_controller/crosshair").gameObject.SetActive(true);
            _playerAnimator.SetTrigger("Idle");
            _weaponAnimator.SetTrigger("Idle");
        }
        if (_weapon.transform.localPosition == _weaponPosition) {
            _input.aim = false;
            _playerStatus = PlayerStatus.Idle;
        } else {
            _weapon.transform.localPosition = Vector3.SmoothDamp(_weapon.transform.localPosition, _weaponPosition, ref _weaponVelocity, 0.05f);
            _player.transform.localPosition = Vector3.SmoothDamp(_player.transform.localPosition, _playerPosition, ref _playerVelocity, 0.05f);
            _camera.m_Lens.FieldOfView = Mathf.SmoothDamp(_camera.m_Lens.FieldOfView, 40, ref _cameraVelocity, 0.05f);
        }
    }

    private void PlayAnimation() {
        ...
        if (_input.aim) {
            if (_playerStatus != PlayerStatus.Aim) {
                Aim();
            } else {
                ExitAim();
            }
        }
    }
}

解释一下代码,我们需要定义一个瞄准时的偏移值,然后保存武器和玩家瞄准时的位置。当玩家点击鼠标右键时,判断当前的状态是否瞄准,如否,则切换到瞄准模式,隐藏十字准星,通过SmoothDamp函数来逐步调节武器和玩家的位置到瞄准模式的位置,同时也逐步调节虚拟摄像机的光圈放大画面。如果当前状态已是瞄准模式,则采取相反的动作,调整玩家和武器的位置到正常位置。

当然我们还需要定义相应的瞄准模式的动画,当处于瞄准模式时,呼吸作用导致的武器上下起伏的幅度应该大幅减小。

另外当处于瞄准模式时,玩家前进或左右转向的速度应该大大降低。

最后完成的效果如下所示:

Aim

画面震动

当玩家进行射击时,由于武器后坐力的作用,我们可以设置画面震动,以达到更好的效果。当然对于玩家受到伤害或者爆炸时,也可以添加画面震动的效果。

在Player的GameObject下添加一个Cinemachine Impulse Source的组件,Impulse Channel设置为Everything,Raw signal选择Handheld_normal_strong,Amplitude Gain设置为0.05, Frequence Gain设置为100。

然后选择我们场景中的Virtual Camera,添加一个Cinemachine Impulse Listener的组件,Channel Mask设置为Everything。

最后在PlayerController里面,Shoot函数中增加一行代码_shootImpulse.GenerateImpulse();即可生成画面震动。

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

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

相关文章

Unity开发微信小游戏(2)分享

目录 1.概述 2.代码 3.示例 4.个人作品 1.概述 这里我们能做有两件事&#xff1a; 1&#xff09;主动发起分享 2&#xff09;监听右上角分享&#xff08;...按钮&#xff0c;发朋友圈也在这里&#xff09; API&#xff1a;官方文档 2.代码 1&#xff09;主动发起分享&…

DHCPv4_CLIENT_ALLOCATING_01: 在其本地物理子网上广播DHCPDISCOVER消息

测试目的&#xff1a; 确保客户端能够在其本地物理子网上广播DHCPDISCOVER消息。 描述&#xff1a; 该测试用例旨在验证DHCP客户端是否能够正确地在其本地物理子网上广播DHCPDISCOVER消息&#xff0c;以便进行IP地址的自动分配。 测试拓扑&#xff1a; 测试步骤&#xff1a…

生产看板:最直观的车间管理方式之一,是马是马户牵出来溜溜。

可视化生产看板在组织工业生产中扮演着重要的角色&#xff0c;它可以提供实时的信息和可视化的数据&#xff0c;帮助团队和管理层更好地监控和管理生产过程。 以下是可视化生产看板在组织工业生产中的作用&#xff1a; 实时监控&#xff1a;可视化生产看板可以显示实时的生产数…

JavaEE初阶-多线程易忘点总结

文章目录 1.PCBPID文件描述符表内存指针状态上下文优先级记账信息tgid 2.线程与进程的区别3.sleep和interrupt方法的关系变量终止线程interrupt方法终止线程 4.线程状态5.出现线程不安全的原因线程在系统中是随即调度&#xff0c;抢占式执行的。多个线程修改同一个变量线程针对…

小白也能微调大模型:LLaMA-Factory使用心得

大模型火了之后&#xff0c;相信不少人都在尝试将预训练大模型应用到自己的场景上&#xff0c;希望得到一个垂类专家&#xff0c;而不是通用大模型。 目前的思路&#xff0c;一是RAG(retrieval augmented generation)&#xff0c;在模型的输入prompt中加入尽可能多的“目标领域…

Linux-管道通信

1. 管道概念 管道&#xff0c;是进程间通信的一种方式&#xff0c;在Linux命令中“ | ”就是一种管道&#xff0c;它可以&#xff0c;连接前一条命令&#xff0c;和后一条命令&#xff0c;把前面命令处理完的内容交给后面&#xff0c;例如 cat filename | grep hello …

富文本编辑器CKEditor4简单使用-07(处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题)

富文本编辑器CKEditor4简单使用-07&#xff08;处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题&#xff09; 1. 前言——CKEditor4快速入门2. 默认情况下的粘贴2.1 先看控制粘贴的3个按钮2.1.1 工具栏粘贴按钮2.1.2 存在的问题 2.2 不解决按钮问题的情况下2.2.1 使用ct…

三维图形学知识分享---求平面与模型相交线

在CGAL&#xff08;Computational Geometry Algorithms Library&#xff09;中&#xff0c;Polygon_mesh_processing模块提供了用于处理多边形网格数据结构的功能。其中&#xff0c;surface_intersection函数是用来计算模型的表面相交线的工具。 CGAL_Mesh mesh_orcl;std::vect…

C++ 函数 参数与返回值

#一 参数与返回值 回顾文件读数据功能 文件读数据 1函数参数传值调用过程 将函数调用语句中的实参的一份副本传给函数的型材。 简单的值的传递&#xff0c;实参的值没有发生变化。 2 函数参数传值调用过程 传地址调用 将变量的地址传递给函数的形参 形参和实参指向了同…

SpringBoot文件上传+拦截器

1、resource static下有个图片&#xff0c;希望浏览器可以查看这个图片 访问&#xff1a; 若yml设置路径&#xff0c;则可以定义在static下才可以访问 classpath代表类路径&#xff0c;都在target下 也就是项目在运行后的resource下的文件都会到classes下去 无需在target下创…

MES(制造执行系统)与PDCA循环,斩不断理还乱的关系。

MES系统算是B端系统中比较复杂的一种&#xff0c;这与我国制造业标准化程度较低有一定的关联&#xff0c;MES的存在就是要更好执行PDCA循环&#xff0c;二者关联是千丝万缕的&#xff0c;B系统提升专家借此为大家分享一下。 一、什么是PDCA PDCA&#xff08;Plan-Do-Check-Ac…

前端Web开发基础知识

HTML定义 超文本标记语言&#xff08;英语&#xff1a;HyperText Markup Language&#xff0c;简称&#xff1a;HTML&#xff09;是一种用于创建网页的标准标记语言。 什么是 HTML? HTML 是用来描述网页的一种语言。 HTML 指的是超文本标记语言: HyperText Markup LanguageH…

# IDEA 复制项目 Module 出现 不同模块下的 Product 类报错

IDEA 复制项目 Module 出现 不同模块下的 Product 类报错 我们 用 IDEA 复制项目 Module 出现 不同模块下的 Product 类报错&#xff0c;发现复制的 module 名称没有改变或者 java 文件夹后面还有原项目 source root 字样&#xff0c;maven 父子项目没有标识等问题。 解决方法…

QQ+微信聊天记录分析工具,allin~

QQ群 ... QQ个人 微信群 个人朋友圈 更多维度有待探索~ 工具下载 TencentRecordAnalysisV1.0.2.zip 蓝奏云&#xff1a;链接: lanzoub.com/b00rn0g47e 密码:9hww 百度云&#xff1a;链接: pan.baidu.com/s/1Gf5EpJ 提取码: hp2p

Stm32CubeMX 为 stm32mp135d 添加 adc

Stm32CubeMX 为 stm32mp135d 添加 adc 一、启用设备1. adc 设备添加2. adc 引脚配置2. adc 时钟配置 二、 生成代码1. optee 配置 adc 时钟和安全验证2. linux adc 设备 dts 配置 bringup 可参考&#xff1a; Stm32CubeMX 生成设备树 一、启用设备 1. adc 设备添加 启用adc设…

R语言学习—1—将数据框中某一列数据改成行名

将数据框中某一列数据改成行名 代码 结果

DHCPv4_CLIENT_ALLOCATING_03: 发送DHCPREQUEST - 必须包含‘服务器标识符‘

测试目的&#xff1a; 验证客户端发送的DHCPREQUEST消息中是否包含“服务器标识符”选项&#xff0c;以指示它选择的服务器。 描述&#xff1a; 本测试用例旨在确保DHCP客户端在广播DHCPREQUEST消息时&#xff0c;必须包含“服务器标识符”选项。该选项用于指明客户端选择了…

Universal Thresholdizer:将多种密码学原语门限化

参考文献&#xff1a; [LS90] Lapidot D, Shamir A. Publicly verifiable non-interactive zero-knowledge proofs[C]//Advances in Cryptology-CRYPTO’90: Proceedings 10. Springer Berlin Heidelberg, 1991: 353-365.[Shoup00] Shoup V. Practical threshold signatures[C…

[嵌入式系统-53]:嵌入式系统集成开发环境大全 ( IAR Embedded Workbench(通用)、MDK(ARM)比较 )

目录 一、嵌入式系统集成开发环境分类 二、由MCU芯片厂家提供的集成开发工具 三、由嵌入式操作提供的集成开发工具 四、由第三方工具厂家提供的集成开发工具 五、开发工具的整合 5.1 Keil MDK for ARM 5.2 IAR Embedded Workbench&#xff08;通用&#xff09;、MDK&…

240503-关于VisualStudio2022社区版的二三事

240503-关于VisualStudio2022社区版的二三事 1 常用快捷键 快捷键描述AltEnter选中代码片段以提取方法Alt上下箭头移动选中的代码片段F12转到方法定义CtrlR*2批量修改选中的变量名称 2 自动生成构造函数 3 快速重写父类方法 4 节约时间&#xff1a;写代码使用“头插法”&…