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

news2025/1/11 22:44:39

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

前排提示,由于一开始仅思考如何完成操作相关功能,以至于到后面重构稍微有些困难,继续写下去恐成屎山,故在搭完射击和武器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()
    {
        //玩家操作本Weapon
        owner.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);
        }
        else
            stateMachine.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 frame
    void 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;

            //显示UI
            foreach (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.coloradmin.cn/o/2105322.html

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

相关文章

SF-HCI-SAP问题收集18:员工上级经理无效

导读 INTRODUCTION CPI集成&#xff1a;这几天一直在处理SuccessFactors与SAP集成的问题&#xff0c;因为没有给deubg权限&#xff0c;排查比较困难&#xff0c;这次讲解的案例就是一个比较难发现的案例&#xff0c;在同步员工主数据的时候提示&#xff0c;经历在某个区间无效…

前端原生Js批量修改页面元素属性的2个方法

前言 嘿&#xff0c;朋友们&#xff01;今天咱们来聊聊一个前端原生 JS 的小技巧。 今天在做一个 RPA 机器人时&#xff0c;碰到一个业务需求&#xff0c;网页上有两个日期控件元素&#xff0c;它们的输入框有 readonly 属性&#xff0c;只能通过选择的方式来输入日期&#x…

数据库和MySQL

ER图 实体&#xff08;矩形&#xff09;&#xff1a;通常是现实世界的业务对象&#xff0c;当然使用一些逻辑对象也可以。 属性&#xff08;椭圆&#xff09;&#xff1a;实体拥有的属性。 联系&#xff08;菱形&#xff09;&#xff1a;实体与实体之间的关系。 函数依赖 函数依…

idea 编译断点运行 tomcat 10.1.28 源码

idea 编译运行 tomcat 10.1.28 源码 1. 所需资源 tomcat 10.1.28 zulu JDK 22 maven idea (支持 JDK 22) 2. Idea 导入项目 10.1.28.tar.gz 解压到指定文件夹 如 ~\tomcat-source\tomcat-10.1.28 这里等待一段时间&#xff0c;生成 ~\tomcat-source\tomcat-10.1.28\.idea 文…

双向链表

双向链表是一种基本的数据结构&#xff0c;它与单向链表的主要区别在于节点的连接方式。下面我将分别描述双向链表的特点以及它与单向链表的区别。 双向链表的特点&#xff1a; 节点结构&#xff1a;在双向链表中&#xff0c;每个节点包含三个部分&#xff1a;数据域、指向前一…

WordPress自适应美图网站整站打包源码

直接服务器整站源码数据库打包了&#xff0c;恢复一下就可以直接投入使用。保证好用易用&#xff0c;无需独立服务器就可以使用。 强调一下&#xff0c;我这个和其他地方的不一样、不一样、不一样。具体的看下面的说明。 现在网络上同样的资源包都是用的加密带后门的主题&…

[C++11#47] (四) function包装器 | bind 函数包装器 | 结合使用

目录 一. function包装器 1. 在题解上 2.bind 绑定 1.调整参数顺序 2.对类中函数的包装方法 一. function包装器 function包装器也叫作适配器。C中的function本质是一个类模板&#xff0c;也是一个包装器。 那么我们来看看&#xff0c;我们为什么需要function呢&#xff1…

[数据集][目标检测]机油泄漏检测数据集VOC+YOLO格式43张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;43 标注数量(xml文件个数)&#xff1a;43 标注数量(txt文件个数)&#xff1a;43 标注类别数…

基于SSM的献血管理系统设计与实现(论文+源码)_kaic

摘 要 近年来&#xff0c;随着科技的飞速发展&#xff0c;在全球经济一体化的大背景下&#xff0c;我们面临着巨大的挑战&#xff0c;互联网技术将进一步提高社会综合发展的效率和速度&#xff0c;而且也会涉及到各个领域。献血管理系统在网络背景下有着无法忽视的作用&#x…

Xilinx FPGA 原语解析(三):OBUFDS差分输出缓冲器(示例源码及仿真)

目录 前言&#xff1a; 一、原语使用说明 二、原语实例化代码模版 三、使用示例 1.设计文件代码 2.仿真文件代码 3.仿真结果 前言&#xff1a; 主要参考 Xilinx 官方文档《Xilinx 7 Series FPGA and Zynq-7000 All Programmable SoC Libraries Guide for HDL Designs》…

网络工程师学习笔记——局域网和城域网(二)

快速以太网 快速以太网&#xff08;&#xff18;&#xff10;&#xff12;.&#xff13;&#xff55; &#xff11;&#xff10;&#xff10;&#xff2d;&#xff42;&#xff50;&#xff53;&#xff09; 其中多模光纤的芯线直径为&#xff16;&#xff12;.&#xff15;…

跟李沐学AI:循环神经网络RNN

循环神经网络 循环神经网络&#xff08;recurrent neural networks&#xff0c;RNNs&#xff09; 是具有隐状态的神经网络。RNN 具有隐状态&#xff08;hidden state&#xff09;的原因在于它需要一种机制来存储之前输入的信息&#xff0c;以便于处理当前输入时能够考虑之前的…

Linux教程8:文本编辑命令vi

一、文本编辑命令介绍 vi&#xff08;Visual Interface&#xff09;是一种广泛使用的文本编辑器&#xff0c;特别是在Unix和类Unix系统&#xff08;如Linux&#xff09;中。尽管现代系统通常提供了更现代的文本编辑器&#xff08;如vim&#xff0c;它是vi的增强版本&#xff0…

vue axios发送post请求跨域解决

跨越解决有两种方案&#xff0c;后端解决&#xff0c;前端解决。后端解决参考Django跨域解决-CSDN博客 该方法之前试着可以的&#xff0c;但是复制到其他电脑上报错&#xff0c;所以改用前端解决 1、main.js做增加如下配置 import axios from axios Vue.prototype.$axios a…

Spring1~~~

快速入门 javaBean规范要有set方法&#xff0c;底层反射赋值 <!-- id表示在该java对象在spring容器中的id--><bean class"spring.bean.Monster" id"monster01"><property name"Id" value"100"/><property nam…

Unity数据持久化 之 向文件流读写(详细Plus版)

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正​​ 在 Unity 手册中&#xff0c;FileStream 并没有单独的详细介绍&#xff0c;因为它是 .NET 框架的一部分&#xff0c;…

已经存在的项目如何变成git的一个repository

已经存在的项目如何被git管理 背景&#xff1a; 有一套代码很敏感&#xff0c;可能动不动就要不能正常工作(硬件开发常事)&#xff0c;那改动一下下就要有个记录&#xff0c;就决定用git管理 已经有了服务里里docker里运行的gitbucket,已经有了开发用的电脑上的git客户端&…

c++ websocket简单讲解

只做简单讲解。 一.定义和原理 WebSocket 是从 HTML5 开始⽀持的⼀种⽹⻚端和服务端保持⻓连接的消息推送机制&#xff0c;传统的 web 程序都是属于 "⼀问⼀答" 的形式&#xff0c;即客⼾端给服务器发送了⼀个 HTTP 请求&#xff0c;服务器给客⼾端返回⼀个 HTTP 响…

亿发进销存一体化解决方案:多终端无缝协同,赋能企业全业务-上

亿发软件凭借对产品、市场、业务的深入理解&#xff0c;在进销存基础上进行了延伸&#xff0c;推出多终端、一体化的“进销存管理系统”多元产品矩阵。在技术上实现电脑端、手机端、PDA端、零售端、商家版以及小程序商城的多终端无缝对接。各个端口间的数据可以互通互联&#x…

Prometheus Blackbox监控网站

Blackbox Exporter简介 blackbox_exporter 是 Prometheus 拿来对 http/https、tcp、icmp、dns、进行的黑盒监控工具&#xff0c;也就是从服务、主机等外部进行探测&#xff0c;来查看服务、主机等是否可用。 Blackbox Exporter 默认端口是 9115&#xff0c; 安装1 wget htt…