Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Unreal随笔系列1: 移动实现中的数学和物理

Unreal随笔系列1: 移动实现中的数学和物理

作者头像
JohnYao
发布于 2023-03-08 07:18:45
发布于 2023-03-08 07:18:45
1.1K01
代码可运行
举报
运行总次数:1
代码可运行

系列说明

23年新挖一个《Unreal随笔系列》的坑。所谓随笔,就是研究过程中的一些想法随时记录;细节可能来不及考证,甚至一些想法可能也不太成熟,有失偏颇;希望读者也可以帮忙指正和讨论。这个系列主要求量,希望每个月给自己布置一些研究小课题,争取今年发满12篇。

引言

近期打算再次对Unreal的移动实现做进一步的研究。在研究过程中,发现Unreal应用了很多数学和物理的公式;虽然公式本身并不复杂,大部分是初高中所学,但每回忆起公式的含义,并搞清楚其应用的原理,就好像淘金人发现遗失的一粒金沙,感觉欣喜万分。

涉及的数学物理知识大致有如下:

  • 弧度角
  • 向量
  • 三角函数
  • 匀加速运动
  • ...

闲话不多讲,开始本文的正题。

一 Unreal移动的概述

关于Unreal移动及其同步,知乎有两篇文章浏览量较高,当然还有官方文档的角色移动组件章节可以参考。

  1. 《Exploring in UE4》移动组件详解
  2. 《UE4移动的网络同步》
  3. Unreal Doc: 角色移动组件

建议读者可以自行阅读上面的文章,了解Unreal移动实现的概要。这里不做详细展开,仅罗列下Unreal移动及其同步的主要流程:

  1. 1P客户端收集玩家输入
  2. 1P进行物理移动模拟
  3. 1P将模拟结果, 通过RPC上报DS
  4. DS进行物理移动模拟
  5. DS通过RPC响应客户端移动,或者通过RPC修正客户端错误
  6. DS将1P的位置信息通过属性同步给其他客户端
  7. 客户端响应移动同步信息
    1. 1P处理DS RPC回包, 或者根据根据修正调整自身位置
    2. 其他客户端收到1P的位置属性,做3P移动表现

本文着重捋下流程前两步的实现细节,对实现中涉及的数学物理公式着重介绍。首先看下收集玩家输入的实现。

二 玩家输入的收集过程

下图展示了收集过程的代码调用层级:

完整的堆栈会更多些层级,但并不影响本文的内容展开。我们首先关注函数调用的顶层栈, APawn::Internal_AddMovementInput,  它的实现比较简单:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void APawn::Internal_AddMovementInput(FVector WorldAccel, bool bForce)
{
    if (bForce || !IsMoveInputIgnored())
    {
        ControlInputVector += WorldAccel;
    }
}

WorldAccel是当帧输入转化为的一个向量,可以理解为力或者加速度的方向。ControlInputVector是Pawn的一个成员变量,记录了未被处理的上次输入。 这两个变量的计算和使用,稍后我们再剖析。这里出现了第一个知识点, 向量。

向量和标量

向量在高中就已经引入, 定义是既有大小又有方向的量. 在几何学里, 它可以表示为一个带箭头的线段。与之对应的是标量,标量是只有大小,没有方向的量。

大学的线性代数引入了代数表示发, "在指定了一个坐标系之后,用一个向量在该坐标系下的坐标来表示该向量"。每个坐标轴对应的数值, 称为分量。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct FVector 
{
public:
    float X;
    float Y;
    float Z;

Unreal中的FVector可以认为是三维坐标系中的一个向量。

向量加法,力和加速度

向量加法可以使用几何的方法, 使用平行四边形法求解.

对于代数表示法, 则是各分向量的和

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FORCEINLINE FVector FVector::operator+(const FVector& V) const
{
    return FVector(X + V.X, Y + V.Y, Z + V.Z);
}

向量加法的实际物理意义可以理解为合力。

物理中的加速度和力, 可以用向量表示。 这里对向量进行加法, 也就是未被消耗的ControlInputVector对应的力和WorldAccel对应的力, 二者产生了一个合力.

一般来讲, ControlInputVector在单帧就会消耗掉, 并置为零向量. 所以一般情况下, 上面的函数会返回WorldAccel。

WorldAccel的计算

形成WorldAccel是在绑定的输入处理函数内. 我们看下Unreal TPS(第三人称射击)模板工程默认生成的默认实现.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void ATShooterCharacter::MoveForward(float Value)
{
    if ((Controller != nullptr) && (Value != 0.0f))
    {
        // find out which way is forward
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);

        // get forward vector
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
        AddMovementInput(Direction, Value);
    }
}

这里的Direction变量,通过计算他的过程和函数名字可以知道, 它是一个单位向量(长度为1的向量),它的方向就是控制器指向的方向。由于角色只是在xy平面移动,所以这里只取了Yaw的分量。不同的游戏类型,可能会有不同的实现。

这里比较复杂的一步是使用了矩阵进行Rotator到Vector的转换。这里为了保证这一小节讲述的完整性,我们将这个矩阵转换放到后面的小节单独展开。

Value的形成也比较繁复,但不是本文的重点, 这里可以先简单的理解为输入映射配置中填入的数值。

得到了Value和移动方向后, 后续执行AddMovementInput函数. 这里会进行向量的标量乘法WorldDirection * ScaleValue。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void APawn::AddMovementInput(FVector WorldDirection, float ScaleValue, bool bForce)
{
    UPawnMovementComponent* MovementComponent = GetMovementComponent();
    if (MovementComponent)
    {
        MovementComponent->AddInputVector(WorldDirection * ScaleValue, bForce);
    }
    else
    {
        Internal_AddMovementInput(WorldDirection * ScaleValue, bForce);
    }
}

向量的标量乘法

向量的标量乘法,就是用标量乘以各个向量的分量。举个物理中的例子,力的大小这个标量乘以力方向的单位向量,得到力的向量。

虽然这里做了向量的标量乘法,似乎输入可以决定力的大小。 但在后续玩家移动的引擎原生实现中,会对这个ControlInputVector再次标准化,最终输入只是提供了移动的方向。

矩阵转换

在进行下一部分前, 我们看下之前的求单位向量的矩阵转换。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);

矩阵的定义如下: 一个m*n的矩阵是一个由m行n列元素排列成的矩形阵列。矩阵里的元素可以是数字、符号或数学式。

FRotationMatrix是一个4*4的矩阵,它的初始化流程如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    T SP, SY, SR;
    T CP, CY, CR;
    FMath::SinCos(&SP, &CP, (T)FMath::DegreesToRadians(Rot.Pitch));
    FMath::SinCos(&SY, &CY, (T)FMath::DegreesToRadians(Rot.Yaw));
    FMath::SinCos(&SR, &CR, (T)FMath::DegreesToRadians(Rot.Roll));

    M[0][0] = CP * CY;
    M[0][1] = CP * SY;
    M[0][2] = SP;
    M[0][3] = 0.f;

    M[1][0] = SR * SP * CY - CR * SY;
    M[1][1] = SR * SP * SY + CR * CY;
    M[1][2] = - SR * CP;
    M[1][3] = 0.f;

    M[2][0] = -( CR * SP * CY + SR * SY );
    M[2][1] = CY * SR - CR * SP * SY;
    M[2][2] = CR * CP;
    M[2][3] = 0.f;

    M[3][0] = Origin.X;
    M[3][1] = Origin.Y;
    M[3][2] = Origin.Z;
    M[3][3] = 1.f;

在填充矩阵值的时候, 用到了三角函数。三角函数中变量使用的弧度。而在Rotator中,使用的是角度,所以这里要将角度转化为弧度。 在研究这个矩阵的使用时,我们回忆下如下数学知识和背景知识。

数学&背景知识

1. 弧度&角度

弧度的定义, 弧长等于半径的弧, 对应的圆心角为1弧度。

一弧度对应多少度呢?根据周长公式,360角度对应的弧长是2π弧度。所以:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1弧度=180/π角度
1角度=π/180弧度

DegreesToRadians函数的实现就遵循上面的公式。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
        return DegVal * (PI / 180.f);
    }

2. Rotator

Rotator向量的各分量含义如下,你可以想象人形actor的初始状态是, 正面朝向x轴方向,肩膀和y轴平行。

1. pitch的含义是向前跌倒,那它对相应的分量就是角色在y轴的旋转。

2. yaw的含义是偏航,那它对相应的分量就是角色在z轴旋转。

3. roll的含义是摇晃,那它对相应的分量就是x轴旋转,左右摇晃。

3. 三角函数

FMath::SinCos函数是在一个函数中,将这个角的正弦余弦求出来,保存在SP,CP中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FMath::SinCos(&SP, &CP, (T)FMath::DegreesToRadians(Rot.Pitch));

正弦就是将弧度角对应的直角三角形中, 该角的对边长度除以斜边长度。 余弦就是将弧度角对应的直角三角形中, 该角的邻边长度除以斜边长度。

4. 反平方根

对于开平方我的印象是很很清楚的,X^(1/2)。

乍看到反平方根时,有点回忆不起其含义。反平方根,就是平方根的倒数,X^(-1/2)。

矩阵转换(续)

有了如上的背景知识,我们继续看下这个矩阵的GetUnitAxis(X)的实现。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<typename T>
inline TVector<T> TMatrix<T>::GetUnitAxis(EAxis::Type InAxis) const
{
    return GetScaledAxis(InAxis).GetSafeNormal();
}

这里的GetSafeNormal就是标准化的过程。简单的讲,向量长度就是xx+y*y+z*z(使用勾股定理)开平方。 除以向量长度,就相当于乘以它的反平方根。为什么直接使用反平方根,可能是这样整体的计算量更小些?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
    const T SquareSum = X*X + Y*Y + Z*Z;

    // Not sure if it's safe to add tolerance in there. Might introduce too many errors
    if(SquareSum == 1.f)
    {
        return *this;
    }       
    else if(SquareSum < Tolerance)
    {
        return ResultIfZero;
    }
    const T Scale = (T)FMath::InvSqrt(SquareSum);
    return TVector<T>(X*Scale, Y*Scale, Z*Scale);
}

GetUnitAxis(EAxis::X)函数调用则返回第一个行向量。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<typename T>
inline TVector<T> TMatrix<T>::GetScaledAxis(EAxis::Type InAxis) const
{
    switch (InAxis)
    {
    case EAxis::X:
        return TVector<T>(M[0][0], M[0][1], M[0][2]);

也就是

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    M[0][0] = CP * CY;
    M[0][1] = CP * SY;
    M[0][2] = SP;
    M[0][3] = 0.f;

结合下图和三角函数,假定向量长度是1的话, z轴坐标就是sin(Pitch),有就是M[0][2]。依次可以推导,x,y坐标。

小结

以上,通过一系列的函数调用及其背后的数学转换,我们最终得到了输入代表的“力”。下面我们继续探究1P角色移动的物理模拟过程,及其中间涉及的物理知识。

三 1P角色移动的物理模拟过程

玩家的物理模拟是在CharacterMovementComponent的TickComponent中实现的。第一步的输入收集是在PlayerController中Tick中实现的。

PlayerController的TickGroup是TG_PrePhysics, 而MovmentComponent的TickGroup是TG_PostPhysics。所以理论上每帧都是先执行输入收集,再执行移动的物理模拟。

整体调用堆栈如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
UCharacterMovementComponent::TickComponent
    UPawnMovementComponent::ConsumeInputVector()
    ControlledCharacterMove
        ReplicateMoveToServer
            PerformMovement
                StartNewPhysics(DeltaSeconds, 0);
                    PhysWalking
                        CalcVelocity
                        MoveAlongFloor
                            MoveUpdatedComponent

输入消耗

在玩家开始真正的物理模拟前, 会获取之前缓存在ControlInputVector的输入数据.

这个函数的实现也比较简单, 就是获取并将缓存变量置0。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
    LastControlInputVector = ControlInputVector;
    ControlInputVector = FVector::ZeroVector;
    return LastControlInputVector;
}

加速度计算

然后将获取到输入向量传入到ControlledCharacterMove函数, 及后续ScaleInputAcceleration函数。 计算移动的物理模拟过程中的加速度。

ScaleInputAcceleration的实现也比较简单, 如果输入的向量长度大于1, 则标准化为单位向量(前面一节已经提过); 否则则采用原始值。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FVector UCharacterMovementComponent::ScaleInputAcceleration(const FVector& InputAcceleration) const
{
    return GetMaxAcceleration() * InputAcceleration.GetClampedToMaxSize(1.0f);
}

FORCEINLINE FVector FVector::GetClampedToMaxSize(float MaxSize) const
{
    if (MaxSize < KINDA_SMALL_NUMBER)
    {
        return FVector::ZeroVector;
    }

    const float VSq = SizeSquared();
    if (VSq > FMath::Square(MaxSize))
    {
        const float Scale = MaxSize * FMath::InvSqrt(VSq);
        return FVector(X*Scale, Y*Scale, Z*Scale);

由于每帧的输入的InputAcceleration都是固定的,在原生实现中,GetMaxAcceleration也是固定的。所以可以看出,原生实现的物理模拟是将角色移动当作匀加速运动来看。

匀加速运动

匀加速运动应该是初中物理知识,就是以固定的加速度进行直线运动。其中涉及的公式,也简单罗列下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
加速度 a=(v-v0)/t
瞬时速度公式 v=v0+at;
位移公式 x=v0t+½at²;
平均速度 v=x/t=(v0+v)/2
导出公式 v²-v0²=2ax

在随后的PhysWalking中,会将tick对应的delta time分解为更小的时间段

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FMath::Min(MaxSimulationTimeStep, RemainingTime * 0.5f);

每个时间段,最多模拟MaxSimulationTimeStep秒。最多分解MaxSimulationIterations个时间段。

这里似乎有点微积分的意思。

在这个循环中,每次都会计算瞬时速度CalcVelocity(最终速度会受最大速度的限制),并计算当次的位移。并利用底层的物理引擎,判断是否产生了碰撞或者overlap,并修正最终位置。

四 结语

以上就是Unreal角色移动,在客户端模拟阶段的一些实现过程。并在代码阅读过程中,对其中涉及的数学,物理知识尝试拆解其应用的原理。

相比光照,渲染,底层物理引擎使用的更复杂的计算公式,这里的内容只能说是非常浅显。本文的目的,也是希望可以激发初学者研究引擎的兴趣,发现引擎实现中的更多科学宝藏。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-01-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
UE网络通信(四)RPC&移动通信
距离上一次发表《UE网络通信》系列的文章已经过去了一年多。这段时间,UE5.0在2022年4月发布;UE5.1在2022年11月发布。好在新版本,引擎在同步方面尚未做大的变更;之前立的关于RPC,底层协议的写作flag,还是可以继续进行。
JohnYao
2023/04/28
2.8K0
UE网络通信(四)RPC&移动通信
Unreal随笔系列3: 移动逻辑
书接上回,在随笔系列的第一篇,我介绍了移动输入和物理模拟的大致过程。第一篇的重点是展示以上过程中,Unreal使用的数学,物理知识。
JohnYao
2023/03/12
9960
UE5中四元数的旋转技巧
旋转角过渡:测试角度: 0,45,0旋转到 120,90,100【可以看到旋转绕了一圈】
Jean
2021/09/07
3.4K0
第4章-变换-4.3-四元数
尽管四元数早在1843年就由William Rowan Hamilton爵士发明,作为复数的扩展,但直到1985年Shoemake[1633]才将它们引入计算机图形领域
charlee44
2021/12/20
1K0
第4章-变换-4.3-四元数
如何通过图像消失点计算相机的位姿?
本文主要是个人在学习过程中的笔记和总结,如有错误欢迎留言指出。也欢迎大家能够通过我的邮箱与博主进行交流或者分享一些文章和技术博客。
点云PCL博主
2022/01/27
4.9K0
如何通过图像消失点计算相机的位姿?
MPU6050姿态解算2-欧拉角&旋转矩阵
注:本篇中的一些图采用横线放置,若观看不方便,可点击文章末尾的阅读原文跳转到网页版
xxpcb
2020/08/26
3.6K0
MPU6050姿态解算2-欧拉角&旋转矩阵
Cocos Creator 3D 物理模块介绍
为了让游戏开发更加简单、友好和高效,Cocos Creator 3D 在研习和摸索中设计了一套比较基础的物理组件,并且还在持续完善中。尽管当前的组件功能还十分有限,但是相信在有了之前的组件设计经验后,很快就可以有更多强大且易用的物理组件。
张晓衡
2019/10/22
2.4K0
Cocos Creator 3D 物理模块介绍
OpenGL ES 2.0 (iOS)[02]:修复三角形的显示
从图可以看出,这三个数据形成的其实是一个等边直角三角形,而在 iOS 模拟器中通过 OpenGL ES 绘制出来的是直角三角形,所以是有问题的,三角形被拉伸了。
半纸渊
2018/09/04
1.3K0
OpenGL ES 2.0 (iOS)[02]:修复三角形的显示
Python中的数学模块:数学和数学
在日常生活中编写程序时,通常会遇到需要使用一些数学知识才能完成任务的情况。 像其他编程语言一样,Python提供了各种运算符来执行基本计算,例如*表示乘法, %表示模数和//表示底数除法。
用户7886150
2020/12/22
1.2K0
UE4-实现星星球Demo
https://hctra.cn/usr/uploads/2020/12/352501977.mp4
六月丶
2022/12/26
1.7K0
UE4-实现星星球Demo
【一统江湖的大前端(8)】matter.js 经典物理
在前端开发领域,物理引擎是一个相对小众的话题,它通常都是作为游戏开发引擎的附属工具而出现的,独立的功能演示作品常常给人好玩但是无处可用的感觉。仿真就是在计算机的虚拟世界中模拟物体在真实世界的表现(动力学仿真最为常见)。仿真能让画面中物体的运动表现更符合玩家对现实世界的认知,比如在《愤怒的小鸟》游戏中被弹弓发射出去小鸟或是因为被撞击而坍塌的物体堆,还有在《割绳子》小游戏中割断绳子后物体所发生的单摆或是坠落运动,都和现实世界的表现近乎相同,游戏体验通常也会更好。
大史不说话
2020/03/12
3.5K1
使用 C# 入门深度学习:线性代数
张量(Tensor):在 Pytorch 中,torch.Tensor 类型数据结构就是张量,结构跟数组或矩阵相似。
痴者工良
2025/03/26
570
使用 C# 入门深度学习:线性代数
刚体力学整理
质点:一个有质量的几何点,忽略其大小、形状及内部结构的影响,在空间只占据一个点的位置。它是对实际研究对象的简化,理想模型。
算法之名
2023/10/16
1.2K0
刚体力学整理
【Unity3d游戏开发】Unity3D中的3D数学基础---向量
向量是2D、3D数学研究的标准工具,在3D游戏中向量是基础。因此掌握好向量的一些基本概念以及属性和常用运算方法就显得尤为重要。在本篇博客中,马三就来和大家一起回顾和学习一下Unity3D中那些常用的3D数学知识。
马三小伙儿
2018/09/12
2.3K0
【Unity3d游戏开发】Unity3D中的3D数学基础---向量
【笔记】《计算机图形学》(16)——计算机动画
这一章介绍了计算机动画相关的内容, 主要介绍了动画的基本概念, 动画之间的插值, 几何变形, 角色动画, 物理动画, 生成式动画和对象组动画这几个领域, 这些领域书中都只介绍了最基础的内容, 想要深入了解某个领域的话必须阅读其它资料.
ZifengHuang
2021/07/23
1.8K0
【笔记】《计算机图形学》(16)——计算机动画
【笔记】《游戏编程算法与技巧》1-6
本篇是看完《游戏编程算法与技巧》后做的笔记的上半部分. 这本书可以看作是《游戏引擎架构》的入门版, 主要介绍了游戏相关的常见算法和一些基础知识, 很多知识点都在面试中会遇到, 值得一读.
ZifengHuang
2022/08/30
4.3K0
【教程】详解相机模型与坐标转换
由于复制过来,如果有格式问题,推荐大家直接去我原网站上查看: 相机模型与坐标转换 - 生活大爆炸
小锋学长生活大爆炸
2024/05/25
8580
【教程】详解相机模型与坐标转换
扩展卡尔曼滤波(EKF)理论讲解与实例(matlab、python和C++代码)「建议收藏」
我们上篇提到的 卡尔曼滤波(参见我的另一篇文章: 卡尔曼滤波理论讲解与应用(matlab和python))是用于线性系统,预测(运动)模型和观测模型是在假设高斯和线性情况下进行的。简单的卡尔曼滤波必须应用在符合高斯分布的系统中,但是现实中并不是所有的系统都符合这样 。另外高斯分布在非线性系统中的传递结果将不再是高斯分布。那如何解决这个问题呢?扩展卡尔曼滤波就是干这个事的。
全栈程序员站长
2022/07/04
1.9K0
扩展卡尔曼滤波(EKF)理论讲解与实例(matlab、python和C++代码)「建议收藏」
使用物理引擎为三维场景增加物理效果
在threejs中使用Ammo.js来实现物理效果,Ammo.js 使用Emscripten将 Bullet物理引擎 直接移植到JavaScript。源代码被直接翻译成JavaScript,未进行人工重写,因此功能与原始项目相同。
程序你好
2021/07/23
2.5K0
使用物理引擎为三维场景增加物理效果
线性变换(linear transformation)
国外很多线性代数课程的第一课便是线性变换,这个概念比矩阵来的更早。物理学家们通常更关注这个概念本身,关注它们是怎么变换的。但是在我们的学习中为了更方便的计算,引入了坐标系及坐标轴,并且使每一个线性变换都对应一个矩阵,矩阵背后也同样是线性变换的概念。
为为为什么
2023/03/19
1.1K0
线性变换(linear transformation)
相关推荐
UE网络通信(四)RPC&移动通信
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验