Unity开发一个FPS游戏之四

2024-05-03 07:52
文章标签 开发 游戏 unity fps 之四

本文主要是介绍Unity开发一个FPS游戏之四,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在前面的系列中,我已介绍了如何实现一个基本的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 updatevoid 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 framevoid 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();即可生成画面震动。

这篇关于Unity开发一个FPS游戏之四的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/956215

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧

v0.dev快速开发

探索v0.dev:次世代开发者之利器 今之技艺日新月异,开发者之工具亦随之进步不辍。v0.dev者,新兴之开发者利器也,迅速引起众多开发者之瞩目。本文将引汝探究v0.dev之基本功能与优势,助汝速速上手,提升开发之效率。 何谓v0.dev? v0.dev者,现代化之开发者工具也,旨在简化并加速软件开发之过程。其集多种功能于一体,助开发者高效编写、测试及部署代码。无论汝为前端开发者、后端开发者

国产游戏崛起:技术革新与文化自信的双重推动

近年来,国产游戏行业发展迅猛,技术水平和作品质量均得到了显著提升。特别是以《黑神话:悟空》为代表的一系列优秀作品,成功打破了过去中国游戏市场以手游和网游为主的局限,向全球玩家展示了中国在单机游戏领域的实力与潜力。随着中国开发者在画面渲染、物理引擎、AI 技术和服务器架构等方面取得了显著进展,国产游戏正逐步赢得国际市场的认可。然而,面对全球游戏行业的激烈竞争,国产游戏技术依然面临诸多挑战,未来的