Les mathématiques dans Gamedev sont simples. Vecteurs et intégrales

Bonjour à tous! Aujourd'hui, je voudrais parler des mathématiques. Les mathématiques sont une science très intéressante et peuvent être très utiles lors du développement de jeux, et en général lorsque vous travaillez avec des graphiques informatiques. Beaucoup (en particulier les débutants) ne savent tout simplement pas comment il est utilisé dans le développement. Il existe de nombreux problèmes qui ne nécessitent pas une compréhension approfondie de concepts tels que: intégrales, nombres complexes, groupes, anneaux, etc., mais grâce aux mathématiques, vous pouvez résoudre de nombreux problèmes intéressants. Dans cet article, nous considérons les vecteurs et les intégrales. Si vous êtes intéressé, bienvenue au chat. L'illustration du projet Unity, comme toujours, est jointe.



Mathématiques de vecteur.

Les vecteurs et les mathématiques vectorielles sont des outils essentiels pour le développement de jeux. De nombreuses opérations et actions y sont entièrement liées. C'est drôle que pour implémenter une classe qui affiche la flèche d'un vecteur dans Unity, cela a déjà pris la plupart des opérations typiques. Si vous connaissez bien les mathématiques vectorielles, ce bloc ne vous intéressera pas.

Arithmétique vectorielle et fonctions utiles

Les formules analytiques et autres détails sont faciles à google, nous ne perdrons donc pas de temps à ce sujet. Les opérations elles-mêmes seront illustrées par des animations gif ci-dessous.

Il est important de comprendre que tout point essentiel est un vecteur dont le point de départ est zéro.



Les gifs ont été créés en utilisant Unity, il serait donc nécessaire d'implémenter une classe qui est responsable du rendu des flèches. Une flèche vectorielle se compose de trois composants principaux: une ligne, une pointe et du texte avec le nom d'un vecteur. Pour tracer une ligne et une astuce, j'ai utilisé LineRenderer. Regardons la classe du vecteur lui-même:

Classe de flèche
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; } } 


Puisque nous voulons que le vecteur soit d'une certaine longueur et corresponde exactement aux points que nous spécifions, la longueur de la ligne est calculée par la formule:

 _VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength 

Dans cette formule (_VectorEnd - _VectorStart). Normalisé est la direction du vecteur. Cela peut être compris à partir de l'animation avec la différence de vecteurs, en supposant que _VectorEnd et _VectorStart sont des vecteurs avec un début à (0,0,0).

Ensuite, nous analysons les deux opérations de base restantes:


Trouver la normale (perpendiculaire) et le milieu du vecteur sont des tâches très courantes lors du développement de jeux. Analysons-les par l'exemple de la mise en place d'une signature sur un vecteur.

 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; 

Pour placer le texte perpendiculairement au vecteur, nous avons besoin d'une normale. Dans les graphiques 2D, la normale est assez simple.

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

Nous avons donc obtenu la normale au segment.

normal = normal.y> 0? normal: -normal; - cette opération est chargée de s'assurer que le texte est toujours affiché au-dessus du vecteur.

Il reste ensuite Ă  le placer au milieu du vecteur et Ă  le relever normalement Ă  une distance qui sera magnifique.

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

Le code utilise des positions locales pour que vous puissiez déplacer la flèche résultante.

Mais il s'agissait de 2D, mais qu'en est-il de la 3D?

En 3D, plus ou moins est le même. Seule la formule normale diffère, car la normale est déjà prise non pas dans le segment, mais dans le plan.

Script pour appareil photo
 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; } } 



Dans cet exemple de contrôle, la normale à l'avion est utilisée pour décaler le point final de la trajectoire vers la droite afin que l'interface ne bloque pas la planète. Normal dans les graphiques 3D est un produit vectoriel normalisé de deux vecteurs. Ce qui est pratique, dans Unity il y a ces deux opérations et nous obtenons un beau disque compact:

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

Je pense que pour beaucoup de ceux qui pensent que les mathématiques ne sont pas nécessaires et pourquoi vous devez le savoir, il est devenu un peu plus clair quels problèmes peuvent être résolus avec cela simplement et avec élégance. Mais c'était une option simple que chaque développeur de jeu ne devrait pas connaître en tant que stagiaire. Élevez la barre - parlez des intégrales.

Intégrales

En général, les intégrales ont de nombreuses applications, telles que: les simulations physiques, les effets visuels, l'analyse et bien plus encore. Je ne suis pas prêt à tout décrire en détail maintenant. Je veux décrire un simple et visuellement compréhensible. Parlons de physique.

Supposons qu'il y ait une tâche - déplacer un objet vers un certain point. Par exemple, lorsque vous entrez un certain déclencheur, les livres des étagères doivent s'envoler. Si vous voulez vous déplacer uniformément et sans physique, alors la tâche est triviale et ne nécessite pas d'intégrales, mais lorsqu'un fantôme pousse un livre hors d'une étagère, une telle distribution de vitesse sera complètement différente.

Qu'est-ce qu'une intégrale?

En fait, c'est l'aire sous la courbe. Mais qu'est-ce que cela signifie dans le contexte de la physique? Supposons que vous ayez une distribution de vitesse dans le temps. Dans ce cas, la zone sous la courbe est le chemin que traversera l'objet, et c'est exactement ce dont nous avons besoin.



Passer de la théorie à la pratique, Unity a un excellent outil appelé AnimationCurve. En l'utilisant, vous pouvez spécifier la distribution de la vitesse dans le temps. Créons une telle 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; } } 


La méthode GetApproxSquareAnimCurve est notre intégration. Nous en faisons la méthode numérique la plus simple, il suffit de parcourir les valeurs des fonctions et de les résumer un certain nombre de fois. J'ai mis 1000 pour la fidélité, en général, vous pouvez choisir le meilleur.

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

Grâce à cette zone, nous savons déjà quelle est la distance relative. Et puis en comparant les deux chemins parcourus, nous obtenons le coefficient de vitesse speedK, qui est responsable de s'assurer que nous marchons sur la distance donnée.




Vous pouvez remarquer que les objets ne correspondent pas exactement, cela est dû à une erreur de flottement. En général, vous pouvez recalculer la même chose en décimal, puis dépasser en flottant pour une plus grande précision.

En fait, c'est tout pour aujourd'hui. Comme toujours, Ă  la fin, il y a un lien vers le projet GitHub , dans lequel se trouvent toutes les sources de cet article. Et vous pouvez jouer avec eux.

Si l'article arrive, je ferai une suite dans laquelle je vous parlerai de l'utilisation de concepts légèrement plus complexes, tels que des nombres complexes, des champs, des groupes, etc.

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


All Articles