Hola a todos! Hoy me gustaría hablar de matemáticas. La matemática es una ciencia muy interesante y puede ser muy útil cuando se desarrollan juegos y, de hecho, cuando se trabaja con gráficos por computadora. Muchos (especialmente los principiantes) simplemente no saben cómo se usa en el desarrollo. Hay muchos problemas que no requieren una comprensión profunda de conceptos tales como: integrales, números complejos, grupos, anillos, etc., pero gracias a las matemáticas puedes resolver muchos problemas interesantes. En este artículo, consideramos vectores e integrales. Si está interesado, bienvenido a cat. Se adjunta el proyecto ilustrativo de Unity, como siempre.
Matemáticas vectorialesLos vectores y las matemáticas vectoriales son herramientas esenciales para el desarrollo del juego. Muchas operaciones y acciones están vinculadas a él por completo. Es curioso que para implementar una clase que muestra la flecha de un vector en Unity, ya haya tomado la mayoría de las operaciones típicas. Si conoce bien las matemáticas vectoriales, este bloque no será interesante para usted.
Aritmética vectorial y funciones útiles.Las fórmulas analíticas y otros detalles son fáciles de buscar en Google, por lo que no perderemos tiempo en esto. Las operaciones en sí se ilustrarán con animaciones gif a continuación.
Es importante entender que cualquier punto en esencia es un vector con un comienzo en el punto cero.Los gifs se hicieron con Unity, por lo que sería necesario implementar una clase que sea responsable de representar las flechas. Una flecha vectorial consta de tres componentes principales: una línea, una punta y un texto con el nombre de un vector. Para dibujar una línea y una punta, utilicé LineRenderer. Veamos la clase del vector en sí:
Clase de flechausing 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 el vector tenga una cierta longitud y se corresponda exactamente con los puntos que especificamos, la fórmula calcula la longitud de la línea:
_VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength
En esta fórmula
(_VectorEnd - _VectorStart) .normalized es la dirección del vector. Esto se puede entender a partir de la animación con la diferencia de vectores, suponiendo que
_VectorEnd y
_VectorStart son vectores con un comienzo en (0,0,0).
A continuación, analizamos las dos operaciones básicas restantes:
Encontrar el normal (perpendicular) y el medio del vector son tareas muy comunes cuando se desarrollan juegos. Analicémoslos con el ejemplo de colocar una firma en un 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;
Para colocar el texto perpendicular al vector, necesitamos una normal. En gráficos 2D, lo normal es bastante simple.
var dv = _VectorEnd - _VectorStart; var normal = new Vector3(-dv.y, dv.x).normalized;
Entonces tenemos lo normal en el segmento.
normal = normal.y> 0? normal: normal - esta operación es responsable de garantizar que el texto siempre se muestre sobre el vector.
Luego queda colocarlo en el medio del vector y elevarlo a una distancia que se verá hermosa.
_Label.transform.localPosition = (_VectorEnd + _VectorStart) / 2 + normal * TextOffsetY;
El código usa posiciones locales para que pueda mover la flecha resultante.
Pero se trataba de 2D, pero ¿qué pasa con 3D?
En 3D, más o menos es lo mismo. Solo la fórmula normal difiere, ya que la normal ya no se lleva al segmento, sino al plano.
Guión para cámara 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; } }
En este ejemplo de control, lo normal al plano se usa para desplazar el punto final de la trayectoria hacia la derecha para que la interfaz no bloquee el planeta. Normal en gráficos 3D es un producto vectorial normalizado de dos vectores. Lo cual es conveniente, en Unity existen ambas operaciones y obtenemos un hermoso registro compacto:
var right = Vector3.Cross(hit.normal, Vector3.up).normalized;
Creo que para muchos que piensan que las matemáticas no son necesarias y por qué necesitan saberlas, ha quedado un poco más claro qué problemas se pueden resolver de manera simple y elegante. Pero era una opción simple que todo desarrollador de juegos no debería conocer como pasante. Sube el listón: habla de las integrales.
IntegralesEn general, las integrales tienen muchas aplicaciones, como: simulaciones físicas, efectos visuales, análisis y mucho más. No estoy listo para describir todo en detalle ahora. Quiero describir una simple y visualmente comprensible. Hablemos de física.
Supongamos que hay una tarea: mover un objeto a un cierto punto. Por ejemplo, al ingresar a un cierto disparador, los libros de los estantes deberían salir volando. Si desea moverse de manera uniforme y sin física, la tarea es trivial y no requiere integrales, pero cuando un fantasma empuja un libro de un estante, esa distribución de velocidad se verá completamente diferente.
¿Qué es una integral?
De hecho, esta es el área bajo la curva. Pero, ¿qué significa esto en el contexto de la física? Supongamos que tiene una distribución de velocidad en el tiempo. En este caso, el área debajo de la curva es el camino por el que pasará el objeto, y esto es exactamente lo que necesitamos.

Pasando de la teoría a la práctica, Unity tiene una gran herramienta llamada AnimationCurve. Al usarlo, puede especificar la distribución de la velocidad en el tiempo. Creemos una clase así.
clase 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; } }
El método GetApproxSquareAnimCurve es nuestra integración. Lo convertimos en el método numérico más simple, simplemente revise los valores de las funciones y sume un cierto número de veces. Configuré 1000 para fidelidad, en general, puedes elegir el mejor.
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; }
Gracias a esta área, ya sabemos cuál es la distancia relativa. Y luego, al comparar los dos caminos recorridos, obtenemos el coeficiente de velocidad speedK, que es responsable de garantizar que recorremos la distancia dada.
Puede notar que los objetos no coinciden exactamente, esto se debe a un error flotante. En general, puede recalcular lo mismo en decimal y luego adelantar en flotante para una mayor precisión.
En realidad eso es todo por hoy. Como siempre, al final hay un
enlace al proyecto GitHub , en el que se encuentran todas las fuentes de este artículo. Y puedes jugar con ellos.
Si aparece el artículo, haré una secuela en la que te contaré sobre el uso de conceptos un poco más complejos, como números complejos, campos, grupos y más.