【Unity案例】搭建射击系统与UI

上期将基础的移动系统搭建完毕后就可以开始搭建更加复杂的系统部分了

前排提示,由于一开始仅思考如何完成操作相关功能,以至于到后面重构稍微有些困难,继续写下去恐成屎山,故在搭完射击和武器UI后不再继续泛化到敌人和敌人状态机

本次主要完成了

自由配置武器参数:武器所需的所有参数都可进行调整

请添加图片描述

武器的追随准星:

根据输入的武器槽位自动平滑的跟随指定武器的枪线,换弹时也会有相应提示

在这里插入图片描述

武器的切换:

不同武器根据数字键进行切换,在UI和准星上也会有所体现

在这里插入图片描述

第一第三人称的切换:

可以在第一第三人称间无缝切换

在这里插入图片描述

请看VCR!

Unity机甲2

文章目录

    • 总览
    • 武器系统
    • 子弹
    • 第一第三人称转换
    • UI

总览

类图结构

请添加图片描述

武器系统

武器使用状态机进行实现,WeaponState继承自State,内部持有Weapon的引用

Weapon是挂载在玩家身上的主要类,一个武器对应一个Weapon,手动输入索引标识其所属的武器槽位

public class Weapon : MonoBehaviour
{public enum FireMode{Single,Brust,Auto}private Entity owner;public int weaponSelectIndex = 0;public bool selected { get; private set; } = false;[Header("武器性能")][SerializeField] private int maxAmmo = 1;                                //最大弹匣弹药量[SerializeField] private int maxPrepareAmmo = 16;                //最大后备弹药量[SerializeField] private float shootingInterval = 0.1f;            //射击间隔[SerializeField] public float reloadTime = 2;                            //换弹时间[Header("开火模式")][SerializeField] public FireMode fireMode = FireMode.Single;      //开火模式 [SerializeField] private int brustNum = 3;                                      //brust一次开火射出的子弹数[SerializeField] private float brustTime = 1;                                  //两次brust开火之间的间隔//[Header("武器状态参数")]private int curAmmo;                                    //当前弹药量private int curPrepareAmmo;                       //当前后备弹药量private float shootingIntervalTimer = 0;    //射击间隔计时器private float brustTimeTimer = 0;               //brust射击间隔计时器private int brustCounter;                             //brust计数器[Header("发射物")][SerializeField] public Transform fireSocket;           [SerializeField] private GameObject bulletPrefab;   public float bulletVelocity = 100;                              public float inertialVelocityMultipler = 10;               public bool constantSpeed = false;                             //本来想做个委托外包出去,想了想不如直接集成在类里得了[Header("特效效果")][SerializeField] private GameObject fireFX;private CinemachineImpulseSource impulseSource;public float cameraShakeMultipler = 1f;[Header("音频")][SerializeField] private AudioClip fireSound;[SerializeField] private AudioClip reloadSound;[SerializeField] private float soundMultipler = 1f;//玩家的输入对应的委托转发public UnityAction onFireStart, onFiring, onFireEnd, onReload;//换弹时的委托,与UI通信使用public UnityAction onReloadStart, onReloadEnd;public UnityAction<float> onReloading;public UnityAction<int, int> onAmmoChanged;//是否选中public UnityAction<bool> onSelectChanged;//自己的状态机private StateMachine stateMachine = new StateMachine();public WeaponIdleState idleState;public WeaponFireState fireState;public WeaponReloadState reloadState;private void Awake(){owner = GetComponent<Entity>();impulseSource = fireSocket.GetComponent<CinemachineImpulseSource>();if (weaponSelectIndex == 1)selected = true;//初始化数据curAmmo = maxAmmo;curPrepareAmmo = maxPrepareAmmo;brustTimeTimer = brustTime;brustCounter = brustNum;//状态初始化idleState = new WeaponIdleState(stateMachine, this);fireState = new WeaponFireState(stateMachine, this);reloadState = new WeaponReloadState(stateMachine, this);//自身赋值到Controller方便其他组件引用PlayerController.Ins.weapons[weaponSelectIndex] = this;}private void Start(){//玩家操作本Weaponowner.onFireStart += () => onFireStart?.Invoke();owner.onFireEnd += () => onFireEnd?.Invoke();owner.onReload += () => onReload?.Invoke();owner.onSelect += (num) =>{selected = num == weaponSelectIndex;onSelectChanged?.Invoke(selected);};owner.onAllSelect += () =>{selected = true;onSelectChanged?.Invoke(selected);};//初始化状态机stateMachine.Init(idleState);}private void Update(){stateMachine.Update();if (shootingIntervalTimer > 0)shootingIntervalTimer -= Time.deltaTime;if (brustCounter <= 0 && brustTimeTimer > 0){brustTimeTimer -= Time.deltaTime;if (brustTimeTimer <= 0)brustCounter = brustNum;}if(owner.firing)onFiring?.Invoke();}public void ModifyAmmo(int amount){curAmmo += amount;curAmmo = Mathf.Clamp(curAmmo, 0, maxAmmo);onAmmoChanged?.Invoke(curAmmo, curPrepareAmmo);}public void Fire(){if (!CanFire())return;//发射投射物Bullet bullet = Instantiate(bulletPrefab, fireSocket.position, fireSocket.rotation).GetComponent<Bullet>();bullet.Init(bulletVelocity, constantSpeed, new Vector3(owner.velocity.x, 0, owner.velocity.z) * inertialVelocityMultipler, owner.flag);//数据更新ModifyAmmo(-1);shootingIntervalTimer = shootingInterval;if (fireMode == FireMode.Brust)                 //如果是Brust模式{brustCounter--;if (brustCounter <= 0)brustTimeTimer = brustTime;}//播放枪口特效Instantiate(fireFX, fireSocket.position, fireSocket.rotation);//震动!impulseSource.m_DefaultVelocity.x = Random.Range(-1f, 1f) * cameraShakeMultipler;impulseSource.m_DefaultVelocity.y = Random.Range(-1f, 1f) * cameraShakeMultipler;impulseSource.m_DefaultVelocity.z = Random.Range(-1f, 1f) * cameraShakeMultipler;impulseSource.GenerateImpulse();//枪口音效AudioManager.PlayClipAtPoint(fireSound, fireSocket.position, soundMultipler);}public void Reload(){int needAmmo = maxAmmo - curAmmo;curAmmo = Mathf.Min(curPrepareAmmo, maxAmmo);curPrepareAmmo = Mathf.Max(curPrepareAmmo - needAmmo, 0);onAmmoChanged?.Invoke(curAmmo, curPrepareAmmo);AudioManager.PlayClipAtPoint(reloadSound, owner.transform.position, soundMultipler);}public bool HaveAmmo() => curAmmo > 0;public bool HavePrepareAmmo() => curPrepareAmmo > 0;public bool CanFire(){if (!selected)return false;//没有子弹if (!HaveAmmo())return false;//没有结束冷却if (shootingIntervalTimer > 0)return false;//如果在Brust模式//如果Counter小于等于0,说明打完了,否则不管//如果打完了并且还没过brust冷却,那就不能打if (fireMode == FireMode.Brust && brustCounter <= 0 && brustTimeTimer > 0)return false;return true;}public bool CanBrust() => brustCounter > 0;public float GetReloadTime() => reloadTime;public int GetCurAmmo() => curAmmo;public int GetCurPrepareAmmo() => curPrepareAmmo;public bool CanReload() => curPrepareAmmo > 0 && curAmmo < maxAmmo;public State GetCurState() => stateMachine.curState;
}

武器的主要逻辑存在于状态机中

在这里插入图片描述

但是由于武器的开火分为连发,单发,爆发,因此还需要做一些特殊的处理

以下是WeaponFireState

在使用爆发模式时,即使玩家松开开火键也不能立即停止开火

public class WeaponFireState : WeaponCommonState
{public bool readyEnd;public WeaponFireState(StateMachine stateMachine, Weapon weapon) : base(stateMachine, weapon){}public override void Enter(){base.Enter();weapon.onFireEnd += OnFireEnd;FireOrReload();readyEnd = false;}public override void Exit(){base.Exit();weapon.onFireEnd -= OnFireEnd;}private void OnFireEnd(){if (weapon.fireMode != Weapon.FireMode.Brust)stateMachine.ChangeState(weapon.idleState);readyEnd = true;}public override void Update(){base.Update();//爆发模式if (weapon.fireMode == Weapon.FireMode.Brust){if (weapon.CanBrust())FireOrReload();else if(readyEnd)stateMachine.ChangeState(weapon.idleState);}else if (weapon.fireMode == Weapon.FireMode.Auto)FireOrReload();}private void FireOrReload(){if (weapon.HaveAmmo()){weapon.Fire();if (!weapon.HaveAmmo())stateMachine.ChangeState(weapon.reloadState);}elsestateMachine.ChangeState(weapon.reloadState);}
}

其他部分较为简短不特别描述

子弹

子弹同样使用一个通用的类进行配置

public class Bullet : MonoBehaviour
{[Header("Physical")]private Rigidbody rb;private Vector3 lastPosition;[Header("Attribute")]//子弹所属派系,可以设定是否开启友军伤害,-1为中立派系public int flag = -1;public float velocity = 100;public bool constantSpeed = false;public float lifeTime = 6f;private float lifeTimer;public float gravityMultiper = 1f;[Header("VFX")]public GameObject explosionPrefab;public GameObject trailPrefab;private void Awake(){rb = GetComponent<Rigidbody>();lastPosition = transform.position;lifeTimer = lifeTime;}public void Init(float velocity, bool constantSpeed, Vector3 inertialVelocity /*惯性力*/, int flag = -1){this.velocity = velocity;this.constantSpeed = constantSpeed;rb.velocity += transform.forward * velocity * Time.fixedDeltaTime / rb.mass;rb.AddForce(transform.forward * velocity + inertialVelocity, ForceMode.Impulse);this.flag = flag;}private void Update(){lifeTimer -= Time.deltaTime;if (lifeTimer < 0)OnCollisionEnter(null);}void FixedUpdate(){//防止错过刚体,对即将经过的间隔做一个射线检测if (Physics.Raycast(lastPosition, rb.velocity.normalized, out RaycastHit hitInfo, rb.velocity.magnitude * Time.fixedDeltaTime)){transform.position = hitInfo.point;rb.velocity = Vector3.zero;return;}//持久动力if (constantSpeed)rb.AddForce(transform.forward * velocity);//调整旋转朝向transform.forward = rb.velocity.normalized;//应用重力乘数rb.velocity += new Vector3(0, 9.8f * (1 - gravityMultiper) * Time.fixedDeltaTime, 0);//记录位置lastPosition = transform.position;}private void OnCollisionEnter(Collision collision){if(collision != null && collision.gameObject.TryGetComponent(out Entity entity)){if (entity.flag == flag)return;}if (trailPrefab){trailPrefab.transform.parent = null;var particleSystems = trailPrefab.GetComponentsInChildren<ParticleSystem>();foreach (var particle in particleSystems){var main = particle.main;main.loop = false;}}Instantiate(explosionPrefab, transform.position, Quaternion.identity);Destroy(gameObject);}private void DelayTrail(){trailPrefab.SetActive(true);}    
}

其内部包含初始惯性处理,持续动力,防止高速穿过物体的处理以及视觉和销毁时如果有拖尾的处理

第一第三人称转换

这一块比较简单,直接使用Cinemachine自带的混合,代码只需要控制两个虚拟相机的激活即可

public class PlayerCameraController : MonoBehaviour
{//Third Person Camera[SerializeField] private CinemachineVirtualCamera thirdPersonCamera;private Cinemachine3rdPersonFollow thirdCameraBody;public float freeLookSide = 0;public float freeLookDistance = 20;float cameraSide;float cameraDistance;//First Peroson Camera[SerializeField] private CinemachineVirtualCamera firstPersonCamera;private Cinemachine3rdPersonFollow firstCameraBody;private Quaternion lastQuaternion;private void Awake(){thirdCameraBody = thirdPersonCamera.GetCinemachineComponent<Cinemachine3rdPersonFollow>();cameraSide = thirdCameraBody.CameraSide;cameraDistance = thirdCameraBody.CameraDistance;firstCameraBody = firstPersonCamera.GetCinemachineComponent<Cinemachine3rdPersonFollow>();}// Update is called once per framevoid Update(){HandleFreeLook();HandleSwitchView();}private void HandleFreeLook(){if (Input.GetKeyDown(KeyCode.C)){//控制器冻结lastQuaternion = PlayerController.GetControllerRotation();PlayerController.SetPause(true);//第三人称参数thirdCameraBody.CameraSide = freeLookSide;thirdCameraBody.CameraDistance = freeLookDistance;//隐藏准星(有视觉Bug)foreach(var hair in UIManager.Ins.crossHairs){hair.gameObject.SetActive(false);}}if (Input.GetKeyUp(KeyCode.C)){//控制器恢复PlayerController.SetControllerRotation(lastQuaternion);PlayerController.SetPause(false);//第三人称参数thirdCameraBody.CameraSide = cameraSide;thirdCameraBody.CameraDistance = cameraDistance;//显示UIforeach (var hair in UIManager.Ins.crossHairs){hair.gameObject.SetActive(PlayerController.Ins.weapons[hair.weaponIndex].selected);}}}private void HandleSwitchView(){if (Input.GetKeyDown(KeyCode.V)){firstPersonCamera.gameObject.SetActive(!firstPersonCamera.gameObject.activeSelf);thirdPersonCamera.gameObject.SetActive(!thirdPersonCamera.gameObject.activeSelf);}}
}

UI

比较重要的地方是丝滑的UI跟随以及实时的武器栏

后者只需要在制作时留意委托就可以很方便的调用,前者则需要一些不同空间的变换知识

准星的跟随部分

一开始我在Canvas中选择的渲染模式是覆盖,后来发现在覆盖的模式下不能添加自发光,导致UI较暗,于是调整为了摄像机空间,但调整后导致原本跟踪正确的准星又不再正确,下面是解决办法

public class CrossHairUI : MonoBehaviour
{//自身private RectTransform rect, parent;[SerializeField] private GameObject aimHair, reloadHair;[SerializeField] private TextMeshPro reloadTxt;//武器public int weaponIndex = 0;private Weapon weapon;private Transform fireSocket;public float lerpMultipler = 0.1f;void Start(){rect = GetComponent<RectTransform>();parent = rect.parent.GetComponent<RectTransform>();weapon = PlayerController.Ins.weapons[weaponIndex];fireSocket = weapon.fireSocket;weapon.onReloadStart += OnReloadStart;weapon.onReloading += OnReloading;weapon.onReloadEnd += OnReloadEnd;weapon.onSelectChanged += (selected) =>{gameObject.SetActive(selected);};gameObject.SetActive(weapon.selected);}void Update(){if (Physics.SphereCast(fireSocket.position, .5f, fireSocket.forward, out RaycastHit hitInfo, 1000)){if(RectTransformUtility.ScreenPointToLocalPointInRectangle(parent, RectTransformUtility.WorldToScreenPoint(Camera.main, hitInfo.point), Camera.main, out Vector2 localPoint)){rect.localPosition = Vector2.Lerp(rect.localPosition, localPoint, lerpMultipler);}}else{Vector3 point = fireSocket.position + fireSocket.forward * 3000;if (RectTransformUtility.ScreenPointToLocalPointInRectangle(parent, RectTransformUtility.WorldToScreenPoint(Camera.main, point), Camera.main, out Vector2 localPoint)){rect.localPosition = Vector2.Lerp(rect.localPosition, localPoint, lerpMultipler);}}}private void OnReloadStart(){aimHair.SetActive(false);reloadHair.SetActive(true);}private void OnReloading(float remainTime){reloadTxt.SetText(remainTime.ToString("0.00"));}private void OnReloadEnd(){aimHair.SetActive(true);reloadHair.SetActive(false);}
}

在Update中首先SphereCast来获取击中的点,再将其WorldToScreenPoint变换到屏幕空间,如果是覆盖的渲染模式,此时已经结束了,但由于是摄像机模式,因此需要再多一个变换即ScreenPointToLocalPointInRectangle将其变换到面板上的相对位置。之后使用插值即可实现丝滑的跟踪准

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

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

相关文章

本地Gitblit使用

首先创建一个本地的gitblit的服务&#xff0c;创建流程如下&#xff1a; 【GitBlit】Windows搭建Git服务器详细教程_搭建gitblit服务-CSDN博客 GitBlit的使用教程-CSDN博客 创建好一个仓库后&#xff0c;分配好用户权限&#xff0c;再将项目拉下来&#xff0c;这里是再visua…

零信任赋予安全牙齿,AI促使它更锋利

距离上次写关于安全的文字已经过去了很久很久&#xff0c;久到上次看到的AI还停留在TTS、ASR等最初的语音交互搜索类似的各种智能音箱以及通过关键字匹配的基于知识库的聊天的机器人。之后的几年各种视觉识别遍地开花&#xff0c;AI四小龙在人脸识别上成熟应用&#xff0c;再然…

浏览器百科:网页存储篇-如何在Chrome打开localStorage窗格(五)

1.引言 在前面的章节中&#xff0c;我们详细介绍了 localStorage 的基本概念、特性及其常用方法&#xff0c;帮助开发者在网页应用中实现数据的持久化存储。为了更好地管理和调试这些存储的数据&#xff0c;了解如何打开和使用浏览器的 localStorage 窗格是非常重要的。本篇文…

科研绘图系列:R语言差异基因四分图(Quad plot)

介绍 四分图(Quad plot)是一种数据可视化技术,通常用于展示四个变量之间的关系。它由四个子图组成,每个子图都显示两个变量之间的关系。四分图的布局通常是2x2的网格,每个格子代表一个变量对的散点图。 在四分图中,通常: 第一个子图显示变量A和B的关系。第二个子图显示…

海外云服务器安装 MariaDB10.6.X (Ubuntu 18.04 记录篇二)

本文首发于 秋码记录 MariaDB 的由来&#xff08;历史&#xff09; 谈起新秀MariaDB&#xff0c;或许很多人都会感到陌生吧&#xff0c;但若聊起享誉开源界、业界知名的关系型数据库——Mysql&#xff0c;想必混迹于互联网的人们&#xff08;coder&#xff09;无不知晓。 其…

信捷 XD PLC 位软元件

位软元件的种类相对简单&#xff0c;一般为常见的 X、Y、M、HM、S、HS、T、HT、C、HC&#xff0c; 除此之外&#xff0c; 还可由寄存器中的某一位来表示。 1&#xff09;继电器 ⚫ 输入继电器 X&#xff0c;八进制表示法。 ⚫ 输出继电器 Y&#xff0c;八进制表示法。 ⚫ …

SQL语言的规则和规范

规则 是什么呢&#xff0c;规则就是我们最基本&#xff0c;每时每刻都要遵守的比如人行道靠右&#xff0c;不能逆行&#xff0c; 规范 呢就是锦上添花&#xff0c;如果你不这么做&#xff0c;是不那么道德&#xff0c;不那么好的&#xff0c;就像小学生见到老师要问好&#…

C++11(1)

目录 前言 小故事 C11优势 统一的列表初始化 1.{}初始化 2. std::initializer_list 声明 1.auto 2.decltype 3.nullptr 前言 小故事 1998 年是 C 标准委员会成立的第一年&#xff0c;本来计划以后每 5 年视实际需要更新一次标准&#xff0c; C 国际 标准委员会在研究…

Axure制作圆球在区域范围内移动效果的案例

在Axure RP中&#xff0c;我们可以通过设置多个交互动作和动态面板来创建复杂的动画效果&#xff0c;比如实现一个圆球在指定区域内通过八个方向按钮控制移动的效果。以下是一个详细的步骤介绍&#xff0c;帮助你理解并制作这一效果。 预览&#xff1a; https://1zvcwx.axshare…

简述CCS平面线性光源

光源在机器视觉系统中起着重要作用&#xff0c;不同环境、场景及应用合适光源都不一样&#xff0c;今天我们来看看LFX3-PT系列平面线性光源。它是最适合检测镜面物体的凹凸,外壳小巧的光源。备有根据检测条件可选的2种线间距。1mm型&#xff08;型号末尾&#xff1a;A&#xff…

idea一键自动化部署项目

文章目录 前言一、 IDEA插件安装1. 首先下载 Alibaba Cloud Toolkit 插件2. 插件下载完成后重启IDEA 二、SpringBoot项目准备1. pom.xml 文件2. controller3. 启动类 三、SpringBoot项目jar包部署1. Alibaba Cloud Toolkit 插件服务器配置2. 主机 IP、用户名、密码 点击测试链接…

日志系统(最新版)

基础知识 日志&#xff0c;由服务器自动创建&#xff0c;并记录运行状态&#xff0c;错误信息&#xff0c;访问数据的文件。 同步日志&#xff0c;日志写入函数与工作线程串行执行&#xff0c;由于涉及到I/O操作&#xff0c;当单条日志比较大的时候&#xff0c;同步模式会阻塞…

file | 某文件夹【解耦合】下的文件查找功能实现及功能单元测试

文件查找工具 概要思路OS模块 --- 学习版os.getcwd()os.path.dirname(os.getcwd())os.path.dirname() 和 os.path.basename() OS模块 — 实战版单元测试解耦合 概要 梳理业务主逻辑&#xff1a; 查看存放被采集JSON数据的文件夹内的文件列表【所有 包含文件夹下的文件夹下的文…

【Anaconda】修改jupyter notebook默认打开的工作目录、jupyter notebook快捷键

jupyter notebook快捷键 针对单元格的颜色蓝色命令行模式绿色编辑模式 两种模式的切换编辑模式切换到命令行模式 >>> esc键命令行模式切换到编辑模式 >>> 鼠标左键或者直接按enter键1.标题的书写方式1:1.esc进入命令行模式2.按m键3.写内容4.运行单元格即可方…

SprinBoot+Vue健康管管理微信小程序的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue3.6 uniapp代码 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平…

LabVIEW水泵机组监控系统

介绍了一种基于LabVIEW的水泵机组智能监控系统。该系统结合先进的传感器和数据采集技术&#xff0c;实时监控水泵机组的运行状态&#xff0c;有效预防故障&#xff0c;提高运行效率。通过LabVIEW平台的集成开发环境&#xff0c;系统实现了高效的数据处理和友好的用户界面。 项…

SpringCloud-02 Consul服务注册与发现

Consul是一种用于服务发现、配置和分布式协调的开源工具。Consul提供了以下主要功能&#xff1a; 1.服务发现&#xff1a;Consul允许开发人员在微服务架构中注册和发现服务。它可以自动检测新添加的服务并为它们分配唯一的网络地址。 2.健康检查&#xff1a;Consul可以定期检查…

一篇文档教会你从JavaScript语法走进DOM,让你的网页动起来

目录 JavaScript与WebAPI WebAPI简介 DOM 获取元素 事件 事件三要素 常见的事件类型 获取修改元素属性 基本介绍和使用 案例1&#xff1a;实现文本框内数字计数 案例2&#xff1a;实现“全部选中”按钮触发时相应的效果&#xff08;worth trying for a freshman&…

turbovnc 服务端、客户端安装

turbovnc 可以方便地远程登录带界面的linux系统&#xff0c;比如xbuntu、kali等&#xff1b;远程windows11系统&#xff0c;经过亲身测试体验&#xff0c;感觉还是不如windows自带的rdp服务&#xff08;mstsc命令连接&#xff09;好用。 一、安装客户端 下载最新版本的客户端…

力扣面试经典算法150题:接雨水

接雨水 今天的题目是力扣面试经典算法150题中的困难难度数组题目&#xff1a;分发糖果。 题目链接&#xff1a;https://leetcode.cn/problems/trapping-rain-water/description/?envTypestudy-plan-v2&envIdtop-interview-150 题目描述 给定 n 个非负整数表示每个宽度为…