JavaScript is required
Back

类刺客信条跑酷系统开发日记

2025/05/11

类刺客信条跑酷系统开发日记

Parkour Climbing System

Parkour Climbing System开发日记

——类刺客信条跑酷系统

Day1 摄像头脚本

在unity中,xyz轴是右手坐标系,即x水平向右,y垂直向上,z水平向前

public class CameraController : MonoBehaviour
{
    //摄像机跟随的目标
    [SerializeField] Transform followTarget;

    // Update is called once per frame
    void Update()
    {
        //摄像机放在目标后面5个单位的位置
        transform.position = followTarget.position - new Vector3(0, 0, 5);
    }
}

怎么旋转这个相机呢?

1746860633930
1746860633930

摄像机向后移动的参量乘一个水平旋转角度

所以,引入四元数点欧拉Quaternion.Euler

这个水平视角旋转角度需要绕y轴的旋转角度,还需要鼠标控制这个角度

并且,当摄像头旋转的时候,摄像头始终对着player

public class CameraController : MonoBehaviour
{
    //摄像机跟随的目标
    [SerializeField] Transform followTarget;
    //距离
    [SerializeField] float distance;

    //绕y轴的旋转角度
    float rotationY;

    private void Update()
    {
        //鼠标x轴控制rotationY
        rotationY += Input.GetAxis("Mouse X");
        //水平视角旋转参量
        //想要水平旋转视角,所以需要的参量为绕y轴旋转角度
        var horizontalRotation = Quaternion.Euler(0, rotationY, 0);

        //摄像机放在目标后面5个单位的位置
        transform.position = followTarget.position - horizontalRotation * new Vector3(0, 0, distance);
        //摄像机始终朝向目标
        transform.rotation = horizontalRotation;
    }
}

1746863453449
1746863453449

完成了水平视角的旋转

让相机垂直旋转

1746863550195
1746863550195

还需要在垂直视角旋转的时候合理的限幅:让视角最高不超过45°,最低到人物的胸部位置

public class CameraController : MonoBehaviour
{
    //摄像机跟随的目标
    [SerializeField] Transform followTarget;
    [SerializeField] float rotationSpeed = 1.5f;
    //距离
    [SerializeField] float distance;

    //绕y轴的旋转角度——水平视角旋转
    float rotationY;
    //绕x轴的旋转角度——垂直视角旋转
    float rotationX;
    //限制rotationX幅度
    [SerializeField] float minVerticalAngle = -20;
    [SerializeField] float maxVerticalAngle = 45;
    //框架偏移向量——摄像机位置视差偏移
    [SerializeField] Vector2 frameOffset;

    private void Update()
    {
        //鼠标x轴控制rotationY
        rotationY += Input.GetAxis("Mouse X") * rotationSpeed;
        //鼠标y轴控制rotationX
        rotationX += Input.GetAxis("Mouse Y") * rotationSpeed;
        //限制rotationX幅度
        rotationX = Mathf.Clamp(rotationX, minVerticalAngle, maxVerticalAngle);

        //视角旋转参量
        //想要水平旋转视角,所以需要的参量为绕y轴旋转角度
        var targetRotation = Quaternion.Euler(rotationX, rotationY, 0);

        //摄像机的焦点位置
        var focusPosition = followTarget.position + new Vector3(frameOffset.x, frameOffset.y, 0);
        //摄像机放在目标后面5个单位的位置
        transform.position = focusPosition - targetRotation * new Vector3(0, 0, distance);
        //摄像机始终朝向目标
        transform.rotation = targetRotation;
    }
}

1746864912217
1746864912217

大致实现了摄像机跟随人物进行旋转

还需要一些细节调整:

    private void Start()
    {
        //隐藏光标
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

    }

考虑到存在大多数角色控制器都有控制反转的选项

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

public class CameraController : MonoBehaviour
{
    //摄像机跟随的目标
    [SerializeField] Transform followTarget;
    [SerializeField] float rotationSpeed = 1.5f;
    //距离
    [SerializeField] float distance;

    //绕y轴的旋转角度——水平视角旋转
    float rotationY;
    //绕x轴的旋转角度——垂直视角旋转
    float rotationX;
    //限制rotationX幅度
    [SerializeField] float minVerticalAngle = -20;
    [SerializeField] float maxVerticalAngle = 45;
    //框架偏移向量——摄像机位置视差偏移
    [SerializeField] Vector2 frameOffset;

    //视角控制反转
    [Header("视角控制反转:invertX是否反转垂直视角,invertY是否反转水平视角")]
    [SerializeField] bool invertX;
    [SerializeField] bool invertY;

    float invertXValue;
    float invertYValue;

    private void Start()
    {
        //隐藏光标
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

    }

    private void Update()
    {
        //视角控制反转参数
        invertXValue = (invertX)? -1 : 1;
        invertYValue = (invertY)? -1 : 1;

        //水平视角控制——鼠标x轴控制rotationY
        rotationY += Input.GetAxis("Mouse X") * rotationSpeed * invertYValue;
        //垂直视角控制——鼠标y轴控制rotationX
        rotationX += Input.GetAxis("Mouse Y") * rotationSpeed * invertXValue;
        //限制rotationX幅度
        rotationX = Mathf.Clamp(rotationX, minVerticalAngle, maxVerticalAngle);

        //视角旋转参量
        //想要水平旋转视角,所以需要的参量为绕y轴旋转角度
        var targetRotation = Quaternion.Euler(rotationX, rotationY, 0);

        //摄像机的焦点位置
        var focusPosition = followTarget.position + new Vector3(frameOffset.x, frameOffset.y, 0);
        //摄像机放在目标后面5个单位的位置
        transform.position = focusPosition - targetRotation * new Vector3(0, 0, distance);
        //摄像机始终朝向目标
        transform.rotation = targetRotation;
    }
}

我通常喜欢这样选择,垂直反转(鼠标向上就看上面),水平不反转(鼠标向左就看左边)

1746872861555
1746872861555

就是让摄像机视角和鼠标移动方向对我来说是同步的,相当于第一人称视角控制的习惯

1746866907598
1746866907598

Day2 第三人称人物控制脚本

前序准备

先创建个人物模型( 从Mixamo下载的)

导入unity中,选择模型后点开inspector-Materials-Textures,选一个文件夹存放纹理

1746867236092
1746867236092

1746867473999
1746867473999

OK,下面就开始为这个角色写控制脚本吧!

最简化的第三人称角色控制

public class PlayerController : MonoBehaviour
{
    [SerializeField]float moveSpeed = 5f;
    private void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        //标准化 moveInput 向量
        var moveInput = new Vector3(h, 0, v).normalized;

        transform.position += moveInput * moveSpeed * Time.deltaTime;

    }

}

需要注意的:

  1. .normalized
    如果不进行标准化,moveInput 向量的长度会变得大于1(具体来说,比如h,v长度都为1,\(\sqrt{h^2 + 0^2 + v^2} = \sqrt{1^2 + 0^2 + 1^2} = \sqrt{2} \approx 1.414\))。这意味着在对角线方向上移动时,玩家的移动速度会比只在一个方向上移动时快。为了确保玩家在所有方向上移动时速度一致,需要对向量进行标准化。
  2. Time.deltaTime
    Time.deltaTime 是Unity引擎提供的一个浮点数,表示从上一帧到当前帧所用的时间(以秒为单位)。使用 Time.deltaTime 可以确保玩家的移动速度在不同帧率下保持一致。如果不使用 Time.deltaTime,在高帧率下玩家会移动得更快,在低帧率下玩家会移动得更慢。

改进

上面这样显然不能满足角色控制,因为当我们按下前进方向键的时候,人物并没有根据当前摄像机显示的方向移动

还需要进行如下改进:

在CameraController.cs里面加入

    //水平方向的旋转,返回摄像机的水平旋转四元数。
    public Quaternion PlanarRotation => Quaternion.Euler(0, rotationY, 0);

这里提一句C#中的特性:

大多数语言中想要获取一个返回值,需要定义一个函数,然后返回

    public Quaternion GetPlanarRotation()
    {
        return Quaternion.Euler(0, rotationY, 0);
    }

但是C#可以优雅的利用表达式主体定义的属性,直接获取这个属性

然后在PlayerController.cs里调用这个返回值

public class PlayerController : MonoBehaviour
{
    [SerializeField]float moveSpeed = 5f;

    CameraController cameraController;

    private void Awake()
    {
        //相机控制器设置为main camera
        cameraController = Camera.main.GetComponent<CameraController>();
    }
    private void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        float moveAmount = Mathf.Abs(h) + Mathf.Abs(v);

        //标准化 moveInput 向量
        var moveInput = new Vector3(h, 0, v).normalized;

        //让人物移动方向关联相机的朝向
        var moveDir = cameraController.PlanarRotation * moveInput;

        //每次判断moveAmount的时候,确保只有在玩家实际移动时才会更新移动+转向
    //没有输入就不更新转向,也就不会回到初始朝向
        if (moveAmount > 0)
        {
            //帧同步移动
            transform.position += moveDir * moveSpeed * Time.deltaTime;
            //人物模型转起来:让人物朝向与移动方向一致
            transform.rotation = Quaternion.LookRotation(moveDir);
        }

    }

}

这里解决了一个问题:

当方向键输入结束,人物模型朝向又回到了初始状态朝向

所以需要实时响应输入

  • if (moveAmount > 0)只有输入的时候才会更新人物朝向
  • 确保模型始终朝向移动方向。

但是还有一个问题:

人物朝向切换太快了,需要设置一个转向速度,让人物从当前朝向到目标朝向慢慢转向

    [SerializeField]float rotationSpeed = 10f;

    Quaternion targetRotation;


        //每次判断moveAmount的时候,确保只有在玩家实际移动时才会更新移动+转向
        //没有输入就不更新转向,也就不会回到初始朝向
        if (moveAmount > 0)
        {
            //帧同步移动
            transform.position += moveDir * moveSpeed * Time.deltaTime;
            //人物模型转起来:让人物朝向与移动方向一致
            targetRotation = Quaternion.LookRotation(moveDir);
        }
        //更新transform.rotation:让人物从当前朝向到目标朝向慢慢转向
        transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation,
                         rotationSpeed * Time.deltaTime);

实现效果如下:

1746878411104
1746878411104

该部分完整代码:

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

public class PlayerController : MonoBehaviour
{
    [Header("玩家属性")]
    [SerializeField]float moveSpeed = 5f;
    [SerializeField]float rotationSpeed = 10f;

    Quaternion targetRotation;

    CameraController cameraController;

    private void Awake()
    {
        //相机控制器设置为main camera
        cameraController = Camera.main.GetComponent<CameraController>();
    }
    private void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        float moveAmount = Mathf.Abs(h) + Mathf.Abs(v);

        //标准化 moveInput 向量
        var moveInput = new Vector3(h, 0, v).normalized;

        //让人物移动方向关联相机的朝向
        var moveDir = cameraController.PlanarRotation * moveInput;

        //每次判断moveAmount的时候,确保只有在玩家实际移动时才会更新移动+转向
        //没有输入就不更新转向,也就不会回到初始朝向
        if (moveAmount > 0)
        {
            //帧同步移动
            transform.position += moveDir * moveSpeed * Time.deltaTime;
            //人物模型转起来:让人物朝向与移动方向一致
            targetRotation = Quaternion.LookRotation(moveDir);
        }
        //更新transform.rotation:让人物从当前朝向到目标朝向慢慢转向
        transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation,
                         rotationSpeed * Time.deltaTime);

    }

}

Day3 Animation动画

前序准备

有一个待解决的问题:如何让动画匹配任意人物模型?

  1. 找到人物模型,进行如下设置:

1746881639467
1746881639467

应用后点configure查看骨骼映射情况,如果有没匹配上的需要手动调整

1746882155347
1746882155347

最后Done完成

  1. 找到要用到的动画,如下设置:

1746881909962
1746881909962

注意Avatar Definition要选择 从其他avatar复制,然后在source里面选择要应用的avatar

  1. 然后每个动画都进行如下设置:

1746886211629
1746886211629

注意要选择Loop Pose,如果loop match 的话可以不勾选Bake Into Pose

1746886650167
1746886650167

而且几个动画的Length值最好要尽可能接近,以免后面切换的时候出现问题

  1. 新建一个角色控制器的动画脚本

1746883164590
1746883164590

  1. 记得在player的Animator属性里添加这个脚本

万事俱备,下面开始编写动画相关脚本!

Animator组件——动画蓝图

新建一个Blend Tree

1746884538405
1746884538405

拖入对应动画

1746884573800
1746884573800

在PlayerController.cs里写动画播放逻辑

    Animator animator;


    private void Awake()
    {
        //相机控制器设置为main camera
        cameraController = Camera.main.GetComponent<CameraController>();
        //角色动画
        animator = GetComponent<Animator>();
    }

Update()方法:

        //把moveAmount限制在0-1之间(混合树的区间)
        float moveAmount = Mathf.Clamp01(Mathf.Abs(h) + Mathf.Abs(v));

Blender Tree里moveMount的区间是(0,1)

        #region 角色动画控制
        //角色动画播放
        animator.SetFloat("moveAmount", moveAmount,0.2f,Time.deltaTime);

        #endregion

SetFloat()有四个参数的重载,第三个参数是要平滑到达的值

基本的第三人称角色控制器效果如下:

1746887121425
1746887121425

修改后的PlayerController.cs完整代码:

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

public class PlayerController : MonoBehaviour
{
    [Header("玩家属性")]
    [SerializeField]float moveSpeed = 5f;
    [SerializeField]float rotationSpeed = 10f;

    Quaternion targetRotation;

    CameraController cameraController;
    Animator animator;

    private void Awake()
    {
        //相机控制器设置为main camera
        cameraController = Camera.main.GetComponent<CameraController>();
        //角色动画
        animator = GetComponent<Animator>();
    }
    private void Update()
    {
        #region 角色输入控制
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        //把moveAmount限制在0-1之间(混合树的区间)
        float moveAmount = Mathf.Clamp01(Mathf.Abs(h) + Mathf.Abs(v));

        //标准化 moveInput 向量
        var moveInput = new Vector3(h, 0, v).normalized;

        //让人物移动方向关联相机的朝向
        var moveDir = cameraController.PlanarRotation * moveInput;

        //每次判断moveAmount的时候,确保只有在玩家实际移动时才会更新移动+转向
        //没有输入就不更新转向,也就不会回到初始朝向
        if (moveAmount > 0)
        {
            //帧同步移动
            transform.position += moveDir * moveSpeed * Time.deltaTime;
            //人物模型转起来:让人物朝向与移动方向一致
            targetRotation = Quaternion.LookRotation(moveDir);
        }
        //更新transform.rotation:让人物从当前朝向到目标朝向慢慢转向
        transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation,
                         rotationSpeed * Time.deltaTime);
        #endregion

        #region 角色动画控制
        //角色动画播放
        animator.SetFloat("moveAmount", moveAmount,0.2f,Time.deltaTime);

        #endregion


    }

}

Day4 物理引擎——碰撞体和重力




转载声明
本文内容出自网络,非原创作品。由于无法确认原始来源和作者信息,在此对原作者表示感谢。
如涉及版权问题,请联系 [联系邮箱],我们将及时处理。