détection des collisions entre des balles

macounettemacounette Membre
mars 2009 modifié dans API UIKit #1
rebonjour !

Toujours dans mon appli, j'ai réussi à  afficher plusieurs balles en mouvement grâce à  vous :
http://www.objective-cocoa.org/forum/index.php?topic=3361.0

Maintenant je me suis attaquée à  la gestion des collisions. Alors attention j'ai réussi à  faire quelque chose qui marche correctement. C'est pas encore parfait.

Petit résumé :

J'ai une BallView IUView qui lance un timer
<br />@implementation BallView<br /><br />- (id)initWithFrame:(CGRect)frameRect {<br /><br />&nbsp; &nbsp; &nbsp; &nbsp;ballArray = [[NSMutableArray alloc] initWithCapacity:ballNumber];<br />&nbsp; &nbsp; &nbsp; &nbsp;for(int i=0; i&lt;ballNumber; i++) {<br />		//ici j&#39;ai une fonction qui crée des instances de Ball et qui les ajoute dans mon tableau<br />	}<br /><br />&nbsp; &nbsp; &nbsp; &nbsp;timer = [NSTimer scheduledTimerWithTimeInterval:1.0 / 30.0 target:self selector:@selector(repaint)&nbsp; &nbsp; userInfo:nil repeats:YES];<br />}<br /><br />- (void)repaint {<br />	<br />	for(int i=0; i&lt;ballNumber; i++) {<br />		Ball* ball = [ballArray objectAtIndex:i];<br />		[ball move]; // MAJ des coordonnées de balles<br />		[ball checkCollision:ballArray:ballNumber:i]; //je vérifie si j&#39;ai des collisions<br />	}<br />	[self setNeedsDisplay];<br />}<br /><br />- (void)drawRect:(CGRect)rect {<br />	CGContextRef context = UIGraphicsGetCurrentContext();<br />	<br />	for(int i=0; i&lt;ballNumber; i++) {<br />		Ball* ball = [ballArray objectAtIndex:i];<br />		[ball draw:context];<br />	}<br />}<br />


Mes objets Ball :
<br />@implementation Ball<br /><br />// met a jour la position du cercle à  dessiner<br />- (void)move {<br />	velocity.y += gravity;<br />&nbsp; &nbsp; position.x += velocity.x;<br />&nbsp; &nbsp; position.y += velocity.y;<br />	<br />&nbsp; &nbsp; if(position.x + radius&gt; 320.0) {<br />&nbsp; &nbsp; &nbsp; &nbsp; position.x = 320.0 - radius;<br />&nbsp; &nbsp; &nbsp; &nbsp; velocity.x *= bounce;<br />&nbsp; &nbsp; }<br />&nbsp; &nbsp; else if(position.x - radius &lt;0.0) {<br />&nbsp; &nbsp; &nbsp; &nbsp; position.x = radius;<br />&nbsp; &nbsp; &nbsp; &nbsp; velocity.x *= bounce;<br />&nbsp; &nbsp; }<br />	<br />&nbsp; &nbsp; if(position.y + radius&gt; 460.0) {<br />&nbsp; &nbsp; &nbsp; &nbsp; position.y = 460.0 - radius;<br />&nbsp; &nbsp; &nbsp; &nbsp; velocity.y *= bounce;<br />&nbsp; &nbsp; }<br />&nbsp; &nbsp; else if(position.y - radius &lt;0.0) {<br />&nbsp; &nbsp; &nbsp; &nbsp; position.y = radius;<br />&nbsp; &nbsp; &nbsp; &nbsp; velocity.y *= bounce;<br />&nbsp; &nbsp; }	<br />}<br /><br />-(void)checkCollision:(NSMutableArray*)ballArray:(int)ballNumber:(int)i {<br />	<br />	for (int j=i+1; j&lt;ballNumber; j++) <br />		{<br />			Ball* ballj = [ballArray objectAtIndex:j]; //je récupère mes autres balles<br />			<br />			float dy = position.y - ballj.position.y;<br />			float dx = position.x - ballj.position.x;<br />	<br />			float r2 = 2*radius;<br />			<br />			if (((dy&lt;r2)&amp;&amp;(dy&gt;-r2))||((dx&lt;r2)&amp;&amp;(dx&gt;-r2)))<br />			{<br />				<br />				// distance^2 between ball i and ball j<br />				float d =dx*dx+dy*dy;<br />				<br />				// too close!<br />				if (d&lt;(r2*r2))<br />				{<br />					// There is &quot;real&quot; collision only if they approach each other<br />					if (((velocity.x*dx+velocity.y*dy)-(ballj.velocity.x*dx+ballj.velocity.y*dy))&lt;0)<br />					{<br />						<br />						float txj = (velocity.x*dx*dx + velocity.y*dx*dy + ballj.velocity.x*dy*dy - ballj.velocity.y*dx*dy)/d;<br />						float tyj = (velocity.x*dx*dy + velocity.y*dy*dy - ballj.velocity.x*dx*dy + ballj.velocity.y*dx*dx)/d;<br />						<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; //je change la vélocité de ma balle courante<br />						velocity.x= (ballj.velocity.x*dx*dx + ballj.velocity.y*dx*dy + velocity.x*dy*dy - velocity.y*dx*dy)/d; <br />						velocity.y= (ballj.velocity.x*dx*dy + ballj.velocity.y*dy*dy - velocity.x*dx*dy + velocity.y*dx*dx)/d;<br /><br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//et aussi celle de l&#39;autre balle responsable de la collision<br />						[ballj velocity:txj:tyj];<br />					}<br />				}<br />			}<br />		}<br />}<br /><br />


Je me suis beaucoup appuyée sur cet algo :
http://home.hkstar.com/~yyf/game1.html
EDIT : Et de ça aussi http://www.phy.ntnu.edu.tw/ntnujava/index.php?topic=4.0

Bon pour 3 ou 4 balles ça marche plutôt bien.
Mais alors pour une dizaine de balle voire plus, c'est nul de chez nul ! C'est hyper saccadé : à  peu près 2 rafraà®chissements de l'écran par seconde !!!
Donc bof bof ce que j'ai fais. Ca me sert pas à  grand chose si pour plus de 10 balles ça ne marche plus :( Snif j'étais plutôt contente de moi, mais vous m'aviez prévenu que ça allait pas être si simple !

Des idées ? Des suggestions ? Des pistes ?


Réponses

  • AliGatorAliGator Membre, Modérateur
    21:01 modifié #2
    Il me semble que l'on avait déjà  abordé la question, mais en fait, le problème n'est pas le calcul de collision en soi, mais l'optimisation et la décomposition du calcul en étapes de la moins complexe (et donc rapide) à  la plus complexe (et donc plus longue mais plus précise)

    L'idée est donc :
    1) de ne surtout pas comparer chaque balle avec chacune des autres balles pour tester leur collision, mais de commencer par éliminer rapidement les balles dont on est sûr qu'elles sont trop loin pour entrer en collision.
    2) De faire une comparaison rapide pour les balles proches avant d'envisager une comparaison plus fine

    Les diverses astuces auxquelles je pense sur le moment :

    1) Fonctionner avec des entiers plutôt que des flottants (les calculs sur les entiers sont beaucoup plus rapides)

    2) Découper le "plateau de jeu" en secteurs rectangulaires et garder d'un moyen ou d'un autre la liste des balles dans chaque secteur. Et comme ça tu peux ne tester que les collisions des balles dans le même "secteur" plutôt que toutes les balles du plateau. Bon il faut que les secteurs se superposent un peu pour les cas des balles à  la limite entre 2 secteurs. Mais ça limite le nombre de tests.
    On peut avoir le même principe aussi avec une organisation des balles en "graphe", mais c'est un peu plus compliqué à  gérer. Alors que savoir dans quel secteur rectangulaire se trouve la balle peut être fait rapidement (par exemple un décalage de bits genre x>>5 pour diviser super rapidement x par 2^5 = 32 si tes secteurs rectangulaires font 32 de large). Rien que ça ça élimine très rapidement des balles à  tester

    3) Pour les balles qui n'ont pas été éliminées par l'algo précédent, faire un test rapide, genre rectangle englobant : récupérer le rectangle englobant des 2 balles à  tester et vérifier si l'intersection de ces CGRects n'est pas nulle.

    4) Si l'intersection est nulle, pas la peine d'aller plus loin les balles ne se choqueront pas. Si elles le sont, alors tu peux faire un test plus fin, c'est à  dire là  enfin vérifier que la distance entre les 2 centres des balles est inférieure à  la somme de leurs rayons pour savoir si elles se touchent ou pas (et là  tu peux faire le calcul en floats et pas int)

    En tout cas ce qu'il ne faut pas faire c'est :
    1) tester toutes les balles directement avec les distances, genre sqrt((x2-x1)^2+(y2-y1)^2)<(r1+r2) car ça nécessite deux différences entre des floats + deux mises au carré (multiplications) + un calcul de racine carré... ce qui est énorme comme utilisation CPU
    2) Calculer la racine carré pour la distance! Il faut mieux tester que ((x2-x1)^2+(y2-y1)^2)<(r2-r1)^2 plutôt ! Beaucoup moins consommateur de ressources aussi.

    Mais déjà  imagine si tu as N balles et que tu testes chaque balle avec toutes les autres, ça fait N*(N-1) tests (en réalité normalement N*(N-1)/2 puisque si tu testes la collision entre A et B pas besoin de tester aussi la collision entre B et A)... mais vu comment les calculs entre les multiplications sont coûteuses...

    Imagine, tester les collisions sur 10 balles ça fait 10*9/2 = 45 tests. Sur 7 balles ça fait 7*6/2 = 21, sur 4 balles ça fait 4*3/2 = 6 balles... comme quoi ça descent très vite ! Donc ça vaut énormément le coup de faire des tests éliminatoires rapides avant !
  • MalaMala Membre, Modérateur
    21:01 modifié #3
    La première chose à  faire c'est de voir ce qui fait ralentir ton appli.  A dix contre un je pense que c'est parce que tu passes pas par OpenGL ES.

  • MalaMala Membre, Modérateur
    mars 2009 modifié #4
    dans 1236534791:

    Imagine, tester les collisions sur 10 balles ça fait 10*9/2 = 45 tests.

    Ca me semble pas la mer à  boire pour l'iPhone non?

    Edit:Voici ce que je tire de mon iPhone avec le simu de particules -> Benchmark iPhone
  • AliGatorAliGator Membre, Modérateur
    21:01 modifié #5
    Bah j'ai dit que 10 mais apparemment il compte monter à  bcp plus...
    Et surtout 45 calculs de sqrt((x2-x1)^2+(y2-y1)^2) contre 45 calculs de CGRectIntersect(rct1,rct2) la différence est pas mince surtout à  chaque boucle de composition...
  • MalaMala Membre, Modérateur
    21:01 modifié #6
    J'aimerais bien voir ce que dit Shark.

    Au niveau de la boucle de collision, il me semble y avoir beaucoup de calculs redondants aussi. Je suis pas sûr que GCC optimise tout ça au mieux. Après vérifier aussi que le mode Thumb est désactivé car pour ce genre de calcul il est important de basculer en ARM.
  • MalaMala Membre, Modérateur
    21:01 modifié #7
    dans 1236535286:

    Bah j'ai dit que 10 mais apparemment il compte monter à  bcp plus...
    Et surtout 45 calculs de sqrt((x2-x1)^2+(y2-y1)^2) contre 45 calculs de CGRectIntersect(rct1,rct2) la différence est pas mince surtout à  chaque boucle de composition...

    J'ai édité mon poste entre temps. Regardes le liens que j'ai ajouté à  ma réponse. Je monte à  20 img/s avec 384 particules sur mon simu en force brute.
  • schlumschlum Membre
    21:01 modifié #8
    Pour être vraiment optimisé, ce genre de calculs doit se faire "a priori"...
    On calcule mathématiquement précisément les trajectoires sur n secondes (analytiques... pour ce cas, ce sont des fonctions paramétriques affines par morceaux), puis pendant l'affichage, on calcule (dans un autre thread) les trajectoires pendant les secondes suivantes etc.
  • macounettemacounette Membre
    21:01 modifié #9
    J'ai pris un peu mon temps pour pouvoir essayer 2 ou trucs avant de poster ici.

    Suite à  vos conseils, j'ai découpé ma vue principale BallView (mon plateau de jeu) en case.
    J'ai créé un NSObject Case

    <br />@interface Case : NSObject {<br />	int numero;<br />	CGPoint position;<br />	NSMutableArray * iconArray; //les balles qui se trouvent dans ma case<br />}<br /><br />@interface BallView : UIView {<br />	NSMutableArray * ballArray; //un tableau contenant mes balles<br />	NSMutableArray * caseArray; // un tableau contenant mes cases<br />&nbsp; &nbsp; &nbsp; &nbsp; NSTimer * timer;<br />	int ballNumber;<br />} <br /><br />@interface Ball : NSObject&nbsp; {<br />	CGPoint position;<br />&nbsp; &nbsp; &nbsp; &nbsp; Case * maCase; //la case dans laquelle se trouve ma balle<br />}<br />
    


    Dans BallView mon timer appelle la méthode repaint :

    <br />- (void)repaint {<br />	<br />	BOOL inCase;<br />	for(Ball* ball in iconArray) { //Pour chaque balle dans mon jeu<br />		for(Case* cases in caseArray) { //Pour chaque case<br />			inCase=[cases fillWithBall:ball]; //Je vérifie si ma balle se trouve dans la case, si oui je l&#39;ajoute<br />		}<br />	}<br /><br />&nbsp; &nbsp; &nbsp; &nbsp; //à  ce moment du code, je sais dans quelle(s) case(s) sont mes balles.<br /><br />	for(Ball* ball in iconArray) { //Encore pour chaque balle<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; //ici je vérifie les collisions de ma ball seulement avec les autres balles voisines dans la case dans laquelle elle se trouve <br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for(Ball * ballj in ball.maCase.iconArray) {<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [ball checkCollision:ballj];<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }<br /><br />		[ball move]; //et après je bouge mes balles<br />	}<br />	<br />	for(Case* cases in caseArray) <br />		[cases removeIconArray]; //Je vide les cases, les balles ont bougé<br />		<br />	// force le rafraichissement de la vue<br />	// appellera la methode drawRect qui mettra à  jour le layer<br />	[self setNeedsDisplay];<br />}<br />
    


    Ouah ! vous avez vu le nombre de boucles que je fais pour y arriver ! Du coup même sans vérifier les collisions entre balles, mon animation sacade  :-\\ Et j'ai que 2 balles dans mon test. C'est embêtant...
    Je crois que je m'y suis pris comme un pied, mais je n'arrive pas à  limiter mes boucles... Ca commencer à  me rendre dingue cette histoire.

    Et pour info dans mon checkCollision

    <br />-(void)checkCollision:(Ball*)ballj {<br />	<br />		CGRect rect1 = CGRectMake(position.x, position.y, radius * 2.0, radius * 2.0);<br />		CGRect rect2 = CGRectMake(ballj.position.x, ballj.position.y , ballj.radius * 2.0, ballj.radius * 2.0);<br />		<br />		if (CGRectIntersectsRect(rect1, rect2) == 1) {<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; je calcule ma distance etc...<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }<br />}<br />
    





  • CéroceCéroce Membre, Modérateur
    21:01 modifié #10
    -(void)checkCollision:(Ball*)ballj {<br />	<br />		CGRect rect1 = CGRectMake(position.x, position.y, radius * 2.0, radius * 2.0);<br />		CGRect rect2 = CGRectMake(ballj.position.x, ballj.position.y , ballj.radius * 2.0, ballj.radius * 2.0);<br />		<br />		if (CGRectIntersectsRect(rect1, rect2) == 1) {<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; je calcule ma distance etc...<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }<br />}<br />
    


    Ta méthode est fausse.
    Le membre cgRect.origin représente le coin inférieur gauche du rectangle.
    Regarde la définition de CGRect pour plus d'infos.

    La formule valide est donc
    CGRect rect1 = CGRectMake(position.x - radius, position.y - radius, 2*radius, 2*radius);
    


    Note que ta méthode fonctionne tout de même (puisque l'erreur est la même pour toutes les balles).
Connectez-vous ou Inscrivez-vous pour répondre.