Gamedev中的数学很简单。 向量和积分

大家好! 今天我想谈谈数学。 数学是一门非常有趣的科学,它在开发游戏以及使用计算机图形学时非常有用。 许多(尤其是初学者)根本不知道如何在开发中使用它。 有许多问题不需要深入了解诸如积分,复数,组,环等概念,但是由于有了数学,您可以解决许多有趣的问题。 在本文中,我们考虑向量和积分。 如果有兴趣,欢迎来猫。 一如既往地说明了Unity项目。



向量数学。

向量和向量数学是游戏开发的基本工具。 许多操作和动作都与之完全相关。 有趣的是,要实现一个在Unity中显示矢量箭头的类,它已经执行了大多数典型操作。 如果您精通矢量数学,那么对您来说,这个模块将不会很有趣。

向量算术和有用函数

分析公式和其他详细信息易于谷歌搜索,因此我们不会在此浪费时间。 这些操作本身将通过下面的gif动画进行说明。

重要的是要理解,本质上的任何一点都是从零点开始的向量。



Gif是使用Unity制作的,因此有必要实现一个负责渲染箭头的类。 矢量箭头由三个主要部分组成-直线,笔尖和带有矢量名称的文本。 为了画一条线和一个笔尖,我使用了LineRenderer。 让我们看一下向量本身的类:

箭类
using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; public class VectorArrow : MonoBehaviour { [SerializeField] private Vector3 _VectorStart; [SerializeField] private Vector3 _VectorEnd; [SerializeField] private float TextOffsetY; [SerializeField] private TMP_Text _Label; [SerializeField] private Color _Color; [SerializeField] private LineRenderer _Line; [SerializeField] private float _CupLength; [SerializeField] private LineRenderer _Cup; private void OnValidate() { UpdateVector(); } private void UpdateVector() { if(_Line == null || _Cup == null) return; SetColor(_Color); _Line.positionCount = _Cup.positionCount = 2; _Line.SetPosition(0, _VectorStart); _Line.SetPosition(1, _VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength); _Cup.SetPosition(0, _VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength); _Cup.SetPosition(1, _VectorEnd ); if (_Label != null) { var dv = _VectorEnd - _VectorStart; var normal = new Vector3(-dv.y, dv.x).normalized; normal = normal.y > 0 ? normal : -normal; _Label.transform.localPosition = (_VectorEnd + _VectorStart) / 2 + normal * TextOffsetY; _Label.transform.up = normal; } } public void SetPositions(Vector3 start, Vector3 end) { _VectorStart = start; _VectorEnd = end; UpdateVector(); } public void SetLabel(string label) { _Label.text = label; } public void SetColor(Color color) { _Color = color; _Line.startColor = _Line.endColor = _Cup.startColor = _Cup.endColor = _Color; } } 


由于我们希望向量具有一定的长度,并且正好对应于我们指定的点,因此线的长度由以下公式计算:

 _VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength 

在此公式(_VectorEnd-_VectorStart)中,归一化为向量的方向。 可以从动画中了解到向量的差异,假设_VectorEnd_VectorStart是以(0,0,0)开始的向量。

接下来,我们分析剩下的两个基本操作:


在开发游戏时,找到法线(垂直)和向量的中间位置是非常常见的任务。 让我们通过在矢量上放置签名的示例来分析它们。

 var dv = _VectorEnd - _VectorStart; var normal = new Vector3(-dv.y, dv.x).normalized; normal = normal.y > 0 ? normal : -normal; _Label.transform.localPosition = (_VectorEnd + _VectorStart) / 2 + normal * TextOffsetY; _Label.transform.up = normal; 

为了使文本垂直于矢量,我们需要一个法线。 在2D图形中,法线非常简单。

 var dv = _VectorEnd - _VectorStart; var normal = new Vector3(-dv.y, dv.x).normalized; 

因此,我们得到了该细分的法线。

正常=正常。y> 0? 正常:-正常; -此操作负责确保文本始终显示在矢量上方。

然后将其放置在矢量的中间,然后将其垂直提升到看起来很漂亮的距离。

 _Label.transform.localPosition = (_VectorEnd + _VectorStart) / 2 + normal * TextOffsetY; 

该代码使用本地位置,以便您可以移动结果箭头。

但这是关于2D的,但是关于3D呢?

在3D中,正负相同。 只有法线公式不同,因为法线已经被带到了平面上,而不是线段上。

相机脚本
 using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class SphereCameraController : MonoBehaviour { [SerializeField] private Camera _Camera; [SerializeField] private float _DistanceFromPlanet = 10; [SerializeField] private float _Offset = 5; private bool _IsMoving; public event Action<Vector3, Vector3, Vector3, float, float> OnMove; private void Update() { if (Input.GetMouseButtonDown(0) && !_IsMoving) { RaycastHit hit; Debug.Log("Click"); var ray = _Camera.ScreenPointToRay(Input.mousePosition); if(Physics.Raycast(ray, out hit)) { Debug.Log("hit"); var startPosition = _Camera.transform.position; var right = Vector3.Cross(hit.normal, Vector3.up).normalized; var endPosition = hit.point + hit.normal * _DistanceFromPlanet + right * _Offset; StartCoroutine(MoveCoroutine(startPosition, endPosition, hit.point + right * _Offset)); OnMove?.Invoke(startPosition, hit.point, hit.normal, _DistanceFromPlanet, _Offset); } } } private IEnumerator MoveCoroutine(Vector3 start, Vector3 end, Vector3 lookAt) { _IsMoving = true; var startForward = transform.forward; float timer = 0; while (timer < Scenario.AnimTime) { transform.position = Vector3.Slerp(start, end, timer / Scenario.AnimTime); transform.forward = Vector3.Slerp(startForward, (lookAt - transform.position).normalized, timer / Scenario.AnimTime); yield return null; timer += Time.deltaTime; } transform.position = end; transform.forward = (lookAt - transform.position).normalized; _IsMoving = false; } } 



在此控制示例中,平面的法线用于将轨迹的端点向右移动,以使界面不会阻挡行星。 3D图形中的法线是两个向量的归​​一化向量积。 这很方便,在Unity中有这两种操作,我们得到了一个漂亮的紧凑记录:

 var right = Vector3.Cross(hit.normal, Vector3.up).normalized; 

我认为对于许多认为不需要数学以及为什么需要完全了解数学的人来说,用简单而优雅的方法可以解决什么问题变得更加清楚。 但这是每个游戏开发人员都不应该知道的实习生的简单选择。 提高标准-讨论积分。

积分

通常,积分具有许多应用程序,例如:物理模拟,VFX,分析等。 我现在不准备详细描述所有内容。 我想描述一个简单并且在视觉上可以理解的东西。 让我们谈谈物理学。

假设有一项任务-将对象移动到特定点。 例如,当输入某个触发器时,书架上的书本应飞出。 如果要均匀地移动而没有物理学,则该任务是微不足道的,不需要积分,但是当幻影将书从架子上推开时,这种速度分布看起来将完全不同。

什么是积分?

实际上,这是曲线下的面积。 但是,这在物理学的背景下意味着什么? 假设您具有随时间变化的速度。 在这种情况下,曲线下方的区域就是对象将要经过的路径,而这正是我们所需要的。



从理论到实践,Unity有一个很棒的工具,称为AnimationCurve。 使用它,您可以指定速度随时间的分布。 让我们创建一个这样的类。

MoveObj类
 using System.Collections; using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class MoveObject : MonoBehaviour { [SerializeField] private Transform _Target; [SerializeField] private GraphData _Data; private Rigidbody _Rigidbody; private void Start() { _Rigidbody = GetComponent<Rigidbody>(); Move(2f, _Data.AnimationCurve); } public void Move(float time, AnimationCurve speedLaw) { StartCoroutine(MovingCoroutine(time, speedLaw)); } private IEnumerator MovingCoroutine(float time, AnimationCurve speedLaw) { float timer = 0; var dv = (_Target.position - transform.position); var distance = dv.magnitude; var direction = dv.normalized; var speedK = distance / (Utils.GetApproxSquareAnimCurve(speedLaw) * time); while (timer < time) { _Rigidbody.velocity = speedLaw.Evaluate(timer / time) * direction * speedK; yield return new WaitForFixedUpdate(); timer += Time.fixedDeltaTime; } _Rigidbody.isKinematic = true; } } 


GetApproxSquareAnimCurve方法是我们的​​集成。 我们将其设为最简单的数值方法,只需遍历函数的值并将它们相加一定次数即可。 我将保真度设置为1000,通常可以选择最佳。

  private const int Iterations = 1000; public static float GetApproxSquareAnimCurve(AnimationCurve curve) { float square = 0; for (int i = 0; i <= Iterations; i++) { square += curve.Evaluate((float) i / Iterations); } return square / Iterations; } 

由于有了这个区域,我们已经知道相对距离是多少。 然后比较两条行进的路径,我们得到速度系数speedK,它负责确保我们行走给定的距离。




您可能会注意到对象不完全匹配,这是由于浮动错误引起的。 通常,您可以用十进制重新计算相同的值,然后以浮点数重新计算以提高准确性。

其实今天就这些。 与往常一样,最后有一个指向GitHub项目链接,本文的所有资源都在其中。 您可以和他们一起玩。

如果有文章发表,我将做一个续集,在其中向您介绍使用稍微复杂一些的概念,例如复杂数字,字段,组等等。

Source: https://habr.com/ru/post/zh-CN430146/


All Articles