Unity 物理系列三 角色控制器

参考
https://docs.unity.cn/cn/2019.4/Manual/CharacterControllers.html

第一人称或第三人称游戏中的角色通常需要一些基于碰撞的物理效果,这样角色就不会跌穿地板或穿过墙壁。但是,通常情况下,角色的加速度和移动在物理上并不真实,因此角色可以不受动量影响而几乎瞬间加速、制动和改变方向。
在 3D 物理中,可以使用角色控制器创建此类行为。该组件为角色提供了一个始终处于直立状态的简单胶囊碰撞体。控制器有自己的特殊函数来设置对象的速度和方向,但与真正的碰撞体不同,控制器不需要刚体,动量效果也不真实。
角色控制器无法穿过场景中的静态碰撞体,因此将紧贴地板并被墙壁阻挡。控制器可以在移动时将刚体对象推到一边,但不会被接近的碰撞加速。这意味着可以使用标准 3D 碰撞体来创建供控制器行走的场景,但您不受角色本身的真实物理行为的限制。

一、参考B站 视频【简明Unity教程】漫谈Unity中的各种移动方法
image.png

直接修改Postion简单粗暴,无法处理复杂情况比如物理碰撞,障碍物。比如控制一个小球移动:

void Update(){
   float x = Input.GetAxis("Horizontal");
   transform.Translate(x*Time.deltaTime*turnSpeed,0,speed*Time.deltaTime);
   //上面的Translate等价于这样写:
   transform.position += new Vector3(x*Time.deltaTime*turnSpeed,0,speed*Time.deltaTime);
}

关于position和Translate的区别,可以参考Unity基础篇:Unity中的世界坐标和局部坐标,Transform和Translate等问题的讨论。

对于translate(a,b)函数,如果省略b,系统将把它缺省为space.self,意思是在自身坐标系进行a向量方向的移动,如果b为space.world,那么物体就在世界坐标系上进行a向量方向的移动。
transform.position 本身就是世界坐标。transform.position+= vector3.forward等于是在世界坐标的z轴前进。
Vector3.forward和transform.forward的区别,Vector3.forward的值永远是(0,0,1)(这里的(0,0,1)是世界坐标的(0,0,1)),而transform.forward我们可以理解为其对应物体的z轴方向,是一个向量,而不是一个坐标,但是我们应当把它看成世界坐标系内的,而不是局部坐标系内的。

二、参考【移动专题】20分钟学会使用3D模型和动画
1.去Store下载并导入这两个资源
image.png

image.png
2.给模型添加动画和脚本

这一步可以参考视频,使用的知识点可以参考之前的动画系列:Unity 动画系列三 Animator Controller

脚本就简单粗暴移动position:

using UnityEngine;

public class CharMove : MonoBehaviour
{
    public float speed = 3;

    Animator anim;
    Vector3 move;

    private void Start()
    {
        anim = GetComponent<Animator>();
    }

    void Update()
    {
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        move = new Vector3(x, 0, z);
        updateAnim();
        //往哪边走就往哪看
        transform.LookAt(transform.position + new Vector3(x, 0, z));
        transform.position += new Vector3(x, 0, z) * speed * Time.deltaTime;
    }

    void updateAnim()
    {
        anim.SetFloat("speed", move.magnitude);
    }
}

这里视频中能看到如何解决以下两个问题:

  • 小人移动过快,这是因为使用的是RM动作,即root motion,动画播放时也进行了位移,需要暂时取消root motion。
  • 小人刚开始移动时,会在待机动作下移动一段距离后,才开始跑动。这是因为动作切换时,没有强行打断。需要在translation中,把has exit time取消勾选。这样就可以随走随停了。
三、参考【移动专题】实用的物理刚体角色控制器
1.给角色添加rigidbody和胶囊体,在场景中添加Cube进行测试
  • 角色撞到Cube会停下来,然后摔倒。这个修改rigidbody的freeze rotation,勾选即可解决。
  • 角色撞到Cube后,会发生抖动。这个需要将transfrom移动方式,改为刚体移动。

Unity 脚本生命周期中,详细解释了update和FixedUpdate区别,输入事件获取放在update中,刚体的移动操作则应该放在FixedUpdate:

using UnityEngine;

public class CharMove : MonoBehaviour
{
    public float speed = 3;

    Animator anim;
    Rigidbody rigid;
    Vector3 move;

    private void Start()
    {
        anim = GetComponent<Animator>();
        rigid = GetComponent<Rigidbody>();
    }

    void Update()
    {
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        move = new Vector3(x, 0, z);
        //往哪边走就往哪看
        //transform.LookAt(transform.position + new Vector3(x, 0, z));
        //transform.position += new Vector3(x, 0, z) * speed * Time.deltaTime;
        updateAnim();
    }

    private void FixedUpdate()
    {
        rigid.velocity = move * speed;
    }

    void updateAnim()
    {
        anim.SetFloat("speed", move.magnitude);
    }
}
2.新的转向方式
using UnityEngine;

public class CharMove : MonoBehaviour
{
    public float speed = 3;
    public float turnspeed = 10;

    Animator anim;
    Rigidbody rigid;
    Vector3 move;

    float forwardAmount;//前进量
    float turnAmount;//转向量

    private void Start()
    {
        anim = GetComponent<Animator>();
        rigid = GetComponent<Rigidbody>();
    }

    void Update()
    {
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        move = new Vector3(x, 0, z);
        //往哪边走就往哪看
        //transform.LookAt(transform.position + new Vector3(x, 0, z));
        //transform.position += new Vector3(x, 0, z) * speed * Time.deltaTime;

        //世界坐标转本地坐标
        Vector3 localMove = transform.InverseTransformVector(move);
        //本地坐标的Z永远是角色的正前方
        forwardAmount = localMove.z;
        //转向量则是两个分量的比值,x越大z越小就转得越快
        turnAmount = Mathf.Atan2(localMove.x, localMove.z);
        updateAnim();
    }

    private void FixedUpdate()
    {
        //rigid.velocity = move * speed;
        rigid.velocity = forwardAmount * transform.forward * speed;
        //转向,只需要转水平方向的。新朝向=老朝向*一个角度
        //turnAmout本身有正负性,已经能表示是左转还是右转
        rigid.MoveRotation(rigid.rotation * Quaternion.Euler(0, turnAmount * turnspeed, 0));
    }

    void updateAnim()
    {
        anim.SetFloat("speed", move.magnitude);
    }
}

3.对动画进行融合

使用的知识点可以参考Unity 动画系列六 BlendTree混合树

在游戏动画中一个常见的任务是将两个或多个相似的动作混合在一起。也许最著名的例子就是根据角色的速度混合行走和跑步的动画。另一个例子是一个角色在跑步时向左或向右倾斜,就是根据参数来混合,决定当前播放的是哪个动画。

1.blend type 选2D Freeform Cartesian

2D Freeform Cartesian(2D自由笛卡儿):当混合的2个参数不代表不同的方向时使用。使用Freeform Cartesian,参数X和Y可以表示不同的概念类型,例如角速度和线速度。

先增加两个参数Forward,Turn。然后Add Motion Field:


image.png
2.拖动画
image.png

image.png

image.png

拖入上述3个动画

3.设置参数
image.png
image.png

把右下角的预览播放按钮打开,然后拖动红点位置,从停止状态一直拖到跑动状态,就能看到不同的Forward值,融合出来的动画效果。脚本修改如下:

    void updateAnim()
    {
        //anim.SetFloat("speed", move.magnitude);
        anim.SetFloat("Forward", forwardAmount);
        anim.SetFloat("Turn", turnAmount);
    }
4.给予角色走的能力
    void Update()
    {
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        move = new Vector3(x, 0, z);

        //往哪边走就往哪看
        //transform.LookAt(transform.position + new Vector3(x, 0, z));
        //transform.position += new Vector3(x, 0, z) * speed * Time.deltaTime;

        //世界坐标转本地坐标
        Vector3 localMove = transform.InverseTransformVector(move);
        //本地坐标的Z永远是角色的正前方
        forwardAmount = localMove.z;
        //转向量则是两个分量的比值,x越大z越小就转得越快
        turnAmount = Mathf.Atan2(localMove.x, localMove.z);

        //按下左SHIFT键,进入走路状态
        if (Input.GetButton("Fire3"))
        {
            forwardAmount *= 0.3f;
        }
        updateAnim();
    }

不过现在这样,角色虽然走起来了,动画播放速度有点快。可以改下播放速度:


image.png
四、参考【移动专题】一起学习别人家的角色控制器
1.下载standard assets资源包并导入
image.png
2.打开CharacterThirdPerson场景
3.ThirdPersonUserControl.cs
private void FixedUpdate()
{
    // read inputs
    float h = CrossPlatformInputManager.GetAxis("Horizontal");
    float v = CrossPlatformInputManager.GetAxis("Vertical");
    bool crouch = Input.GetKey(KeyCode.C);

    // calculate move direction to pass to character
    if (m_Cam != null)
    {
        // calculate camera relative direction to move:
        m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;
        m_Move = v*m_CamForward + h*m_Cam.right;
    }
    else
    {
        // we use world-relative directions in the case of no main camera
        m_Move = v*Vector3.forward + h*Vector3.right;
    }
#if !MOBILE_INPUT
    // walk speed multiplier
    if (Input.GetKey(KeyCode.LeftShift)) m_Move *= 0.5f;
#endif

    // pass all parameters to the character control script
    m_Character.Move(m_Move, crouch, m_Jump);
    m_Jump = false;
}

这里角色按W向前时,是根据摄像头来计算的。按C键即crouch,是蹲下走路。

4.ThirdPersonUserControl.cs
public void Move(Vector3 move, bool crouch, bool jump)
{

    // convert the world relative moveInput vector into a local-relative
    // turn amount and forward amount required to head in the desired
    // direction.
    if (move.magnitude > 1f) move.Normalize();
    move = transform.InverseTransformDirection(move);
    CheckGroundStatus();
    move = Vector3.ProjectOnPlane(move, m_GroundNormal);
    m_TurnAmount = Mathf.Atan2(move.x, move.z);
    m_ForwardAmount = move.z;

    ApplyExtraTurnRotation();

    // control and velocity handling is different when grounded and airborne:
    if (m_IsGrounded)
    {
        HandleGroundedMovement(crouch, jump);
    }
    else
    {
        HandleAirborneMovement();
    }

    ScaleCapsuleForCrouching(crouch);
    PreventStandingInLowHeadroom();

    // send input and other state parameters to the animator
    UpdateAnimator(move);
}

if (move.magnitude > 1f) move.Normalize();当同时按下WD时,处理斜向移动过快。其它逻辑和上一部分的处理类似。

5.blend tree
image.png
image.png

以地面融合树来看,动画是非常丰富的,Y轴有停走跑3个维度,每个维度分左右,以左方向为例,又分小左转,和急左转。

6.Root Motion

知识点可以参考Unity 动画系列四 代码控制动画实例 和 RootMotion
这里自己实现了OnAnimatorMove,对速度可以自由控制,Root Motion显示为Handled by Script。

7.AICharacterControl.cs

这个是点击一个位置,自动导航的。


image.png
        private void Update()
        {
            if (target != null)
                agent.SetDestination(target.position);

            if (agent.remainingDistance > agent.stoppingDistance)
                character.Move(agent.desiredVelocity, false, false);
            else
                character.Move(Vector3.zero, false, false);
        }

结合点就是agent.desiredVelocity
在unity学习项目中,也有类似处理:

image.png

CharacterControl.cs

最大值不会超过m_Agent的最大速度

五、官方自带的角色控制器

https://docs.unity.cn/cn/2019.4/Manual/class-CharacterController.html

image.png

传统末日风格的第一人称控制在现实中并不真实。该角色每小时能跑 90 英里,可以立即停止并急转弯。因为该角色非常不真实,所以使用刚体和物理组件来创造这种行为有点不切实际,并会让玩家产生错觉。解决方案是使用专门的角色控制器。角色控制器只是一个胶囊形状的碰撞体,可以通过脚本来命令这个碰撞体向某个方向移动。然后,控制器将执行运动,但会受到碰撞的约束。控制器将沿着墙壁滑动,走上楼梯(如果低于 Step Offset 值),并走上 Slope Limit 设置范围内的斜坡。
控制器本身不会对力作出反应,也不会自动推开刚体。
如果要通过角色控制器来推动刚体或对象,可以编写脚本通过 OnControllerColliderHit() 函数对与控制器碰撞的任何对象施力。
另一方面,如果希望玩家角色受到物理组件的影响,那么可能更适合使用刚体,而不是角色控制器。

  • Slope Limit 将碰撞体限制为爬坡的斜率不超过指示值(以度为单位)。
  • Step Offset 仅当角色比指示值更接近地面时,角色才会升高一个台阶。该值不应该大于角色控制器的高度,否则会产生错误。
  • Skin width 两个碰撞体可以穿透彼此且穿透深度最多为皮肤宽度 (Skin Width)。较大的皮肤宽度可减少抖动。较小的皮肤宽度可能导致角色卡住。合理设置是将此值设为半径的 10%。
  • Min Move Distance 如果角色试图移动到指示值以下,根本移动不了。此设置可以用来减少抖动。在大多数情况下,此值应保留为 0。
  • Center 此设置将使胶囊碰撞体在世界空间中偏移,并且不会影响角色的枢转方式。
  • Radius 胶囊碰撞体的半径长度。此值本质上是碰撞体的宽度。
  • Height 角色的胶囊碰撞体高度。更改此设置将在正方向和负方向沿 Y 轴缩放碰撞体。

使用示例可以参考
[Unity3D]最简单的最详细的第一人称角色控制器
Physx CharacterController源码解析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容