首页 > 其他分享 >技术笔记(9)MMORPG人物操作系统

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

时间:2024-03-15 16:44:48浏览次数:16  
标签:inputCode 操作系统 void transform 笔记 CHARACTERSTATE MMORPG ic public

技术笔记(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地面检测没有正确设置,导致检测不够准确
      • 角色与地面碰撞器交互有问题
      • 帧率波动导致物理更新没有与帧正确同步

日期:

标签:inputCode,操作系统,void,transform,笔记,CHARACTERSTATE,MMORPG,ic,public
From: https://www.cnblogs.com/bqza000/p/18075774

相关文章

  • 含有特殊字符导致操作系统无法创建文件各个系统
    Window系统windows中,文件名(包括扩展名)可高达255个字符。文件名可以包含除?/\<>*|:之外的大多数字符;保留文件名的大小写;文件名不区分大小写(由POSIX应用程序使用时除外)。windows系统下文件名长度为:255个英文字符(DOS下8.3格式),包括文件名和扩展名在内,或者是255/2=127......
  • Vue学习笔记52--全局事件总线
    Vue全局事件总线:一种组件之间通信的方式,适用于任意组件之间通信。1.所有组件,即VueComponent所有的组件实例对象vc2.每次使用VueComponent都是new一个新的vc3.Vue.prototype=VueComponent.prototype.__proto__(可以让组件实例对象vc访问到Vue原型上的属性、方法)4.$emit、$o......
  • ELK日志处理部署笔记-2
    logstash概述什么是logstash-是一个数据采集、加工处理以及传输的工具特点-所有类型的数据集中处理-不同模式和格式数据的正常化-自定义日志格式的迅速扩展-为自定义数据源轻松添加插件为什么使用logstash将日志转化为json使elasticasearch可读下载#logstash的配置文件......
  • Java面向对象的一些学习笔记
    1.Private关键字:(1)private关键字是一个权限修饰符(2)可以修饰成员变量和成员方法(3)被private修饰的成员只能在本类中才能访问(4)针对private修饰的成员变量,如果需要被其他类使用,提供相应的操作(5)提供"setXxx(参数)"方法,用于给成员变量赋值,方法用public修饰(6)提供"getXxx(参数)......
  • MATLAB学习笔记1.数组运算
    先来介绍两个常用的,在命令行里边输入“clc”,就会清空以上的命令行(也就是这个直接与你对话的地方)的所有内容;但是并不会把已经设置的变量清空,要想清空变量,则需要在命令行中输入“clear”,这样就可以把右侧已经设置的变量都清空掉了。下面是示例输入回车再输入“clear”并输入......
  • Java学习笔记——第十六天
    集合进阶(二)Collection的其他相关知识前置知识:可变参数就是一种特殊形参,定义在方法、构造器的形参列表里,格式是:数据类型...参数名称。特点可以不传数据给它;可以传一个或者同时传多个数据给它;也可以传一个数组给它。作用常常用来灵活地接收数据。注意事项可变参数在方法......
  • 脑图系列-操作系统IO
     同步、异步、阻塞、非阻塞同步与异步描述的是被调用者的如果是同步,B在接到A的调用后,会立即执行要做的事。A的本次调用可以得到结果。让我干活立马就干,立即反馈结果如果是异步,B在接到A的调用后,不保证会立即执行要做的事,但是保证会去做,B在做好了之后会通知A。A的本次调用......
  • 逻辑学笔记
    逻辑学笔记目录逻辑学笔记写在前面1.概念1.1导论1.2概念的内涵和外延1.3概念的种类1.4概念外延的关系1.5定义1.6划分2.直言命题2.1含义与结构2.2直言命题的种类2.3直言命题的逻辑特征2.4直言命题的对当关系2.5对当关系的直接推理2.6换质法和换位法3.直言三段论3.1直言三段论的含......
  • 笔记本如何在PVE下All in one?—NAS + Linux +win下载机
    没有有线网卡的笔记本在PVE下Allinone|NAS+Linux+Win下载机(保姆级未完成版)  目录:1、前言2、PVE的安装3、PVE联网前的准备工作4、PVE使用无线网卡进行联网5、PVE无线网卡的桥接6、PVE下创建虚拟机:NAS+Linux+Win下载机 1、前言十年前的旧笔记本,......
  • CTF笔记——[GXYCTF2019]禁止套娃 1
    [GXYCTF2019]禁止套娃1打开题目之后什么都没看到所以进行常规的检测漏洞,扫描目录发现存在.git文件夹下的文件存在#DirsearchstartedSunMar1015:19:392024as:D:\Python\Scripts\dirsearch-uhttp://849b4a98-3df3-4abb-927e-1a358a178e30.node5.buuoj.cn:81/-x429......