Avec l'aide de la communauté Stack Overflow, j'ai écrit un simulateur de physique assez basique mais amusant.
Vous cliquez et faites glisser la souris pour lancer une balle. Il rebondira et finira par s'arrêter sur le "sol".
Ma prochaine grande fonctionnalité que je veux ajouter est la collision balle à balle. Le mouvement de la balle est divisé en un vecteur vitesse x et y. J'ai la gravité (petite réduction du vecteur y à chaque étape), j'ai des frictions (petite réduction des deux vecteurs chaque collision avec un mur). Les balles se déplacent honnêtement de manière étonnamment réaliste.
Je suppose que ma question comporte deux parties:
La gestion de la détection de collision des "murs" et des changements de vecteur résultants était facile, mais je vois plus de complications avec les collisions balle-balle. Avec les murs, je devais simplement prendre le négatif du vecteur x ou y approprié et le placer dans la bonne direction. Je ne pense pas que ce soit le cas avec des balles.
Quelques éclaircissements rapides: pour des raisons de simplicité, je suis d'accord avec une collision parfaitement élastique pour le moment, toutes mes balles ont la même masse pour le moment, mais je pourrais changer cela à l'avenir.
Edit: Ressources que j'ai trouvées utiles
Physique des billes 2d avec vecteurs: Collisions bidimensionnelles sans trigonométrie.pdf
2d Exemple de détection de collision de billes: Ajout de la détection de collision
La détection et la réponse de la collision de la balle fonctionnent très bien!
Code pertinent:
Détection de collision:
for (int i = 0; i < ballCount; i++)
{
for (int j = i + 1; j < ballCount; j++)
{
if (balls[i].colliding(balls[j]))
{
balls[i].resolveCollision(balls[j]);
}
}
}
Ceci vérifiera les collisions entre chaque balle, mais évitera les vérifications redondantes (si vous devez vérifier si la balle 1 entre en collision avec la balle 2, vous n'avez pas besoin de vérifier si la balle 2 entre en collision avec la balle 1. Elle ignore également les collisions avec elle-même. ).
Ensuite, dans ma classe de balle, j'ai mes méthodes de collision () et de resolCollision ():
public boolean colliding(Ball ball)
{
float xd = position.getX() - ball.position.getX();
float yd = position.getY() - ball.position.getY();
float sumRadius = getRadius() + ball.getRadius();
float sqrRadius = sumRadius * sumRadius;
float distSqr = (xd * xd) + (yd * yd);
if (distSqr <= sqrRadius)
{
return true;
}
return false;
}
public void resolveCollision(Ball ball)
{
// get the mtd
Vector2d delta = (position.subtract(ball.position));
float d = delta.getLength();
// minimum translation distance to Push balls apart after intersecting
Vector2d mtd = delta.multiply(((getRadius() + ball.getRadius())-d)/d);
// resolve intersection --
// inverse mass quantities
float im1 = 1 / getMass();
float im2 = 1 / ball.getMass();
// Push-pull them apart based off their mass
position = position.add(mtd.multiply(im1 / (im1 + im2)));
ball.position = ball.position.subtract(mtd.multiply(im2 / (im1 + im2)));
// impact speed
Vector2d v = (this.velocity.subtract(ball.velocity));
float vn = v.dot(mtd.normalize());
// sphere intersecting but moving away from each other already
if (vn > 0.0f) return;
// collision impulse
float i = (-(1.0f + Constants.restitution) * vn) / (im1 + im2);
Vector2d impulse = mtd.normalize().multiply(i);
// change in momentum
this.velocity = this.velocity.add(impulse.multiply(im1));
ball.velocity = ball.velocity.subtract(impulse.multiply(im2));
}
Code source: Source complète pour le collisionneur balle à balle.
Si quelqu'un a des suggestions pour améliorer ce simulateur physique de base, faites-le moi savoir! Une chose que je n'ai pas encore ajoutée, c'est angular momentum afin que les balles roulent de manière plus réaliste. D'autres suggestions? Laissez un commentaire!
Pour détecter si deux billes entrent en collision, il suffit de vérifier si la distance entre leurs centres est inférieure à deux fois le rayon. Pour faire une collision parfaitement élastique entre les balles, il suffit de se préoccuper de la composante de la vitesse qui se trouve dans la direction de la collision. L'autre composant (tangent à la collision) restera le même pour les deux balles. Vous pouvez obtenir les composants de collision en créant un vecteur unitaire pointant dans la direction d’une balle à l’autre, puis en prenant le produit scalaire avec les vecteurs vitesse des billes. Vous pouvez ensuite brancher ces composants dans une équation de collision parfaitement élastique 1D.
Wikipedia a un très bon résumé de l'ensemble du processus . Pour les billes de n'importe quelle masse, les nouvelles vitesses peuvent être calculées à l'aide des équations (où v1 et v2 sont les vitesses après la collision, et u1, u2 sont d'avant):
Si les billes ont la même masse, les vitesses sont simplement commutées. Voici un code que j'ai écrit qui fait quelque chose de similaire:
void Simulation::collide(Storage::Iterator a, Storage::Iterator b)
{
// Check whether there actually was a collision
if (a == b)
return;
Vector collision = a.position() - b.position();
double distance = collision.length();
if (distance == 0.0) { // hack to avoid div by zero
collision = Vector(1.0, 0.0);
distance = 1.0;
}
if (distance > 1.0)
return;
// Get the components of the velocity vectors which are parallel to the collision.
// The perpendicular component remains the same for both fish
collision = collision / distance;
double aci = a.velocity().dot(collision);
double bci = b.velocity().dot(collision);
// Solve for the new velocities using the 1-dimensional elastic collision equations.
// Turns out it's really simple when the masses are the same.
double acf = bci;
double bcf = aci;
// Replace the collision velocity components with the new ones
a.velocity() += (acf - aci) * collision;
b.velocity() += (bcf - bci) * collision;
}
En ce qui concerne l'efficacité, Ryan Fox a raison, vous devriez envisager de diviser la région en sections, puis de détecter les collisions dans chaque section. Gardez à l'esprit que les balles peuvent entrer en collision avec d'autres balles sur les limites d'une section, ce qui rendrait votre code bien plus compliqué. L’efficacité n’aura probablement pas d’importance tant que vous n’aurez pas plusieurs centaines de balles. Pour les points bonus, vous pouvez exécuter chaque section sur un noyau différent ou fractionner le traitement des collisions au sein de chaque section.
Eh bien, il y a des années, j'ai conçu le programme comme vous l'avez présenté ici.
Il y a un problème caché (ou plusieurs, selon le point de vue):
Et aussi, presque dans 100% des cas, vos nouvelles vitesses seront fausses. Eh bien, pas vitesses, mais positions. Vous devez calculer les nouvelles vitesses avec précision au bon endroit. Sinon, il vous suffit de décaler les balles sur une petite quantité d'erreur, disponible à partir de l'étape discrète précédente.
La solution est évidente: vous devez diviser le pas de temps, vous devez donc commencer par modifier le lieu, puis entrer en collision, puis passer au reste du temps.
Vous devez utiliser le partitionnement d’espace pour résoudre ce problème.
Lire sur partitionnement d’espace binaire et Quadtrees
Pour clarifier la suggestion de Ryan Fox de scinder l'écran en régions et de ne rechercher que les collisions dans les régions ...
par exemple. divisez l'aire de jeu en une grille de carrés (ce qui veut dire arbitrairement une longueur d'unité par côté), et vérifiez les collisions dans chaque carré de la grille.
C'est absolument la bonne solution. Comme le soulignait une autre affiche, le seul problème est que les collisions entre frontières sont un problème.
La solution consiste à superposer une deuxième grille avec un décalage vertical et horizontal de 0,5 unité par rapport à la première.
Ensuite, toutes les collisions qui traverseraient les limites de la première grille (et ne seraient donc pas détectées) se situeraient dans les carrés de la grille de la deuxième grille. Tant que vous gardez une trace des collisions que vous avez déjà traitées (car il peut y avoir des chevauchements), vous n'avez pas à vous soucier de la gestion des cas Edge. Toutes les collisions se dérouleront dans un carré de la grille.
Un bon moyen de réduire le nombre de contrôles de collision consiste à diviser l'écran en différentes sections. Vous ne comparez alors que chaque balle avec les balles de la même section.
Une chose que je vois ici pour optimiser.
Bien que je convienne que les balles frappées lorsque la distance est la somme de leurs rayons, il ne faut jamais calculer cette distance! Calculez plutôt son carré et travaillez de cette façon. Il n'y a aucune raison pour cette opération coûteuse de racine carrée.
De plus, une fois que vous avez trouvé une collision, vous devez continuer à évaluer les collisions jusqu'à ce qu'il n'en reste plus. Le problème est que le premier risque de causer d’autres problèmes qui doivent être résolus avant d’obtenir une image précise. Considérez ce qui se passe si la balle frappe une balle au bord? La deuxième balle frappe le bord et rebondit immédiatement dans la première balle. Si vous frappez une pile de balles dans le coin, vous risquez de rencontrer quelques collisions qui doivent être résolues avant de pouvoir itérer le cycle suivant.
En ce qui concerne le O (n ^ 2), tout ce que vous pouvez faire est de minimiser le coût de rejeter ceux qui manquent:
1) Une balle qui ne bouge pas ne peut rien toucher. S'il y a un nombre raisonnable de balles qui traînent sur le sol, cela pourrait vous épargner de nombreux tests. (Notez que vous devez toujours vérifier si quelque chose touche la balle immobile.)
2) Quelque chose d’intérêt: divisez l’écran en un certain nombre de zones mais les lignes doivent être floues - les boules situées au bord d’une zone sont répertoriées comme se trouvant dans toutes les zones pertinentes (pouvant être 4). Je voudrais utiliser une grille 4x4, stocker les zones sous forme de bits. Si un ET des zones de deux zones de boules renvoie zéro, fin du test.
3) Comme je l'ai mentionné, ne faites pas la racine carrée.
J'ai trouvé une excellente page avec des informations sur la détection de collision et la réponse en 2D.
http://www.metanetsoftware.com/technique.html
Ils essaient d'expliquer comment cela se passe d'un point de vue académique. Ils commencent par la simple détection de collision d'objet à objet, puis passent à la réaction de collision et à leur mise à l'échelle.
Modifier: Lien mis à jour
Vous avez deux moyens faciles de le faire. Jay a couvert la manière précise de vérifier depuis le centre de la balle.
La méthode la plus simple consiste à utiliser un rectangle de sélection, à définir une taille de 80% de la taille de la balle et vous simulerez très bien une collision.
Ajoutez une méthode à votre classe de balle:
public Rectangle getBoundingRect()
{
int ballHeight = (int)Ball.Height * 0.80f;
int ballWidth = (int)Ball.Width * 0.80f;
int x = Ball.X - ballWidth / 2;
int y = Ball.Y - ballHeight / 2;
return new Rectangle(x,y,ballHeight,ballWidth);
}
Ensuite, dans votre boucle:
// Checks every ball against every other ball.
// For best results, split it into quadrants like Ryan suggested.
// I didn't do that for simplicity here.
for (int i = 0; i < balls.count; i++)
{
Rectangle r1 = balls[i].getBoundingRect();
for (int k = 0; k < balls.count; k++)
{
if (balls[i] != balls[k])
{
Rectangle r2 = balls[k].getBoundingRect();
if (r1.Intersects(r2))
{
// balls[i] collided with balls[k]
}
}
}
}
Cette KineticModel
est une implémentation de l'approche citée en Java.
Je le vois insinué ici et là, mais vous pourriez aussi faire un calcul plus rapide en premier, par exemple, comparer les cadres de sélection pour le chevauchement, puis ENFUIR un chevauchement basé sur le rayon si le premier test réussit.
Le calcul mathématique addition/différence est beaucoup plus rapide pour un cadre de sélection que pour tous les trig pour le rayon, et la plupart du temps, le test du cadre de sélection exclut la possibilité d'une collision. Mais si vous testez à nouveau avec trig, vous obtenez les résultats précis que vous recherchez.
Oui, ce sont deux tests, mais ce sera plus rapide dans l'ensemble.
J'ai implémenté ce code en JavaScript à l'aide de l'élément HTML Canvas et il a produit de superbes simulations à 60 images par seconde. J'ai commencé la simulation avec une collection d'une dizaine de balles à des positions et vitesses aléatoires. J’ai trouvé qu’à des vitesses plus élevées, une collision entre une petite balle et une balle beaucoup plus grosse faisait apparaître la petite balle STICK au bord de la plus grosse degrés autour de la plus grosse boule avant de se séparer. (Je me demande si quelqu'un d'autre a observé ce comportement.)
Certains enregistrements des calculs ont montré que la distance de traduction minimale dans ces cas n'était pas assez grande pour empêcher les mêmes balles d'entrer en collision dans le prochain pas de temps. J'ai expérimenté et découvert que je pouvais résoudre ce problème en augmentant la valeur de la DMT en fonction des vitesses relatives:
dot_velocity = ball_1.velocity.dot(ball_2.velocity);
mtd_factor = 1. + 0.5 * Math.abs(dot_velocity * Math.sin(collision_angle));
mtd.multplyScalar(mtd_factor);
J'ai vérifié qu'avant et après ce correctif, l'énergie cinétique totale était conservée pour chaque collision. La valeur 0,5 dans le paramètre mtd_factor était approximativement la valeur minimale qui entraînait toujours la séparation des billes après une collision.
Bien que ce correctif introduise une petite quantité d’erreur dans la physique exacte du système, le compromis est qu’il est désormais possible de simuler des balles très rapides dans un navigateur sans réduire la taille du pas de temps.