Dans ce didacticiel, nous allons implémenter un terrain de pixel entièrement destructible, à la manière de jeux comme Cortex Command et Worms. Vous apprendrez à faire exploser le monde où que vous tourniez - et à faire en sorte que la "poussière" se dépose sur le sol pour créer un nouveau terrain.
Remarque: Bien que ce tutoriel soit écrit en traitement et compilé avec Java, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..
Vous pouvez aussi jouer à la démo. WASD pour se déplacer, clic gauche pour tirer des balles explosives, clic droit pour pulvériser des pixels.
Dans notre bac à sable à défilement latéral, le terrain sera le principal mécanisme de notre jeu. Des algorithmes similaires ont souvent une image pour la texture du terrain et une autre comme masque noir et blanc pour définir les pixels solides. Dans cette démonstration, le terrain et sa texture ne forment qu'une seule image et les pixels sont fixes, qu'ils soient transparents ou non. L'approche par masque serait plus appropriée si vous souhaitez définir les propriétés de chaque pixel, par exemple son potentiel de délogement ou son caractère rebondissant..
Pour rendre le terrain, le bac à sable dessine les pixels statiques en premier, puis les pixels dynamiques avec tout le reste en haut..
Le terrain dispose également de méthodes permettant de déterminer si un pixel statique situé à un emplacement est solide ou non, et de méthodes pour supprimer et ajouter des pixels. Le moyen le plus efficace de stocker l'image est probablement un tableau à 1 dimension. Obtenir un index 1D à partir d'une coordonnée 2D est assez simple:
index = x + y * largeur
Pour que les pixels dynamiques rebondissent, nous devons pouvoir connaître la normale de la surface à tout moment. Parcourez une zone carrée autour du point souhaité, recherchez tous les pixels solides à proximité et calculez leur position. Prenez un vecteur de cette position au point souhaité, inversez-le et normalisez-le. Il y a votre normal!
Les lignes noires représentent les normales au terrain en différents points.
Voici à quoi ça ressemble dans le code:
normal (x, y) vecteur moy. pour x = -3 à 3 // 3 est un nombre arbitraire pour y = -3 à 3 // nombres plus grands pour des surfaces plus lisses si le pixel est plein à (x + w, y + h) avg - = (x, y) longueur = sqrt (avgX * avgX + avgY * avgY) // distance entre moy et centre retour moy / longueur // normaliser le vecteur en le divisant par cette distance
Le "Terrain" stocke lui-même tous les pixels statiques non mobiles. Les pixels dynamiques sont des pixels actuellement en mouvement et sont stockés séparément des pixels statiques. Lorsque le terrain explose et s’installe, les pixels sont basculés entre les états statique et dynamique lorsqu’ils se délogent et s’entrechoquent. Chaque pixel est défini par un certain nombre de propriétés:
Pour que le pixel se déplace, il faut que sa position soit transmise avec sa vitesse. L'intégration d'Euler, bien qu'inexacte pour les simulations complexes, est assez simple pour nous permettre de déplacer efficacement nos particules:
position = position + vitesse * elapsedTime
le temps écoulé
est la durée écoulée depuis la dernière mise à jour. La précision de toute simulation peut être entièrement brisée si le temps écoulé
est trop variable ou trop grand. Ce n’est pas un problème pour les pixels dynamiques, mais pour d’autres systèmes de détection de collision..
Nous utiliserons des pas de temps fixes, en prenant le temps écoulé et en le scindant en morceaux de taille constante. Chaque morceau est une "mise à jour" complète de la physique, tout reste étant envoyé dans la trame suivante.
elapsedTime = lastTime - currentTime lastTime = currentTime // réinitialiser lastTime // ajouter du temps qui n'a pas pu être utilisé la dernière image elapsedTime + = leftOverTime // le diviser en morceaux de 16 ms timesteps = floor (elapsedTime / 16) // stocker le temps nous ne pouvions pas utiliser pour la prochaine image. leftOverTime = elapsedTime - les pas de temps pour (i = 0; i < timesteps; i++) update(16/1000) // update physics
Détecter les collisions de nos pixels en vol est aussi simple que de tracer des lignes.
L'algorithme de ligne de Bresenham a été développé en 1962 par un homme du nom de Jack E. Bresenham. À ce jour, il a été utilisé pour dessiner efficacement de simples lignes avec alias. L'algorithme s'en tient strictement aux nombres entiers et utilise principalement l'addition et la soustraction afin de tracer efficacement des lignes. Aujourd'hui, nous l'utilisons dans un but différent: la détection de collision.
J'utilise un code emprunté à un article sur gamedev.net. Bien que la plupart des implémentations de l'algorithme de ligne de Bresenham réordonnent l'ordre d'affichage, celui-ci nous permet de toujours numériser du début à la fin. L'ordre est important pour la détection de collision, sinon nous détecterons les collisions à la mauvaise extrémité du chemin du pixel..
La pente est une partie essentielle de l'algorithme de ligne de Bresenham. L'algorithme fonctionne en divisant la pente en ses composants "élévation" et "exécution". Si, par exemple, la pente de la ligne était égale à 1/2, nous pouvons tracer la ligne en plaçant deux points horizontalement, en montant (et à droite) un, puis deux autres.
L'algorithme que je montre ici tient compte de tous les scénarios, que les lignes aient une pente positive ou négative ou qu'elle soit verticale. L'auteur explique comment il le tire sur gamedev.net.
rayCast (int startX, int startY, int lastX, int lastY) int deltax = (int) abs (lastX - startX) int deltay = (int) abs (lastY - startY) int x = (int) startX int y = ( int) startY int xinc1, xinc2, yinc1, yinc2 // Détermine si x et y augmentent ou diminuent si (lastX> = startX) // Les valeurs x augmentent xinc1 = 1 xinc2 = 1 else // Le les valeurs x sont décroissantes xinc1 = -1 xinc2 = -1 if (lastY> = startY) // Les valeurs y sont en augmentation yinc1 = 1 yinc2 = 1 else // Les valeurs y sont en diminution yinc1 = - 1 yinc2 = -1 int den, num, numadd, numpixels if (deltax> = deltay) // Il existe au moins une valeur x pour chaque valeur y xinc1 = 0 // Ne change pas le x quand le numérateur > = dénominateur yinc2 = 0 // Ne changez pas le y pour chaque itération den = deltax num = deltax / 2 numadd = deltay numpixels = deltax // Il y a plus de valeurs x que de valeurs y else // Il y a au moins une valeur y pour chaque valeur x xinc2 = 0 // Ne changez pas le x pour chaque itération yinc1 = 0 // Don't ch fixez y lorsque numérateur> = dénominateur den = deltay num = deltay / 2 numadd = deltax numpixels = deltay // Il existe plus de valeurs y que de valeurs x int prevX = (int) startX int prevY = (int) startY pour (int curpixel = 0; curpixel <= numpixels; curpixel++) if (terrain.isPixelSolid(x, y)) return (prevX, prevY) and (x, y) prevX = x prevY = y num += numadd // Increase the numerator by the top of the fraction if (num >= den) // Vérifie si numérateur> = dénominateur num - = den // Calcule la nouvelle valeur du numérateur x + = xinc1 // Change le x comme il convient y + = yinc1 // Change le y comme il convient x + = xinc2 // Change le x comme il convient y + = yinc2 // Change le y comme il convient return null // rien n'a été trouvé
Le pixel dynamique peut faire l'une des deux choses lors d'une collision.
L'angle de chaque côté de la normale est le même.
// Projetez la vitesse sur la normale, multipliez-la par 2 et soustrayez-la de la vitesse normale = getNormal (collision.x, collision.y) // projetez la vitesse sur la normale à l'aide du produit de point projection = velocity.x * normal.x + vélocité .y * normal.y // vélocité - = normal * projection * 2
Les balles agissent exactement comme des pixels dynamiques. Motion est intégré de la même manière et la détection de collision utilise le même algorithme. Notre seule différence est la gestion des collisions
Après la détection d'une collision, les balles explosent en supprimant tous les pixels statiques situés dans un rayon, puis en plaçant des pixels dynamiques à leur place, leur vitesse étant dirigée vers l'extérieur. J'utilise une fonction pour balayer une zone carrée autour du rayon d'une explosion afin de déterminer les pixels à déloger. Ensuite, la distance du pixel au centre est utilisée pour établir une vitesse.
exploser (x, y, rayon) pour (xPos = x - rayon; xPos <= x + radius; xPos++) for (yPos = y - radius; yPos <= y + radius; yPos++) if (sq(xPos - x) + sq(yPos - y) < radius * radius) if (pixel is solid) remove static pixel add dynamic pixel
Le joueur n’est pas un élément essentiel du mécanisme de terrain destructible, mais cela implique une détection de collision qui sera certainement pertinente pour les problèmes à venir. Je vais expliquer comment la collision est détectée et gérée dans la démo pour le joueur..
Des milliers de pixels sont traités en même temps, ce qui occasionne beaucoup de contraintes pour le moteur physique. Comme toute autre chose, pour faire vite, je vous recommande d’utiliser un langage assez rapide. La démo est compilée en Java.
Vous pouvez également optimiser les algorithmes. Par exemple, le nombre de particules provenant d'explosions peut être réduit en diminuant la résolution de destruction. Normalement, nous trouvons chaque pixel et le transformons en pixel dynamique 1x1. Numérisez tous les 2x2 pixels, ou 3x3, et lancez un pixel dynamique de cette taille. Dans la démo, nous utilisons 2x2 pixels.
Si vous utilisez Java, la récupération de place posera problème. La JVM trouvera périodiquement dans la mémoire des objets qui ne sont plus utilisés, tels que les pixels dynamiques qui sont ignorés en échange de pixels statiques, et essaiera de les éliminer pour faire de la place pour davantage d'objets. La suppression d'objets, de tonnes d'objets, prend cependant du temps et chaque fois que la JVM nettoie un nettoyage, notre jeu se fige brièvement..
Une des solutions possibles consiste à utiliser un cache de quelque sorte. Au lieu de créer / détruire des objets tout le temps, vous pouvez simplement conserver des objets morts (comme des pixels dynamiques) pour les réutiliser ultérieurement.
Utilisez des primitives chaque fois que possible. Par exemple, utiliser des objets pour les positions et les vitesses va rendre les choses un peu plus difficiles pour le ramassage des ordures. Ce serait encore mieux si vous pouviez tout stocker en tant que primitives dans des tableaux unidimensionnels..
Ce mécanisme de jeu vous permet de prendre de nombreuses directions différentes. Des fonctionnalités peuvent être ajoutées et personnalisées pour correspondre à n'importe quel style de jeu.
Par exemple, les collisions entre pixels dynamiques et statiques peuvent être gérées différemment. Un masque de collision sous le terrain peut être utilisé pour définir le caractère collant, l'espace et la force de chaque pixel statique, ou la probabilité d'être déplacé par une explosion..
Vous pouvez également utiliser une variété de choses différentes pour les armes à feu. On peut attribuer une "profondeur de pénétration" aux balles, afin de lui permettre de traverser autant de pixels avant d'exploser. La mécanique traditionnelle des armes à feu peut également être appliquée, comme une cadence de tir variée ou, comme un fusil de chasse, plusieurs balles peuvent être tirées simultanément. Vous pouvez même, comme pour les particules rebondissantes, faire rebondir les balles des pixels métalliques.
La destruction de terrain en 2D n'est pas complètement unique. Par exemple, les classiques Worms and Tanks éliminent des parties du terrain lors d'explosions. Cortex Command utilise des particules rebondissantes similaires à celles que nous utilisons ici. D'autres jeux pourraient aussi bien, mais je n'en ai pas encore entendu parler. J'attends avec impatience de voir ce que les autres développeurs feront de ce mécanicien.
La plupart de ce que j'ai expliqué ici est entièrement mis en œuvre dans la démo. S'il vous plaît jeter un oeil à sa source si quelque chose semble ambigu ou déroutant. J'ai ajouté des commentaires à la source pour que ce soit aussi clair que possible. Merci d'avoir lu!