A matemática em Gamedev é simples. Vetores e integrais

Olá pessoal! Hoje eu gostaria de falar sobre matemática. A matemática é uma ciência muito interessante e pode ser muito útil no desenvolvimento de jogos e, em geral, no trabalho com computação gráfica. Muitos (especialmente iniciantes) simplesmente não sabem como é usado no desenvolvimento. Existem muitos problemas que não exigem uma compreensão profunda de conceitos como: integrais, números complexos, grupos, anéis, etc., mas, graças à matemática, você pode resolver muitos problemas interessantes. Neste artigo, consideramos vetores e integrais. Se estiver interessado, bem-vindo ao gato. Ilustrando o projeto do Unity, como sempre, está anexado.



Matemática de vetor.

Vetores e matemática vetorial são ferramentas essenciais para o desenvolvimento de jogos. Muitas operações e ações estão totalmente ligadas a ela. É engraçado que, para implementar uma classe que exibe a seta de um vetor no Unity, a maioria das operações típicas já seja necessária. Se você é bem versado em matemática vetorial, este bloco não será interessante para você.

Funções aritméticas e úteis do vetor

Fórmulas analíticas e outros detalhes são fáceis de pesquisar no Google, portanto, não perderemos tempo com isso. As próprias operações serão ilustradas pelas animações gif abaixo.

É importante entender que qualquer ponto na essência é um vetor com início no ponto zero.



Os gifs foram criados usando o Unity, portanto, seria necessário implementar uma classe responsável por renderizar flechas. Uma seta de vetor consiste em três componentes principais - uma linha, uma dica e um texto com o nome de um vetor. Para desenhar uma linha e uma dica, usei o LineRenderer. Vejamos a classe do próprio vetor:

Classe Arrow
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; } } 


Como queremos que o vetor tenha um determinado comprimento e corresponda exatamente aos pontos que especificamos, o comprimento da linha é calculado pela fórmula:

 _VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength 

Nesta fórmula (_VectorEnd - _VectorStart) .normalized é a direção do vetor. Isso pode ser entendido a partir da animação com a diferença de vetores, assumindo que _VectorEnd e _VectorStart são vetores com início em (0,0,0).

Em seguida, analisamos as duas operações básicas restantes:


Encontrar o normal (perpendicular) e o meio do vetor são tarefas muito comuns no desenvolvimento de jogos. Vamos analisá-los pelo exemplo de colocar uma assinatura em um vetor.

 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; 

Para colocar o texto perpendicular ao vetor, precisamos de um normal. Nos gráficos 2D, o normal é bastante simples.

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

Então, chegamos ao normal no segmento.

normal = normal.y> 0? normal: -normal; - esta operação é responsável por garantir que o texto seja sempre mostrado acima do vetor.

Em seguida, resta colocá-lo no meio do vetor e elevá-lo normal a uma distância que ficará linda.

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

O código usa posições locais para que você possa mover a seta resultante.

Mas era sobre 2D, mas e sobre 3D?

Em 3D, mais ou menos é o mesmo. Somente a fórmula normal difere, pois a normal já é levada não ao segmento, mas ao plano.

Script para câmera
 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; } } 



Neste exemplo de controle, o normal para o plano é usado para mudar o ponto final da trajetória para a direita, para que a interface não bloqueie o planeta. Normal em gráficos 3D é um produto vetorial normalizado de dois vetores. O que é conveniente, no Unity existem essas duas operações e obtemos um registro compacto bonito:

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

Penso que para muitos que pensam que a matemática não é necessária e por que você precisa conhecê-la, ficou um pouco mais claro que problemas podem ser resolvidos com ela de maneira simples e elegante. Mas era uma opção simples que todo desenvolvedor de jogos não deveria conhecer como estagiário. Levante a barra - fale sobre as integrais.

Integrais

Em geral, integrais têm muitas aplicações, como: simulações físicas, efeitos visuais, análises e muito mais. Não estou pronto para descrever tudo em detalhes agora. Eu quero descrever um simples e visualmente compreensível. Vamos falar sobre física.

Suponha que exista uma tarefa - mover um objeto para um determinado ponto. Por exemplo, ao inserir um determinado gatilho, os livros das prateleiras devem voar para fora. Se você deseja se mover de maneira uniforme e sem física, a tarefa é trivial e não exige integrais, mas quando um fantasma empurra um livro da prateleira, essa distribuição de velocidade parecerá completamente diferente.

O que é uma integral?

De fato, esta é a área abaixo da curva. Mas o que isso significa no contexto da física? Suponha que você tenha uma distribuição de velocidade ao longo do tempo. Nesse caso, a área sob a curva é o caminho que o objeto percorrerá, e é exatamente isso que precisamos.



Passando da teoria para a prática, o Unity tem uma ótima ferramenta chamada AnimationCurve. Usando-o, você pode especificar a distribuição da velocidade ao longo do tempo. Vamos criar essa classe.

classe 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; } } 


O método GetApproxSquareAnimCurve é a nossa integração. Tornamos o método numérico mais simples, basta passar por cima dos valores das funções e resumi-los um certo número de vezes. Eu defino 1000 por fidelidade, em geral, você pode escolher o melhor.

  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; } 

Graças a essa área, já sabemos qual é a distância relativa. E, comparando os dois caminhos percorridos, obtemos o coeficiente de velocidade speedK, responsável por garantir que percorremos a determinada distância.




Você pode perceber que os objetos não correspondem exatamente, isso ocorre devido a um erro de flutuação. Em geral, você pode recalcular o mesmo em decimal e, em seguida, ultrapassar em float para maior precisão.

Na verdade, é tudo por hoje. Como sempre, no final, há um link para o projeto GitHub , no qual estão todas as fontes deste artigo. E você pode brincar com eles.

Se o artigo chegar, farei uma sequência na qual falarei sobre o uso de conceitos um pouco mais complexos, como números, campos, grupos e muito mais.

Source: https://habr.com/ru/post/pt430146/


All Articles