首页 > 其他分享 >【Unity】高级——有限状态机(角色控制)移动、待机

【Unity】高级——有限状态机(角色控制)移动、待机

时间:2023-04-26 11:14:27浏览次数:42  
标签:void private 玩家 状态机 player Unity base 待机 public

简介

有限状态机是unity游戏开发中经常用到的一个概念,能制作敌人AI,玩家控制器等。

有限状态机允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类

实现:将一个个具体的状态类抽象出来

经典案例:玩家行动器

案例中玩家行动包括:待机、移动、跳跃、冲刺、爬墙等

而这么多状态我们再进一步将其分为:

【在墙上的状态】
image

【在地面的状态】
image

【玩家能力状态】
image

比较特殊的是玩家在空中的状态,我们需要做很多判断,所以不将其抽象,同样的还有冲刺状态。

新的输入系统(Input System)

在开始制作状态机前,我们需要将输入系统也更新一下:在包管理器中找到Input System 然后引入

image

然后在创建中找到 Input Actions 将其命名为Player

image

双击进入编辑窗口
image

设置Movement的输入
image

image

录入键盘时点击Listen然后再按你想监听的键
image

记得设置你的方向
image

然后在玩家对象上创建Player Input组件,将刚刚创建好的Input System文件挂载上去
image

创建C# 脚本 PlayerInputManage

public class PlayerInputManage : MonoBehaviour
{
    //新输入系统文件
    private PlayerInput playerInput;

    //初始移动输入
    public Vector2 RawMovementInput { get; private set; }

    //X:横轴输入值 Y:纵轴输入值
    public int NormInputX { get; private set; }
    public int NormInputY { get; private set; }

    private void Start()
    {
        playerInput = GetComponent<PlayerInput>();
    }

    public void OnMoveInput(InputAction.CallbackContext context) 
    {
        RawMovementInput = context.ReadValue<Vector2>();

        NormInputX = Mathf.Abs(RawMovementInput.x) > 0.5f ? (int)(RawMovementInput * Vector2.right).normalized.x : 0;
        NormInputY = Mathf.Abs(RawMovementInput.y) > 0.5f ? (int)(RawMovementInput * Vector2.up).normalized.y : 0;
    }
}

创建好后回到我们的Player Input文件,点开GamePlayer

image

将Player脚本挂载上去

image

然后在PlayerInputManage中找到刚刚写好的OnMoveInput方法

image

玩家数据设计

[CreateAssetMenu(fileName ="newPlayerData",menuName ="Data/Player Data/Base Data")]
public class PlayerData : ScriptableObject
{
    [Header("移动相关")]//移动速度
    public float movementVelocity = 10f;
}

创建完PlayerData脚本后,我们右键 -> 数据 ->

image

玩家状态设计

public class PlayerState
{
    protected Player player;
    protected PlayerStateMachine stateMachine;
    protected PlayerData playerData;

    protected float startTime;//通用计时器

    protected bool isAnimtionFinished;//动画是否完成
    protected bool isExitingState;//是否已经切换完状态

    private string animBoolName;//动画器条件切换

    //初始化
    public PlayerState(Player player, PlayerStateMachine stateMachine, PlayerData playerData,string animBoolName)
    {
        this.player = player;
        this.stateMachine = stateMachine;
        this.playerData = playerData;
        this.animBoolName = animBoolName;
    }

    // 状态开始
    public virtual void Enter()
    {
        Dochecks();
        player.Anim.SetBool(animBoolName,true);
        startTime = Time.time;
        Debug.Log(animBoolName);
        isAnimtionFinished = false;
        isExitingState = false;
    }

    // 状态结束
    public virtual void Exit()
    {
        player.Anim.SetBool(animBoolName,false);
        isExitingState = true;
    }

    // 逻辑更新
    public virtual void LogicUpdate(){ }

    // 物理更新
    public virtual void PhysicsUpdate() => Dochecks();

    // 图层检测
    public virtual void Dochecks() { }

    // 开始动画触发
    public virtual void AnimationTrigger() { }

    // 结束动画触发
    public virtual void AnimtionFinishTrigger() => isAnimtionFinished = true;
}

玩家实体设计

public class Player : MonoBehaviour
{
    //状态变量
    public PlayerStateMachine StateMachine{ get; private set;}//状态管理器

    public PlayerIdleState IdleState { get; private set; }//待机
    public PlayerMoveState MoveState { get; private set; }//移动

    //玩家数据
    [SerializeField]
    private PlayerData playerData;

    //组件变量
    public Animator Anim { get; private set;}//动画器
    public PlayerInputManage InputManage{ get; private set;}//玩家输入管理
    pubilc Rigidbody2D RB { get; private set;}//刚体

    public Vector2 CurrentVelocity { get; private set; }//当前速度
    public int FacingDirection { get; private set; }//面朝方向

    private Vector2 workspace;//工作空间

    private void Awake()
    {
        //AWake在Start前执行,且只执行一次。可用来初始化玩家状态控制器,以及玩家状态
        StateMachine = new PlayerStateMachine();

        IdleState = new PlayerIdleState(this,StateMachine,playerData,"idle");
        MoveState = new PlayerMOveState(this,StateMachine,playerData,"move");
    }

    private void Start()
    {
        //Start也只执行一次,但在AWake之后。通常是为组件赋值以及执行初始化方法
        Anim = GetComponent<Animator>();
        InputManage = GetComponent<PlayerInputManage>();
        RB = GetComponent<Rigidbody2D>();

        FacingDirection = 1;

        StateMachine.Initialize(IdleState);
    }

    private void Update()
    {
        //在每一帧执行:确定当前速度,执行当前状态的任务方法
        CurrentVelocity = RB.velocity;
        StateMachine.CurrentState.LogicUpdate();
    }

    private void FixedUpdate()
    {
        //在Update后执行但并不是每一帧都执行:常用于物理检测
        StateMachine.CurrentState.PhysicsUpdate();
    }
}

玩家状态管理类

public class PlayerStateMachine
{
    //当前状态
    public PlayerState CurrentState { get; private set; }

    //初始化状态
    public void Initialize(PlayerState startingState) 
    {
        CurrentState = startingState;
        CurrentState.Enter();
    }

    //切换状态
    public void ChangesState(PlayerState newState)
    {
        CurrentState.Exit();
        CurrentState = newState;
        CurrentState.Enter();
    }
}

玩家待机/移动

当准备好 Player(实体类)、PlayerState(状态父类)、PlayerStateMachine(状态管理类)后,就可以开始写第一个状态类来体验有限状态机的魅力。

通过设计书发现还有3个超级类:
Grounded(在地面)、TouchingWall(在墙上)、Ability(玩家能力) 它们继承于玩家状态类。

public class PlayerGroundedState : PlayerState
{
    protected int xInput;
    protected int yInput;

    public PlayerGroundedState(Player player, PlayerStateMachine stateMachine, PlayerData playerData, string animBoolName) : base(player, stateMachine, playerData, animBoolName)
    {
    }

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

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

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

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

        xInput = player.InputManage.NormInputX;
        yInput = player.InputManage.NormInputY;
    }

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

因为玩家执行待机以及移动状态时是在地上的,所以需要继承 PlayerGroundedState 这个状态。我们可以先思考一下:在待机状态需要检测什么,以及如何从待机状态切换出去?你可能会想做一个随机的待机动作,也只需要在LogicUpdate中创建随机数,然后根据随机数切换待机动画。

在视觉上我们想要玩家从移动到待机灵敏一点,也就是放开移动,玩家就立马进入待机状态。
回到Player文件中,需要设置一下玩家在X轴上的速度

public class Player : MonoBehaviour
{
    /*状态变量
    ......略
   */

    //玩家数据
    [SerializeField]
    private PlayerData playerData;

    /*组件变量
    ......略
    */

    public Vector2 CurrentVelocity { get; private set; }//当前速度
    public int FacingDirection { get; private set; }//面朝方向

    private Vector2 workspace;//工作空间

    /* Unity 默认方法
    ......略
    */

    //设置X轴的速度
    public void SetVelocityX(float velocity)
    {
        workspace.Set(velocity, CurrentVelocity.y);
        RB.velocity = workspace;
        CurrentVelocity = workspace;
    }
}

这样就可以在待机状态开始时设置玩家在X轴上的速度,你可以注释掉该行代码试一下两者的手感
因为在待机状态我们并不需要做更多物理检测,所以只需要注重逻辑执行就好。

public class PlayerIdleState : PlayerGroundedState
{
    public PlayerIdleState(Player player, PlayerStateMachine stateMachine, PlayerData playerData, string animBoolName) : base(player, stateMachine, playerData, animBoolName)
    {
    }

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

    public override void Enter()
    {
        base.Enter();
        //在状态开始时设置玩家的移动速度为0;
        player.SetVelocityX(0f);
    }

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

    public override void LogicUpdate()
    {
        base.LogicUpdate();
        if(!isExitingState)
        {//没有结束状态
            if (xInput != 0)
            {//且有X轴上的输入
                stateMachine.ChangesState(player.MoveState);
            }
            else if(yInput == -1)
           {//或有Y轴上的向下的输入
                //TODO:切换到下蹲状态
           }
        }
    }

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

接着写玩家的移动状态,由于要写的是2D项目,所以需要检测玩家做左右移动旋转玩家方向,回到Playe实体类中

public class Player : MonoBehaviour
{
    /// <summary>
    /// 检测是否转向
    /// </summary>
    /// <param name="xInput">玩家x轴的输入</param>
    public void CheckIfShouldFilp(int xInput)
    {
        if (xInput != 0 && xInput != FacingDirection)
        {//当玩家输入时,输入值(1,-1)与当前面朝方向不一致时,执行转向
            Filp();
        }
    }

    private void Filp()
    {
        FacingDirection *= -1;//设置面朝方向
        RB.transform.Rotate(0.0f, 180.0f, 0.0f);
    }
}

然后继续写MoveState,同样的移动我们是在地面上,所以要继承PlayerGroundedState

public class PlayerMoveState : PlayerGroundedState
{
    public PlayerMoveState(Player player, PlayerStateMachine stateMachine, PlayerData playerData, string animBoolName) : base(player, stateMachine, playerData, animBoolName)
    {
    }

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

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

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

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

        player.CheckIfShouldFilp(xInput);
        player.SetVelocityX(playerData.movementVelocity * xInput);

        if (!isExitingState)
        {
            if (xInput == 0)
            {
                stateMachine.ChangesState(player.IdleState);
            }
            else if (yInput == -1)
            {
                //TODO:切换到下蹲移动状态
            }
        }
    }

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

做完这些就可以在动画器中创建idle、move的动画了

动画器设计

除了xVelocity、yVelocity是单独设置,其他的均为Bool在角色进入状态时执行,可以查看上面 PlayerStateMachine 中的代码

image

动画退出条件为animBoolName == false ,退出时间与过度时间为0是为了让状态动画间的切换更加丝滑

image

标签:void,private,玩家,状态机,player,Unity,base,待机,public
From: https://www.cnblogs.com/kakaji/p/17327248.html

相关文章

  • Unity性能优化课程学习笔记(Metaverse大衍神君)
    课程来源于:https://space.bilibili.com/1311706157 性能优化之道:      等待函数:  SSAO:  AA方案:  后处理: 渲染提前期优化culling,simplization,batchingCulling     Simplization:      Ba......
  • Unity】一步跳过Unity启动屏/Logo, 全平台适用,Unity官方API支持
    Unity启动Logo让人非常不爽,因为展示unitylogo非常拖沓,延缓了打开游戏的时间,用0元购版本又怕收到律师函。终于....刷github学习的时候意外发现一段有趣的代码,说是能跳过UnityLogo启动屏:https://github.com/psygames/UnitySkipSplash/blob/main/SkipSplash.cs翻了一下UnityA......
  • D. Remove One Element(前缀最大+简单状态机)
    题目D.RemoveOneElement题意输入n(2≤n≤2e5)和长为n的数组a(1≤a[i]≤1e9)。从a中去掉一个数(也可以不去掉)。输出a的最长严格递增连续子数组的长度。思路一种方法是前缀最长和后缀最长,加起来。这种方法比较简单。用状态机来写,定义f[i][0/1]分别表示前缀......
  • Unity3D:目标约束
    推荐:将NSDT场景编辑器加入你的3D工具链3D工具集:NSDT简石数字孪生目标约束(AimConstraints)AimConstraint可旋转游戏对象以朝向其源游戏对象。还可针对另一个轴保持一致方向。例如,可将AimConstraint添加到摄像机。要在约束瞄准摄像机时保持摄像机直立,请指定摄像机的向上轴和......
  • Unity框架:JKFrame2.0学习笔记(十一)——MonoSystem(1)
    内部结构MonoSystemMonoSystem是继承MonoBehaviour的,声明几个action,在MonoBehaviour的声明周期内调用,实现了不继承MonoBehaviour也可以用mono的生命周期。包括以下几个方法可供外部调用:Init:初始化,获取MonoSystem的实例AddUpdateListener:添加Update监听RemoveUpdateListener:移除Upd......
  • Go中的有限状态机FSM的详细介绍
    1、FSM简介1.1有限状态机的定义有限状态机(FiniteStateMachine,FSM)是一种数学模型,用于描述系统在不同状态下的行为和转移条件。状态机有三个组成部分:状态(State)、事件(Event)、动作(Action),事件(转移条件)触发状态的转移和动作的执行。动作的执行不是必须的,可以只转移状态,不指定任何......
  • Unity通过PBXProject生成XCode工程
    Unity版本:2020.3.47f1首先通过PostProcessBuildAttribute监听XCode工程导出完成事件,GetUnityMainTargetGuid是获取XCode工程中"Unity-iPhone"对应的target,GetUnityFrameworkTargetGuid则对应"UnityFramework",在unity中大部分操作会是针对UnityFramework。PBXProject的很多操作都......
  • 在Unity 网络通讯
    usingSystem.Collections;usingUnityEngine;usingUnityEngine.Networking;publicclassHttpTest:MonoBehaviour{voidStart(){StartCoroutine(UnityWebRequestDemo());}IEnumeratorUnityWebRequestDemo(){using(Uni......
  • Unity___QFramework笔记
    引入Event引入事件监听。使用方法先定义一个事件类//定义数据变更事件publicstructCountChangeEvent//++{}//执行事件this.SendEvent<CountChangeEvent>();//++//注册事件this.RegisterEvent<CountChangeEvent>(e......
  • unity 打开电脑本地文件夹
    1.调用方法如下这是选择路径 2.代码如下usingSystem;usingSystem.IO;usingSystem.Runtime.InteropServices;usingUnityEngine;///<summary>///调用系统代码,打开本地文件夹///</summary>[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Auto)]pub......