The math in Gamedev is simple. Vectors and integrals

    Hello! Today I would like to talk about mathematics. Mathematics is a very interesting science and it can be very useful in the development of games, and in general when working with computer graphics. Many (especially beginners) simply do not know how it is used during development. There are many problems that do not require a deep understanding of such concepts as: integrals, complex numbers, groups, rings, etc., but thanks to mathematics you can solve many interesting problems. In this article we will consider vectors and integrals. If interested, welcome under cat. Illustrating the Unity project, as always, is attached.



    Vector math.

    Vectors and vector math are essential tools for game development. Many operations and actions are tied to it entirely. It's funny that to implement a class that displays a vector arrow in Unity, most of the typical operations are already required. If you are well versed in vector mathematics, this block will be of no interest to you.

    Vector arithmetic and useful functions

    Analytical formulas and other details are easy to google, so let's not waste time on it. The operations themselves will be illustrated with the hyphal animations below.

    It is important to understand that any point is essentially a vector with a beginning at the zero point.



    GIFs were made using Unity, so you would need to implement a class responsible for drawing arrows. The vector arrow consists of three main components - the line, the tip and the text with the name of the vector. To draw the line and the tip, I used the LineRenderer. Let's look at the class of the vector itself:

    Arrow class
    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    publicclassVectorArrow : MonoBehaviour
    {
    	[SerializeField] private Vector3 _VectorStart;
    	[SerializeField] private Vector3 _VectorEnd;
    	[SerializeField] privatefloat TextOffsetY;
    	[SerializeField] private TMP_Text _Label;
    	[SerializeField] private Color _Color;
    	[SerializeField] private LineRenderer _Line;
    	[SerializeField] privatefloat _CupLength;
    	[SerializeField] private LineRenderer _Cup;
    	privatevoidOnValidate()
    	{
    		UpdateVector();
    	}
    	privatevoidUpdateVector()
    	{
    		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;
    		}
    	}
    	publicvoidSetPositions(Vector3 start, Vector3 end)
    	{
    		_VectorStart = start;
    		_VectorEnd = end;
    		UpdateVector();
    	}
    	publicvoidSetLabel(string label)
    	{
    		_Label.text = label;
    	}
    	publicvoidSetColor(Color color)
    	{
    		_Color = color;
    		_Line.startColor = _Line.endColor = _Cup.startColor = _Cup.endColor = _Color;
    	}
    }
    


    Since we want the vector to be a certain length and exactly match the points that we specify, the length of the line is calculated by the formula:

    _VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength

    In this formula (_VectorEnd - _VectorStart) .normalized is the direction of the vector. This can be understood from the animation with the difference of vectors, assuming that _VectorEnd and _VectorStart are vectors with the beginning in (0,0,0).

    Next we analyze the remaining two basic operations:


    Finding the normal (perpendicular) and the middle of the vector are very common tasks in game development. Let us analyze them by the example of placing the signature over the vector.

    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;

    In order to place the text perpendicular to the vector, we need a normal. In 2D graphics, the normal is quite simple.

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

    So we got the normal to the segment.

    normal = normal.y> 0? normal: -normal; - this operation is responsible for ensuring that the text is always shown above the vector.

    Then it remains to place it in the middle of the vector and lift it along the normal by a distance that will look beautiful.

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

    The code uses local positions so that you can move the resulting arrow.

    But it was about 2D, but what about 3D?

    In 3D, plus or minus all the same. Only the normal formula is different, since the normal is already taken not to the segment, but to the plane.

    Script for the camera
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    publicclassSphereCameraController : MonoBehaviour
    {
    	[SerializeField] private Camera _Camera;
    	[SerializeField] privatefloat _DistanceFromPlanet = 10;
    	[SerializeField] privatefloat _Offset = 5;
    	privatebool _IsMoving;
    	publicevent Action<Vector3, Vector3, Vector3, float, float> OnMove;
    	privatevoidUpdate()
    	{
    		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);
    			yieldreturnnull;
    			timer += Time.deltaTime;
    		}
    		transform.position = end;
    		transform.forward = (lookAt - transform.position).normalized;
    		_IsMoving = false;
    	}
    }



    In this example of the controller, the normal to the plane is used to shift the end point of the path to the right so that the planet is not blocked by the interface. The normal in 3D graphics is the normalized vector product of two vectors. Conveniently, Unity has both of these operations and we get a beautiful compact notation:

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

    I think that many who think that mathematics is not needed and why to know it at all, it became a little clearer which tasks can be solved simply and elegantly with the help of it. But it was a simple option that every game developer should know is not a trainee. Raise the bar - let's talk about the integrals.

    Integrals

    In general, integrals have many applications, such as: physical simulations, VFX, analytics, and more. I am not ready to describe everything now in detail. I want to describe simple and visually understandable. Let's talk about physics.

    Suppose there is a task - to move an object to a certain point. For example, so that when entering a particular trigger, books should fly out of the shelves. If you want to move evenly and without physics, then the task is trivial and does not require integrals, but when the book pushes a ghost from the shelf, such a distribution of speed will look completely wrong.

    What is the integral?

    In essence, this is the area under the curve. But what does this mean in the context of physics? Suppose you have a speed distribution over time. In this case, the area under the curve is the path that the object will take, and this is exactly what we need.



    Going from theory to practice, Unity has a great tool called AnimationCurve. With it you can set the speed distribution over time. Let's create such a class.

    class MoveObj
    using System.Collections;
    using UnityEngine;
    [RequireComponent(typeof(Rigidbody))]
    publicclassMoveObject : MonoBehaviour
    {
    	[SerializeField] private Transform _Target;
    	[SerializeField] private GraphData _Data;
    	private Rigidbody _Rigidbody;
    	privatevoidStart()
    	{
    		_Rigidbody = GetComponent<Rigidbody>();
    		Move(2f, _Data.AnimationCurve);
    	}
    	publicvoidMove(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;
    	}
    }


    The GetApproxSquareAnimCurve method is our integration. We make it the simplest numerical method, we simply go over the values ​​of the function and sum them a certain number of times. I set 1000 for loyalty, in general, you can choose the best.

    privateconstint Iterations = 1000;
    	publicstaticfloatGetApproxSquareAnimCurve(AnimationCurve curve)
    	{
    		float square = 0;
    		for (int i = 0; i <= Iterations; i++)
    		{
    			square += curve.Evaluate((float) i / Iterations);
    		}
    		return square / Iterations;
    	}

    Thanks to this area, we further know what the relative distance is. And further, comparing the two paths we have, we get the speed factor speedK, which is responsible for ensuring that we cover a given distance.




    You may notice that the objects do not quite coincide, this is due to the float error. In general, you can recalculate the same thing in decimal, and then overtake in float for greater accuracy.

    Actually on this for today. As always at the end of the link to the GitHub project , in which all the sources for this article. And you can play with them.

    If the article goes down - I will do a continuation in which I’ll tell you about the use of slightly more complex concepts, such as complex numbers, fields, groups, and more.

    Also popular now: