Unity学习笔记(六)使用状态机重构角色移动、跳跃、冲刺

前言

本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记

整体状态框架(简化)

在这里插入图片描述

  • Player 是操作对象的类: 继承了 MonoBehaviour 用于定义游戏对象的行为,每个挂载在 Unity 游戏对象上的脚本都需要继承自 MonoBehaviour,才能利用 Unity 的生命周期事件和功能。
  • PlayerState 是定义状态接口,这里定义了状态类的 Enter(进入),Update(更新),Exit(退出)
  • PlayerStateMachine 是定义上下文类,它持有当前状态的引用,并合适的时机调用状态的行为ChangeState
  • 具体的状态
    • PlayerMoveState(移动状态)
    • PlayerJumpState(跳跃状态)
    • PlayerIdleState(站立状态)

PlayerState

玩家状态的基类,包含状态的基本操作构造函数和三个基础抽象函数进入状态、更新状态、退出状态。

public class PlayerState
{
    protected Player3 player;
    protected PlayerStateMachine stateMachine;
    protected Rigidbody2D rb;

    protected float xInput;
    protected float yInput;
    public string animBoolName;

    // 记录状态的开始时间,方便做一些状态的转化
    protected float stateTimer;
    protected bool triggerCalled;

    public PlayerState(Player3 _player, PlayerStateMachine _stateMachine, string _animBoolName)
    {
        this.player = _player;
        this.stateMachine = _stateMachine;
        this.animBoolName = _animBoolName;
    }

    public virtual void Enter()
    {
        player.anim.SetBool(animBoolName, true);
        rb = player.rb;
        triggerCalled = false;
    }

    public virtual void Exit() 
    {
        player.anim.SetBool(animBoolName, false);
    }

    public virtual void Update() 
    {
        stateTimer -= Time.deltaTime;
        xInput = Input.GetAxisRaw("Horizontal");
        yInput = Input.GetAxisRaw("Vertical");
        player.anim.SetFloat("yVelocity", rb.velocity.y);
    }

    public virtual void AnimatorFinishTrigger()
    {
        triggerCalled = true;
    }
}

PlayerStateMachine

玩家状态的转换类,改变状态步骤

  • 退出当前状态
  • 初始化新状态
  • 进入新的状态
public class PlayerStateMachine
{
    public PlayerState currentState { get; private set;}

    public void Initialize(PlayerState _state)
    {
        currentState = _state;
        currentState.Enter();
    }

    public void ChangeState(PlayerState _nextState)
    {
        currentState.Exit();
        currentState = _nextState;
        currentState.Enter();
    }
}

状态类

有两个比较特殊的状态

  • PlayerAirState,为了设置玩家在空中时的动作
  • PlayerGroundedState,这个状态是为了抽象出玩家站立,跳跃,移动的通用代码。这些状态都要求玩家必须在地面上才能转换。

PlayerAirState(玩家在空中状态)

public class PlayerAirState : PlayerState
{
    public PlayerAirState(Player3 _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
    {
    }

    public override void Enter()
    {
        base.Enter();
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();
        // rb.velocity.y or x 静止时 都为 0,所以不需要关注当前位置,只要静止为0
        if (player.IsGroundDetected())
        {
            stateMachine.ChangeState(player.idleState);
        }

        // 跳起来的移动速度会慢一点
        if (xInput != 0)
        {
            player.SetVelocity(player.moveSpeed * .8f * xInput, rb.velocity.y);
        }
    }
}

PlayerGroundedState(玩家在地面状态)

public class PlayerGroundedState : PlayerState
{
    public PlayerGroundedState(Player3 _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
    {
    }

    public override void Enter()
    {
        base.Enter();
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();

        if (Input.GetKeyDown(KeyCode.Mouse0))
        {
            stateMachine.ChangeState(player.moveState);
        }

        if (!player.IsGroundDetected())
        {
            stateMachine.ChangeState(player.airState);
        }

        if (Input.GetKeyDown(KeyCode.Space) && player.IsGroundDetected())
        {
            stateMachine.ChangeState(player.jumpState);
        }
    }

}

PlayerDashState(冲刺状态)

public class PlayerDashState : PlayerState
{
    public PlayerDashState(Player3 _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
    {
    }

    public override void Enter()
    {
        base.Enter();
        stateTimer = player.dashDuration;
    }

    public override void Exit()
    {
        base.Exit();
        // 冲刺结束后x轴不动,在空中就不会一直移动
        player.SetVelocity(0, rb.velocity.y);
    }

    public override void Update()
    {
        base.Update();
        player.SetVelocity(player.dashSpeed * player.dashDir, 0);
        if (stateTimer < 0)
        {
            stateMachine.ChangeState(player.idleState);
        }
    }
}

PlayerIdleState(站立状态)

public class PlayerIdleState : PlayerGroundedState
{
    public PlayerIdleState(Player3 _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
    {
    }

    public override void Enter()
    {
        base.Enter();
        // 将坐标设置为 0,0
        player.SetZeroVelocity();
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();
        if(xInput != 0)
        {
            stateMachine.ChangeState(player.moveState);
        }
    }
}

PlayerJumpState(跳跃状态)

public class PlayerJumpState : PlayerState
{
    public PlayerJumpState(Player3 _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
    {
    }

    public override void Enter()
    {
        base.Enter();
        rb.velocity = new Vector2(rb.velocity.x, player.jumpForce);
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();
        if (rb.velocity.y < 0)
        {
            stateMachine.ChangeState(player.airState);
        }
    }
}

PlayerMoveState(移动状态)

public class PlayerMoveState : PlayerGroundedState
{
    public PlayerMoveState(Player3 _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
    {
    }

    public override void Enter()
    {
        base.Enter();
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();
        player.SetVelocity(xInput * player.moveSpeed, rb.velocity.y);
        if (xInput == 0)
        {
            stateMachine.ChangeState(player.idleState);
        }
    }
}

Player

玩家类,继承自MonoBehaviour,状态机和各类状态等的定义都在这里进行初始化赋值。

我们需要创建一些关键函数:Awark(),Start(),Update()

该方法初始化过程:

暂时无法在飞书文档外展示此内容

下面的类比较复杂,我设置一个简化版和详细版,了解大致流程简化版即可

简化版

public class Player3 : MonoBehaviour
{
    public Animator anim { get; private set; }
    public Rigidbody2D rb { get; private set; }

    #region States
    public PlayerStateMachine stateMachine { get; private set; }
    public PlayerIdleState idleState { get; private set; }
    public PlayerMoveState moveState { get; private set; }
    public PlayerJumpState jumpState { get; private set; }
    public PlayerDashState dashState { get; private set; }
    public PlayerAirState airState { get; private set; }
    #endregion

    private void Awake()
    {
        stateMachine = new PlayerStateMachine();
        idleState = new PlayerIdleState(this, stateMachine, "Idle");
        moveState = new PlayerMoveState(this, stateMachine, "Move");
        dashState = new PlayerDashState(this, stateMachine, "Dash");
        jumpState = new PlayerJumpState(this, stateMachine, "Jump");
        airState = new PlayerAirState(this, stateMachine, "Jump");
    }

    private void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        anim = GetComponentInChildren<Animator>();
        stateMachine.Initialize(idleState);
    }

    private void Update()
    {
        stateMachine.currentState.Update();
        CheckForDashInput();
    }
}

详细版

public class Player3 : MonoBehaviour
{
    public Animator anim { get; private set; }
    public Rigidbody2D rb { get; private set; }

    protected int facingDir = 1;
    protected bool facingRight = true;


    [Header("Move info")]
    public float moveSpeed = 12f;
    public float jumpForce;

    [Header("Dash info")]
    [SerializeField] private float dashCooldown;
    private float dashUsageTimer;
    public float dashSpeed = 5f;
    public float dashDuration = 5f;
    public float dashDir { get; private set; }

    [Header("Collision Info")]
    [SerializeField] protected Transform groundCheck;
    [SerializeField] protected float groundCheckDistance;
    [SerializeField] protected LayerMask whatIsGround;

    #region States
    public PlayerStateMachine stateMachine { get; private set; }
    public PlayerIdleState idleState { get; private set; }
    public PlayerMoveState moveState { get; private set; }
    public PlayerJumpState jumpState { get; private set; }
    public PlayerDashState dashState { get; private set; }
    public PlayerAirState airState { get; private set; }

    #endregion


    private void Awake()
    {
        stateMachine = new PlayerStateMachine();
        idleState = new PlayerIdleState(this, stateMachine, "Idle");
        moveState = new PlayerMoveState(this, stateMachine, "Move");
        dashState = new PlayerDashState(this, stateMachine, "Dash");
        jumpState = new PlayerJumpState(this, stateMachine, "Jump");
        airState = new PlayerAirState(this, stateMachine, "Jump");
    }

    private void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        anim = GetComponentInChildren<Animator>();
        stateMachine.Initialize(idleState);
    }

    private void Update()
    {
        stateMachine.currentState.Update();
        CheckForDashInput();
    }

    // => 可以理解为 简化返回表达式的符号,主要用于单行方法、属性或表达式的定义。
    public virtual bool IsGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);

    private void CheckForDashInput()
    {

        dashUsageTimer -= Time.deltaTime;

        if (Input.GetKeyDown(KeyCode.LeftShift) && dashUsageTimer < 0)
        {
            dashUsageTimer = dashCooldown;
            dashDir = Input.GetAxisRaw("Horizontal");

            if (dashDir == 0)
                dashDir = facingDir;
            stateMachine.ChangeState(dashState);
        }
    }

    public void SetZeroVelocity()
    {
        rb.velocity = new Vector2(0, 0);
    }

    public void SetVelocity(float _xVelocity, float _yVelocity)
    {
        rb.velocity = new Vector2(_xVelocity, _yVelocity);
        FlipController(_xVelocity);
    }

    public virtual void FlipController(float _x)
    {
        if (_x > 0 && !facingRight)
        {
            Flip();
        }
        else if (_x < 0 && facingRight)
        {
            Flip();
        }
    }

    protected virtual void Flip()
    {
        facingDir = facingDir * -1;
        facingRight = !facingRight;
        transform.Rotate(0, 180, 0);
    }
}

小结

使用状态机进行重构我们可以看到,之后如果想新增或者修改状态,只需要去对应状态类中修改即可,不需要在很多地方维护对应代码,对于代码整体也更加清晰。

整体玩家状态机代码重构就是上面,接下来重构玩家动画部分

动画部分重构

重构前,我们是通过维护玩家当前的状态是否来判断是否进入和退出。使用状态机后,我们应该通过状态代表的参数来维护状态机。
在这里插入图片描述

重构后,我们可以看到状态机的方式我们不需要通过playerIdle来转换,每次状态机执行完都会进入 Exit 结点(改结点详情看拓展) ,这样我们不用维护状态之间是否有依赖,更方便后续的拓展

在这里插入图片描述

重构后效果

请添加图片描述

拓展

Unity 如何调用 Awake()

Awake() 方法在 Unity 生命周期中的角色
Awake() 是 Unity 中 MonoBehaviour 类的生命周期方法之一,它的主要功能是在对象被创建时初始化脚本和对象状态。它是 Unity 生命周期中非常重要的一个环节。


Awake() 的调用时机

  1. 在场景加载时
    当一个场景加载完成,所有启用的 GameObject 的组件(脚本)会在它们的 Awake() 方法中执行初始化。

  2. 在 GameObject 动态实例化时
    如果一个 GameObject 在运行时被动态创建(如通过 Instantiate() 方法),其附加的脚本也会在实例化时调用 Awake()

  3. 调用顺序

    • Awake() 的调用顺序不受脚本执行顺序的影响。Unity 会按照 GameObject 被加载的顺序来依次调用这些对象的 Awake() 方法。
    • 重要:如果有依赖其他对象的初始化,可以将逻辑放在 Start() 中,因为 Start() 会在所有 Awake() 调用完成之后执行。

Unity 生命周期的完整流程
以下是 Unity 中 MonoBehaviour 的常见生命周期方法及其顺序:

  1. 脚本的加载和初始化阶段

    • Awake()
      • 在所有脚本的生命周期中最先调用。
      • 用于初始化脚本的内部状态,以及为后续使用的变量赋初始值。
      • Awake() 被调用时,其他组件或 GameObject 可能尚未初始化完成,因此不适合依赖其他对象。
  2. 脚本的启用阶段

    • OnEnable()
      • 在对象被启用时调用。
      • 如果需要在对象启用时执行额外操作,可以在这里添加逻辑。
  3. 场景运行时初始化阶段

    • Start()
      • 在所有对象的 Awake() 方法执行完成后调用。
      • Start() 是初始化逻辑的推荐位置,特别是在需要依赖其他对象的情况下。
  4. 运行时更新阶段

    • Update():每帧调用一次,用于更新逻辑。
    • FixedUpdate():每固定时间间隔调用一次,用于物理计算。
    • LateUpdate():在每帧的所有 Update() 执行完成后调用,用于执行后续逻辑(例如摄像机跟随)。
  5. 销毁阶段

    • OnDisable()
    • OnDestroy()

Awake() 的作用和特点

1. 作用
  • 初始化脚本实例
    用于初始化脚本中的变量和状态,例如分配引用、加载资源、设置默认值等。

  • 加载必要的资源
    比如加载外部的材质、音频或配置文件。

  • 设置依赖项
    如果某些对象或组件需要在脚本激活时使用,可以在 Awake() 中获取或初始化它们。

  1. Start() 的区别
  • Awake()Start() 更早调用
  • Awake() 用于确保脚本自身的初始化,而 Start() 适合处理与其他对象或组件的交互
特性Awake()Start()
调用时机对象加载时立即调用所有对象的 Awake() 执行后
依赖其他对象状态不建议依赖其他对象可安全地依赖其他对象
手动调用不推荐(Unity 会自动调用)可以在特定情况下手动调用

示例:Awake() 与生命周期的关系

using UnityEngine;

public class Example : MonoBehaviour
{
    void Awake()
    {
        Debug.Log("Awake: 初始化脚本变量和资源");
    }

    void OnEnable()
    {
        Debug.Log("OnEnable: 脚本或对象被激活");
    }

    void Start()
    {
        Debug.Log("Start: 在对象所有的初始化完成后调用");
    }

    void Update()
    {
        Debug.Log("Update: 每帧调用");
    }

    void OnDisable()
    {
        Debug.Log("OnDisable: 脚本或对象被禁用");
    }

    void OnDestroy()
    {
        Debug.Log("OnDestroy: 对象被销毁");
    }
}

在场景运行时,执行顺序为:

  1. Awake():初始化。
  2. OnEnable():对象启用时的逻辑。
  3. Start():所有对象的 Awake() 调用完成后。
  4. Update():每帧更新。
  5. OnDisable()OnDestroy():对象被禁用或销毁时。

总结

  • Awake() 的核心作用是初始化脚本变量和状态,在 Unity 生命周期中最早被调用。
  • 适合场景:用来初始化脚本或 GameObject 的自身逻辑,而不依赖其他对象。
  • 与其他方法的关系
    • Awake() 是生命周期的起点。
    • 如果依赖其他对象的初始化,建议将逻辑放到 Start()

希望这些内容能帮助你理解 Unity 的生命周期和 Awake() 方法的作用!如果你有更多问题,随时提问!

Unity 调用 Awake 的简单原理

Unity 调用 Awake() 的原理其实可以简单地理解为以下几个步骤。虽然 Unity 的底层实现细节是封闭的,但我们可以根据其生命周期行为和一些公开信息总结出其大致逻辑。


1. Unity 生命周期的核心
Unity 的生命周期方法(如 Awake()Start())是由 Unity 引擎在运行时按照特定顺序自动调用的。这些方法不需要开发者手动注册或显式调用。以下是基本的执行流程:

  1. 场景加载

    • Unity 会加载场景中的所有 GameObject。
    • 如果某个 GameObject 上挂载了继承自 MonoBehaviour 的脚本,它会参与生命周期流程。
  2. 脚本扫描和方法检测

    • Unity 会通过反射机制检测脚本中是否实现了特定的生命周期方法(如 Awake())。
    • 如果检测到某个生命周期方法,则将其注册到 Unity 的内部执行流程中。
  3. 方法调用

    • 在特定的生命周期阶段(例如场景加载后),Unity 引擎会按顺序调用注册的生命周期方法。

2. 调用 Awake() 的简化原理
以下是 Unity 如何调用 Awake() 的基本逻辑:

步骤 1:场景加载

  • 当场景加载时,Unity 会逐一加载场景中的所有 GameObject 和它们的组件。

步骤 2:反射检测

  • Unity 扫描每个继承自 MonoBehaviour 的脚本,检查是否定义了 Awake() 方法。
    • Unity 使用 反射 来动态检测方法是否存在。
    • 如果 Awake() 存在,Unity 会将该方法存储为一个待调用的任务。

步骤 3:初始化并调用 Awake()

  • 在所有 GameObject 被加载到内存后,Unity 会依次调用这些 GameObject 脚本的 Awake() 方法。
  • 调用顺序:
    • Awake() 是生命周期的第一个阶段。
    • 只要脚本和对象处于启用状态,Awake() 会在对象加载时立即被调用。

代码实现的简化逻辑
以下是 Unity 调用 Awake() 的简单逻辑,用伪代码表示:

// Unity 内部的场景加载流程
void LoadScene(Scene scene)
{
    // 1. 加载场景中的所有 GameObject
    foreach (GameObject go in scene.gameObjects)
    {
        // 2. 获取 GameObject 上的所有 MonoBehaviour 脚本
        foreach (MonoBehaviour script in go.GetComponents<MonoBehaviour>())
        {
            // 3. 检查是否实现了 Awake() 方法
            if (script.HasMethod("Awake"))
            {
                // 4. 调用 Awake()
                script.Awake();
            }
        }
    }
}

3. Awake() 的调用特点

  • 只调用一次

    • Awake() 只会在脚本实例初始化时调用一次,无论该脚本的 GameObject 是否被启用或禁用。
  • 不依赖脚本执行顺序

    • Awake() 的调用顺序是按照 GameObject 加载的顺序。
    • 脚本执行顺序设置对 Awake() 无影响。
  • Start() 之前调用

    • Awake() 是生命周期的第一步,Start() 在所有 Awake() 调用完成后才会被执行。
  • 适合自我初始化

    • Awake() 的设计初衷是用于初始化自身的变量和状态,而不适合依赖其他 GameObject,因为此时其他对象可能尚未完成初始化。

4. 示例代码
以下代码演示了 Unity 如何在场景加载时调用 Awake()Start()

using UnityEngine;

public class Example : MonoBehaviour
{
    void Awake()
    {
        Debug.Log("Awake called: 初始化变量");
    }

    void Start()
    {
        Debug.Log("Start called: 依赖其他对象初始化");
    }
}

假设场景中有两个 GameObject 分别挂载了该脚本,运行时输出可能是:

Awake called: 初始化变量 (GameObject 1)
Awake called: 初始化变量 (GameObject 2)
Start called: 依赖其他对象初始化 (GameObject 1)
Start called: 依赖其他对象初始化 (GameObject 2)

5. 为什么使用 Awake()
Awake() 的核心作用是初始化自身的状态。例如:

  1. 分配变量或资源。
  2. 加载外部数据。
  3. 设置默认值。

6. 与其他生命周期方法的关系

方法名调用时机适合的操作
Awake()GameObject 加载到场景时初始化自身变量,不依赖其他对象
OnEnable()GameObject 或脚本启用时运行需要在启用时触发的逻辑
Start()所有 Awake() 调用完成后,场景运行时初始化需要依赖其他对象的逻辑
Update()每帧调用持续更新逻辑,例如动画、输入检测

总结
Unity 调用 Awake() 的原理可以概括如下:

  1. 加载场景
    • Unity 会加载所有 GameObject 和其组件。
  2. 检测方法
    • Unity 使用反射检测脚本是否实现了 Awake() 方法。
  3. 方法调用
    • Unity 自动调用实现了 Awake() 的脚本,不需要开发者手动调用。

通过这个流程,Unity 实现了生命周期的动态管理,使得开发者只需专注于脚本逻辑的实现,而不用关心具体的调用机制。

Unity状态机的Exit结点

在Unity的Animator状态机(Animator State Machine)中,Exit结点用于表示从当前状态机退出到其父状态机的状态。以下是关于Exit结点的详细说明:


  1. Exit结点是什么?
    Exit结点是Unity Animator中的一个特殊的状态机结点,它表示一个状态机结束的出口点。通常用于嵌套的子状态机(Sub-State Machine)中,告诉父状态机当前子状态机的行为已经完成,可以切换到父状态机中的其他状态。

  1. 使用场景
  • 嵌套状态机(Sub-State Machines)
    当你将一个复杂的动画逻辑封装到一个子状态机中时,Exit结点表示该子状态机完成其逻辑后应该退出,回到父状态机进行下一步。
  • 动画流程控制
    如果子状态机处理完某些特定动画(如攻击动作、过渡动画等),可以通过Exit结点返回父状态机,从而进行主流程的继续。

  1. 如何设置Exit结点
    在Unity中,以下是设置Exit结点的步骤:
  2. 创建子状态机
    在Animator中,右键选择Create Sub-State Machine,创建一个嵌套的子状态机。
  3. 添加状态和过渡
    在子状态机中添加具体的动画状态(如攻击、跳跃等)。
  4. 使用Exit结点
    • 在子状态机中,右键选择Make Transition,并将过渡指向Exit结点。
    • Exit结点是子状态机的默认出口,不需要手动创建。
  5. 在父状态机中配置逻辑
    在父状态机中,可以设置子状态机到其他状态(或反过来)的过渡逻辑。

4. Exit的行为

  • 当动画流转到Exit结点时,子状态机会退出,控制权回到父状态机。
  • 可以通过Animator Controller中的条件(如布尔值、触发器等)控制子状态机何时退出。
  • 在父状态机中,子状态机到Exit的过渡会被认为完成,可以接着切换到其他状态。

  1. 注意事项
  • 不能直接控制Exit结点
    Exit是一个逻辑性的特殊结点,它不能像普通状态一样附加动画或行为。
  • 父状态机的后续逻辑
    确保在父状态机中正确设置过渡条件,否则子状态机退出后可能进入意料之外的状态。

  1. 示例场景
    假设有一个游戏角色的动画逻辑:
  • 父状态机
    包括“待机”、“跑步”、“攻击子状态机”。
  • 子状态机(攻击子状态机):
    包括“攻击准备”、“攻击动作”、“攻击结束”。

当“攻击动作”完成后,子状态机会通过Exit结点返回父状态机,角色的动画状态可能回到“待机”或其他状态。

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

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

相关文章

AIDD-人工智能药物设计-AlphaFold系列:全面回顾AF1-3的关键研究成果及其对科学界的影响

AlphaFold系列&#xff1a;全面回顾AF1-3的关键研究成果及其对科学界的影响 本文章将围绕 AlphaFold 系列模型在蛋白质结构预测领域的前沿研究展开&#xff0c;重点介绍 AlphaFold1、AlphaFold2 与 AlphaFold3 的关键研究成果&#xff0c;以及它们对科学界和制药工业的深远影响…

Pandas-RFM会员价值度模型

文章目录 一. 会员价值度模型介绍二. RFM计算与显示1. 背景2. 技术点3. 数据4. 代码① 导入模块② 读取数据③ 数据预处理Ⅰ. 数据清洗, 即: 删除缺失值, 去掉异常值.Ⅱ. 查看清洗后的数据Ⅲ. 把前四年的数据, 拼接到一起 ④ 计算RFM的原始值⑤ 确定RFM划分区间⑥ RFM计算过程⑦…

Git 入门指南:如何高效管理你的代码库

文章目录 Git 的介绍安装 Git创建仓库Git 三板斧addcommitpush 冲突问题常用 Git 指令 Git 的介绍 Git 是一个分布式版本控制系统&#xff0c;用于跟踪文件的变化并支持团队协作开发。最初由 Linus Torvalds&#xff08;Linux 操作系统的创始人&#xff09;开发&#xff0c;Gi…

execl条件比较两个sheet每个单元格的值

1.把对比的sheet复制到对比文件中 2.选择首个单元格 3.新建规则 4.选择公式 5.编写公式 A3<>Sheet1!A36.选择差异颜色 7.选择应用范围 $1:$655368.选择应用范围

2025新年源码免费送

2025很开门很开门的源码免费传递。不需要馒头就能获取4套大开门源码。 听泉偷宝&#xff0c;又进来偷我源码啦&#x1f44a;&#x1f44a;&#x1f44a;。欢迎偷源码 &#x1f525;&#x1f525;&#x1f525; 获取免费源码以及更多源码&#xff0c;可以私信联系我 我们常常…

本地LLM部署--Open WebUI(多媒体工具FFMPEG作用)

OpenWebUI 和 FFmpeg 的关系主要体现在 多媒体处理需求 上。OpenWebUI 是一个基于 Web 的接口项目&#xff0c;提供与各种 AI 模型交互的功能&#xff0c;而 FFmpeg 则是一种多媒体处理工具&#xff0c;用于处理音视频数据。二者的关系主要体现为 依赖和功能互补&#xff0c;具…

使用双向链表优化数组操作的性能

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 背景 双向链表的优势 实现方案 性能优化 …

Inno Setup制作安装包,安装给win加环境变量

加 ; 加环境变量&#xff0c;开启&#xff0c;下面一行 ChangesEnvironmentyes 和 ; 加环境变量wbrj变量名&#xff0c;{app}\project\bin变量值&#xff0c;{app}\后接文件名&#xff0c;{app}表示安装路径。下面一行,{olddata};原来的值上拼接 Root: HKLM; Subkey: “SYSTEM\…

积分与签到设计

积分 在交互系统中&#xff0c;可以通过看视频、发评论、点赞、签到等操作获取积分&#xff0c;获取的积分又可以参与排行榜、兑换优惠券等&#xff0c;提高用户使用系统的积极性&#xff0c;实现引流。这些功能在很多项目中都很常见&#xff0c;关于功能的实现我的思路如下。 …

Taro+Vue实现图片裁剪组件

cropper-image-taro-vue3 组件库 介绍 cropper-image-taro-vue3 是一个基于 Vue 3 和 Taro 开发的裁剪工具组件&#xff0c;支持图片裁剪、裁剪框拖动、缩放和输出裁剪后的图片。该组件适用于 Vue 3 和 Taro 环境&#xff0c;可以在网页、小程序等平台中使用。 源码 https:…

AI赋能服装零售:商品计划智能化,化危机为转机

在服装零售这片竞争激烈的战场上&#xff0c;每一个细微的决策都可能成为品牌兴衰的关键。当市场波动、消费者口味变化、供应链挑战接踵而至时&#xff0c;许多品牌往往将危机归咎于外部环境。然而&#xff0c;真相往往更为深刻——“危机不是外部的&#xff0c;而是你的商品计…

Flutter:吸顶效果

在分页中&#xff0c;实现tab吸顶。 TDNavBar的screenAdaptation: true, 开启屏幕适配。 该属性已自动对不同手机状态栏高度进行适配。我们只需关注如何实现吸顶。 view import package:ducafe_ui_core/ducafe_ui_core.dart; import package:flutter/material.dart; import p…

企业级PHP异步RabbitMQ协程版客户端 2.0 正式发布

概述 workerman/rabbitmq 是一个异步RabbitMQ客户端&#xff0c;使用AMQP协议。 RabbitMQ是一个基于AMQP&#xff08;高级消息队列协议&#xff09;实现的开源消息组件&#xff0c;它主要用于在分布式系统中存储和转发消息。RabbitMQ由高性能、高可用以及高扩展性出名的Erlan…

信号弱开启手机Wifi通话,MIUI显示/隐藏5G开关的方法

1.开启手机Wi-Fi通话&#xff0c;提升无信号或弱信号时的通话质量 Wi-Fi 通话(Wi-Fi calling)&#xff0c;又称VoWiFi&#xff0c;是一项名为“ Voice over Wi-Fi ”的服务&#xff0c;它允许手机用户使用他们的智能手机使用 Wi-Fi网络拨打电话&#xff0c;即在Wi-Fi环境下就能…

Echarts的认识和基本用法

Echarts介绍和使用 Echarts介绍 官网地址&#xff1a;Apache ECharts Echarts是一个基于JavaScript的开源可视化图表库&#xff0c;由百度前端开发团队研发和维护。它提供了丰富的图表类型、数据统计分析、动态数据更新、多维数据展示等功能&#xff0c;可以帮助开发人员在 W…

在JavaScript开发中,如何判断对象自身为空?

前言 如何判断一个对象为空是我们在开发中经常会遇到的问题&#xff0c;今天我们来聊聊几种经常使用的方法&#xff0c;以及在不同的场景下我们如何去使用。 1. JSON.stringify JSON.stringify 方法可以使对象序列化&#xff0c;转为相应的 JSON 格式。 const obj {};cons…

大语言模型训练的数据集从哪里来?

继续上篇文章的内容说说大语言模型预训练的数据集从哪里来以及为什么互联网上的数据已经被耗尽这个说法并不专业&#xff0c;再谈谈大语言模型预训练数据集的优化思路。 1. GPT2使用的数据集是WebText&#xff0c;该数据集大概40GB&#xff0c;由OpenAI创建&#xff0c;主要内…

Wireshark 学习笔记1

1.wireshark是什么 wireshark是一个可以进行数据包的捕获和分析的软件 2.基本使用过程 &#xff08;1&#xff09;选择合适的网卡 &#xff08;2&#xff09;开始捕获数据包 &#xff08;3&#xff09;过滤掉无用的数据包 &#xff08;4&#xff09;将捕获到的数据包保存为文件…

RK3568平台(USB篇)禁用USB端口

一.linux中怎样查看usb的端口号 在USB口插入U盘: [ 198.141319][ T106] usb 3-1.3: new SuperSpeed Gen 1 USB device number 5 using xhci-hcd [ 198.161695][ T106] usb 3-1.3: New USB device found, idVendor=0781, idProduct=5591, bcdDevice= 1.00 [ 198.161721]…

3298.统计重新排列后包含另一个字符串的字符串数目 I II滑动窗口 优化思路解析全网最详细

II相比于I是数据范围变成了10的6次方了 我们来维护大小关系&#xff0c;把不用的都去掉&#xff0c;优化到O&#xff08;26n&#xff09; 首先判断一下要找子字符串的s长度是否小于t字符串&#xff0c;如果小于的话直接返回0 初始答案变量和left左指针为0 用Counter来记录t中所…