Gamasutra : Blog de Prokhor Vernikovsky – Génération de courbes d’exécution à l’aide d’objets scriptables

Le billet de blog suivant, sauf indication contraire, a été écrit par un membre de la communauté Gamasutra.
Les pensées et opinions exprimées sont celles de l’auteur et non de Gamasutra ou de sa société mère.


Résumé

Tôt ou tard, chaque développeur de jeu est confronté à un problème pour implémenter un mouvement incurvé dans son jeu. Par exemple, vous pouvez utiliser cette fonctionnalité pour définir des trajectoires de projectiles plus sophistiquées ou des trajectoires de monstres volants. Ce didacticiel vous montrera les concepts qui vous aideront à définir une forme générale, puis à construire de tels chemins dynamiquement à partir du code avec les positions arbitraires d’origine et de cible.

Un merci spécial à Sebastian Lague pour sa brillante solution de courbe.

Glossaire

Chemin – une trajectoire courbe constituée d’une série de points d’ancrage reliés par des segments de droite.

Ancre – un point sur le chemin qui marque la fin d’un segment de courbe et le début d’un autre

Origine – un point de départ d’un chemin

Cible – point d’arrivée d’un chemin

Modèle de forme – un ensemble de points qui définissent un modèle de forme général sur la base duquel nous créons un chemin réel avec l’origine arbitraire et les points cibles pendant l’exécution.

Conditions préalables

Le seul actif dont nous aurons besoin est un actif de Bézier. Vous pouvez utiliser n’importe quel actif, mais pour ce guide, choisissons le projet gratuit et open source Path Creator de Sebastian Lague.

Vous pouvez le télécharger ici : https://github.com/SebLague/Path-Creator

Création d’un modèle de forme

Que peut être un modèle pour une courbe? Un ensemble de points de contrôle ou d’ancrages. Regardons l’image ci-dessous : nous pouvons définir une forme de notre trajectoire potentielle avec un ensemble de points. L’origine et la cible seraient les premier et dernier points de notre tableau de points, et tous les points entre eux serviront d’ancres par lesquelles le chemin viendra.

D’accord, nous avons la forme de trajectoire souhaitée, mais nous devons avoir mis à l’échelle entre deux points quelconques, car l’origine et la cible peuvent se trouver n’importe où dans l’environnement de jeu d’exécution. C’est parce que nous ne pouvons pas utiliser de chemins prédéfinis et devrions utiliser un objet où notre forme serait stockée. Un simple ScriptableObject peut vous aider.

Modèle de chemin

Créons une nouvelle classe ScriptableObject qui encapsulerait toutes les données et la logique nécessaires de notre modèle de chemin et fournirait une gestion facile des divers modèles de chemin dans notre projet, et nommez-la PathTemplate. Ce scriptable doit stocker des données sur les points d’ancrage, tous les paramètres supplémentaires qui nous aident à créer le chemin dont nous avons besoin et une méthode qui renvoie un objet chemin prêt.

Données d’ancrage

Comment décrire mathématiquement un ensemble d’ancres entre une origine et une cible ? Le moyen le plus simple consiste à stocker les ancres en tant que décalages à partir d’un point d’origine.

AbsoluteOffsetOfAnchor (De l’origine du modèle de forme) = Anchor – Origin

Mais les décalages absolus ne fonctionneront pas si le vecteur entre l’origine et la cible varie considérablement de celui que nous avons configuré dans notre modèle de forme. Si vous vous souvenez, nous avons créé des formes lorsque la cible est située à une certaine distance sous l’origine, mais que se passe-t-il si notre vraie cible est située trop loin ou trop près ou trop loin dans un axe et trop près dans un autre ? Le chemin résultant ne ressemblera pas à notre forme et peut devenir trop étiré ou tordu.

Lire aussi  AI repère des vidéos deepfake du président ukrainien Volodymyr Zelenskyy

Pour résoudre ce problème, nous pouvons utiliser un tableau de décalages d’ancrage relatifs comme le rapport du décalage de chaque ancrage par rapport à l’origine au décalage total entre la cible et l’origine dans le modèle de forme :

RelativeOffsetOfAnchor(ToTotalOffset du modèle de forme) = (Anchor – Origin)/(Target – Origin)

  //Absolute offsets of an anchor from origin
  public Vector2[] AnchorAbsoluteOffsets;
  
  //Relative offsets of an anchor from origin  
  public Vector2[] AnchorRelativeOffsets;

Ajoutons maintenant des booléens (séparément pour x et y-axis ) qui définissent la logique à utiliser pour la création de chemin dans un PathTemplate Scriptable particulier.

  //Which set of offsets should be used in our construction path logic separately for x and y
  public bool isXRelative = false;
  public bool isYRelative = false;

Règles pour la création de chemin.

Qu’en est-il des règles supplémentaires avec lesquelles nous pouvons enrichir notre modèle de chemin ?

Nous pouvons inverser le chemin dans un ou les deux axes :

  //Can be used for rotating constructed path sideways(xDirection) or updown(yDirection)
  // 1 - construct as is, -1 - mirror path along the axis, 0 - strip axis offset from path
  [Range(-1, 1)]
  public int xDirection = 1;
  [Range(-1, 1)]
  public int yDirection = 1;

… et nous pouvons randomiser ancres de notre chemin final dans le rayon spécifié :

//Random radius for anchors between start and end point
//If 0f, then no randomisation applied
public float RandomRadius = 0f;

Méthode de création de chemin

Concluons cette section en décrivant la méthode qui renvoie un chemin.

Notez que vous pouvez utiliser n’importe quel système de création de Bézier que vous souhaitez, que ce soit le vôtre ou un élément tiers. La seule chose qui peut changer si vous changez de système de Bézier sera un type de valeur de retour et d’arguments.

Pour ce didacticiel, nous nous en tiendrons à l’actif PathCreator de Sebastian Lague, qui peut créer un objet Bézier (BezierPath) avec un ensemble d’ancres, puis le diviser en un tableau de sommets le long du chemin (VertexPath) à travers lequel nous pouvons déplacer nos projectiles et monstres constamment à vitesse constante.

Passons donc directement à la méthode elle-même :

public VertexPath BuildPath(Transform start, Vector2 end, Bounds bounds)
    {
        var newAnchorsArray = new Vector2[AnchorRelativeOffsets.Length];
        Vector2 totalOffset = end - (Vector2)start.position;

        newAnchorsArray[0] = new Vector2(0,0);

        for (int i = 1; i < AnchorRelativeOffsets.Length; i++)
        {          
            float x = xDirection*(IsXRelative ? totalOffset.x * AnchorRelativeOffsets[i].x : AnchorAbsoluteOffsets[i].x);

            float y = yDirection * (IsYRelative ? totalOffset.y * AnchorRelativeOffsets[i].y : AnchorAbsoluteOffsets[i].y);

            newAnchorsArray[i] = new Vector2(x, y);

            if (RandomRadius != 0)
            {
                newAnchorsArray[i] = newAnchorsArray[i] + (Random.insideUnitCircle * RandomRadius);
            }

            if (bounds !=null && !bounds.Contains(newAnchorsArray[i]))
            {
                newAnchorsArray[i].x = Mathf.Clamp(newAnchorsArray[i].x, bounds.min.x, bounds.max.x);
                newAnchorsArray[i].y = Mathf.Clamp(newAnchorsArray[i].y, bounds.min.y, bounds.max.y);
            }
        }

        newAnchorsArray[newAnchorsArray.Length-1] = totalOffset;
        BezierPath bezierPath = new BezierPath(newAnchorsArray, false, PathSpace.xy);
        var vertex = new VertexPath(bezierPath, start);
        return vertex;

Nous utilisons la transformation d'origine, la position de fin et les limites pour les paramètres car nous devons contraindre le chemin dans une zone de jeu.

Lire aussi  Des comportements étranges de chat auxquels vous devriez prêter attention sont révélés

Nous pouvons commencer à reconstruire les ancres de l'origine à la cible en utilisant des arguments dans la variable newAnchorsArray selon les règles stockées dans PathTemplate. Le premier élément de newAnchorsArray doit toujours être égal à la position d'origine et le dernier élément doit être égal à la position cible.

Puis dans la boucle for, on calcule chaque ancre entre l'origine et la cible en fonction de la valeur booléenne IsXRelative et IsYRelative (pour choisir si on doit utiliser un décalage absolu ou relatif). La multiplication par les variables xDirection et yDirection est effectuée pour refléter le chemin le long de l'axe souhaité.

Nous pouvons ajouter une certaine randomisation avec Random.insideUnitCircle non sophistiqué multiplié par RandomRadius et contraindre le point résultant dans des limites.

N'oubliez pas de définir le dernier point du tableau sur la position cible et de le convertir en objet final (puisque nous utilisons PathCreator dans notre exemple, nous utiliserons BezierPath pour la courbe d'origine, puis le convertirons en VertexPath pour assurer un mouvement fluide à vitesse constante si l'on souhaite utiliser ce chemin pour le déplacement).

Voilà, nous l'avons fait : le PathTemplate Scriptable est prêt à l'emploi !

Je vais vous montrer comment créer ces modèles et les utiliser dans notre code pour déplacer des objets dans les prochaines parties.

Un moyen simple de remplir PathCreator avec des décalages de forme

Il peut être utile d'initialiser PathTemplate automatiquement en fonction d'un ensemble de points pré-arrangés.

Créez une scène vide et organisez les objets du jeu avec le moteur de rendu de sprite (même la transformation vide fonctionnera) dans la forme souhaitée. Regardez l'image : j'ai fait quelques gameobjects où Origin est vert, les ancres sont jaunes et la cible est orange et je les ai placés sous un GameObject vide « Points prédéfinis ».

Nous avons besoin d'un script qui calcule les décalages pour ces points et les enregistre dans un objet PathTemplate fourni. Créez ce script et ajoutez-le sur GameObject « Points prédéfinis ».

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class PathTemplateBuilder : MonoBehaviour
{
    //Inititalise this instance of PathTemplate
    public PathTemplate path;

    //Array of points that define a general shape of trajectory
    // First element should be origin, last element should be target
    public GameObject[] Anchors;

    public void Start()
    {
        //Creating arrays to store abslute and relatinve offsets of each point compared to origin
        var absoluteOffsets = new Vector2[Anchors.Length];
        var relativeOffsets = new Vector2[Anchors.Length];

        //Save total offset from Origin to Target to calculate relative offsets
        float dXFromOrigin = Anchors[Anchors.Length - 1].transform.position.x - Anchors[0].transform.position.x;
        float dYFromOrigin = Anchors[Anchors.Length - 1].transform.position.y - Anchors[0].transform.position.y;

        // Calculate offsets for each gameobject in Anchors[]
        for (int i = 0; i < Anchors.Length; i++)
        {
            //Calculate absolute offset
            var dX = Anchors[i].transform.position.x - Anchors[0].transform.position.x;
            var dY = Anchors[i].transform.position.y - Anchors[0].transform.position.y;

            absoluteOffsets[i] = new Vector2(dX, dY);

            //Calculate relative offset
            relativeOffsets[i] = new Vector2(dX / dXFromOrigin, dY / dYFromOrigin);
        }

        //Save offsets to supplied PathTemplate
        path.AnchorAbsoluteOffsets = absoluteOffsets;
        path.AnchorRelativeOffsets = relativeOffsets;
    }
}

Continuez avec la création d'un PathTemplate Scriptable, comme indiqué dans l'image suivante.

Nous devons maintenant fournir des ancres et PathTemplate à notre script :

Appuyez sur Play dans l'éditeur pour exécuter le script et vérifier quelles valeurs avons-nous dans notre PathTemplate :

Lire aussi  Grâce à l'intelligence artificielle, les vêtements ont été retirés des photographies de filles mineures

Nous avons initialisé avec succès des tableaux de décalages et pouvons personnaliser ce modèle en fonction de besoins particuliers : définissons IsYRelative sur vrai parce que nous voulons que ce chemin soit mis à l'échelle dans le sens haut-bas.

Déplacer un objet le long d'une trajectoire

Pour déplacer GameObjects le long d'un chemin courbe, nous avons besoin d'un script qui contient VertexPath et déplace GameObject le long de celui-ci. Créons un script et nommons-le CurveMover :

using PathCreation;
using UnityEngine;

public class CurveMover : MonoBehaviour
{
    //Current path
    public VertexPath Path;

    //Speed of projectile
    public float Speed;

    //Behaviour type for the VertexPath when object has reached the EndOfPath (for use with PathCreator asset only)
    public EndOfPathInstruction endInstruction = EndOfPathInstruction.Stop;

    //VFX Prefab when object is being destroyed (in real project it should be stored in another class)
    public GameObject VFXOnDestroy;

    //Distance already travelled used for calculating positions in the Update
    private float dstTravelled = 0;



    //Reset and init new path for the object
    public void Init(VertexPath path)
    {
        dstTravelled = 0;
        Path = path;
    }

    public void Update()
    {
        if (Path != null)
        {
            //What should happen when the object have reached the target
            if (dstTravelled >= Path.length)
            {
                GameObject blast = Instantiate(VFXOnDestroy, this.transform.position, Quaternion.identity);
                Destroy(this.gameObject);
                Destroy(blast, 0.5f);
            }
            //Continue to move to the target
            else
            {
                //Increment dstTravelled
                dstTravelled += Speed * Time.deltaTime;
                //Change object's position based on dstTravelled
                transform.position = Path.GetPointAtDistance(dstTravelled, endInstruction);
            }
        }
    }
}

Créez un préfabriqué avec le script CurveMover et initialisez-le avec les valeurs suivantes :

Enfin, nous avons un projectile et un PathTemplate pour le déplacer. Passons à la partie la plus intéressante et amusante : le tir !

Lancer et tirer des projectiles

C'est simple, et la façon dont vous pouvez le faire dépend de votre projet particulier : instanciez (ou utilisez un objet existant) GameObject avec le script CurveMover attaché et initialisez-le avec VertexPath à partir de PathTemplate :

//Instantiate a projectile
var obj = Instantiate(myProjectile, StartPosition.position, Quaternion.identity);
//Paste your values here for constraints
var bounds = new Bounds(new Vector2(0, 0), new Vector2(40, 40));
//Get VertexPath from PathTemplate
var path = MyPathTemplate.CalculatePath(gp.transform, EndPositions[Random.Range(0,EndPositions.Length)].position, bounds);
//Initialise projectile it and see how it flies.
obj.GetComponent().Init(path);

Il est important de noter que si vous soumettez une transformation d'un objet en mouvement comme origine du chemin, votre chemin résultant se déplacera avec lui. Pour cette raison, vous devez instancier un GameObject vide pour gérer de tels cas et n'oubliez pas de le détruire (ou de le remettre dans le pool si vous utilisez le système de pooling) une fois le chemin terminé.

Aller plus loin

Ce guide présente un flux de travail gérable qui nous aide à créer des modèles basés sur des courbes et à les utiliser. Vous pouvez améliorer ce flux de travail comme vous le souhaitez : par exemple, créez des ancres à partir du point de fin ou même interpolez votre position d'ancrage entre les positions de début et de fin, mettez toute logique supplémentaire dans PathTemplate, mettez à niveau le comportement de CurveMover ou passez à une autre solution de Bézier.

N'hésitez pas à écrire en commentaires avec vos suggestions, et merci de votre attention !

.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Recent News

Editor's Pick