Dans ce tutoriel, nous allons simuler un plan d’eau dynamique en 2D à l’aide de techniques physiques simples. Nous allons utiliser un mélange de rendu de ligne, de rendu de maille, de déclencheurs et de particules pour créer notre effet. Le résultat final est complété par des vagues et des éclaboussures, prêt à être ajouté à votre prochain match. Une source de démonstration Unity (Unity3D) est incluse, mais vous devriez pouvoir implémenter quelque chose de similaire en utilisant les mêmes principes dans n’importe quel moteur de jeu..
Articles SimilairesVoici ce que nous allons finir avec. Vous aurez besoin du plug-in de navigateur Unity pour l'essayer..
Cliquez pour créer un nouvel objet à déposer dans l'eau.Dans son tutoriel, Michael Hoffman a montré comment modéliser la surface de l’eau avec une rangée de sources.
Nous allons rendre le haut de notre eau en utilisant l'un des rendus de ligne d'Unity, et utiliser tellement de nœuds qu'il apparaît comme une onde continue..
Nous devrons cependant suivre les positions, les vitesses et les accélérations de chaque nœud. Pour ce faire, nous allons utiliser des tableaux. Donc, en haut de notre classe, nous ajouterons ces variables:
float [] xpositions; float [] ypositions; float [] vélocités; float [] accélérations; LineRenderer Body;
le LineRenderer
va stocker tous nos nœuds et définir notre plan d'eau. Nous avons toujours besoin de l'eau elle-même, cependant; nous allons créer cela avec Mailles
. Nous allons aussi avoir besoin d'objets pour tenir ces mailles.
GameObject [] meshobjects; Maillage [] mailles;
Nous allons aussi avoir besoin de collisionneurs pour que les choses puissent interagir avec notre eau:
Collisionneurs GameObject [];
Et nous allons aussi stocker toutes nos constantes:
const float springconstant = 0.02f; amortissement constant du flottant = 0,04f; étalement constant du flottant = 0,05f; const float z = -1f;
Ces constantes sont du même genre que celles discutées par Michael, à l’exception de z
-c'est notre z-offset pour notre eau. Nous allons utiliser -1
pour cela afin qu'il soit affiché devant nos objets. (Vous voudrez peut-être changer cela en fonction de ce que vous voulez voir devant et derrière; vous devrez utiliser la coordonnée z pour déterminer où se situent les sprites par rapport à celle-ci.)
Ensuite, nous allons conserver certaines valeurs:
float baseheight; flotter à gauche; fond de flotteur;
Ce ne sont que les dimensions de l'eau.
Nous allons aussi avoir besoin de variables publiques que nous pouvons définir dans l'éditeur. Tout d'abord, le système de particules que nous allons utiliser pour nos éclaboussures:
Splash public de GameObject:
Ensuite, le matériel que nous allons utiliser pour notre moteur de rendu en ligne (au cas où vous voudriez réutiliser le script pour l’acide, la lave, les produits chimiques ou toute autre chose):
Matière publique:
De plus, le type de maille que nous allons utiliser pour la masse d’eau principale:
Watermesh GameObject public:
Ceux-ci vont tous être basés sur les préfabriqués, qui sont tous inclus dans les fichiers source.
Nous voulons un objet de jeu capable de contenir toutes ces données, d’agir en tant que gestionnaire et d’engendrer notre corps d’eau dans les règles de l’usage. Pour ce faire, nous allons écrire une fonction appelée SpawnWater ()
.
Cette fonction prendra les entrées du côté gauche, de la largeur, du haut et du bas de la masse d’eau..
public spid SpawnWater (float Left, float Width, float Top, float Bottom)
(Bien que cela semble incohérent, il agit dans l'intérêt d'une conception à niveaux rapides lors de la construction de gauche à droite).
Nous allons maintenant découvrir le nombre de nœuds dont nous avons besoin:
int edgecount = Mathf.RoundToInt (Width) * 5; int nodecount = edgecount + 1;
Nous allons en utiliser cinq par unité de largeur, afin de nous offrir un mouvement fluide et peu exigeant. (Vous pouvez varier cela pour équilibrer l'efficacité et la douceur.) Cela nous donne toutes nos lignes, alors nous avons besoin de + 1
pour le noeud supplémentaire à la fin.
La première chose que nous allons faire est de rendre notre masse d’eau avec le LineRenderer
composant:
Body = gameObject.AddComponent(); Body.material = mat; Body.material.renderQueue = 1000; Body.SetVertexCount (nodecount); Body.SetWidth (0,1f, 0,1f);
Ce que nous avons également fait ici est de sélectionner notre matériau et de le configurer pour le rendu au-dessus de l'eau en choisissant sa position dans la file d'attente de rendu. Nous avons défini le nombre correct de nœuds et défini la largeur de la ligne sur 0,1
.
Vous pouvez le varier en fonction de l'épaisseur souhaitée de votre ligne. Vous avez peut-être remarqué que SetWidth ()
prend deux paramètres; ce sont la largeur au début et à la fin de la ligne. Nous voulons que cette largeur soit constante.
Maintenant que nous avons créé nos nœuds, nous allons initialiser toutes nos principales variables:
xpositions = new float [nodecount]; ypositions = new float [nodecount]; vélocités = new float [nodecount]; accélérations = new float [nodecount]; meshobjects = new GameObject [edgecount]; meshes = new Mesh [edgecount]; colliders = new GameObject [edgecount]; baseheight = Top; en bas = en bas; gauche = gauche;
Alors maintenant, nous avons tous nos tableaux, et nous conservons nos données.
Maintenant, définissons réellement les valeurs de nos tableaux. Nous allons commencer avec les nœuds:
pour (int i = 0; i < nodecount; i++) ypositions[i] = Top; xpositions[i] = Left + Width * i / edgecount; accelerations[i] = 0; velocities[i] = 0; Body.SetPosition(i, new Vector3(xpositions[i], ypositions[i], z));
Ici, nous définissons toutes les positions y pour qu'elles se trouvent en haut de l'eau, puis nous ajoutons progressivement tous les nœuds côte à côte. Nos vitesses et accélérations sont nulles initialement, car l'eau est encore.
Nous terminons la boucle en définissant chaque nœud de notre LineRenderer
(Corps
) à leur position correcte.
Voici où ça devient difficile.
Nous avons notre ligne, mais nous n'avons pas l'eau elle-même. Et la façon dont nous pouvons faire cela utilise Meshes. Nous allons commencer par créer ces:
pour (int i = 0; i < edgecount; i++) meshes[i] = new Mesh();
Maintenant, Meshes stocke un tas de variables. La première variable est assez simple: elle contient tous les sommets (ou coins).
Le diagramme montre à quoi nous voulons que nos segments de maillage ressemblent. Pour le premier segment, les sommets sont mis en évidence. Nous en voulons quatre au total.
Vecteur3 [] sommets = nouveau vecteur3 [4]; Sommets [0] = nouveau vecteur3 (xpositions [i], ypositions [i], z); Sommets [1] = nouveau vecteur3 (xpositions [i + 1], ypositions [i + 1], z); Sommets [2] = nouveau vecteur3 (xpositions [i], bottom, z); Sommets [3] = nouveau vecteur3 (xpositions [i + 1], en bas, z);
Maintenant, comme vous pouvez le voir ici, sommet 0
est le haut gauche, 1
est en haut à droite, 2
est le bas-gauche, et 3
est en haut à droite. Nous devrons nous rappeler que pour plus tard.
La deuxième propriété dont les mailles ont besoin est les UV. Les maillages ont des textures et les UV choisissent quelle partie des textures nous voulons saisir. Dans ce cas, nous voulons seulement les coins en haut à gauche, en haut à droite, en bas à gauche et en bas à droite de notre texture..
Vecteur2 [] UVs = nouveau vecteur2 [4]; UVs [0] = nouveau vecteur2 (0, 1); UV [1] = nouveau vecteur 2 (1, 1); UV [2] = nouveau vecteur 2 (0, 0); UV [3] = nouveau vecteur 2 (1, 0);
Nous avons maintenant besoin de ces chiffres d'avant. Les mailles sont constituées de triangles et nous savons que tout quadrilatère peut être composé de deux triangles. Nous devons donc maintenant expliquer au maillage comment il doit dessiner ces triangles..
Regardez les coins avec l'ordre des nœuds étiqueté. Triangle UNE connecte les nœuds 0
, 1
et 3
; Triangle B connecte les nœuds 3
, 2
et 0
. Par conséquent, nous voulons créer un tableau contenant six entiers, reflétant exactement cela:
int [] tris = new int [6] 0, 1, 3, 3, 2, 0;
Cela crée notre quadrilatère. Maintenant, nous définissons les valeurs de maillage.
mailles [i] .vertices = sommets; mailles [i] .uv = UVs; mailles [i]. triangles = tris;
Nous avons nos mailles, mais nous n'avons pas d'objets de jeu pour les restituer dans la scène. Nous allons donc les créer à partir de notre Watermesh
préfabriqué contenant un rendu de grille et un filtre de grille.
meshobjects [i] = Instanciez (watermesh, Vector3.zero, Quaternion.identity) en tant que GameObject; meshobjects [i] .GetComponent() .mesh = mailles [i]; meshobjects [i] .transform.parent = transformer;
Nous mettons en place le maillage, et nous le faisons pour être l'enfant du gestionnaire de l'eau, pour ranger les choses.
Maintenant, nous voulons aussi notre collisionneur:
colliders [i] = new GameObject (); colliders [i] .name = "Déclencheur"; collisionneurs [i] .AddComponent(); colliders [i] .transform.parent = transformer; collisionneurs [i] .transform.position = nouveau vecteur3 (gauche + largeur * (i + 0,5f) / edgecount, Top - 0,5f, 0); colliders [i] .transform.localScale = new Vector3 (Width / edgecount, 1, 1); collisionneurs [i] .GetComponent () .isTrigger = true; collisionneurs [i] .AddComponent ();
Ici, nous fabriquons des collisionneurs de caisses en leur donnant un nom afin qu'ils soient un peu plus rangés dans la scène et en les faisant à nouveau enfants du gestionnaire de l'eau. Nous définissons leur position à mi-chemin entre les nœuds, définissons leur taille et ajoutons un WaterDetector
classe à eux.
Maintenant que nous avons notre maillage, nous avons besoin d’une fonction pour le mettre à jour au fur et à mesure que l’eau se déplace:
void UpdateMeshes () pour (int i = 0; i < meshes.Length; i++) Vector3[] Vertices = new Vector3[4]; Vertices[0] = new Vector3(xpositions[i], ypositions[i], z); Vertices[1] = new Vector3(xpositions[i+1], ypositions[i+1], z); Vertices[2] = new Vector3(xpositions[i], bottom, z); Vertices[3] = new Vector3(xpositions[i+1], bottom, z); meshes[i].vertices = Vertices;
Vous remarquerez peut-être que cette fonction utilise simplement le code que nous avons écrit auparavant. La seule différence est que cette fois, nous n’avons pas à régler les tris et les UV, car ils restent les mêmes..
Notre prochaine tâche est de faire fonctionner l'eau elle-même. Nous allons utiliser FixedUpdate ()
pour les modifier tous progressivement.
void FixedUpdate ()
Tout d'abord, nous allons combiner la loi de Hooke avec la méthode d'Euler pour trouver les nouvelles positions, accélérations et vitesses.
Donc, la loi de Hooke est \ (F = kx \), où \ (F \) est la force produite par une source (rappelez-vous, nous modélisons la surface de l'eau sous la forme d'une rangée de sources), \ (k \) est la constante du ressort et \ (x \) est le déplacement. Notre déplacement va simplement être la position y de chaque nœud moins la hauteur de base des nœuds.
Ensuite, nous ajoutons un facteur d'amortissement proportionnelle à la vitesse de la force pour amortir la force.
pour (int i = 0; i < xpositions.Length ; i++) float force = springconstant * (ypositions[i] - baseheight) + velocities[i]*damping ; accelerations[i] = -force; ypositions[i] += velocities[i]; velocities[i] += accelerations[i]; Body.SetPosition(i, new Vector3(xpositions[i], ypositions[i], z));
La méthode d'Euler est simple. nous ajoutons simplement l'accélération à la vitesse et la vitesse à la position, chaque image.
Remarque: je viens de supposer que la masse de chaque nœud était 1
ici, mais vous voudrez utiliser:
accélérations [i] = -force / masse;
si vous voulez une masse différente pour vos nœuds.
Pointe: Pour une physique précise, nous utiliserions l'intégration de Verlet, mais comme nous ajoutons de l'amortissement, nous ne pouvons utiliser que la méthode d'Euler, qui est beaucoup plus rapide à calculer. En général, cependant, la méthode d'Euler introduira de manière exponentielle l'énergie cinétique de nulle part dans votre système physique. Ne l'utilisez donc pas pour quelque chose de précis..
Maintenant nous allons créer propagation des ondes. Le code suivant est adapté du tutoriel de Michael Hoffman.
float [] leftDeltas = new float [xpositions.Length]; float [] rightDeltas = new float [xpositions.Length];
Ici, nous créons deux tableaux. Pour chaque nœud, nous allons comparer la hauteur du nœud précédent à celle du nœud actuel et mettre la différence entre gaucheDeltas
.
Ensuite, nous vérifierons la hauteur du nœud suivant par rapport à celle du nœud que nous vérifions, et indiquerons cette différence dans rightDeltas
. (Nous allons également multiplier toutes les valeurs par une constante de propagation).
pour (int j = 0; j < 8; j++) for (int i = 0; i < xpositions.Length; i++) if (i > 0) leftDeltas [i] = spread * (ypositions [i] - ypositions [i-1]); vitesses [i - 1] + = leftDeltas [i]; si je < xpositions.Length - 1) rightDeltas[i] = spread * (ypositions[i] - ypositions[i + 1]); velocities[i + 1] += rightDeltas[i];
Nous pouvons modifier immédiatement les vitesses en fonction de la différence de hauteur, mais nous ne devrions enregistrer que les différences de positions à ce stade. Si nous modifions immédiatement la position du premier nœud, lorsque nous aurions examiné le deuxième nœud, le premier nœud aura déjà été déplacé, ce qui gâchera tous nos calculs..
pour (int i = 0; i < xpositions.Length; i++) if (i > 0) ypositions [i-1] + = leftDeltas [i]; si je < xpositions.Length - 1) ypositions[i + 1] += rightDeltas[i];
Donc, une fois que nous avons collecté toutes nos données de hauteur, nous pouvons les appliquer à la fin. Nous ne pouvons pas regarder à droite du noeud à l'extrême droite, ni à gauche du noeud à l'extrême gauche, d'où les conditions je> 0
et je < xpositions.Length - 1
.
Notez également que nous avons contenu tout ce code dans une boucle et que nous l'avons exécuté huit fois. En effet, nous souhaitons exécuter ce processus à petites doses plusieurs fois, plutôt qu’un seul calcul volumineux, qui serait beaucoup moins fluide..
Maintenant, nous avons de l'eau qui coule et ça se voit. Ensuite, nous devons pouvoir déranger l'eau!
Pour cela, ajoutons une fonction appelée Éclaboussure()
, qui vérifiera la position x du splash et la vitesse de tout ce qui le frappe. Il devrait être public afin que nous puissions l'appeler plus tard de nos collisionneurs.
public void Splash (float xpos, vitesse de flottement)
Premièrement, nous devons nous assurer que la position spécifiée est réellement dans les limites de notre eau:
if (xpos> = xpositions [0] && xpos <= xpositions[xpositions.Length-1])
Et puis on changera xpos
il nous donne donc la position par rapport au début de la masse d’eau:
xpos - = xpositions [0];
Ensuite, nous allons découvrir quel nœud il touche. Nous pouvons calculer cela comme ceci:
int index = Mathf.RoundToInt ((xpositions.Length-1) * (xpos / (xpositions [xpositions.Length-1] - xpositions [0])));
Alors, voici ce qui se passe ici:
xpos
).0,75
.vitesses [index] = vitesse;
Maintenant, nous fixons la vitesse de l'objet qui a touché notre eau à la vitesse de ce nœud, de sorte qu'il soit entraîné par l'objet.
Remarque: Vous pouvez changer cette ligne comme bon vous semble. Par exemple, vous pouvez ajouter la vélocité à sa vélocité actuelle ou vous pouvez utiliser la quantité de mouvement au lieu de la vélocité et la diviser par la masse de votre nœud..
Nous voulons maintenant créer un système de particules qui produira le splash. Nous avons défini cela plus tôt. ça s'appelle "splash" (assez créativement). Assurez-vous de ne pas le confondre avec Éclaboussure()
. Celui que je vais utiliser est inclus dans les fichiers source.
Tout d'abord, nous voulons que les paramètres du splash changent avec la vélocité de l'objet.
durée de vie float = 0.93f + Mathf.Abs (vélocité) * 0.07f; splash.GetComponent() .startSpeed = 8 + 2 * Mathf.Pow (Mathf.Abs (vélocité), 0.5f); splash.GetComponent () .startSpeed = 9 + 2 * Mathf.Pow (Mathf.Abs (vélocité), 0.5f); splash.GetComponent () .startLifetime = durée de vie;
Ici, nous avons pris nos particules, défini leur durée de vie afin qu'elles ne meurent pas peu de temps après avoir atteint la surface de l'eau et réglé leur vitesse sur le carré de leur vitesse (plus une constante, pour les petites projections).
Vous pouvez regarder ce code et penser: "Pourquoi at-il défini la startSpeed
deux fois? ", et vous auriez raison de vous demander. Le problème est que nous utilisons un système de particules (Shuriken, fourni avec le projet) dont la vitesse de démarrage est définie sur" aléatoire entre deux constantes ". Malheureusement, nous ne pas avoir beaucoup d'accès sur Shuriken par les scripts, donc pour que ce comportement fonctionne, nous devons définir la valeur deux fois.
Maintenant, je vais ajouter une ligne que vous pouvez ou non omettre de votre script:
Position Vector3 = new Vector3 (xpositions [index], ypositions [index] -0.35f, 5); Quaternion rotation = Quaternion.LookRotation (nouveau Vector3 (xpositions [Mathf.FloorToInt (xpositions.Length / 2)], baseheight + 8, 5) - position);
Les particules de Shuriken ne seront pas détruites lorsqu'elles heurteront vos objets. Par conséquent, si vous voulez vous assurer qu'elles ne vont pas atterrir devant vos objets, vous pouvez prendre deux mesures:
5
).La deuxième ligne de code prend le milieu des positions, se déplace légèrement vers le haut et pointe l'émetteur de particules vers lui. J'ai inclus ce comportement dans la démo. Si vous utilisez une étendue d'eau très large, vous ne voulez probablement pas ce comportement. Si votre eau est dans une petite piscine dans une pièce, vous voudrez peut-être l'utiliser. Alors, n'hésitez pas à supprimer cette ligne sur la rotation.
GameObject splish = Instanciez (splash, position, rotation) en tant que GameObject; Détruire (éclat, durée de vie + 0.3f);
Nous faisons maintenant des éclaboussures et nous lui disons de mourir un peu après la mort des particules. Pourquoi un peu après? Parce que notre système de particules envoie quelques rafales séquentielles de particules, alors même si le premier lot ne dure que Temps.Temps + durée de vie
, nos dernières rafales seront encore autour un peu après.
Oui! Nous avons enfin fini, à droite?
Faux! Nous devons détecter nos objets, ou c'était tout pour rien!
Rappelez-vous que nous avons ajouté ce script à tous nos colliders auparavant? Celui appelé WaterDetector
?
Eh bien, nous allons le faire maintenant! Nous ne voulons qu’une fonction:
annuler OnTriggerEnter2D (Collider2D Hit)
En utilisant OnTriggerEnter2D ()
, nous pouvons spécifier ce qui se passe chaque fois qu'un corps rigide 2D entre dans notre masse d'eau. Si on passe un paramètre de Collider2D
nous pouvons trouver plus d'informations sur cet objet.
if (Hit.rigidbody2D! = null)
Nous ne voulons que des objets contenant un corps rigide2D
.
transform.parent.GetComponent() .Splash (transform.position.x, Hit.rigidbody2D.velocity.y * Hit.rigidbody2D.mass / 40f);
Maintenant, tous nos collisionneurs sont des enfants du gestionnaire de l'eau. Alors on attrape le Eau
composant de leur parent et appel Éclaboussure()
, de la position du collisionneur.
Rappelez-vous encore une fois, j'ai dit que vous pouviez soit laisser passer la vitesse soit la vitesse, si vous vouliez que cela soit plus précis physiquement? Eh bien voici où vous devez passer le bon. Si vous multipliez la vitesse y de l'objet par sa masse, vous aurez son élan. Si vous voulez juste utiliser sa vélocité, supprimez la masse de cette ligne.
Enfin, vous aurez envie d'appeler SpawnWater ()
de quelque part. Faisons-le au lancement:
void Start () SpawnWater (-10,20,0, -10);
Et maintenant nous avons fini! Maintenant tout corps rigide2D
avec un collisionneur qui frappe l'eau va créer une éclaboussure, et les vagues se déplaceront correctement.
En prime, j’ai ajouté quelques lignes de code en haut de SpawnWater ()
.
gameObject.AddComponent(); gameObject.GetComponent () .center = nouveau vecteur2 (gauche + largeur / 2, (haut + bas) / 2); gameObject.GetComponent () .size = nouveau vecteur 2 (largeur, haut - bas); gameObject.GetComponent () .isTrigger = true;
Ces lignes de code ajouteront un collisionneur de boîtes à l'eau elle-même. Vous pouvez utiliser ceci pour faire flotter des choses dans votre eau, en utilisant ce que vous avez appris.
Vous aurez envie de faire une fonction appelée OnTriggerStay2D ()
qui prend un paramètre de Collider2D Hit
. Ensuite, vous pouvez utiliser une version modifiée de la formule de ressort utilisée précédemment pour vérifier la masse de l'objet et ajouter une force ou une vitesse à votre corps rigide2D
pour le faire flotter dans l'eau.
Dans ce didacticiel, nous avons implémenté une simulation simple de l'eau à utiliser dans les jeux 2D avec un code physique simple et un rendu de ligne, des rendus de maille, des déclencheurs et des particules. Vous ajouterez peut-être des masses d’eau fluide ondulées comme un obstacle pour votre prochain jeu de plateforme, prêtes à être plongées ou traversées avec précaution par vos personnages flottants, ou peut-être pourriez-vous l’utiliser dans un jeu de voile ou de planche à voile vous sautez simplement des roches sur l'eau d'une plage ensoleillée. Bonne chance!