技术笔记(9)MMORPG人物操作系统

技术笔记(9)MMORPG人物操作系统

  • 希望实现的功能或目标:

    • 实现人物在场景内的移动、转向、跳跃、落地判断
    • 实现有限状态机

  • 学习笔记:

    • PlayerMovementController类

      • 作用:负责玩家的行为控制

      • 挂载到Player游戏物体身上,Player游戏物体没有刚体和碰撞体,取而代之的是CharacterController组件。

      • 当前类声明使用到的变量:

        • 组件

          • private CharacterController characterController;​​​
          • public Transform groundCheckPointTrans;​​​
        • 旋转

          • public float rotateSpeed = 120;​​​
        • 移动

          • public float moveSpeed = 3;​​​
        • 跳跃

          • public float gravity = 9.8f;​​​
          • public float verticalVelocity = 0;​​​
          • public float MaxJumpHeight = 1.7f;​​​
        • 判断接地

          • public float checkSphereRadius = 0.1f;​​​
          • public LayerMask groundLayer;​​​
          • public bool isGround;​​​
        • 状态机相关

          • private InputController ic;
          • private CharacterFSM characterFSM;
      • 事件函数:

        • Awake方法中,去拿到游戏物体的CharacterController组件、判断接地的transform、当前角色模型;然后为其挂载上InputController类和CharacterFSM类脚本组件,并把角色的动画控制器和输入管理ic传进去,初始化有限状态机。

          • private void Awake()
            {
                characterController = GetComponent<CharacterController>();
                groundCheckPointTrans = transform.Find("GroundCheckPoint");
                playerModel = transform.GetChild(0).gameObject;
                ic = gameObject.AddComponent<InputController>();
                characterFSM = gameObject.AddComponent<CharacterFSM>();
            
                characterFSM.InitFSM(playerModel.GetComponent<Animator>(), ic);
            }
            
        • Update方法中调用写好的旋转视角和人物移动跳跃方法

          • void Update()
            {
                PlayerRotateViewControl();
                PlayerMoveAndJumpControl();
            }
            
      • 旋转视角方法:

        • 调用transform的Rotate方法,由于玩家模型和摄像机都归属于Player,作为其子物体,故而人物和摄像机会随着父物体一起旋转.
        • private void PlayerRotateViewControl()
          {
              transform.Rotate(Vector3.up * Input.GetAxis("Mouse X") 
          						* rotateSpeed * Time.deltaTime);
          }
          
      • 移动和跳跃方法:

        • 防止角色下沉,如果角色跳跃后,身体陷入到地板里,就帮他恢复到地板上

        • 首先将移动向量motionVector设置为Vector3.zero,由于本方法放在Update函数中,每帧都会被调用,且后续计算前后、左右的移动和上下跳跃时用的是向量相加。所以需要每帧先归零再去与各个向量相加。

        • 获取Horizontal和Vertical的AxisRaw输入,用浮点类型变量h和v接收(后续添加InputController类之后,改为调用ic.GetFloatInputValue()来获取这两个输入)

        • 拿到h和v之后,先判断一下是让状态机进IDLE状态还是MOVE状态

        • motionVector先加等transform.forward * 移速 * v * Time.deltaTime

          再加等transform.right * moveSpeed * h * Time.deltaTime

        • 在角色脚底放个空物体,使用射线检测,在空物体的位置做一个球体的检测,若球半径内有目标层级的物品,则能获得一个true的返回值

        • 判断如果不在地上,就将垂直速度减等 重力加速度 * 间隔时间

        • motionVector此时再加等 Vector3.up * 垂直方向速度 * 间隔时间

        • 如果按下跳跃键(更改为从ic获取跳跃输入,且状态机当前状态不是跳跃状态),并且判断还在地上,就套用物理公式 v2 = 2gh 来算想要达到某个目标高度,需要给到多少的初速度。

        • 调用characterController的Move方法,将motionVector作为参数传入

        • 判断如果到了地上,且垂直速度还小于0。就将垂直速度归零,并将物体的position的y也归零。

          这部分判断和处理放到Move方法之后,主要是为了处理其造成的人物跳跃后接触不到地面或是陷入地面的bug,强行把position的y归零。

        • private void PlayerMoveAndJumpControl()
          {
              if (transform.position.y < 0)
              {
                  transform.position = new Vector3(transform.position.x, 0f, transform.position.z);
              }
              Vector3 motionVector = Vector3.zero;
              //float h = Input.GetAxisRaw("Horizontal");
              //float v = Input.GetAxisRaw("Vertical");
              float h = ic.GetFloatInputValue(InputCode.HorizontalMoveValue);
              float v = ic.GetFloatInputValue(InputCode.VerticalMoveValue);
              if (characterFSM.GetCurrentState() != CHARACTERSTATE.JUMP)
              {
                  JudgeAndChangeStateIdleOrMove(h, v);
              }
              motionVector += transform.forward * moveSpeed * v * Time.deltaTime;
              motionVector += transform.right * moveSpeed * h * Time.deltaTime;
              isGround = Physics.CheckSphere(groundCheckPointTrans.position, checkSphereRadius, groundLayer);
          
            
              if(!isGround)
              {
                  verticalVelocity -= gravity * Time.deltaTime;
              }
              motionVector += Vector3.up * verticalVelocity * Time.deltaTime;
              //if (Input.GetButtonDown("Jump"))
              if (ic.GetBoolInputValue(InputCode.JumpState)&& characterFSM.GetCurrentState() != CHARACTERSTATE.JUMP)
              {
                  if (isGround)
                  {
                      verticalVelocity = Mathf.Sqrt(2 * gravity * MaxJumpHeight);
                  }
            
              }
              characterController.Move(motionVector);
              if (isGround)
              {
                  if (verticalVelocity < 0)
                  {
                      verticalVelocity = 0;
                      transform.position = new Vector3(transform.position.x, 0f, transform.position.z);
                  }
              }
          }
          
      • 判断进IDLE还是进MOVE状态的方法:

        • 如果h和v有一个不为0,进MOVE;否则进IDLE
        •     private void JudgeAndChangeStateIdleOrMove(float h,float v)
              {
                  if (v != 0 || h != 0)
                  {
                      characterFSM.ChangeState(CHARACTERSTATE.MOVE);
                  }
                  else
                  {
                      characterFSM.ChangeState(CHARACTERSTATE.IDLE);
                  }
              }
          
    • 状态枚举类型CHARACTERSTATE

      • public enum CHARACTERSTATE
        { 
            //NONE,
            IDLE,
            MOVE,
            RUN,
            JUMP,
            ATTACK,
            HIT,
            DEAD
        }
        
    • 状态基类BaseState

      • 存储与自己相关联的有限状态机CharacterFSM​protected CharacterFSM cfsm;​​

      • 存储要去调整的Animatorprotected Animator animator;​​

      • 存储与当前状态类相关联的状态枚举public CHARACTERSTATE stateType;​​

      • 当前状态要做的事:

        • 初始化:public abstract void InitState();​​
        • 进入时:public abstract void EnterState();​​
        • 退出时:public abstract void ExitState();​​
        • 持续运行:public abstract void UpdateState();​​
    • 静态输入字符类InputCode

      • 存储几个const常量和一个静态字符串数组,记录输入的名称
      •     public const string HorizontalMoveValue = "HorizontalMoveValue";
            public const string VerticalMoveValue = "VerticalMoveValue";
            public const string MoveRotateState = "MoveRotateState";
            public const string HorizontalRotateValue = "HorizontalRotateValue";
            public const string JumpState = "JumpState";
            public const string EquipState = "EquipState";
            public const string AttackState = "AttackState";
            public static string[] skillsState = new string[] {"SkillState0", "SkillState1",
                "SkillState2","SkillState3","SkillState4","SkillState5","SkillState6" };
        
    • 输入管理类InputController

      • 变量

        • <string, bool>字典private Dictionary<string, bool> inputBoolValueDict;
        • <string, float>字典private Dictionary<string, float> inputFloatValueDict;
      • 事件函数:

        • 在Start中把会用到的输入值存到两个字典中并设默认值

          •     void Start()
                {
                    inputBoolValueDict = new Dictionary<string, bool>()
                    {
                        { InputCode.JumpState,false }
                    };
                    inputFloatValueDict = new Dictionary<string, float>()
                    {
                        { InputCode.HorizontalRotateValue,0 },
                        { InputCode.HorizontalMoveValue,0 },
                        { InputCode.VerticalMoveValue,0 }
                    };
                }
            
        • 在Update中多次调用SetInputValue方法将InputCode和对应的输入相关联并存入字典

          •     void Update()
                {
                    SetInputValue(InputCode.HorizontalMoveValue, Input.GetAxis("Horizontal"));
                    SetInputValue(InputCode.VerticalMoveValue, Input.GetAxis("Vertical"));
                    SetInputValue(InputCode.HorizontalRotateValue, Input.GetAxisRaw("Mouse X"));
                    SetInputValue(InputCode.JumpState,Input.GetButtonDown("Jump"));
                }
            
      • 方法:

        • SetInputValue方法:有两个重载,一是对bool型的,一是对float型的

          • 都是先查一下字典中有没有这个名字的输入,有的话就更新那个值,没有的话就输出错误日志信息
          •     public void SetInputValue(string inputCode, bool inputValue)
                {
                    if (inputBoolValueDict.ContainsKey(inputCode))
                    {
                        inputBoolValueDict[inputCode] = inputValue;
                    }
                    else
                    {
                        Debug.Log("设置输入码错误,错误码为" + inputCode);
                    }
                }
            
                public void SetInputValue(string inputCode, float inputValue)
                {
                    if (inputFloatValueDict.ContainsKey(inputCode))
                    {
                        inputFloatValueDict[inputCode] = inputValue;
                    }
                    else
                    {
                        Debug.Log("设置输入码错误,错误码为" + inputCode);
                    }
                }
            
        • GetBoolnputValue()、GetFloatInputValue():查一下字典里有没有,有就返回出来,没有就日志报错

    • 有限状态机CharacterFSM类

      • 变量:

        • 记录<状态枚举,状态类>键值对的字典

          private Dictionary<CHARACTERSTATE, BaseState> statesDict;​​

        • 当前状态 private BaseState currentState;​​

        • 上一个状态 private BaseState lastState;​​

        • 输入管理 private InputController ic;​​

      • 方法:

        • 初始化有限状态机:把会用到的状态以<状态枚举,状态类对象>键值对形式存进字典里,获取外界的InputController,把各个状态和相关参数置默认值初始化

          •     public void InitFSM(Animator currentAnimator,InputController inputController)
                {
                    statesDict = new Dictionary<CHARACTERSTATE, BaseState>()
                    {
                        {CHARACTERSTATE.IDLE, new IdleState(this,currentAnimator,CHARACTERSTATE.IDLE) },
                        {CHARACTERSTATE.MOVE, new MoveState(this,currentAnimator,CHARACTERSTATE.MOVE) },
                        {CHARACTERSTATE.JUMP, new JumpState(this,currentAnimator,CHARACTERSTATE.JUMP) },
                    };
                    ic = inputController;
                    SetDefaultState();
                }
            
        • 状态设默认值:调用字典里所有状态类的初始化方法,并把当前状态设置为IDLE,并调用当前状态的EnterState方法

          •     private void SetDefaultState()
                {
                    foreach(var item in statesDict)
                    {
                        item.Value.InitState();
                    }
                    currentState = statesDict[CHARACTERSTATE.IDLE];
                    currentState.EnterState();
                }
            
        • 改变状态:参数传入一个状态枚举changeState,先检查一下,要改变到的状态在字典中是否存在。如果在,就把字典中,这个状态枚举键对应的状态类对象拿出来,和FSM记录的当前状态currentState对比,如果当前不在这个状态。就调用当前所已记录状态的Exit方法,并用lastState记录,currentState替换为changeState,并调用其Enter方法。

          •     public void ChangeState(CHARACTERSTATE newStateType)
                {
                    if(statesDict.ContainsKey(newStateType))
                    {
                        BaseState changeState = statesDict[newStateType];
                        if(changeState != currentState)
                        {
                            currentState.ExitState();
                            lastState = currentState;
                            currentState = changeState;
                            currentState.EnterState();
                        }
                    }
                }
            
        • 获取当前状态:返回当前状态类对象的状态枚举,而非直接返回状态对象

          •     public CHARACTERSTATE GetCurrentState()
                {
                    return currentState.stateType;
                }
            
        • 输入值的设置和获取:调用InputContraoller对象中的设置和获取输入值方法

          •     public void SetInputValue(string inputCode, bool inputValue)
                {
                    ic.SetInputValue(inputCode, inputValue);
                }
                public bool GetBoolInputValue(string inputCode)
                {
            
                    return ic.GetBoolInputValue(inputCode);
                }
            
                public void SetInputValue(string inputCode, float inputValue)
                {
                    ic.SetInputValue(inputCode, inputValue);
                }
                public float GetFloatInputValue(string inputCode)
                {
                    return ic.GetFloatInputValue(inputCode);
                }
            
      • 事件函数:

        • 在Update中调用当前状态类对象的UpdateState方法

          •     void Update()
                {
                    if(currentState != null)
                    {
                        currentState.UpdateState();
                    }
                }
            
    • 关系图

      • - PlayerMovementController
          - InputController
            - inputBoolDic<InputCode.string , bool>
              - Jump
            - inputFloatDic<InputCode.string , float>
              - HorizontalRotate
              - HorizontalMove
              - VertivalMove
          - CharacterFSM
            - currentState
            - lastState
            - ic(InputController)
            - stateDic<CHARACTERSTATE , BaseState>
              - IdleState
              - MoveState
              - JumpState
        

  • 实现过程中产生的疑惑:

    • transform.Rotate()传入的参数为什么有Vector3.up即(0,1,0)?这不是指向场景正上方的吗?难道是作为旋转轴?

    • 为什么人物的移动的方向向量、跳跃的垂直方向速度等都要乘Time.deltaTime?不乘会怎么样?

    • CharacterController组件

      • stepoffset
      • Move()
    • 每次跳跃后与地面不完全接触?

  • 对疑惑的解答:

    • 往transform.Rotate()传入Vector3.up,是围绕y轴旋转的意思,例如(0,90,0)就是根据左手规则围绕y轴旋转90度;而只传了一个参数,意味着是以物体本身的坐标系为基准。其内部具体实现时,先将Euler形式的向量转化为四元数,如果以自身坐标系为参考,则直接将四元数累乘;如果是以别的空间坐标系作为参考,则需要先逆转原本的旋转,累乘新旋转后再累乘会原有的旋转。

      •     public void Rotate(Vector3 eulers, [DefaultValue("Space.Self")] Space relativeTo)
            {
                Quaternion quaternion = Quaternion.Euler(eulers.x, eulers.y, eulers.z);
                if (relativeTo == Space.Self)
                {
                    localRotation *= quaternion;
                }
                else
                {
                    rotation *= Quaternion.Inverse(rotation) * quaternion * rotation;
                }
            }
        
    • 乘Time.deltaTime是为了确保游戏运动平滑且与帧率无关,这意味着无论游戏运行得多块或多慢,游物体的移动速度和加速度都会保持一致。确保所有玩家都有相同的游戏体验

      这位这种放在Update事件函数中的方法,每帧调用一次,意味着如果不乘上Time.deltaTime的话:

      • 帧率高,那就调用得多,运动得远;帧率低,就调用得少,运动得短
      • 于是移动速度也随帧率的高而高,低而低
      • 在不同的硬件和性能条件下,就会表现出较大的差距
    • CharacterController用于简化角色控制,允许轻松创建出受碰撞约束的移动,而无需处理刚体

      • 关于该组件的部分属性理解:

        Slope Limit:斜坡限制,以度为单位,限制角色能够爬升的最大斜坡角度。
        Step Offset:步长偏移,指定角色能够步越的最大高度。如果障碍物的高度低于此值,角色可以步越它。
        Skin Width:皮肤宽度,表示角色碰撞器在碰撞时可以穿透的深度,它像一个围绕角色的层。
        Min Move Distance:最小移动距离,如果角色尝试移动的距离小于此值,它将不会移动。这可以用来减少抖动。
        Center:中心,指定角色胶囊碰撞器相对于变换位置的中心偏移。
        Radius:半径,胶囊碰撞器的半径,本质上是碰撞器的宽度。
        Height:高度,角色胶囊碰撞器的高度。改变这个值将沿Y轴正负方向缩放碰撞器。
        Layer Overrides:层覆盖,允许你为CharacterController指定额外的层,以决定它可以与哪些其他碰撞器接触。

    • 关于跳跃后与地面的接触问题,可能造成的原因有:

      • Physics.CheckSphere地面检测没有正确设置,导致检测不够准确
      • 角色与地面碰撞器交互有问题
      • 帧率波动导致物理更新没有与帧正确同步

日期:

posted @   静候霜白  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示