【Unity】简单机甲运动系统——坦克式操控方式

最近两天想做一个人形机甲的游戏,由于本人又是一个拟真军事爱好者,不太喜欢机动特别高的,所以打算参考坦克类游戏来制作一个脚!踏!实!地!的机甲游戏

这个运动系统基本实现了逻辑和动画的分离,同时也是使用状态机的方式来进行运动的控制

案例分为三大部分

Unity机甲

文章目录

    • 前言
    • 第一部分:状态机部分
      • 一、图
      • 二、基础代码
        • State
        • StateMachine
        • PlayerState
        • PlayerStateMachine
      • 三、上层状态代码
        • PlayerGroundState
        • PlayerIdleState
        • PlayerMoveState
        • PlayerJumpState
        • PlayerFallingState
      • 四、使用状态机
    • 第二部分:预制动画部分
      • AnimatorController
      • PlayerAnimator.cs
    • 第三部分:输入驱动部分
      • PlayerController
    • 额外章节

前言

我的代码是在官方的HDRP3DSample里提供的PlayerMovementCameraController的基础上改的,但基本就是复用了里面对移动速度的计算和光标输入的接受和隐藏这些

鉴于UE仍然是我接触代码前最常使用的游戏引擎,因此我在地图原点放了一个空物体,充当PlayerController,我的这个PlayerController只负责接收玩家的鼠标输入,并使用其修改自身的旋转。该PlayerController还是一个单例类,对外提供了获取自身的Quaternion和Transform的方法

我还在PlayerController中制作了一个列表,编辑器界面可以对其添加物体,添加的物体有以下字段

  • 跟随PlayerController旋转的Transform
  • Transform的父级(null则会寻找直接父级)
  • 是否同步水平/垂直旋转
  • 是否限制水平/垂直旋转
  • 水平/垂直的限制范围(-180 ~ 180)
  • 水平/垂直的旋转速率(度每秒)

这样包括机甲的炮塔,炮管,玩家的摄像机都可以通过将其添加到这个列表来解决旋转的问题

这一块也花费了我大半天的时间才调试完成

详细信息放在第三部分进行阐述



第一部分:状态机部分

一、图

目前运动比较简单,只提供Idle,Walk,Jump,Falling四个状态,之后新的状态也可以直接扩展,比较方便

在这里插入图片描述

在Idle和Move上再抽象出一层GroundState,可以方便的处理二者共同的转换条件

二、基础代码

先看一下状态机的基础部分,即State和StateMachine

State

我在State中创建委托来防止在Enter和Exit中书写过多其他逻辑,实现相关代码的解耦

其他的就是普通的State的所需函数Enter,Exit,Update

值得注意的是该类不需要继承自Monobehavior,这也意味着我们可以使用构造函数来为类赋初值

public class State
{public StateMachine stateMachine;public UnityAction onStateEnter;public UnityAction onStateExit;public UnityAction<bool> onStateChanged;    //进入true,离开falsepublic State(StateMachine stateMachine){this.stateMachine=stateMachine;}public virtual void Enter(){onStateEnter?.Invoke();onStateChanged?.Invoke(true);}public virtual void Exit(){onStateExit?.Invoke();onStateChanged?.Invoke(false);}public virtual void Update(){}
}
StateMachine

为了保证通用性,我在普通的StateMachine中只提供了最低限度的函数和字段,即一个记录当前状态,一个初始化函数,一个切换状态的函数和一个调用状态的Update的函数

值得注意的是该类也不需要继承自Monobehavior,因为这个类将作为其他类的字段出现,依附于其他类的生命周期函数即可,无需自身也继承。

public class StateMachine
{public State curState;public virtual void Init(State state){curState = state;curState.Enter();}public virtual void Update(){curState.Update();}public virtual void ChangeState(State newState){curState.Exit();curState = newState;curState.Enter();}
}
PlayerState

在PlayerState中,为了后续类继承使用的方便,我分别新增了一个玩家字段和一个更确定的PlayerStateMachine字段

因为显而易见,PlayerState肯定是依赖于PlayerStateMachine,因此将原本State中的StateMachine字段覆盖为PlayerStateMachine是完全合理的。

public class PlayerState : State
{public PlayerMovement player;public new PlayerStateMachine stateMachine;public PlayerState(PlayerMovement player, PlayerStateMachine stateMachine) : base(stateMachine){this.player = player;this.stateMachine = stateMachine;}
}
PlayerStateMachine

因为为了外部更方便的调用,并且实现区分运动状态的目标,我在PlayerStateMachine中存储了所有状态,并使用枚举值对其进行区分,外界可直接使用枚举进行获取。

虽然理论上可以在初始化时直接实例化所有状态,但我还是选择了提供一个AddPlayerState来让外界去访问并传入对应的对象,原因如下:

  • 状态类特殊字段: 某些派生的状态可能希望获取一些特殊的值,这些值在外部构造其时可以提供给其,在StateMachine内部构造则不一定能获取到这些值
  • 多态: 外部可根据自身情况传入不同的状态子类对象,从而实现更多不同的功能,充分利用多态的特点
public class PlayerStateMachine : StateMachine
{public enum EState{Idle,Move,Jump,Falling}private Dictionary<EState, PlayerState> states = new Dictionary<EState, PlayerState>();public virtual void Init(EState type, PlayerState state){AddPlayerState(type, state);base.Init(state);}public override void Update(){base.Update();}public virtual void ChangeState(EState type){base.ChangeState(GetPlayerState(type));}public State GetPlayerState(EState type){return states[type];}public void AddPlayerState(EState type, PlayerState state){if(!states.ContainsKey(type)){states.Add(type, state);}}
}

三、上层状态代码

接下来就是实际的状态代码了!

PlayerGroundState

为了保证低耦合的设计,我在player中创建了一系列委托来提供给State进行订阅

由上面的图可以很显然的知道,Ground状态最主要就是和Jump,FallingState进行转换

  • 玩家输入Jump指令就转换为Jump状态
  • 玩家脚不着地就转为Falling状态
public class PlayerGroundState : PlayerState
{public PlayerGroundState(PlayerMovement player, PlayerStateMachine stateMachine) : base(player, stateMachine){}public override void Enter(){base.Enter();base.player.onJump += OnJump;}public override void Exit(){base.Exit();player.onJump -= OnJump;}public override void Update(){base.Update();if (!player.isGrounded){stateMachine.ChangeState(PlayerStateMachine.EState.Falling);}}private void OnJump(){stateMachine.ChangeState(PlayerStateMachine.EState.Jump);}
}
PlayerIdleState

继承自Ground状态,主要负责转向Move状态,绑定玩家输入,一旦输入移动就转到移动状态

public class PlayerIdleState : PlayerGroundState
{public PlayerIdleState(PlayerMovement player, PlayerStateMachine stateMachine) : base(player, stateMachine){}public override void Enter(){base.Enter();base.player.onMove += OnMove;}public override void Exit(){base.Exit();player.onMove -= OnMove;}private void OnMove(Vector2 move){if(move.magnitude > 0){stateMachine.ChangeState(PlayerStateMachine.EState.Move);return;}}
}
PlayerMoveState

该Move状态主要负责移动和转为Idle状态

注意该移动是相对于玩家控制器的移动

public class PlayerMoveState : PlayerGroundState
{private CharacterController controller;private float speed;public PlayerMoveState(PlayerMovement player, PlayerStateMachine stateMachine) : base(player, stateMachine){this.controller = player.controller;this.speed = player.speed;}public override void Enter(){base.Enter();player.onMove += OnMove;}public override void Exit(){base.Exit();player.onMove -= OnMove;}private void OnMove(Vector2 input){//规格化的水平移动方向Vector3 move = PlayerController.GetControllerTransform().right * input.x + PlayerController.GetControllerTransform().forward * input.y;move.y = 0;move.Normalize();//该速度乘数意味着玩家朝向与输入方向差异越大,则速度乘数越小float speedMultipler = (Vector3.Dot(new Vector3(move.x, 0, move.z), player.transform.forward) + 1) / 2;controller.Move(move * speed * speedMultipler * Time.deltaTime);if (input.magnitude < 0.01)stateMachine.ChangeState(PlayerStateMachine.EState.Idle);}
}
PlayerJumpState

因为机甲蓄力一下再起跳会比较真实,因此引入了起跳准备这一概念,体现在该State中就是使用一个Timer来延迟起跳的时间

public class PlayerJumpState : PlayerState
{private CharacterController controller;private float speed;private float airSpeedMultipler;private float jumpHeight;private float jumpReadyTime;private float jumpReadyTimer;public PlayerJumpState(PlayerMovement player, PlayerStateMachine stateMachine) : base(player, stateMachine){controller = player.controller;speed = player.speed;jumpHeight = player.jumpHeight;jumpReadyTime = player.jumpReadyTime;}public override void Enter(){base.Enter();jumpReadyTimer = jumpReadyTime;}public override void Update(){base.Update();jumpReadyTimer -= Time.deltaTime;if(jumpReadyTimer < 0 ){//起跳速度计算player.velocity.y = Mathf.Sqrt(jumpHeight * -2f * player.gravity) + player.gravity * Time.deltaTime;controller.Move(player.velocity * Time.deltaTime);stateMachine.ChangeState(PlayerStateMachine.EState.Falling);}}
}
PlayerFallingState

这个状态实际上也是我决定把一个PlayerMovement类扩展为一整个状态机的原因,因为分辨不了在空中究竟是玩家掉下去的还是起跳悬空的

同时使用一个airSpeedMultipler来修改玩家在空中的灵活度

public class PlayerFallingState : PlayerState
{private CharacterController controller;private float speed;private float airSpeedMultipler;public PlayerFallingState(PlayerMovement player, PlayerStateMachine stateMachine) : base(player, stateMachine){controller = player.controller;speed = player.speed;airSpeedMultipler = player.airSpeedMultipler;}public override void Enter(){base.Enter();player.onMove += OnMove;}public override void Exit(){base.Exit();player.onMove -= OnMove;}public override void Update(){base.Update();if (player.isGrounded)stateMachine.ChangeState(PlayerStateMachine.EState.Idle);}private void OnMove(Vector2 input){Vector3 move = PlayerController.GetControllerTransform().right * input.x + PlayerController.GetControllerTransform().forward * input.y;move.y = 0;move.Normalize();controller.Move(move * speed * airSpeedMultipler * Time.deltaTime);}
}

四、使用状态机

状态机的代码书写完毕,接下来是使用状态机。将移动的代码移动到状态里后,原本的PlayerMovement就只需要去处理重力即可,因为无论什么时候都会存在重力

同时该类也提供所有外部可以自定义的角色属性

public class PlayerMovement : MonoBehaviour
{public CharacterController controller;[Header("Movement")]public float speed = 12f;public float gravity = -10f;public float jumpHeight = 2f;public float jumpReadyTime = 0.5f;public float airSpeedMultipler = 0.5f;public float turnSpeed = 4;[Header("Collision")]public Transform groundCheck;public Vector3 groundCheckRange;public LayerMask groundMask;//self state[HideInInspector] public Vector3 velocity;private Vector3 lastPosition;public bool isGrounded { get; private set; }//public actionpublic UnityAction<Vector2> onMove;public UnityAction onJump;//inputInputAction movement;InputAction jump;//All Statepublic PlayerStateMachine stateMachine { get; private set;} = new PlayerStateMachine();private void Awake(){stateMachine.Init(PlayerStateMachine.EState.Idle, new PlayerIdleState(this, stateMachine));stateMachine.AddPlayerState(PlayerStateMachine.EState.Move, new PlayerMoveState(this, stateMachine));stateMachine.AddPlayerState(PlayerStateMachine.EState.Jump, new PlayerJumpState(this, stateMachine));stateMachine.AddPlayerState(PlayerStateMachine.EState.Falling, new PlayerFallingState(this, stateMachine));lastPosition = transform.position;}void Start(){movement = new InputAction("PlayerMovement");movement.AddCompositeBinding("Dpad").With("Up", "<Keyboard>/w").With("Up", "<Keyboard>/upArrow").With("Down", "<Keyboard>/s").With("Down", "<Keyboard>/downArrow").With("Left", "<Keyboard>/a").With("Left", "<Keyboard>/leftArrow").With("Right", "<Keyboard>/d").With("Right", "<Keyboard>/rightArrow");jump = new InputAction("PlayerJump");jump.AddBinding("<Keyboard>/space");movement.Enable();jump.Enable();movement.performed += (InputAction.CallbackContext context) => jump.performed += (InputAction.CallbackContext context) => onJump?.Invoke();}// Update is called once per framevoid Update(){stateMachine.Update();HandleGravity();HandleMove();}void HandleGravity(){isGrounded = Physics.CheckBox(groundCheck.position, groundCheckRange, Quaternion.identity, groundMask);if (isGrounded && velocity.y < 0){velocity.y = -2f;}velocity.y += gravity * Time.deltaTime;controller.Move(velocity * Time.deltaTime);velocity.x = transform.position.x - lastPosition.x;velocity.z = transform.position.z - lastPosition.z;lastPosition = transform.position;}//这个并非处理移动的,而是处理自身旋转朝向运动方向的void HandleMove(){Vector2 input = movement.ReadValue<Vector2>();onMove?.Invoke(input);Vector3 move = PlayerController.GetControllerTransform().right * input.x + PlayerController.GetControllerTransform().forward * input.y;move.y = 0;move.Normalize();if (move.magnitude > 0.1){Quaternion q = Quaternion.LookRotation(move);transform.rotation = Quaternion.Slerp(transform.rotation, q, turnSpeed * Time.deltaTime);}}
}

第二部分:预制动画部分

预制动画部分也是就三个部分

  • 行走动画
  • 起步,行走循环
  • 起跳,悬空循环,落地

AnimatorController

接下来是动画状态机,实际上可能也是最难的地方,因为太容易结成蜘蛛网了()

然一共就四个状态,但还是加了两个额外的动画作为过渡,分别是Falling_End和Walk_Start

在这里插入图片描述

PlayerAnimator.cs

动画Animator的参数修改是专门建一个类进行管理,这也是解耦的地方

玩家的速度用于行走动画的速度更改,写在Update中

public class PlayerAnimator : MonoBehaviour
{private Animator animator;private PlayerMovement player;public float velocityToAnimationMultiper = 2.5f;public float minAnimSpeed = 0.5f;public float maxAnimSpeed = 10f;private void Awake(){animator = GetComponent<Animator>();player = GetComponentInParent<PlayerMovement>();}// Start is called before the first frame updatevoid Start(){//状态变量绑定player.stateMachine.GetPlayerState(PlayerStateMachine.EState.Idle).onStateChanged += (enter) => animator.SetBool("IsIdle", enter);player.stateMachine.GetPlayerState(PlayerStateMachine.EState.Move).onStateChanged += (enter) => animator.SetBool("IsMove", enter);player.stateMachine.GetPlayerState(PlayerStateMachine.EState.Jump).onStateChanged += (enter) => animator.SetBool("IsJump", enter);player.stateMachine.GetPlayerState(PlayerStateMachine.EState.Falling).onStateChanged += (enter) => animator.SetBool("IsFalling", enter);}private void Update(){Vector2 velocity = new Vector2(player.velocity.x, player.velocity.z);animator.SetFloat("WalkSpeedMultipler", Mathf.Clamp(velocity.magnitude * velocityToAnimationMultiper, minAnimSpeed, maxAnimSpeed));}
}

第三部分:输入驱动部分

为了区分玩家的朝向和摄像机的朝向,并且希望能够限制跟随摄像机旋转的物体的速率和角度

我借鉴UE的做法,在地图上放置了一个空物体用于记录玩家的Controller旋转,和玩家操控的Actor旋转做个区分

鉴于当前只有一个玩家,因此使用单例方便进行获取控制器的旋转和变换

以下是控制器内部代码

PlayerController

public class PlayerController : MonoBehaviour
{[System.Serializable]public class Limit{public float min = -90;	//限制最小角度public float max = 90;	//限制最大角度}[System.Serializable]public class SyncObject{public Transform syncTransform;		//需要同步旋转的组件public Transform parentTransform;	//该组件的父级(不赋值就寻找直接父级),有的可能希望计算限制角时和与自己同级或没关系的物体进行计算,因此专门使用一个字段进行赋值//对外界提供控制暂停,该项可以忽略暂停//比如使用自由查看,即其他物体暂停跟随移动,摄像机可以继续环绕玩家观察,就可以把摄像机勾上这个选项public bool ignorePause = false;[Header("Horizontal")]public bool SyncY = true;	//是否同步public float RateY = 3600;	//旋转速率public bool LimitY = false;	//是否限制public Limit LimitYRange;	//限制范围//垂直与水平同理[Header("Vertical")]public bool SyncX = true;public float RateX = 3600;public bool LimitX = false;public Limit LimitXRange;}private static PlayerController m_instance;float RotX = 0, RotY = 0;private bool paused = false;public List<SyncObject> syncControllerRotationObjects;//单例常规操作private void Awake(){if(m_instance){Destroy(gameObject);return;}m_instance = this;}//从原本Demo的CameraController里粘贴过来的private void Update(){bool unlockPressed = false, lockPressed = false;float mouseX = 0, mouseY = 0;//捕捉输入if (Mouse.current != null){var delta = Mouse.current.delta.ReadValue() / 15.0f;mouseX += delta.x;mouseY += delta.y;lockPressed = Mouse.current.leftButton.wasPressedThisFrame ||Mouse.current.rightButton.wasPressedThisFrame;}if (Keyboard.current!= null){unlockPressed = Keyboard.current.escapeKey.wasPressedThisFrame;}//按照输入进行光标设置if (unlockPressed){Cursor.lockState = CursorLockMode.None;Cursor.visible = true;}if (lockPressed){Cursor.lockState = CursorLockMode.Locked;Cursor.visible = false;}if (Cursor.lockState == CursorLockMode.Locked){//控制器旋转RotX -= mouseY;RotY += mouseX;//限制控制器的俯仰角RotX = RotX > 180 ? RotX - 360 : RotX;RotX = Mathf.Clamp(RotX, -89, 89);transform.rotation = Quaternion.Euler(RotX, RotY, 0f);//遍历需要跟踪控制器旋转的物体,对其进行旋转赋值foreach (var obj in syncControllerRotationObjects){//对跟踪的物体进行修改旋转,下面细说这一块}}}//设置控制器旋转 float floatpublic static void SetControllerRotation(float rotX, float rotY){m_instance.RotX = rotX;m_instance.RotY = rotY;m_instance.transform.rotation = Quaternion.Euler(rotX, rotY, 0f);}//设置控制器旋转 Quaternionpublic static void SetControllerRotation(Quaternion quaternion){SetControllerRotation(quaternion.eulerAngles.x, quaternion.eulerAngles.y);}//获取旋转和Transformpublic static Quaternion GetControllerRotation() => m_instance.transform.rotation;public static Transform GetControllerTransform() => m_instance.transform;//设置暂停public static void SetPause(bool paused) => m_instance.paused = paused;
}

最重量级的部分就是上述遍历每一个需要跟踪的物体的部分,下面单独贴出来

//遍历需要跟踪控制器旋转的物体,对其进行旋转赋值
foreach (var obj in syncControllerRotationObjects)
{if (paused && !obj.ignorePause)continue;//控制器原始角度	0 ~ 360float x = transform.rotation.eulerAngles.x;float y = transform.rotation.eulerAngles.y;//旋转物体和其父级的角度(名为dir而已)Vector3 dir = obj.syncTransform.eulerAngles;Vector3 parentDir = obj.parentTransform ? obj.parentTransform.eulerAngles : obj.syncTransform.parent.eulerAngles;//控制器角度(映射到了-180 ~ 180)float conDirX = x > 180 ? x - 360 : x;float conDirY = y > 180 ? y - 360 : y;//当前的旋转角度(映射到了-180 ~ 180)float dirX = dir.x > 180 ? dir.x - 360 : dir.x;float dirY = dir.y > 180 ? dir.y - 360 : dir.y;//父级的旋转角度(映射到了-180 ~ 180)float parentDirX = parentDir.x > 180 ? parentDir.x - 360 : parentDir.x;float parentDirY = parentDir.y > 180 ? parentDir.y - 360 : parentDir.y;//当前和父级的相对角度(用于计算限制角度)float angleX = parentDirX - dirX;float angleY = dirY - parentDirY;//用于线性平滑旋转//控制器的旋转角度(偏移一下,以dir为起点,方便计算到目标所需角度)float targetAngleX = (x - dir.x + 360) % 360;float targetAngleY = (y - dir.y + 360) % 360;//接着将0~360映射为 -180 ~ 180,此为在以当前朝向为原点下,控制器与原点的相对角度差targetAngleX = targetAngleX > 180 ? targetAngleX - 360 : targetAngleX;targetAngleY = targetAngleY > 180 ? targetAngleY - 360 : targetAngleY;if (obj.SyncX){Vector2 rotation = obj.syncTransform.rotation.eulerAngles;//如果当前角度差绝对值小于旋转速率,直接转到目标角度if (Mathf.Abs(targetAngleX) <= obj.RateY * Time.deltaTime){rotation.x = x;angleX = parentDirX - conDirX;}//目标在当前物体朝向右边,增大自身角度else if (targetAngleX > 0){rotation.x += obj.RateX * Time.deltaTime;angleX -= obj.RateX * Time.deltaTime;}//目标在当前物体朝向左边,减小自身角度else{rotation.x -= obj.RateX * Time.deltaTime;angleX += obj.RateX * Time.deltaTime;}//角度限制判断,angleX已经是变换后的角度了if((obj.SyncX && angleX >= obj.LimitXRange.min && angleX <= obj.LimitXRange.max) || !obj.LimitX){obj.syncTransform.rotation = Quaternion.Euler(rotation);}}//该轴与上面的计算同理if(obj.SyncY){Vector2 rotation = obj.syncTransform.rotation.eulerAngles;if (Mathf.Abs(targetAngleY) <= obj.RateY * Time.deltaTime){rotation.y = y;angleY = parentDirY - conDirY;}else if (targetAngleY > 0){rotation.y += obj.RateY * Time.deltaTime;angleY -= obj.RateY * Time.deltaTime;}else{rotation.y -= obj.RateY * Time.deltaTime;angleY += obj.RateY * Time.deltaTime;}if(!obj.LimitY || (angleY >= obj.LimitYRange.min && angleY <= obj.LimitYRange.max)){obj.syncTransform.rotation = Quaternion.Euler(rotation);}}
}

除此之外就是在编辑器中的实际应用,下面贴几个

在这里插入图片描述

额外章节

这一块讲一下关于相机的问题,我使用的是Cinemachine

我希望通过Cinemachine来创建一个过肩动画,有碰撞,有跟随延迟

最开始采用Freelook + CameraCollider + CameraOffset的方式,结果发现CameraCollider 会默认在摄像机原点检测碰撞,而CameraOffset会偏移出这个碰撞体,不得已只能去寻找下一个解决办法

最后我采用的是和Unity官方的第三人称一样的解决办法,使用VirtualCamera组件下的3rdPersonFollow,通过在玩家身上增加一个空物体作为跟随点,再加上3rdPersonFollow自带的参数微调即可达到较为满意的效果

在这里插入图片描述

并且可以方便的切换左右肩,后续似乎也可以和瞄准,第一人称进行很好的配合

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

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

相关文章

低代码用户中心的构建与应用

引言 在现代软件开发中&#xff0c;低代码平台因其高效、灵活、用户友好的特性而逐渐受到青睐。特别是在用户中心的构建方面&#xff0c;低代码平台能够显著提升开发效率&#xff0c;降低开发成本。本文将探讨如何利用低代码平台构建一个高效的用户中心&#xff0c;并分享一些…

002:显示DICOM图像(替换掉 vtkImageViewer2 )

VTK 医学图像处理---DICOM图像显示 对第一个DICOM显示例子的展开&#xff08;替换掉vtkImageViewer2类&#xff09; 两个例子实现的效果对比&#xff0c;其中右侧是对第一个例子展开后的显示效果&#xff0c;展示了一个完整的VTK渲染管线的过程。 目录 VTK 医学图像处理---DIC…

神策SDK不支持Windows客户端全埋点,怎么实现用户统计分析?

本文将介绍&#xff0c;ClkLog针对神策不支持全埋点的客户端实现用户访问基础统计分析 1。 客户遇到的问题 ClkLog的用户访问基础统计分析功能是基于神策SDK的全埋点来实现的。 我们遇到有些客户是使用C、C#等语言来开发的客户端&#xff0c;然而神策此类SDK&#xff08;如C, C…

[B站大学]Zotero7教程

参考资料: https://www.bilibili.com/video/BV1PSvUetEQX 2. 账号注册与同步 本节内容参考zotero中文社区文档&#xff1a;https://zotero-chinese.com/user-guide/sync 2.1 数据同步 首先注册一个Zotero官方账户。登录账号密码。 2.2 文件同步 按照文档&#xff0c;推荐…

快消品渠道开发方案,让你拥有源源不断的批发客户!

对于快消品行业来说&#xff0c;优质批发渠道客户&#xff0c;决定了你产品的销量。这篇文章&#xff0c;和大家分享下&#xff0c;如何开发渠道端客户&#xff01; 1、分析目标市场 首先&#xff0c;分析快消品行业的趋势&#xff0c;以及你们的产品&#xff0c;目前市场占有…

Java-线程的生命周期7大状态

在 Java 中&#xff0c;线程的生命周期可以分为多个状态&#xff0c;这些状态描述了线程从创建到终止的整个过程。Java 线程的生命周期主要包括以下七大状态&#xff1a; 1.新建状态&#xff08;New&#xff09; 当一个线程对象被创建但尚未调用 start() 方法时&#xff0c;线…

Apache SeaTunnel Zeta 引擎源码解析(一)Server端的初始化

引入 本系列文章是基于 Apache SeaTunnel 2.3.6版本&#xff0c;围绕Zeta引擎给大家介绍其任务是如何从提交到运行的全流程&#xff0c;希望通过这篇文档&#xff0c;对刚刚上手SeaTunnel的朋友提供一些帮助。 我们整体的文章将会分成三篇&#xff0c;从以下方向给大家介绍&am…

鸿蒙(API 12 Beta6版)图形【使用Drawing实现图形绘制与显示 (C/C++)】方舟2D图形服务

场景介绍 Native Drawing模块提供了一系列的接口用于基本图形和字体的绘制。 Drawing绘制的内容无法直接在屏幕上显示&#xff0c;需要借用XComponent以及Native Window的能力支持&#xff0c;将绘制的内容通过Native Window送显。 接口说明 Drawing常用接口如下表所示。 …

二分查找:手拿把掐!------Java代码实现

“没有天赋,那就不断重复.” 文章目录 前言文章有误敬请斧正 不胜感恩&#xff01;模板一:(最基本的)**左闭右闭:** [left,right] 模板二:**左闭右开区间模板:**区间:左闭右开[left,right): 模板三:开区间模板:(left,right) 循环不变量:二分查找易错点:做题经验:疑问及解答&…

内衣内裤衣机什么牌子好?五款口碑爆棚王炸机型推荐

如今科技是越来越发展了&#xff0c;迷你洗衣机的功能也是越来越强大了&#xff0c;这样小户型的家庭甚是喜爱&#xff0c;不仅解决了清洗衣物的问题&#xff0c;还能让小型洗衣机在家中起到一定的装饰效果。在清洁衣物的污渍的同时&#xff0c;还能有效除去衣物上的各种细菌。…

upload-labs闯关攻略

pass-1 提前准备好的一个PHP木马&#xff0c;然后将后缀名改为jpg上传 然后在上传的过程中利用抓包&#xff0c;将抓取到的包里面的后缀jpg改为php如图所示&#xff0c;然后放行 接着我们去访问上传的图片信息&#xff0c;如下图所示就为成功 pass-2 提前准备好的一个PHP木马…

http连接处理(最新版)

分析http类及请求接收 基础 epoll epoll_create函数 #include <sys/epoll.h> int epoll_create(int size) 创建一个指示epoll内核事件表的文件描述符&#xff0c;该描述符将用作其他epoll系统调用的第一个参数&#xff0c;size不起作用。 epoll_ctl函数 #include …

紫光同创——PLL IP 的使用(Logos2)

本文档主要针对 Logos2 系列的 PLL 配置&#xff0c;至于 Logos 系列的 PLL&#xff0c;可以参考《PLLIP 的使用(Logos)》的文档。 一、PLL IP 介绍 1、PLL 基本配置模式 Basic Configurations PLL IP 是紫光同创基于 PLL 及时钟网络资源设计的 IP&#xff0c;通过不同的参数配…

牛客周赛 Round 58(ABCDF)

目录 A.会赢吗&#xff1f; B.能做到的吧 C.会赢的&#xff01; D.好好好数 F.随机化游戏时间 A.会赢吗&#xff1f; 思路&#xff1a; 签到题&#xff0c;比大小 void solve() {double a,b;cin>>a>>b;if(a>b) cout<<"NO";else cout<&…

ByteTrack多目标跟踪(一)—理论基础

ByteTrack多目标跟踪 算法概述 github: https://github.com/ifzhang/ByteTrack ByteTrack是一种基于Tracking-by-Detection范式的多目标跟踪算法。 先前的多目标追踪算法一般在完成当前帧的目标检测后只会保留置信度比较大的检测框用于进行目标跟踪&#xff0c;比如图中置信度…

思维导图在线制作怎么制作?5个软件教你快速进行思维导图制作

思维导图在线制作怎么制作&#xff1f;5个软件教你快速进行思维导图制作 思维导图是一种用于组织信息、梳理思路和激发创意的可视化工具。在线制作思维导图可以帮助你随时随地进行创作和分享&#xff0c;以下是五款在线思维导图工具&#xff0c;可以帮助你快速进行思维导图的制…

828华为云征文|基于华为云Flexus云服务器X搭建FTP服务器

❀目录 ❀概述❀特点❀环境准备❀安装❀配置文件修改❀创建目录、修改权限❀控制台安全组开启21端口❀工具验证❀总结 ❀概述 FTP文件传输协议是一种在网络中进行文件传输的广泛使用的标准协议。作为网络通信中的基础工具&#xff0c;FTP允许用户通过客户端软件与服务器进行交…

Java技术栈 —— Spark入门(二)之实时WordCount

Java技术栈 —— Spark入门&#xff08;二&#xff09; 一、kafka1.1 创建topic1.2 准备input与查看output 二、spark2.1 spark下的程序文件2.2 用spark-submit提交作业 参考文章&#xff1a; 参考文章或视频链接[1] 《Kafka Spark Stream实时WordCount》 实验环境&#xff…

【AQS源码】深入理解AQS的工作原理

【AQS源码】深入理解AQS的工作原理-CSDN博客

从零开始掌握容器技术:Docker的奇妙世界

容器技术在当今的云计算和软件开发领域中扮演着越来越重要的角色。如果你是一名计算机专业的学生或从事IT行业的从业者&#xff0c;可能已经听说过Docker这个词。它在软件开发、部署、运维等环节中大放异彩&#xff0c;但对于刚接触这个概念的朋友来说&#xff0c;可能还是有些…