【unity实战】制作一个类帝国时代、红警——RTS战略性游戏

文章目录

  • 先来看看实现的最终效果
  • 什么是RTS游戏
  • 一、两种方法实现相机的移动+旋转+缩放以及拖拽功能
    • 前言
    • 准备
    • 第一种办法
      • 1. 移动
        • 1.1 代码实现,里面都写了详细的中文注释,就不过多解释了
        • 1.2 效果:
        • 1.3 问题:
      • 2. 缩放
        • 2.1 代码
      • 3. 限制范围
      • 4. 完整代码
    • 第二种办法
      • 1. 移动代码
        • 1.1 代码,使用Lerp插值函数实现移动缓冲效果
        • 1.2 按Shift加速
      • 2. 旋转
      • 3. 缩放
      • 4. 鼠标操作
        • 4.1 鼠标拖拽平移代码
        • 4.2 鼠标滚轮缩放
        • 4.3 旋转
      • 5. 完整代码
  • 二、箱型框选角色与角色底部光圈的放大动画
    • 1. 首先我们希望框选中的角色人物底座位置,会有一个明显的光圈显示
      • 1.1 这里我们给每个角色创建一个空物体:Selected Sprite,然后添加SpriteRenderer组件
      • 1.2 选择贴图
      • 1.3 调整角度和大小
      • 1.4 调整亮度
    • 2. 接看我们来做一个可视化的,标拖拽之后可见的箱型框选的一个透明的范围
      • 2.1 这里我创建了一个3D立方体来进行显示,也就是cube
      • 2.2 新建材质,控制立方体的透明的
      • 2.3 角色脚底选中圈代码
      • 2.3 ActorManager代码
      • 3. UI遮挡的问题
  • 三、角色的移动、采集,鼠标与对象的高亮,以及DOTween动画解决反复按下畸形问题
    • 1. 移动
      • 1.1 导航烘培
      • 1.2 角色移动代码
      • 1.3 ActorManager代码
    • 2. 高亮
    • 3. 采集物和动画
      • 3.1 我们创建一个Damageablel脚本
      • 3.2 在Resources中调用
    • 4. 畸变
    • 5. 采集
    • 6. 添加粒子效果
      • 6.1 UnityEvent事件方法实现
      • 6.2 c#事件方法实现
  • 最终代码
  • 参考
  • 完结

先来看看实现的最终效果

在这里插入图片描述

什么是RTS游戏

当提到RTS(即实时战略)游戏时,这种类型的游戏通常强调玩家在实时环境中进行战略和战术决策的能力。这些游戏要求玩家同时管理资源、建设基地、招募和指挥军队,以达到战胜敌人的目标。以下是几个知名的RTS游戏及其例子:

1.《星际争霸II》(StarCraft II):这是一款由暴雪娱乐开发的经典RTS游戏系列。玩家可以选择三个不同的种族(人类、神族和虫族),并在一个未来宇宙中进行战斗。游戏以其复杂的战略和丰富多样的游戏机制而闻名,包括资源管理、建筑构筑、技能升级和战术指挥。

2.《红色警戒》系列(Command & Conquer):这是一系列由西木开发的RTS游戏。玩家可以扮演全球势力的指挥官,参与战略和战役,控制军队、建造基地、研究科技和使用各种武器来实现胜利。这个系列中有许多不同的游戏,如《红色警戒2》和《红色警戒3》。

3.《帝国时代》系列(Age of Empires):这是一款由微软游戏工作室开发的RTS游戏系列。该系列以历史背景为基础,玩家可以选择不同的文明(如希腊、罗马和蒙古),发展城市、建造建筑、训练军队,并进行战争与贸易。《帝国时代II》和《帝国时代III》是该系列的两个受欢迎的作品。

4.《战争三国志》系列(Total War: Three Kingdoms):这是一款由Creative Assembly开发的RTS游戏系列,以中国历史的三国时期为背景。玩家可以扮演不同的将领,统一中国、扩张领土,并进行各种政治和军事策略。该系列以其大规模战争和复杂的战略系统而闻名。

这些只是RTS游戏领域中的一小部分例子。还有许多其他优秀的游戏,如《帝国时代II:征服者》、《魔兽争霸III》和《公司·英雄》等,每个游戏都有其独特的特点和玩法。

一、两种方法实现相机的移动+旋转+缩放以及拖拽功能

前言

标准的即时战略类游戏,会有资源的采集、基地的建造等众多元素,玩家可以指挥控制独立的单位,亦或者是群组式的控制,在游戏中发挥自己战争领袖的才华,不同于第一人称主视角,以及越肩式相机,RTS游戏中的相机更加"自由”,需要可见的地图范围、缩放程度也各有不同,相机的很多控制,需要同时满足【键盘】和【鼠标】的多功能需求.

实现相机控制的两种思路
第一种:通过键盘输入控制相机
第二种:主相机为子物体,按键输入和鼠标拖拽等Raycast与偏移量控制相机

准备

为了省事,我们直接下载官方的案例进行修改,省去了搭建环境等次重要的繁琐步骤,这里主要是对制作RTS类游戏的相机控制进行讨论

【百度云】链接: https://pan.baidu.com/s/1LMT-n4SqCIGTscDKpyjCDA 密码: te3j

删除原来控制的相机移动缩放的部分,及【Chinemachine Brain组件】移除,我们要重写这一部分
在这里插入图片描述

第一种办法

1. 移动

我们要实现的是通过【键盘】来控制相机的移动,并且会通过鼠标去触碰屏幕的边缘,来进行相机的移动

1.1 代码实现,里面都写了详细的中文注释,就不过多解释了

using UnityEngine;

public class CameraController01 : MonoBehaviour
{
    private Vector3 moveInput;//接收键盘的输入量
    [SerializeField] private float panSpeed;//相机平移的速度

    [SerializeField] private float scrollSpeed;//鼠标滚动的速度

    private void Update()
    {
        HandleMovementInput();
    }

    private void HandleMovementInput()
    {
        //我们其实动态改变的是Main Camera的Trans组件的Pos
        Vector3 pos = transform.position;
		
		//接收键盘输入量
        //moveInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));//考虑性能,不要有太多的new
        moveInput.Set(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));

		//接收鼠标输入量,实现触屏幕边缘移动
        Vector2 mousePos = Input.mousePosition;
        if (mousePos.x > Screen.width * 0.9f && mousePos.x < Screen.width)
            moveInput.x = 1;
        if (mousePos.x < Screen.width * 0.1f && mousePos.x > 0)
            moveInput.x = -1;
        if (mousePos.y > Screen.height * 0.9 && mousePos.y < Screen.height)
            moveInput.z = 1;
        if (mousePos.y < Screen.height * 0.1 && mousePos.y > 0)
            moveInput.z = -1;

		//相机平移
        pos.x += moveInput.x * panSpeed * Time.deltaTime;
        pos.z += moveInput.z * panSpeed * Time.deltaTime;
        transform.position = pos;
    }
}

1.2 效果:

在这里插入图片描述

1.3 问题:

如果你想完全通过panSpeed对相机进行匀速平移的话,应该将movelnput进行归一化/向量化/矢量化,我们的向量通过「归一化」之后,才能保证相机的移动完全是通过panSpeed来控制的,因为这个例子中我们使用的是GetAxisRawGetAxisRaw返回的数值只有可能是0、-1或者1,但是如果你去使用的是GetAxis,那么在Update的方法中其实,你每一帧的os的增量,它并不是一个固定的数值,「归一化」的好处就在于,它会将movelnput的大小(Magnitude)向量的模长保证是在1

实际上我们只希望去获取moveInput这个向量的方向,而不在乎它的大小,所以呢1*N=N,所以呢我们只希望获取它的方向,从而呢完全通过panSpeed来进行匀速的控制

修改

//相机平移
pos.x += moveInput.normalized.x * panSpeed * Time.deltaTime;
pos.z += moveInput.normalized.z * panSpeed * Time.deltaTime;

2. 缩放

接着,我们来快速实现相机的缩放功能,相机的缩放,改变的其实是相机Y轴的数值,也就需要改变的是变量pos中的y轴分量
那么我们的pos中y轴分量是什么呢?其实就是我们鼠标【滚轮的输入量Input.GetAxis("Mouse ScrollWheel'")】去乘以scrollSpeed,再去乘以Time.deltaTime

2.1 代码

pos.x += moveInput.normalized.x * panSpeed * Time.deltaTime;
pos.y += Input.GetAxis("Mouse ScrollWheel") * scrollSpeed * Time.deltaTime;//Y轴滚轮输入量,实现缩放
pos.z += moveInput.normalized.z * panSpeed * Time.deltaTime;

3. 限制范围

当我们缩小到一定程度,还可以继续缩小,并且相机是可以无限范围进行平移的,它可以移动到场景之外

我们这里可以去使用一个叫做【Mathf.Clamp方法】加以限制,Clamp的原意可以表示为夹子,想象成把第一个参数夹在第二个参数和第三个参数这个范围之内

代码

pos.x = Mathf.Clamp(pos.x, -10, 10);
pos.y = Mathf.Clamp(pos.y, 5, 30);
pos.z = Mathf.Clamp(pos.z, -25,5);//根据自己的地图范围进行调整,可以设置为变量,方便修改

4. 完整代码

好了,这就是第一种「基础版」的相机移动了,主要通过【键盘的输入】以及鼠标【靠近屏幕边缘】来触发相机的移动,其中包括了相机滚轮的缩放和施加一个限制范围。

using UnityEngine;

public class CameraController01 : MonoBehaviour
{
    private Vector3 moveInput;//接收键盘的输入量
    [SerializeField] private float panSpeed;//相机平移的速度

    [SerializeField] private float scrollSpeed;//鼠标滚动的速度

    private void Update()
    {
        HandleMovementInput();
    }

    private void HandleMovementInput()
    {
        //我们其实动态改变的是Main Camera的Trans组件的Pos
        Vector3 pos = transform.position;

        //moveInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));//性能?
        moveInput.Set(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));

        Vector2 mousePos = Input.mousePosition;
        if (mousePos.x > Screen.width * 0.9f && mousePos.x < Screen.width)
            moveInput.x = 1;
        if (mousePos.x < Screen.width * 0.1f && mousePos.x > 0)
            moveInput.x = -1;
        if (mousePos.y > Screen.height * 0.9 && mousePos.y < Screen.height)
            moveInput.z = 1;
        if (mousePos.y < Screen.height * 0.1 && mousePos.y > 0)
            moveInput.z = -1;

        pos.x += moveInput.normalized.x * panSpeed * Time.deltaTime;
        pos.y += Input.GetAxis("Mouse ScrollWheel") * scrollSpeed * Time.deltaTime;//Y轴滚轮输入量
        pos.z += moveInput.normalized.z * panSpeed * Time.deltaTime;

        pos.x = Mathf.Clamp(pos.x, -10, 10);
        pos.y = Mathf.Clamp(pos.y, 5, 30);
        pos.z = Mathf.Clamp(pos.z, -25, 5);//根据自己的地图范围进行调整,可以设置为变量,方便嘛!

        transform.position = pos;
    }
}

第二种办法

我们的第二个方法,可以去通过控制【父物体Camera Rig】的移动,从而间接的让子物体的相机一起跟随着【父物体】移动、旋转等一系列的功能实现

1. 移动代码

1.1 代码,使用Lerp插值函数实现移动缓冲效果

if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W))
	newPos += transform.forward * panSpeed * Time.deltaTime;//相机平移向上
if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S))
    newPos -= transform.forward * panSpeed * Time.deltaTime;
if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
    newPos += transform.right * panSpeed * Time.deltaTime;//相机平移向右
if(Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
    newPos -= transform.right * panSpeed * Time.deltaTime;
    
//transform.position = newPos;//AxisRaw / Axis
//Lerp方法:当前位置,目标位置,最大距离:速度 * 时间 =>从当前位置,到目标位置,需要多少时间到达
transform.position = Vector3.Lerp(transform.position, newPos, moveTime * Time.deltaTime);

1.2 按Shift加速

如果你觉得平移速度很慢的时候,想按下shift加快相机的平移,我们可以设置两个不同的平移速度,当我们按下shift按钮之后,赋值panSpeed = fastSpeed;

[SerializeField] private float normalSpeed, fastSpeed;

if (Input.GetKey(KeyCode.LeftShift))
	panSpeed = fastSpeed;
else
    panSpeed = normalSpeed;

2. 旋转

实际上我们需要改变的是Camera Rig游戏对象上的rotation Y的数值

在这里插入图片描述
代码实现

private Vector3 newPos;
private Quaternion newRotation;
[SerializeField] private float rotationAmount;//旋转的程度

private void Start()
{
	newPos = transform.position;
    newRotation = transform.rotation;
}
private void Update()
{
	if (Input.GetKey(KeyCode.Q))
        newRotation *= Quaternion.Euler(Vector3.up * rotationAmount);//Q:逆时针
    if (Input.GetKey(KeyCode.E))
        newRotation *= Quaternion.Euler(Vector3.down * rotationAmount);//(0,-1,0)顺时针

	//transform.position = newPos;//AxisRaw / Axis
    //Lerp方法:当前位置,目标位置,最大距离:速度 * 时间 =>从当前位置,到目标位置,需要多少时间到达
    transform.position = Vector3.Lerp(transform.position, newPos, moveTime * Time.deltaTime);

    //transform.rotation = newRotation;
    transform.rotation = Quaternion.Lerp(transform.rotation, newRotation, moveTime * Time.deltaTime);
}

3. 缩放

如果想以相机中心点来进行缩放的话,这里提供另一种思路:点击子物体Main Camera,选择Local局部坐标系、自身坐标系下,我们可以通过拖拽z轴,那实际上在Main Camera当中,就是它的z和它的y的数值发生了改变

这里要注意的是,由于Main Camera属于子物体的原因,所以这里改变的Trans组件position中的数值,它其实是属于LocalPosition,而如果一个物体它作为父物体的话,那么这个游戏对象的Trans组件中的position,才隶属于世界坐标系

也就是我刚才所说的,在Sart方法中初始的缩放应该是子物体相机的当前localPosition,并非空物体Camera Rig原点位置处,我们这里所有的缩放操作,是基于子物体Main Camera中的localPosition进行的更新

private Transform cameraTrans;//子物体~主相机Trans,要改YZ数值
[SerializeField] private Vector3 zoomAmount;//要改YZ数值,设置zoomAmount结构体中YZ的数值
private Vector3 newZoom;

private void Start()
{
    cameraTrans = transform.GetChild(0);
    newZoom = cameraTrans.localPosition;
}

private void Update()
{
	if (Input.GetKey(KeyCode.R))
        newZoom += zoomAmount;//放大功能:Y越来越小,Z越来越大
    if (Input.GetKey(KeyCode.F))
        newZoom -= zoomAmount;//缩小:Y越来越大,Z越来越小
            
	cameraTrans.localPosition = Vector3.Lerp(cameraTrans.localPosition, newZoom, moveTime * Time.deltaTime);
}

如果你想放大缩小给一个限定范围,就可以参考刚才的Clamp方法,这里就不再过多的去重复了

4. 鼠标操作

如果想通过鼠标拖动地图来进行操作相机的平移、旋转、缩放

4.1 鼠标拖拽平移代码

private Vector3 dragStartPos, dragCurrentPos;//鼠标拖拽的起始点,和鼠标拖拽的当前位置
private Vector3 rotateStart, rotateCurrent;//鼠标初始位置和当前位置,用来计算相机旋转角度

if(Input.GetMouseButtonDown(1))//鼠标按下一瞬间!
{
    Plane plane = new Plane(Vector3.up, Vector3.zero);
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    float distance;
    if(plane.Raycast(ray, out distance))//out输出参数,一般方法返回一个数值,out则返回return和out数值,2个结果
    {
        dragStartPos = ray.GetPoint(distance);
    }
}

if (Input.GetMouseButton(1))//鼠标按着(当前)
{
    Plane plane = new Plane(Vector3.up, Vector3.zero);
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    float distance;
    if (plane.Raycast(ray, out distance))//out输出参数,一般方法返回一个数值,out则返回return和out数值,2个结果
    {
        dragCurrentPos = ray.GetPoint(distance);

        Vector3 difference = dragStartPos - dragCurrentPos;//大家可以试试反过来写,效果雷同只是方向相反
        newPos = transform.position + difference;
    }
}

4.2 鼠标滚轮缩放

newZoom += Input.mouseScrollDelta.y * zoomAmount;

4.3 旋转

我们希望通过鼠标【中键】的拖拽,实现原本键盘实现的相机旋转的效果

if (Input.GetMouseButtonDown(2))
	rotateStart = Input.mousePosition;
if(Input.GetMouseButton(2))
{
    rotateCurrent = Input.mousePosition;
    Vector3 difference = rotateStart - rotateCurrent;

    rotateStart = rotateCurrent;//赋值最新的鼠标位置

	//x y控制水平还是垂直方向拖动控制的旋转	
    newRotation *= Quaternion.Euler(Vector3.up * -difference.x / 20);//水平方向触发旋转
    //newRotation *= Quaternion.Euler(Vector3.up * -difference.y / 20);//垂直方向
}

5. 完整代码

using System;
using UnityEngine;

public class CameraController02 : MonoBehaviour
{
    private float panSpeed;
    [SerializeField] private float moveTime;//缓冲时间,用于之后的Vector3.Lerp和Quaternion.Lerp方法/函数
    [SerializeField] private float normalSpeed, fastSpeed;

    private Vector3 newPos;
    private Quaternion newRotation;
    [SerializeField] private float rotationAmount;//旋转的程度

    private Transform cameraTrans;//子物体嘛~主相机Trans,要改YZ数值
    [SerializeField] private Vector3 zoomAmount;//要改YZ数值,设置zoomAmount结构体中YZ的数值
    private Vector3 newZoom;

    private Vector3 dragStartPos, dragCurrentPos;//鼠标拖拽的起始点,和鼠标拖拽的当前位置
    private Vector3 rotateStart, rotateCurrent;//鼠标初始位置和当前位置,用来计算相机旋转角度

    private void Start()
    {
        newPos = transform.position;
        newRotation = transform.rotation;

        cameraTrans = transform.GetChild(0);
        newZoom = cameraTrans.localPosition;
    }

    private void Update()
    {
        HandleMovementInput();//通过键盘控制相机
        HandleMouseInput();//通过鼠标控制相机
    }

    private void HandleMouseInput()
    {
        if(Input.GetMouseButtonDown(1))//鼠标按下一瞬间!
        {
            Plane plane = new Plane(Vector3.up, Vector3.zero);
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            float distance;
            if(plane.Raycast(ray, out distance))//out输出参数,一般方法返回一个数值,out则返回return和out数值,2个结果
            {
                dragStartPos = ray.GetPoint(distance);
            }
        }

        if (Input.GetMouseButton(1))//鼠标按着(当前)
        {
            Plane plane = new Plane(Vector3.up, Vector3.zero);
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            float distance;
            if (plane.Raycast(ray, out distance))//out输出参数,一般方法返回一个数值,out则返回return和out数值,2个结果
            {
                dragCurrentPos = ray.GetPoint(distance);

                Vector3 difference = dragStartPos - dragCurrentPos;//大家可以试试反过来写,效果雷同只是方向相反
                newPos = transform.position + difference;
            }
        }

        newZoom += Input.mouseScrollDelta.y * zoomAmount;

        if (Input.GetMouseButtonDown(2))
            rotateStart = Input.mousePosition;
        if(Input.GetMouseButton(2))
        {
            rotateCurrent = Input.mousePosition;
            Vector3 difference = rotateStart - rotateCurrent;

            rotateStart = rotateCurrent;//赋值最新的鼠标位置
            newRotation *= Quaternion.Euler(Vector3.up * -difference.x / 20);//水平方向触发旋转
            //newRotation *= Quaternion.Euler(Vector3.up * -difference.y / 20);//垂直方向
        }
    }

    private void HandleMovementInput()
    {
        if (Input.GetKey(KeyCode.LeftShift))
            panSpeed = fastSpeed;
        else
            panSpeed = normalSpeed;

        if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W))
            newPos += transform.forward * panSpeed * Time.deltaTime;//相机平移向上
        if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S))
            newPos -= transform.forward * panSpeed * Time.deltaTime;
        if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
            newPos += transform.right * panSpeed * Time.deltaTime;//相机平移向右
        if(Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
            newPos -= transform.right * panSpeed * Time.deltaTime;

        if (Input.GetKey(KeyCode.Q))
            newRotation *= Quaternion.Euler(Vector3.up * rotationAmount);//Q:逆时针
        if (Input.GetKey(KeyCode.E))
            newRotation *= Quaternion.Euler(Vector3.down * rotationAmount);//(0,-1,0)顺时针

        if (Input.GetKey(KeyCode.R))
            newZoom += zoomAmount;//放大功能:Y越来越小,Z越来越大
        if (Input.GetKey(KeyCode.F))
            newZoom -= zoomAmount;//缩小:Y越来越大,Z越来越小

        //transform.position = newPos;//AxisRaw / Axis
        //Lerp方法:当前位置,目标位置,最大距离:速度 * 时间 =>从当前位置,到目标位置,需要多少时间到达
        transform.position = Vector3.Lerp(transform.position, newPos, moveTime * Time.deltaTime);

        //transform.rotation = newRotation;
        transform.rotation = Quaternion.Lerp(transform.rotation, newRotation, moveTime * Time.deltaTime);

        cameraTrans.localPosition = Vector3.Lerp(cameraTrans.localPosition, newZoom, moveTime * Time.deltaTime);
    }
}

二、箱型框选角色与角色底部光圈的放大动画

这里不详细环境搭建和人物动画的实现,我们直接下载unity官方的demo进行修改学习,你可以直接下载《项目开始版》开始学习,里面删除很多原先的功能,我们来进行重开发

【项目开始版】https://pan.baidu.com/s/17JFX1ihjQdWQn9iFqVkGGw 密码: up15

1. 首先我们希望框选中的角色人物底座位置,会有一个明显的光圈显示

在这里插入图片描述

1.1 这里我们给每个角色创建一个空物体:Selected Sprite,然后添加SpriteRenderer组件

在这里插入图片描述
在这里插入图片描述

1.2 选择贴图

在这里插入图片描述

1.3 调整角度和大小

在这里插入图片描述

1.4 调整亮度

如果觉得圈的亮度太暗,我们可以新建材质,通过控制材质颜色透明的控制亮度,最省事的方法有一个"万金油"的shader,叫做Legacy Shaders/Paticles/Additive,设置好记得先禁用SpriteRenderer组件,因为默认角色是不选中的
在这里插入图片描述

2. 接看我们来做一个可视化的,标拖拽之后可见的箱型框选的一个透明的范围

2.1 这里我创建了一个3D立方体来进行显示,也就是cube

一会儿我们会进行鼠标的拖拽,通过鼠标位置的不同,改变它的LocalScale的数值,改变这个箱型框选范围,呈现对应的形状大小
在这里插入图片描述

2.2 新建材质,控制立方体的透明的

在这里插入图片描述

2.3 角色脚底选中圈代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;

public class ActorVisualHandler : MonoBehaviour
{
    public SpriteRenderer spriteRenderer;

    public void Select()
    {
        spriteRenderer.enabled = true;//开启
        //使用DOTween实现动画效果
        spriteRenderer.transform.DOScale(0, 0.35f).From().SetEase(Ease.OutBack);

    }

    public void Deselect()
    {
        spriteRenderer.enabled = false;//SR组件关闭
    }

}

2.3 ActorManager代码

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ActorManager : MonoBehaviour
{
    public static ActorManager instance;//单例模式

    [SerializeField] private Transform selectedArea;//框选的范围,实际是一个立方体,一会显示框选范围
    public List<Actor> allActors = new List<Actor>();//场景中所有己方角色
    [SerializeField] private List<Actor> selectedActors = new List<Actor>();//当前选中的角色(们)

    //鼠标拖拽的起始点,终点,和计算显示的框选Cube的中心位置和实际大小,用于localScale的实现
    private Vector3 dragStartPos, dragEndPos, dragCenter, dragSize;
    public LayerMask mouseDragLayerMask;

    private bool isDragging;
    public LayerMask dragSelectedLayerMask;//只框选角色,即BoxCastAll方法中的指定层

    private void Awake()
    {
        if(instance == null)
        {
            instance = this;
        }
        else
        {
            if (instance != this)
                Destroy(gameObject);
        }
        DontDestroyOnLoad(gameObject);//防止场景转换时,保持唯一性
    }

    private void Start()
    {
        selectedArea.gameObject.SetActive(false);//一开始框选是不可见的
        //所有在场景中的角色,都应该添加到allActors这个List中
        foreach(Actor actor in GetComponentsInChildren<Actor>())
        {
            allActors.Add(actor);//相当于游戏开始后完成了“注册”的工作
        }
    }

    private void Update()
    {
        MouseInput();
    }

    private void MouseInput()
    {
        if(Input.GetMouseButtonDown(0))//按下鼠标「开始拖拽」时,需要存储这个点在【世界坐标系】下的位置信息
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            //if(Physics.Raycast(ray, out RaycastHit raycastHit, 100, LayerMask.GetMask("Level"))) 
            if (Physics.Raycast(ray, out RaycastHit raycastHit, 100, mouseDragLayerMask))
            {
                dragStartPos = raycastHit.point;//raycastHit结构体,out是输出参数,作为方法的第二个输出使用
            }
        } 
        else if (Input.GetMouseButton(0))//「按住」鼠标左键的时候
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray, out RaycastHit raycastHit, 100, mouseDragLayerMask))
            {
                dragEndPos = raycastHit.point;//raycastHit结构体,out是输出参数,作为方法的第二个输出使用
            }

            //这里我们希望的是只有拖动了一段距离以后,才会出现框选的范围,而不是细微的变化就会出现框选
            if(Vector3.Distance(dragStartPos, dragEndPos) > 1)
            {
                isDragging = true;//正在拖动
                selectedArea.gameObject.SetActive(true);//框选范围可见

                //生成一个范围大小和坐标了
                dragCenter = (dragStartPos + dragEndPos) / 2;//0, 20 -> 10 | -40, 90 -> 25
                dragSize = dragEndPos - dragStartPos;
                selectedArea.transform.position = dragCenter;
                selectedArea.transform.localScale = dragSize + Vector3.up;//提高一点框选可见范围的空中高度
            } 
        } 
        else if(Input.GetMouseButtonUp(0))
        {
            //情况1: 我们之前还在拖动,范围内全选的工作
            if(isDragging == true)
            {
                //松开dragSelectedLayerMask
                SelectActors();
                isDragging = false;
                selectedArea.gameObject.SetActive(false);
            }
            else//情况2: 我们之前其实没有在拖动,这时候鼠标松开其实纯碎只是鼠标左键的点击,可能是角色的移动、攻击/采集
            {
                SetTask();
            }
        }

        //if(UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
        //{
        //    isDragging = false;
        //    selectedArea.gameObject.SetActive(false);
        //    return;
        //}
    }

    private void SetTask()
    {
        //Debug.Log("Set Task!");

        //首先要注意一点,如果我们框选空气的话,或者随便点击左键,触发这个方法都不该有什么逻辑执行
        if (selectedActors.Count == 0)
            return;

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Collider collider;
        if(Physics.Raycast(ray, out RaycastHit hitInfo, 100))
        {
            collider = hitInfo.collider;//获取射线检测到的这个点的Collider组件
            //如果射线检测到的,鼠标点的这个玩意是「地形」的话,移动到这个位置
            if(collider.CompareTag("Terrain"))
            {
                foreach(Actor actor in selectedActors)
                {
                    Ray _ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                    if(Physics.Raycast(_ray, out RaycastHit raycast, 100, mouseDragLayerMask))
                    {
                        Vector3 _targetPos = raycast.point;
                        actor.SetDestination(_targetPos);
                    }
                }
            }
            //如果射线检测到的,鼠标点的这个玩意是「石头/敌人/马车」的话,采集/攻击
        }
    }

    private void SelectActors()
    {
        DeselectActors();//每次框选其实都是重新选择,所以需要删去上一次的所有选中的角色

        //Debug.Log("We have Selected Actors!!!!!!");
        //dragSize = new Vector3(Mathf.Abs(dragSize.x), 1f, Mathf.Abs(dragSize.z / 2));//这里是Z在3D世界
        dragSize.Set(Mathf.Abs(dragSize.x / 2), 1f, Mathf.Abs(dragSize.z / 2));//HalfExtent
        RaycastHit[] hits = Physics.BoxCastAll(dragCenter, dragSize, Vector3.up, Quaternion.identity, 0, dragSelectedLayerMask);
        foreach(RaycastHit hit in hits)
        {
            Actor actor = hit.collider.GetComponent<Actor>();//我们要检测,框选到的是不是Actor角色
            if(actor != null)
            {
                selectedActors.Add(actor);//将选中的角色,添加到selectedActor这个List中
                actor.visualHandler.Select();
            }
        }
    }

    private void DeselectActors()
    {
        foreach(Actor actor in selectedActors)
        {
            actor.visualHandler.Deselect();//将之前所有已经选中的Actors的下标光圈关闭
        }

        selectedActors.Clear();//Clear删除之前SelectedActors这个List中的所有元素
    }

}

挂载脚本
在这里插入图片描述

3. UI遮挡的问题

我们可以去使用EventSystem.current.IsPointerOverGameObject(),这个方法将会检测是否点击在UI上,如果拖拽的时候正好在UI上的时候,那么我们不妨让拖拽立即停止isDraggingi=false;并且框选立方体不可见,而且直接return,不执行之后所有的内容

代码

if(UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameobject())
{
	isDragging false;
	selectedArea.gameobject.SetActive(false);
	return;
}

三、角色的移动、采集,鼠标与对象的高亮,以及DOTween动画解决反复按下畸形问题

1. 移动

1.1 导航烘培

先进行导航场景地图烘培,不懂得可以看我之前的文章:导航系统
在这里插入图片描述

1.2 角色移动代码

Actor代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Actor : MonoBehaviour
{
    [HideInInspector] public ActorVisualHandler visualHandler;
    private NavMeshAgent agent;
    private Animator animator;

    private void Start()
    {
        visualHandler = GetComponent<ActorVisualHandler>();
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponentInChildren<Animator>();
    }

    private void Update()
    {
        //Animator是通过人物的速度Speed来切换不同的动画状态片段的
        animator.SetFloat("Speed", Mathf.Clamp(agent.velocity.magnitude, 0, 1));
    }

    //人物的移动, _target表示人物应该移动到的鼠标位置,由于是未知的,所以设置为参数
    public void SetDestination(Vector3 _target)
    {
        agent.SetDestination(_target);
    }

}

1.3 ActorManager代码

private void SetTask()
{
     //Debug.Log("Set Task!");

     //首先要注意一点,如果我们框选空气的话,或者随便点击左键,触发这个方法都不该有什么逻辑执行
     if (selectedActors.Count == 0)
         return;

     Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
     Collider collider;
     if(Physics.Raycast(ray, out RaycastHit hitInfo, 100))
     {
         collider = hitInfo.collider;//获取射线检测到的这个点的Collider组件
         //如果射线检测到的,鼠标点的这个玩意是「地形」的话,移动到这个位置
         if(collider.CompareTag("Terrain"))
         {
             foreach(Actor actor in selectedActors)
             {
             	actor.SetDestination(hitInfo.point);
             }
         }
         //如果射线检测到的,鼠标点的这个玩意是「石头/敌人/马车」的话,采集/攻击
     }
 }

这里做一个分段,有人可能对移动已经很熟悉了,而且移动也是比较常规的操作,所以这里放一个项目开始版供下载:
https://pan.baidu.com/s/1oI4n4KCSRUox4goPqtDdyg 密码: b1fk

2. 高亮

当我们鼠标放在游戏对象上,我们想去实现高亮的效果,并且不同资源分类还需要进行划分

如何实现高亮?其实本质改变石头材质的HDR Color数值,实现高亮效果
在这里插入图片描述
在这里插入图片描述
创建C#脚本:Resources,我们先将脚本添加到两个Prefab中,代码实现如下:

using DG.Tweening;

public enum Resourcetype { Wood, Stone }
public class Resources : MonoBehaviour
{
    [SerializeField] private Resourcetype resourceType;
    public bool isHovering;//判断采集物是否要高亮
    [SerializeField] private int bounsAmount;
    private MeshRenderer meshRenderer;
    private Color originalColor;

	private void Start()
    {
        meshRenderer = GetComponent<MeshRenderer>();
        //originalColor = meshRenderer.material.color;//获取的其实只是材质的Base Color
        originalColor = meshRenderer.material.GetColor("_EmissionColor");
    }
    
	private void OnMouseEnter()//含Collider
    {
        isHovering = true;
        meshRenderer.material.SetColor("_EmissionColor", Color.gray);
    }

	private void OnMouseExit()
    {
        isHovering = false;
        meshRenderer.material.SetColor("_EmissionColor", originalColor);
    }
}

3. 采集物和动画

这里我们会涉及到一个游戏当中非常常见的一个问题,也就是角色的攻击力,它对采集物(敌方),生命值的影响

3.1 我们创建一个Damageablel脚本

using System;

//通常情况下,这里我会使用接口
//这个案例中这个脚本会涉及字段、事件、方法,所以使用简单的类
public class Damageable : MonoBehaviour
{
    [SerializeField] private int maxHp = 100;
    private int currentHp;

    //不去使用案例中的UnityEvent
    public event Action OnHit;//只需要+=添加事件就行了
    public event Action OnDestroy;

    private void Start()
    {
        currentHp = maxHp;
    }

    public void Hit(int _damageAmount)
    {
        OnHit();//OnHit.Invoke();
        currentHp -= _damageAmount;
        if(currentHp <= 0)
        {
            OnDestroy();//调用事件
            Destroy(gameObject);
        }
    }
}

3.2 在Resources中调用

private Damageable damageable;

private void Start()
{
    damageable = GetComponent<Damageable>();
}

我们继续去写HitResources方法,将会添加到OnHit事件上,如果采集物被击中,触发OnHit事件的同时,就会调用添加在OnHit事件上的这个HitResources方法,下面也是同样一个道理,只是添加的事件变成了OnDestroy事件

比如我们想给它一个简单的小动画,可以引入DG.Tweening命名空间

 //这个方法将会+=到OnHit事件上,表示当击中目标后,调用的方法
 public void HitResources()
 {
     //每次击中后,调用一个小动画
     transform.DOShakeScale(0.5f, 0.2f, 10, 90, true);
 }

 //这个方法将会+=到OnDestroy事件上,表示目标阵亡后,调用这个方法
 public void AddResources()
 {
     Debug.Log("Resources : " + resourceType + " increase : " + bounsAmount + " !");
 }

订阅和取消事件

private void Start()
{
     damageable = GetComponent<Damageable>();
     damageable.OnHit += HitResources;
     damageable.OnDestroy += AddResources;
 }

private void OnDestroy()
{
    damageable.OnHit -= HitResources;
    damageable.OnDestroy -= AddResources;//事件的取消订阅,为了防止出现内存泄漏的问题!
}

private void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
        HitResources();//仅做测试使用
}

4. 畸变

如果我们连续多按几次以后就会发现,相同的对象在动画完成之后,却出现了不同的形状的畸变,有的马车你会看到它被压缩的极其的变形
原因就是因为我们连续的按下Space按钮调用动画的时候,我们在上一个动画还没有完成的情况下,紧接着就进行下一个动画,造成了大小畸形的变化(形状不统一是因为随机种子在影响,形状未还原是因为动画未完成又再次进行)

在这里插入图片描述
解决

那就是调用它DOTween API当中的一个叫做:transform.DOComplete();就可以了

//这个方法将会+=到OnHit事件上,表示当击中目标后,调用的方法
public void HitResources()
{
    //每次击中后,调用一个小动画
    transform.DOComplete();
    transform.DOShakeScale(0.5f, 0.2f, 10, 90, true);
}

5. 采集

也就是当鼠标点击石块,角色开始移动到附近,并且开始采集、播放攻击动画

攻击动画在合适位置添加关键帧事件
在这里插入图片描述
Actor 代码

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Actor : MonoBehaviour
{
    [HideInInspector] public ActorVisualHandler visualHandler;
    private NavMeshAgent agent;
    private Animator animator;

    //角色需要一个当前的攻击目标,可受伤的攻击目标Damageable
    private Damageable damageableTarget;
    private AnimationEventListener animationEvent;
    public int attack = 10;//角色攻击力 = 10,如果采集物的生命值=100,就要敲击10次

    private Coroutine currentTask;

    private void Start()
    {
        visualHandler = GetComponent<ActorVisualHandler>();
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponentInChildren<Animator>();

        animationEvent = GetComponentInChildren<AnimationEventListener>();
        animationEvent.attackEvent.AddListener(Attack);//将Attack作为事件,在动画帧上执行
    }

    private void Update()
    {
        //Animator是通过人物的速度Speed来切换不同的动画状态片段的
        animator.SetFloat("Speed", Mathf.Clamp(agent.velocity.magnitude, 0, 1));
    }

    //人物的移动, _target表示人物应该移动到的鼠标位置,由于是未知的,所以设置为参数
    public void SetDestination(Vector3 _target)
    {
        agent.SetDestination(_target);
    }

    public void Attack()
    {
        if (damageableTarget)
            damageableTarget.Hit(attack);
    }

    //方法:采集/攻击,这个方法将会在ActorManager脚本中的另一种情况下调用
    public void AttackTarget(Damageable _target)
    {
        StopTask();//这里你要想到一个问题就是,每次采集的都是不同的,所以要忘记上次的任务

        damageableTarget = _target;
        //先走到采集物/敌人的附近,再开始采集/攻击
        currentTask = StartCoroutine(StartAttack());
    }

    IEnumerator StartAttack()
    {
        //情况1: 有点击到敌人,敌人还活着,能采集,继续采集
        while(damageableTarget)
        {
            //首先,我们要走到这个地方啊
            SetDestination(damageableTarget.transform.position);
            //那走到啥位置停下来呢?
            yield return new WaitUntil(() => agent.remainingDistance <= agent.stoppingDistance && !agent.pathPending);
            while(damageableTarget && Vector3.Distance(damageableTarget.transform.position, transform.position) < 4f)
            {
                yield return new WaitForSeconds(1);//每间隔1秒攻击一次,如果太短的话可能会在采集物消失后,多一次攻击动画
                if(damageableTarget)
                {
                    animator.SetTrigger("Attack");//调用Attack动画,采集物消失后,由于角色速度小于等于0.01,就回到Idle动画片段
                    //Instantiate(hitEffect, damageableTarget.transform.position, Quaternion.identity);//别写在这个地方
                }
            }
        }

        //情况2: 采集完了,没东西了
        currentTask = null;
    }

    private void StopTask()
    {
        damageableTarget = null;//首先,让采集目标为空
        if (currentTask != null)//停止正在手头的这份工作,即:立即停止协程的执行
            StopCoroutine(currentTask);
    }
}

ActorManager.cs中调用

private void SetTask()
{
    //首先要注意一点,如果我们框选空气的话,或者随便点击左键,触发这个方法都不该有什么逻辑执行
    if (selectedActors.Count == 0)
        return;

    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    Collider collider;
    if(Physics.Raycast(ray, out RaycastHit hitInfo, 100))
    {
        collider = hitInfo.collider;//获取射线检测到的这个点的Collider组件
        //如果射线检测到的,鼠标点的这个玩意是「地形」的话,移动到这个位置
        if(collider.CompareTag("Terrain"))
        {
            //...
        }
        //如果射线检测到的,鼠标点的这个玩意是「石头/敌人/马车」的话,采集/攻击
        else if(!collider.CompareTag("Player"))
        {
            Damageable damageable = collider.GetComponent<Damageable>();//获取到点击的这个含collider组件的游戏对象
            if(damageable != null)
            {
                foreach(Actor actor in selectedActors)
                {
                    actor.AttackTarget(damageable);//满足条件的选中角色,向鼠标点击的这个含Damageable脚本的游戏对象,调用AttackTarget方法
                }
            }
        }
    }
}

6. 添加粒子效果

“饱暖思淫欲”,我们开始添加一点粒子效果来丰富一下和这个场景的互动

6.1 UnityEvent事件方法实现

修改Actor.cs代码

public GameObject hitEffect;

public void AttackEffect()
{
    if (damageableTarget)
        Instantiate(hitEffect, damageableTarget.transform.position, Quaternion.identity);
}

Actor.cs前面添加事件

private void Start()
{
    animationEvent.attackEvent.AddListener(AttackEffect);
}

拖入粒子特效
在这里插入图片描述
效果
在这里插入图片描述

6.2 c#事件方法实现

我们刚才使用的是UnityEvent事件来完成的,那现在呢,我们用c#事件来看看,比如说销毁的时候啊,我们也给它来一个效果,这里我们新创建了一个脚本:DestroyVisual,将两种类型的采集物,添加到这个脚本

由于我们的事件在Damageable脚本中,我们之后的方法肯定也是添加到这个脚本的事件上的,所以,我们声明并且获取相应的引用,声明销毁的粒子效果,这里创建一个方法,作为事件响应者的事件处理器,简单的Instantiate

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyVisual : MonoBehaviour
{
    private Damageable damageable;
    public GameObject destroyParticle;

    private void Start()
    {
        damageable = GetComponent<Damageable>();
        damageable.OnDestroy += DestroyEffect;//事件的添加
    }

    private void OnDestroy()
    {
        damageable.OnDestroy -= DestroyEffect;//事件的取消订阅,防止内存泄漏
    }

    public void DestroyEffect()
    {
        Instantiate(destroyParticle, transform.position + Vector3.up, Quaternion.identity);
    }

}

拖入销毁粒子效果
在这里插入图片描述
效果
在这里插入图片描述

最终代码

完整版: https://pan.baidu.com/s/1alZejJUkcbwvdYJpdGxq-A 密码: s412

参考

【unity官方短视频】https://www.bilibili.com/video/BV1fy4y1J7Es
【视频】:https://www.bilibili.com/video/BV1zK4y1Q74m/?spm_id_from=333.999.0.0&vd_source=2526a18398a079ddb95468a0c73f126e
【视频】:https://www.bilibili.com/video/BV1wz4y1m7LM/?spm_id_from=333.999.0.0&vd_source=2526a18398a079ddb95468a0c73f126e
【视频】:https://www.bilibili.com/video/BV1jV411v74U/?spm_id_from=333.999.0.0&vd_source=2526a18398a079ddb95468a0c73f126e

完结

如果你有其他更好的方法也欢迎评论分享出来,当然如果发现文章中出现了问题或者疑问的话,也欢迎评论私信告诉我哦

好了,我是向宇,https://xiangyu.blog.csdn.net/

一位在小公司默默奋斗的开发者,出于兴趣爱好,于是开始自习unity。最近创建了一个新栏目【你问我答】,主要是想收集一下大家的问题,有时候一个问题可能几句话说不清楚,我就会以发布文章的形式来回答。 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

XSS 攻击的检测和修复方法

XSS 攻击的检测和修复方法 XSS&#xff08;Cross-Site Scripting&#xff09;攻击是一种最为常见和危险的 Web 攻击&#xff0c;即攻击者通过在 Web 页面中注入恶意代码&#xff0c;使得用户在访问该页面时&#xff0c;恶意代码被执行&#xff0c;从而导致用户信息泄露、账户被…

C# Linq 详解一

目录 一、概述 二、Where 三、Select 四、GroupBy 五、First / FirstOrDefault 六、Last / LastOrDefault​​​​​​​ 一、概述 语言集成查询 (LINQ) 是一系列直接将查询功能集成到 C# 语言的技术统称。 数据查询历来都表示为简单的字符串&#xff0c;没有编译时类型…

51单片机--定时器与按键控制流水灯模式

文章目录 定时器定时器的介绍定时器的作用定时器框图定时器的工作模式计数器原理图&#xff1a; 中断系统中断程序流程STC89C52的中断资源定时器和中断系统 定时器的相关寄存器TCONTMODTH与TL有关中断的寄存器 按键控制流水灯模式 定时器 定时器的介绍 51单片机上的定时器是一…

基于Springboot的宠物店管理系统(源代码+数据库)087

基于Springboot的宠物店管理系统(源代码数据库)087 一、系统介绍 本系统分为管理员、店员两种角色 店员角色包含以下功能&#xff1a; 登录、宠物主人管理、宠物管理、宠物医疗管理、宠物销售管理、宠物寄养管理、宠物用品管理、宠物日常服务管理、宠物常见问题、个人中心、…

CSDN 个性化推荐系统的设计和演进

个性化推荐项目 个性化推荐的设计和演进项目概览项目梳理依赖管理实现代码的重构和改进持续演化 个性化推荐的设计和演进 CSDN 的个性化推荐系统&#xff0c;是从既有的推荐项目中剥离出来的一个子项目&#xff0c;这个项目随后移交到了我们AI组。在近一年的时间内&#xff0c…

Apollo、RocketMQ加载顺序问题

在SpringCloudAlibaba框架中&#xff0c;因Nacos配置中心管理权限过于简单&#xff0c;决定用Apollo代替Nacos配置中心&#xff0c;但在启动时&#xff0c;Nacos、Redis等配置读取正常&#xff0c;RocketMQ由于启动过早&#xff0c;无法从Apollo读取自己的服务地址配置。 报错…

第41节:cesium 闪烁效果-熊出没(含源码+视频)

结果示例: 点的闪烁:1.逐渐放大后消失;2.点闪烁。 图的闪烁:熊出没,含自定义显示文字效果。 完整源码: <template><div class="viewer"><vc-viewer @ready="ready" :logo="false">

科技资讯|苹果Vision Pro手部追踪和手势相关新专利曝光

近日&#xff0c;美国专利商标局正式授予苹果一项与 Apple Vision Pro 主要功能相关的专利&#xff1a;手部追踪和手指手势。 苹果专利指出&#xff0c;沉浸感的质量取决于几个重要因素。例如&#xff0c;显示器的特性&#xff0c;如图像质量、帧率、像素分辨率、高动态范围 …

信号波形时序图常用工具推荐

Refer: 下载&#xff1a;画时序图的四大神器_可编程器件-面包板社区 (eet-china.com) 软件工程师&#xff0c;习惯使用StarUML画Timing Diagram, 硬件工程师建议使用一下软件。 1、AndyTiming 免费的&#xff0c;这个有一个知乎的博文可以参考 https://zhuanlan.zhihu.com/p…

如何成为微软MVP?

对一个普通的开发人员来说&#xff0c;最大的认可就是得到微软官方的MVP 认证了&#xff0c;是一份对技术人的荣誉证书。 微软的MVP是相对公平公正的&#xff0c;只要你热爱技术&#xff0c;热爱分享&#xff0c;在一定的领域里有足够的深度&#xff0c;就会得到微软官方的认证…

MURF2080CT-ASEMI快恢复二极管对管MURF2080CT

编辑&#xff1a;ll MURF2080CT-ASEMI快恢复二极管对管MURF2080CT 型号&#xff1a;MURF2080CT 品牌&#xff1a;ASEMI 封装&#xff1a;TO-220F 恢复时间&#xff1a;75ns 正向电流&#xff1a;20A 反向耐压&#xff1a;800V 芯片个数&#xff1a;2 引脚数量&#xf…

〖码银送书第三期〗《Python机器学习:基于PyTorch和Scikit-Learn》

前言 近年来&#xff0c;机器学习方法凭借其理解海量数据和自主决策的能力&#xff0c;已在医疗保健、 机器人、生物学、物理学、大众消费和互联网服务等行业得到了广泛的应用。自从AlexNet模型在2012年ImageNet大赛被提出以来&#xff0c;机器学习和深度学习迅猛发展&#xf…

WebDAV之π-Disk派盘 + PDF Expert

PDF Expert 支持WebDAV方式连接π-Disk派盘。 PDF Expert是一款macOS上的办公软件,它具有专业的PDF编辑功能,可以快速从邮件、网页支持PDF打开,支持用户进行阅读、批注等功能,用户可以直接在PDF上进行编辑文字图片,表单文档、创建笔记、添加书单等自定义使用,大大提高工…

C国演义 [第五章]

第五章 子集题目理解步骤树形结构递归函数递归结束的条件单层逻辑 代码 子集II题目理解步骤树形结构递归函数递归结束的条件单层逻辑 代码 子集 力扣链接 给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。…

HarmonyOS/OpenHarmony应用开发-程序包多HAP机制(上)

一、多HAP机制设计目标 方便开发者模块化的管理应用&#xff0c;好的应用一般都是模块化管理&#xff0c;模块之间属于松耦合关系。多HAP方便了开发者将业务划分成多个模块&#xff0c;每个模块放到独立的HAP中。例如支付类应用&#xff0c;有统一的主界面&#xff0c;主界面管…

Windows mingw64 最简易 安装配置

其实挺简单一件事 很多教程都搞复杂了 自己写一个 只需要两步 1. 下载压缩包并解压 2. 配置环境变量 (1). GitHub 下载地址 Releases niXman/mingw-builds-binaries GitHub 如果GitHub下载太慢可以来这里加速 或者用地址2 GitHub Proxy 代理加速 (ghproxy.com) (2). 下…

学无止境·MySQL⑥(数据库备份和还原)

数据库备份和还原 备份和还原练习1、创建库和表2、使用mysqldump命令备份数据库中的所有表3、备份booksDB数据库中的books表4、使用mysqldump备份booksDB和test数据库5、使用mysqldump备份服务器中的所有数据库6、使用mysql命令还原第二题导出的book表7、进入数据库使用source命…

Rainbond开源

Rainbond的 Gateway API 插件制作实践 Gateway API 作为新一代的流量管理标准&#xff0c;对原有 Ingress 的扩展不规范、移植性差等问题做出了改进。从兼容K8s生态和优化网关体验出发&#xff0c;Rainbond 支持以插件的形式扩展平台网关能力&#xff0c;目前已经有多家社区提供…

领域知识图谱的医生推荐系统:利用BERT+CRF+BiLSTM的医疗实体识别,建立医学知识图谱,建立知识问答系统

项目设计集合&#xff08;人工智能方向&#xff09;&#xff1a;助力新人快速实战掌握技能、自主完成项目设计升级&#xff0c;提升自身的硬实力&#xff08;不仅限NLP、知识图谱、计算机视觉等领域&#xff09;&#xff1a;汇总有意义的项目设计集合&#xff0c;助力新人快速实…

DevOps基础服务1——版本控制gitlab

文章目录 一、基本了解1.1 安装git客户端1.2 git命令1.2.1 本地仓库1.2.2 远程仓库 二、安装gitlab三、功能管理3.1 创建账号3.2 用户注册授权通知功能3.3 创建项目远程库3.4 ssh设置3.5 克隆远程库项目到本地3.6 上传本地项目代码到远程库3.7 授权用户查看项目权限 一、基本了…