Unity3D鼠标控制角色移动

一直都有一颗文学逗比的心,很中二和玛丽苏的想写那种龙傲天的小说。所以这个寒假就非常想敲出个RPG游戏来抒发心中的这份狂热。一开始是想用Three.js来做,后来转用Unity3D来做了,毕竟相对简单一点。好多东西不用自己去写,也可以避免心中这份狂热不至于还没把基础框架搭建好就降为0度了。

角色移动的例子

控制角色移动,对于PC端而言就是键盘或者鼠标。其中键盘控制角色移动的是经典的fps游戏中wasd四个方向按键。而鼠标控制角色移动一般常见于MMORPG。我比较倾向于MMORPG风格控制角色移动,所以选择这一类。
Unity3D官方提供的是javascript写的一个基于键盘控制的小demo。对我而言,javascript虽然目前很火,但是还是觉得这门语言缺陷太严重,所以改用C#,期间碰到了一个很奇怪的问题:其中一个函数像是被注释掉一样死活不执行,所以就放在另一个方法执行体内了,看起来丑丑的。还有一个就是被MonoDevelop整得微醺。还是很赞Mono哒~
大致是借鉴了官方提供的例子,在此基础上做了一些小修改。
还是详细说一下具体思路,在文章中贴出完整代码(ps:肯定不完整的啦,动画还没加呢,只是能动了XD)

定义类

类中分为四部分:字段、属性、本地方法、继承方法
官方例子中,将角色分为几种状态,根据不同的状态赋予角色不同的运动速度和显示动画,为了之后方便操作,提供了可以在Unity3D直接进行修改的公有字段(这里很费解,为什么不是公有属性,可能大师的设计不是能理解的),这里就大致精简和照抄照搬了:

字段和属性

enum CharacterState : int{
        Standing = 0,
        Walking = 1,
        Running = 2,
    }

    #region 公有字段

    #region 动作动画
    /// <summary>
    /// 站立动画
    /// </summary>
    public AnimationClip Standing;
    /// <summary>
    /// 奔跑动画
    /// </summary>
    public AnimationClip Running;
    /// <summary>
    /// 步行动画
    /// </summary>
    public AnimationClip Walking;
    #endregion

    #region 参数设置
    /// <summary>
    /// 步行速度
    /// </summary>
    public float WalkSpeed = 0.3f;
    /// <summary>
    /// 奔跑速度
    /// </summary>
    public float RunSpeed = 0.8f;
    /// <summary>
    /// 重力加速度
    /// </summary>
    public float Gravity = 5.0f;
    /// <summary>
    /// 到达误差
    /// </summary>
    public float ArriveError = 1.0f;
    /// <summary>
    /// 速度平滑度
    /// </summary>
    public float SpeedSmoothing = 5.0f;
    /// <summary>
    /// 角色接受的最大坡度
    /// </summary>
    public float SlopeLimit = 45.0f;
    /// <summary>
    /// 角色旋转速度
    /// </summary>
    public float RotateSpeed = 20.0f;
    /// <summary>
    /// 空中阻力
    /// </summary>
    public float InAirControlAcceleration = 4.0f;
    /// <summary>
    /// 最大速度
    /// </summary>
    public float MaxSpeed = 2.0f;
    #endregion

    #endregion

然后就是私有字段了,大致就是角色类内的各种状态,这里先给“最后距离”这一个字段立个flag,因为它解决了一个坑了我很久的问题:


    #region 私有字段
    /// <summary>
    /// 角色状态
    /// </summary>
    private CharacterState _characterState;
    /// <summary>
    /// 碰撞标签
    /// </summary>
    private CollisionFlags _collisionFlag;
    /// <summary>
    /// 垂直速度
    /// </summary>
    private float _verticalSpeed = 0.0f;
    /// <summary>
    /// 移动方向
    /// </summary>
    private Vector3 _moveDirection = Vector3.zero;
    /// <summary>
    /// 在空中的速度
    /// </summary>
    private Vector3 _inAirVelocity = Vector3.zero;
    /// <summary>
    /// 移动速率
    /// </summary>
    private float _moveSpeed = 0.0f;
    /// <summary>
    /// 目标位置
    /// </summary>
    private Vector3 _targetPosition = Vector3.zero;
    /// <summary>
    /// 目标是否有效
    /// </summary>
    private bool _isValidTarget = false;
    /// <summary>
    /// 控制器
    /// </summary>
    private CharacterController _controller;
    /// <summary>
    /// 最后距离
    /// </summary>
    private float _lastDistance = 0.0f;
    #endregion

目前属性设置了三个,其中一个略显鸡肋。
这里解释说明一下:
Unity3D提供了CharacterController 角色控制器这样一种很好用的东西。

摘自网络的Physx参考:
character一般用于主角这类用户控制的物体,它不会受到scene的重力影响,不会被其他物体推。
程序中可以使用它的move方法移动它,当他碰到静态物体时,会停下来,遇到动态物体时会推开他,当然,这些都是可以通过activegroup来控制的。group最多有32组。因为他是一个NxU32,并通过每一位代表一个组。
move的一个参数用来告诉程序,character的当前状态。(collisionFlags)
当他遇到物体的时候,如果设置了回调函数,系统会通过回调函数通知程序。。(NxControllerDesc.callback)
character还有上楼梯模式,在某些高度的台阶,可以直接上去。(NxControllerDesc.stepOffset)
character还可以设置可以走上去的斜坡。(NxControllerDesc.slopeLimit)
由于character不受场景的重力影响,所以,用户要在move函数中自己添加重力因素,也就是说,character可以浮在空中,除非那里有其他activegroup物体。

这是我不用Three.js原因中很大一部分原因。就是避免重复造轮子但是学人家造轮子就是另一回事了。

IsGrounded正是借用CharacterController.move()返回的collisionFlag来判断是否到达地面。
IsTouchedWall 这个属性很鸡肋,没有任何必要去讲。当时是因为当角色在斜坡上横向移动时,会因为判定为悬空而在竖直方向上有个非常大的速度,导致移动速度非常快。所以加上这样的属性,但是效果并不明显,它是检测竖直方向上的速度,而非横向速度。所以是没用的鸡肋的。其实跟CharacterController.slopeLimit功能应该是一样的。
SetCharacterState 这个是个只写属性,主要是用于修改角色状态,以及变换角色动画,switch里边填充XD

#region 属性
    /// <summary>
    /// 是否跌落至地面
    /// </summary>
    /// <value><c>true</c> if this instance is grounded; otherwise, <c>false</c>.</value>
    private bool IsGrounded { get { return (this._collisionFlag & CollisionFlags.CollidedBelow) != 0; } }
    /// <summary>
    /// 是否碰触到墙体
    /// </summary>
    /// <value><c>true</c> if this instance is touched wall; otherwise, <c>false</c>.</value>
    private bool IsTouchedWall
    {
        get
        {
            if(this._moveSpeed != 0){
                Vector3 newPosition = this.transform.position + (this.transform.rotation * Vector3.forward * this._moveSpeed);
                newPosition.y = Terrain.activeTerrain.SampleHeight(newPosition);

                float deltaHeight = newPosition.y - Terrain.activeTerrain.SampleHeight(this.transform.position);
                float deltaLength = Vector3.Distance(newPosition, this.transform.position);
                Debug.DrawLine(this.transform.position, newPosition);
                float alpha = Mathf.Asin(deltaHeight / deltaLength) * Mathf.Rad2Deg;
                if(alpha <= this.SlopeLimit){
                    return false;
                }
                else{
                    return true;
                }
            }
            return false;
        }
    }
    /// <summary>
    /// 设置角色状态
    /// </summary>
    /// <value>The state of the set character.</value>
    private CharacterState SetCharacterState
    {
        set
        {
            //惰性更新动画
            if(this._characterState != value)
            {
                this._characterState = value;
            }

            switch(this._characterState)
            {

            default:
                break;
            }
        }
    }
    #endregion

方法

本地方法提供两个(其实是三个,由于一个死活不执行,所以只好两个功能相近的方法合并为一个)。

第一个方法:UpdateMovementTarget
用于更新运动目标
方法执行一开始,首先判断当前的移动目标是否有效
如果有效,则判断是否到达目的点(即当前位置与目的位置的距离小于容差值),
如果到达目的点,则停止运动,
否则,判断是否开始与目的点距离变大,如果变大,则立即停止运动。否则记录最新的与目的点距离。

判断是否有鼠标右键点击中断(可以设置为公有字段),
如果传入鼠标右键点击中断,则从摄像机位置发射射线,与地面碰撞的点即为目标点,由于在平面移动时竖直方向上的值并不影响,且在之前容错时会导致错误,所以将目标点的高度设置为0,并记录距离。

计算角色的移动方向,这里不赘述。

之后根据之前的状态,来改变角色的速度。

第二个方法:ApplyGravity
这个方法主要用作重力加速度,再次吐槽IsTouchedWall的鸡肋。

/// <summary>
    /// 更新运动方向
    /// </summary>
    private void UpdateMovementTarget()
    {
        Vector3 memoryPosition = this.transform.position;
        memoryPosition.y = 0;
        if(this._isValidTarget)
        {
            float tempDistance = Vector3.Distance(memoryPosition, this._targetPosition);
            if(tempDistance <= this.ArriveError)
            {
                this._isValidTarget = false;
                this._lastDistance = 0.0f;
            }
            else if(tempDistance >= this._lastDistance)
            {
                this._isValidTarget = false;
                this._lastDistance = 0.0f;
            }
            else
            {
                this._lastDistance = tempDistance;
            }
        }

        if(Input.GetMouseButtonDown(1))
        {
            this._moveDirection = Vector3.zero;
            this._isValidTarget = false;

            Ray targetRay = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;
            if(Physics.Raycast(targetRay, out hitInfo, Mathf.Infinity))
            {
                if(hitInfo.collider.gameObject.tag == "Terrain")
                {
                    this._targetPosition = hitInfo.point;
                    this._targetPosition.y = 0;
                    this._isValidTarget = true;

                    this._lastDistance = Vector3.Distance(memoryPosition, this._targetPosition);
                }
            }
        }

        Vector3 targetDirection = (this._targetPosition - this.transform.position).normalized;

        if(this._isValidTarget)
        {
            this._moveDirection = Vector3.RotateTowards(this._moveDirection, targetDirection, this.RotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);
            this._moveDirection.y = 0;
            this._moveDirection = this._moveDirection.normalized;
        }

        if(!this.IsGrounded)
        {
            this._inAirVelocity += targetDirection.normalized * Time.deltaTime * this.InAirControlAcceleration;
        }
        else
        {
            this._inAirVelocity = Vector3.zero;
            this._verticalSpeed = 0.0f;
        }

        if(this.IsTouchedWall)
        {
            this._isValidTarget = false;
        }

        if(this._isValidTarget){
            float targetSpeed = Mathf.Min(this._moveDirection.magnitude, 1.0f);
            float currentSmooth = this.SpeedSmoothing * Time.deltaTime;

            if(Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
            {
                targetSpeed *= this.RunSpeed;
                this.SetCharacterState = CharacterState.Running;
            }
            else
            {
                targetSpeed *= this.WalkSpeed;
                this.SetCharacterState = CharacterState.Walking;
            }
            this._moveSpeed = Mathf.Lerp(this._moveSpeed, targetSpeed, currentSmooth);
        }
        else
        {
            this._moveSpeed = 0.0f;
            this._moveDirection = Vector3.zero;
            this.SetCharacterState = CharacterState.Standing;
        }
    }
    /// <summary>
    /// 重力加速度
    /// </summary>
    private void ApplyGravity()
    {
        if(this.IsGrounded || this.IsTouchedWall)
        {
            this._verticalSpeed = 0.0f;
            this._inAirVelocity = Vector3.zero;
        }
        else
        {
            this._verticalSpeed -= this.Gravity * Time.deltaTime;
        }
    }
    #endregion

接下来是继承方法,MonoBehavior默认提供两种继承方法:Start和Update.
Start执行时间在Update函数第一次被调用前调用。
Update执行时间为当MonoBehaviour启用时,其Update在每一帧被调用。


    // Use this for initialization
    void Start ()
    {
        //初始设置
        this.SetCharacterState = CharacterState.Standing;
        //目标无效
        this._isValidTarget = false;
        //初始化控制器
        this._controller = GetComponent<CharacterController>();
    }

    // Update is called once per frame
    void Update ()
    {
        //更新移动目标
        this.UpdateMovementTarget();
        //添加重力影响
        this.ApplyGravity();


        this._controller.slopeLimit = this.SlopeLimit;

        //合成速度
        Vector3 movement = this._moveDirection * this._moveSpeed + (new Vector3(0, this._verticalSpeed, 0)) + this._inAirVelocity;
        if(movement.magnitude > this.MaxSpeed)
        {
            movement = movement.normalized * this.MaxSpeed;
        }
        //角色转向
        if(this._moveDirection != Vector3.zero)
            this.transform.rotation = Quaternion.LookRotation(this._moveDirection);
        //角色移动
        _collisionFlag = this._controller.Move(movement);

    }

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。