掘金 后端 ( ) • 2021-06-08 18:44
.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1{font-size:30px;margin-bottom:5px}.markdown-body h2{padding-bottom:12px;font-size:24px;border-bottom:1px solid #ececec}.markdown-body h3{font-size:18px;padding-bottom:0}.markdown-body h4{font-size:16px}.markdown-body h5{font-size:15px}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body img{max-width:100%}.markdown-body hr{border:none;border-top:1px solid #ddd;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;border-radius:2px;overflow-x:auto;background-color:#fff5f5;color:#ff502c;font-size:.87em;padding:.065em .4em}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{text-decoration:none;color:#0269c8;border-bottom:1px solid #d1e9ff}.markdown-body a:active,.markdown-body a:hover{color:#275b8c}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #f6f6f6}.markdown-body thead{background:#f6f6f6;color:#000;text-align:left}.markdown-body tr:nth-child(2n){background-color:#fcfcfc}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:4px solid #cbcbcb;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body .contains-task-list{padding-left:0}.markdown-body .task-list-item{list-style:none}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}

今天实现的内容:

动画机设计理念

要我说,动画机真的是老生常谈的东西了。做了好几次了,目前也就是将角色的地面动画做进去。 设计理念就是使用混合树ground将地面移动动画统一管理起来。

image.png

动画机的运用及模型旋转

要运用动画机,需要将动画机和输入模块串接起来。所以我们需要新的脚本,没错,PlayerController。同样的,顺手解决角色的旋转功能,旋转的思路为直接修改模型的forward。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    // 玩家的人物模型 用来获取模型身上的动画机以及其它
    public GameObject model;
    // 输入模块
    public PlayerInput pi;
    // 动画机
    private Animator anim;

    // Awake适合用来做GetComponent
    void Awake()
    {
        anim = model.GetComponent();
        pi = GetComponent();
    }

    // Update is called once per frame
    void Update()
    {
        // 将输入转换为速度 赋值给动画机相关参数
        anim.SetFloat("forward", pi.dirMag);
        // 只在有速度时能够旋转 防止原地旋转
        if(pi.dirMag > 0.1f)
            model.transform.forward = pi.dirVec;
    }
}


复制代码

在我们的方案中,旋转的只是模型,PlayerController所在的父物体节点不会旋转

image.png

我们可以将旋转和输入的模长计算放到PlayerInput中进行,能让我们的代码看起来更漂亮。

    // 玩家的输入模长量 用于当成向前量大小作为动画控制
    public float dirMag;
    // 玩家的方向 用于旋转模型
    public Vector3 dirVec;

    // Update is called once per frame
    void Update()
    {
        // 计算输入模长
        dirMag = Mathf.Sqrt((dirUp * dirUp) + (dirRight * dirRight));
        // 计算玩家的方向
        dirVec = dirRight * transform.right + dirUp * transform.forward;
// ...
}

复制代码

玩家角色的位移

对于移动,这次我们将采用Rigidbody方案。通过直接修改刚体的位置来移动。

    // 刚体
    public Rigidbody rigidbody;
    // 行走速度
    public float walkSpeed = 2.0f;
    // 角色的位移大小
    private Vector3 movingVec;
    
    // Update is called once per frame
    void Update()
    {
// ...

        // 计算移动量 速度向量
        movingVec = pi.dirMag * model.transform.forward * walkSpeed;

    }
    
    // 处理刚体的操作
    private void FixedUpdate()
    {
        // 直接修改position实现位移
        rigidbody.position += movingVec * Time.fixedDeltaTime;
     修改rb.velocity来实现位移
        //rb.velocity = new Vector3(m_planarVec.x, rb.velocity.y, m_planarVec.z)
    }

复制代码

爬坡测试

据说黑魂里面的斜楼梯实际上都是斜坡,这就是我们采用Rigidbody的原因,因为Rigidbody处理爬坡比较简单,相对的CharacterController处理爬楼梯比较简单。

image.png

坡度比较缓的坡我们可以爬上去,将来会继续完善爬坡的能力。

跑步

在原基础上加入奔跑。首先加入新的输入按键。并且在Update中判断是否按下对应按键。

    public string keyRun; //跑步键
    
    // 是否正在奔跑 按压信号
    public bool run; 
    
    // Update is called once per frame
    void Update()
    {
        // 跑步信号
        run = Input.GetKey(keyRun);
        // ...
}

复制代码

根据run的真假来调整PlayerController中的动画参数和移动速度。

    // Update is called once per frame
    void Update()
    {
        // 将输入转换为速度 赋值给动画机相关参数
        anim.SetFloat("forward", pi.dirMag * (pi.run ? 2.0f : 1.0f));
        // 计算移动量 速度向量
        movingVec = pi.dirMag * model.transform.forward * walkSpeed * (pi.run ? runMultiplier : 1.0f);
        // ...
    }

复制代码

旋转的优化

按照之前的设计,旋转速度会很快。这里我们使用Slerp给旋转一个缓动的效果。

    // Update is called once per frame
    void Update()
    {
        // 只在有速度时能够旋转 防止原地旋转
        if(pi.dirMag > 0.1f)
        {
            // 运用旋转 使用Slerp进行效果优化   
            model.transform.forward = 
            Vector3.Slerp(model.transform.forward, pi.dirVec, 0.3f);
        }

// ...
    }

复制代码

跑步动画的优化

// 将输入转换为速度 赋值给动画机相关参数
anim.SetFloat("forward", pi.dirMag * (pi.run ? 2.0f : 1.0f));

复制代码

按以上代码设计,如果我们在走路时突然切换到跑步,由于dirMag是不变的,我们只是直接将它乘以2,会导致混合树的参数直接突变到2,行走到跑步动画的切换就没有一个过渡了。

    void Update()
    {
// 将现在的动画参数forward值通过lerp变化得到
        anim.SetFloat("forward", Mathf.Lerp(anim.GetFloat("forward"), pi.dirMag * (pi.run ? 2.0f : 1.0f), 0.1f));
// ...
}

复制代码

采用以上方案能改善问题,forward的值将通过lerp去修改。

BUG以及缺陷:

模型旋转错误,不管向那个方向旋转,只要松开按键,输入变成0,模型就会自动回归原方向的旋转。这是因为旋转控制参数dirVec受输入归零的影响。要解决这个物体,我们可以在dirMag小于某个值时不再进行旋转。但是这样会导致无法在不移动时原地旋转,话说黑魂能不能原地旋转来着?好像不能吧。

// 只在有速度时能够旋转 防止原地旋转
if(pi.dirMag > 0.1f)
model.transform.forward = pi.dirVec;

复制代码

移动还是要注意那个问题,在这里也就是要将dirMag最大设置为1。否则同时按下两个方向时速度会因为叠加变快。 这里老师找了一篇专门解决这个问题的论文。因为如果按照之前处理FPS的办法,会导致斜向虽然最大速度一致,但加速还是快于单向。

image.png

这个实现的应该是,将方形映射到圆形,这样以前是根号2的对角现在长度也变成1了。这样我们在程序中运用下面这个公式就行。

    // Update is called once per frame
    void Update()
    {
   // ...
    
// 转换输入的坐标从方向到圆形
        m_dirAxis = SquareToCircle(new Vector2(dirRight, dirUp));

        // 计算输入模长 
        dirMag = Mathf.Sqrt(m_dirAxis.y * m_dirAxis.y + m_dirAxis.x * m_dirAxis.x);
        // 计算玩家的方向
        dirVec = m_dirAxis.x * transform.right + m_dirAxis.y * transform.forward;
// ...
}

    // 用于将得到的输入从方形坐标映射到圆形坐标 实现不同输入得到的dirMag长度都一致
    private Vector2 SquareToCircle(Vector2 input)
    {
        Vector2 output = Vector2.zero;

        // 运用公式
        output.x = input.x * Mathf.Sqrt(1 - (input.y * input.y) / 2.0f);
        output.y = input.y * Mathf.Sqrt(1 - (input.x * input.x) / 2.0f);

        return output;
    }

复制代码

关于使用rigidbody.velocity来实现位移有一个问题,这个办法需要你在传入rigidbody.velocity的vector3的y分量中有关于重力的计算。否则关于重力的速度计算会被你完全清零。

image.png

上图没有考虑重力方向的速度,下图为解决方案。

image.png

值得注意的: GetComponent更适合在Awake中做,而不是Start

// Awake适合用来做GetComponent
    void Awake()
    {
        anim = model.GetComponent();
        pi = GetComponent();
        // ... 
    }

复制代码

不要在Update中对Rigidbody做任何操作,逻辑上的设置除外,Rigidbody中的操作要在FixedUpdate中做。因为在Unity中Update的调用速度和Rigidbody的物理模拟速度很可能不一样。

控制器还会有跳跃,落地,翻滚等功能。这些将写在别的随手记中

这一条更新于2021.4.26,今后的位移将使用修改刚体速度的方式来实现。以获得和老师的一样的效果。

    // 处理刚体的操作
    private void FixedUpdate()
    {
         直接修改position实现位移
        //if(!m_lockMovement) rigidbody.position += m_planarVec * Time.fixedDeltaTime;
         运用各项冲量
        //rigidbody.velocity += m_thrustVec;
         清除冲量
        //m_thrustVec = Vector3.zero;

        // 修改rb.velocity来实现位移
        if (!m_lockMovement) rb.velocity = new Vector3(m_planarVec.x, rb.velocity.y, m_planarVec.z) + m_thrustVec;
        // 清除冲量
        m_thrustVec = Vector3.zero;

    }

复制代码

文章图片来源:h5游戏