Partage
  • Partager sur Facebook
  • Partager sur Twitter

Mon jeu laggue énormément, mauvaise optimisation ?

Ou le langage ?

Sujet résolu
    17 mars 2018 à 1:25:52

    Je suis en train de développer un jeu et j'utilise le module pygame, pour avoir un aperçu rapide du gameplay : https://www.youtube.com/watch?v=5htHyoWdxHM

    Je jeu fonctionne très bien dans la vidéo, mais il peut ramer très facilement, si je lance un logiciel ou une tache assez gourmand en arrière plan je peut avoir une chute de 10 voir 30-40 fps ! (je jeu est configuré pour 60 fps max)

    Et cela dépend beaucoup du nombre d'objets dans le niveau aussi, pour de meilleures collisions et éviter certains bugs le programme découpe le vecteur de mouvement du joueur et vérifie les colisions pixels par pixels avec tout les objets de la map (qui ne sont que des rectangles et des cercles), tout ça pour chaque frames. Donc plus j'avance vite pour le temps de calculs est long.

    Il y a aussi les objets tourelles, qui doivent effectuer une simulation instantanée toutes les dixièmes de secondes d'un "faux" projectile pour tester si il y a collision avec le joueur ou si il y a un obstacle, j'utilise la même fonction de découpage des pixels pour la simulation mais par tranche de quatre cette fois

    Voici le code, je n'ai mis que ce qui peut avoir un rapport avec ces lags: 

    Ma fonction de découpage de vecteur (vecteur x, vecteur y, range) :

    def preciseMotion(x,y,pxRange):
    
    	array = []
    	if math.fabs(x) > math.fabs(y):
    		for nb in range(0,int(math.fabs(x)),pxRange):
    			if x > 0:
    				array.append([pxRange,0])
    			else:
    				array.append([-pxRange,0])
    		for nb in range(0,int(math.fabs(y)),pxRange):
    			if y > 0:
    				array[int(nb/y*x/pxRange)][1] = pxRange
    			else:
    				array[int(nb/y*x/pxRange)][1] = -pxRange
    	else:
    		for nb in range(0,int(math.fabs(y)),pxRange):
    			if y > 0:
    				array.append([0,pxRange])
    			else:
    				array.append([0,-pxRange])
    		for nb in range(0,int(math.fabs(x)),pxRange):
    			if x > 0:
    				array[int(nb/x*y/pxRange)][0] = pxRange
    			else:
    				array[int(nb/x*y/pxRange)][0] = -pxRange
    	for pixels in array:
    		yield pixels

    Fonction de collision entre un cercle (joueur ou projectile) et un rectangle (obstacle) :

    def circleRect_collision(rect,r,center): ##
    
    	circle_distance_x = abs(center[0]-rect.centerx)
    	circle_distance_y = abs(center[1]-rect.centery)
    	if circle_distance_x > rect.w/2.0+r or circle_distance_y > rect.h/2.0+r:
    		return False
    	if circle_distance_x <= rect.w/2.0 or circle_distance_y <= rect.h/2.0:
    		return True
    	corner_x = circle_distance_x-rect.w/2.0
    	corner_y = circle_distance_y-rect.h/2.0
    	corner_distance_sq = corner_x**2.0 +corner_y**2.0
    	return corner_distance_sq <= r**2.0

    Fonction de collision entre deux cercles :

    def collideCircle(r1,r2):
    
        radii = (r1.w + r2.w) / 2
        squared_distance = (r1.centerx - r2.centerx) ** 2 + (r1.centery - r2.centery) ** 2
        return squared_distance <= radii ** 2

    Fonction collide des objets :

    	def collide(self,rect):
    
    		if self.shape == 0:
    			return fct.circleRect_collision(self.hitbox,rect.w/2,rect.center)
    		elif self.shape == 1:
    			return fct.collideCircle(self.rect,rect)

    Déplacement du joueur :

    		deplaced = 0
    		if fct.distancePoint(self.rect.center,activeGame.mousePos) > 20 and activeGame.unlock:
    			newRect = self.rect
    			for px in fct.preciseMotion(MooveCoord[0],MooveCoord[1],1):
    				prevRect = newRect
    				newRect = newRect.move(px[0],0)
    				for obstacle in activeGame.objects:
    					if obstacle.collide(newRect):
    						if obstacle.solid:
    							newRect = prevRect
    							if obstacle.danger:
    								pygame.mixer.Channel(1).play(medias["playerDeath"])
    								activeGame.end = "loose"
    						elif obstacle.danger:
    							pygame.mixer.Channel(1).play(medias["playerDeath"])
    							activeGame.end = "loose"
    						break
    
    				prevRect = newRect
    				newRect = newRect.move(0,px[1])
    				for obstacle in activeGame.objects:
    					if obstacle.collide(newRect):
    						if obstacle.solid:
    							newRect = prevRect
    							if obstacle.danger:
    								pygame.mixer.Channel(1).play(medias["playerDeath"])
    								activeGame.end = "loose"
    						elif obstacle.danger:
    							pygame.mixer.Channel(1).play(medias["playerDeath"])
    							activeGame.end = "loose"
    						break
    			deplaced = fct.distancePoint(self.rect.center,newRect.center)
    			self.rect = newRect

    (oui je sais j'ai mis deux fois exactement le même code pour la mort du joueur)

    Et enfin la simulation de tir des tourelles :

    				if self.simulationWait <= 0:
    					self.simulationWait = 10
    					simulation = list(self.rect.center)
    					projId = projectile_models[self.proj]["n"]
    					simRect = res.images["projectiles"][projId].get_rect(center=simulation)
    					simRect = simRect.inflate(2,2)
    					motion = fct.get_motion(self.angle,fct.distancePoint(self.rect,activeGame.player.rect.center))
    					self.activated = True
    					stopSim = False
    					for px in fct.preciseMotion(motion[0],motion[1],4):
    						if fct.collideCircle(simRect,activeGame.player.rect):
    							self.activated = True
    							stopSim = True
    						for obstacle in activeGame.solids:
    							if obstacle.collide(simRect) and not obstacle == obj:
    								self.activated = False
    								stopSim = True
    								break
    						simulation[0] += px[0]
    						simulation[1] += px[1]
    						simRect.center = simulation
    						if stopSim:
    							break
    				else:
    					self.simulationWait -= 1


    Je pense que c'est à peut près tout qui pourrait concerner ce problème, j'ai une intel I5 soit une puissance de calcul moyenne et ça me décevrait que mes utilisateurs n'aient pas la puissance pour faire tourner ce petit jeu...

    Après quand je me rend compte que j'arrive à faire tourner des assez bons jeux en 3D sans problème, je me demance si ce ne serait pas le choix de ce langage qui soit le principal problème

    • Partager sur Facebook
    • Partager sur Twitter
      17 mars 2018 à 11:53:07

      Salut,

      Je suis en ce moment sur mobile, donc je ne peux pas trop élaborer. 

      Si tu as un problème de performances, il ne faut pas tenter de deviner d’où ça provient. Il faut le mesurer ! Je te conseille dès lors de profiler ton code afin de comprendre quelles sont les fonctions dans lesquelles se trouvent les éventuelles goulots d’etranglement. 

      Si tu ne sais pas ce que veut dire profiler, il faut que tu lises un tutoriel sur le sujet. Ensuite fait des petits tests dans un code pas trop compliqué. Une fois à l’aise, profile ton jeu et tu comprendras où se trouvent les problèmes. 

      Certes Python n’est pas hyper performant, mais c’est plus souvent un problème de conception que de language. Tu ne fais pas non plus le prochain GTA en Python. ;) 

      • Partager sur Facebook
      • Partager sur Twitter
        18 mars 2018 à 1:02:44

        Dan737 a écrit:

        Salut,

        Je suis en ce moment sur mobile, donc je ne peux pas trop élaborer. 

        Si tu as un problème de performances, il ne faut pas tenter de deviner d’où ça provient. Il faut le mesurer ! Je te conseille dès lors de profiler ton code afin de comprendre quelles sont les fonctions dans lesquelles se trouvent les éventuelles goulots d’etranglement. 

        Si tu ne sais pas ce que veut dire profiler, il faut que tu lises un tutoriel sur le sujet. Ensuite fait des petits tests dans un code pas trop compliqué. Une fois à l’aise, profile ton jeu et tu comprendras où se trouvent les problèmes. 

        Certes Python n’est pas hyper performant, mais c’est plus souvent un problème de conception que de language. Tu ne fais pas non plus le prochain GTA en Python. ;) 


        Merci, je ne conaissait pas le profilage, j'ai fait une analyse avec cProfile de ma fonction d'update du jeu

        C'est bien ce que je pensais, ce sont toutes les détections de collisions qui font lagger le jeu, mais aussi les trainées de pixels opaques derrière les projecticles (ça encore ça peut être paramétré par l'utilisateur)

        J'ai déjà réussis à bien optimiser le mouvement du joueur, j'ai refait toute la fonction, maintenant il reste principalement les collisions des projectiles et la simulation (qui est un peut la même chose)

        -
        Edité par ReFlix 18 mars 2018 à 1:02:54

        • Partager sur Facebook
        • Partager sur Twitter
          18 mars 2018 à 11:23:15

          Salut,

          Il existe aussi le module pstats qui permet d'analyser un peu plus facilement les résultats retournés par cProfile. Pourrais-tu mettre ici le rapport de pstats ?

          Si ce sont les détections de collisions qui sont responsables du ralentissement du jeu, il ne faut pas sauter directement sur ces fonctions mais se demander qui les appelle ? Généralement on découvre que l'algorithme utilisé pour la détection des collisions est alors non-optimale, ce qui rend le jeu lent.

          clemozoir a écrit:

          Et cela dépend beaucoup du nombre d'objets dans le niveau aussi, pour de meilleures collisions et éviter certains bugs le programme découpe le vecteur de mouvement du joueur et vérifie les colisions pixels par pixels avec tout les objets de la map (qui ne sont que des rectangles et des cercles), tout ça pour chaque frames. Donc plus j'avance vite pour le temps de calculs est long.

           J'ai pas tout compris, mais ça semble en effet être quelque chose de bien compliqué... On ne vérifie pas les collisions pixel par pixel, sans quoi tu ne jouerais jamais à Call of Duty ! On utilise (pour la 2D) des formes géométriques simples, tels des cercles et des rectangles. Il semblerait toutefois que tu les utilises, donc je ne comprends pas ce que cette détection de collision pixel par pixel vient faire ici...

          Pour l'histoire de vecteur de mouvement et éviter certains bugs, je me demande si tu n'as pas justement un problème dans ta manière de permettre à ton joueur de longer les obstacles sans entrer dedans. J'ai dans ma signature toute une explication sur la gestion des collisions.

          Comme je l'ai dit plus haut, je ne comprends rien à preciseMotion. C'est généralement une mauvaise chose car tu ne sais pas recevoir de l'aide si on ne comprends pas ce que tu as fait. D'où l'intérêt des docstrings !

          circleRect_collision est compliquée à comprendre. Je ne comprends déjà pas à quoi correspondent les arguments. Je m'attends à recevoir un rectangle et un cercle, mais j'ai trois arguments ! Et de nouveau aucun docstring pour m'éclairer. :( L'algorithme de collision entre un cercle de centre \(C (c_x, c_y)\) et de rayon \(R\) et un rectangle de centre \(A (a_x, a_y)\) et de dimension \((h_x, h_y)\) pour la largeur et la hauteur est le suivant. On crée le vecteur \(\vec{V}\) qui va du centre du rect au centre du cercle. \(\vec{V} = (c_x-a_x, c_y-a_y) = (v_x, v_y)\). On contraint \(\vec{V}\) à rester entre les valeurs \((-h_x, -h_y) et (h_x, h_y)\) afin d'obtenir un vecteur \(\vec{V}\) qui pointe vers le bord du rectangle le plus proche du centre du cercle. Appelons ce nouveau vecteur \(\vec{P}\). Donc \(\vec{P} = ( max(min(v_x, h_x), -h_x), max(min(v_y, h_y), -h_y) ) = (p_x, p_y)\). Maintenant il ne reste plus qu'à vérifier que la distance entre ce point \(P\) et le centre du cercle est inférieur au rayon du cercle pour qu'il y ait collision. Si \(R^2 < (p_x - c_x)^2 + (p_y - c_y)^2\), alors il y a collision.

          J'ai été regardé brièvement tes vidéos. J'ai vu dans la toute première pas mal de choses qui pourraient aussi être la cause de ces ralentissements comme par exemple dans la fonction animatePlayer où tu as un test du style if self.mousePos[0] in range(self.player_R.centerx-15, self.player_R.centery+15). Cette fonction a peut-être changée mais là en l'état c'est horriblement inefficace. Python va devoir tester chaque valeur du range pour voir si mousePos s'y trouve. Alors qu'une simple inégalité est faite directment if self.player_R.centerx-15 < self.mousePos[0] < self.player_R.centery+15.

          clemozoir a écrit:

          Après quand je me rend compte que j'arrive à faire tourner des assez bons jeux en 3D sans problème, je me demance si ce ne serait pas le choix de ce langage qui soit le principal problème

           Avec les même algorithmes, mais écrit en C++, tu aurais aussi des problèmes dans ton jeu. Donc ne désespère pas. Ce n'est pas le langage choisi le problème dans ton cas.

          -
          Edité par Dan737 18 mars 2018 à 11:29:11

          • Partager sur Facebook
          • Partager sur Twitter
            19 mars 2018 à 17:58:21

            Encore merci de ta réponse
            Alors dans mon expliquation de la collision pixels par pixels, ce n'est pas un mask, en fait imaginons à un vecteur de mouvement de 4;-2 pixels, d'abord la fonction precise motion va découper ce vecteur et va parcourir dans la boucle : [1:-1] [1:0] [1:-1] [1:0]
            Ensuite, dans boucle de déplacement va déplacer le joueur pixel par pixels en x et y, à chaque déplacement cela va tester si il rentre en collision avec n'importe quel objet de la map, si il y a une collision, le joueur reprend son ancienne place.
            Avant cette méthode faisait beaucoup ralentir le jeu, maintenant j'ai réussit à bien optimiser en faisant une prévisualisation des objets en collision à la fin du trajet, puis déplacer pixel par pixel en ne vérifiant la collision que pour ces objets là
            J'ai pris la fonction circleRect sur internet, je n'ai pas tout compris non plus mais les trois arguments sont le rectangle, le radius du cercle et le centre du cercle, les objets "cercle" n'existe pas dans mon programme
            Tout le code avant la version 0.4 est très différent de la version actuelle, j'ai tout restructuré car j'étais parti sur de mauvaises base, j'ai tout refait à zeroa. Dansmon code actuel, il n'y a pas de fonction animatePlayer mais c'est un sprite player qui va être update, et la ligne de code que tu désigne n'existe plus, j'ai fais plutôt avec la distance
            Je ne vais pas baisser les bras sur mon jeu, je vais déjà chercher un nouveau système d'optimisation des projectiles et de simulations
            Edit Voici les résultats des tests :

            Voici le profilage que j'ai effectué dans cette situation là : 

            Les visuels A sont ceux au premiers plans qui ne sont pas présent ici, les viuels B sont toutes les particules

            Alors bien évidemment c'est la méthode update des sprites qui prend le plus de temps, et surtout les projectiles comme je le pensais

            Mais aussi tout ce qui est création des particules, la création du sprite et le vecteur de mouvement généré aléatoirement de la particule, encore ça ce n'est pas trop grave je vais permettre plus tard à l'utilisateur de choisir ses niveaux de graphismes

            Maintenant avec le joueur à l'abrit des tourelles, mais les tourelles devront quand même effectuer la simulation (je ne vais pas remettre un screen du jeu c'est la même map, juste les tourelles qui ne tirent pas) :

            Là ce sont justement les simulations (map object update) qui prennent plus de temps, car elles durent jusqu'a qu'elles recontrent le joueur ou un obstacle, dans l'autre situation le joueur était plus proche et la simulation s'arrêtait tôt

            Donc en conclusion il faut que je trouve un nouvel algo à utiliser pour les simulation et les projectiles

            -
            Edité par ReFlix 19 mars 2018 à 23:37:43

            • Partager sur Facebook
            • Partager sur Twitter
              20 mars 2018 à 10:34:22

              Salut,

              Bravo pour tout ce travail de profilage ! Maintenant on peut parler concrètement, sans se lancer dans d'hypothétiques gains de performance.

              Tu as correctement identifié le problème. Et en effet ton algorithme d'update des projectiles est trop gourmand. Cependant sans voir tout ton code, c'est difficile pour moi de pointer avec exactitude sur ce qui coince.

              Ta description de comment tu gères les collisions me paraît terriblement coûteuse. Normalement on ne fait que détecter les collisions pour une nouvelle frame avec la position finale de l'objet, pas pixel par pixel. S'il y a collision, alors on fait un calcul plus coûteux, à savoir quelle est la distance de pénétration pour pouvoir ressortir l'objet et qu'il soit juste contre l'objet avec lequel il était entré en collision. Dans ma signature, tu trouveras tout un article sur la gestion des collisions avec Pygame. Je ne vais donc pas tout répéter. :) Cette technique cependant a une limitation. Si entre 2 frames, un projectile a une telle vitesse que sa nouvelle position est par delà un obstacle, alors on ne peut pas détecter la collision. Autrement dit, si le plus petit obstacle fait 20 px et que le projectile a une vitesse supérieure à 20 px, il se pourrait qu'à la frame N-1 il soit juste à côté de l'obstacle et à la frame N il soit de l'autre côté de l'obstacle. Mais ces cas sont normalement assez rare, pour peu que tes obstacles ne soient pas minuscules et que ton frame rate soit suffisamment élevé pour que le déplacement en 1 frame ne soit que de quelques pixels au maximum.

              Aussi le nombre de détections pour les collisions augmente en \(O(n^2)\). C'est à dire que si tu as 2 fois plus d'objets, tu auras 4 fois plus de tests à faire. Une technique est déjà de ne pas tout tester. Par exemple les projectiles ne doivent pas tester les collisions entre eux. Une autre technique est t'utiliser un partitionnement spatial. C'est à dire que par exemple tu pourrais diviser l'espace du jeu (ou de l'écran) en plusieurs grandes cases. Seuls les obstacles dans la même case que le projectile doivent être considérés. On peut ainsi ignorer des objets plus distants. Tu peux aussi regarder du côté des QuadTree qui est une autre manière plus efficace de partitionner l'espace.

              En regardant tes stats, as-tu remarqué que pygame\sprite.py:303(sprite) prenait 0.024 sec ? En allant voir le code, cette méthode est horriblement inefficace puisqu'elle retourne une liste des clé de spritedict. Depuis Python 3, il serait beaucoup plus efficace de retourner un itérateur. Ici à chaque appel de sprites(), Python va créer une liste en mémoire, la remplir avec les références vers les sprites du groupe, et puis une fois que l'itération est terminée, la liste va être détruite. C'est vachement inutile, alors que tout ce qu'on veut faire c'est itérer sur ces clés... Cette méthode est utilisée dans la méthode AbstractGroup.update(). Donc perso je remplacerais cette méthode update avec un simple

              for s in self.spritedict:
                  s.update(*args)

              Le problème avec ton dernier profilage est qu'il n'a duré que 0.001 sec. Donc il ne montre rien d'intéressant. Il faudrait le laisser tourner au moins 1 seconde pour y voir plus clair.

              Pour gérer les tours, on utilise normalement du ray casting. C'est à dire qu'on trace un trait dans la géométrie représentant les entités du jeu, et on détermine s'il y a une collision. Ca se fait avec du calcul vectoriel. J'ai déjà écrit une tartine, donc je n'ai plus le courage de tout expliqué. Mais je te mets un code de démo que j'avais fait à l'époque et qui montre le point jaune comme étant la première collision possible avec un obstacle.

              import pygame
              
              def dot(v1, v2):
                  "Dot product of vectors v1 and v2"
                  x1, y1 = v1
                  x2, y2 = v2
                  return x1*x2 + y1*y2
              
              def raycast_rect(rect, orig, dest):
                  """Calculate the point of intersection between ray and rect.
              
                  Ray start in `orig` and stops at `dest`. `rect` is the obstacle.
                  Args:
                      rect (pygame.Rect): obstacle
                      orig (tuple[float, float]): point of origin of the ray.
                      dest (tuple[float, float]): point of destination of the ray.
                  
                  Returns:
                      float or None: The percentage along the ray where the collision happens.
                                     So the return value is contrained between [0.0, 1.0].
                                     If there is no collision, returns None.
                  """
                  vertices, normals = ((rect.bottomleft, rect.bottomright, rect.topright, rect.topleft),
                                      ((0, 1), (1, 0), (0, -1), (-1, 0)))
                  lower, upper = 0.0, 1.0
                  x0, y0 = orig
                  x1, y1 = dest
                  ray = (x1-x0, y1-y0)
                  intersection = False
              
                  for vertice, normal in zip(vertices, normals):
                      orig_to_vertice = (vertice[0]-x0, vertice[1]-y0)
                      n = normal
              
                      numerator = dot(n, orig_to_vertice)
                      denominator = dot(n, ray)
                      if denominator == 0.0:
                          if numerator < 0.:
                              return None
                      else:
                          if denominator < 0. and numerator < lower * denominator:
                              intersection = True
                              lower = numerator / denominator
                          elif denominator > 0. and numerator < upper * denominator:
                              upper = numerator / denominator
              
                      if upper < lower:
                          return None
              
                  if intersection:
                      return lower
                  return None
              
              def raycast(rects, orig, dest):
                  """Raycast from orig to dest in the sequence of rects.
              
                  Returns:
                      float or None: the percentage of the ray where the closest collision happens.
                                     If no collisions, returns None.
                  """
                  fractions = (raycast_rect(rect, orig, dest) for rect in rects)
                  fraction = min((fraction for fraction in fractions if fraction is not None), default=None) # python 3.4
                  # Use the following if python version < 3.4
                  # try:
                  #     fraction = min(fraction for fraction in fractions if fraction is not None)
                  # except ValueError: # min on empty list
                  #     return None
              
                  return fraction
              
              if __name__ == '__main__':
                  "Demo of raycasting"
                  from pygame.locals import *
                  import math
                  import random
              
                  pygame.init()
                  taille_fenetre = (800, 600)
                  fenetre_rect = pygame.Rect((0, 0), taille_fenetre)
                  screen_surface = pygame.display.set_mode(taille_fenetre)
                  timer = pygame.time.Clock()
              
                  rects = [pygame.Rect(i*200, j*200, 100, 100) 
                           for i in range(4) 
                              for j in range(3) 
                                  if not (i == 2 and j == 1)]
              
                  angle = 0
                  orig = (450, 250)
                  dest = (450 + 200 * math.cos(angle), 250 + 200 * math.sin(angle))
              
                  continuer = True
                  while continuer:
                      for event in pygame.event.get():
                          if event.type == QUIT:
                              continuer = False
              
                      timer.tick(30)
                      angle += 0.01
                      angle = angle % (2 * math.pi)
                      dest = (400 + 400 * math.cos(angle), 300 + 400 * math.sin(angle))
              
                      fraction = raycast(rects, orig, dest)
              
                      screen_surface.fill(0)
                      for rect in rects:
                          pygame.draw.rect(screen_surface, (255, 0, 0), rect, 1)
                      pygame.draw.line(screen_surface, (255, 0, 0), orig, dest)
                      
                      if fraction:
                          xo, yo = orig
                          xd, yd = dest
                          intersection = (int(xo + fraction * (xd-xo)), int(yo + fraction * (yd-yo)))
                          pygame.draw.circle(screen_surface, (255, 255, 0), intersection, 4)
              
                      pygame.display.flip()
                  pygame.quit()

              Quand j'aurai trouvé un peu de courage, je repasserai expliquer le raycasting. :)

              EDIT : j'ai oublié de demander : pour la partie graphique, tu utilises bien pygame.display.update(rects) en passant la liste des dirty rects ? Car si tu blit tout l'écran, c'est aussi une perte de perf. Pour ne plus te tracasser de la gourmandise graphique, passer sur pyglet permettrait d'utiliser OpenGL et donc d'alléger le CPU.

              -
              Edité par Dan737 20 mars 2018 à 10:36:56

              • Partager sur Facebook
              • Partager sur Twitter
                20 mars 2018 à 19:15:12

                Salut, tout d'abord merci beaucoup pour ces fonctions du ray casting ! :) (j'avais pensé à utiliser une méthode comme ça au codage des tourelles mais je suis pas très très bon en maths)

                Par rapport à l'algo de collision que tu as présenté, je l'avais déjà utilisé auparavant, mais cela causait des bugs comme un que j'ai schématisé : 

                Le nouvel algo ne fait casi aucun bug, en fait je fais d'abord une prévisualisation des obstacles solides qui entreraient en collision avec le joueur pour celui déplacé en x seulement et déplacé en y seulement (pour être sûr de ne rien rater). Les obstacles concernés sont mis dans une liste et je teste les collision pixels pas pixels uniquement sur cette liste (qui ne comporte que 2-3 sprites max). Je ne fais pas la même chose pour les collisions avec les projectiles et les obstacles de type danger, le déplacement du joueur est le seul cas dans le programme où j'utilise cette méthode.

                Je sais très bien que le nombre de collisions à tester est énorme avec le nombre de projectiles, mais je pensais naïvement que mon pc aurait pu me calculer tout ça facilement :/

                J'ai plusieurs groupes de sprites pour les blocs de la map :

                - object : Tout les blocks visibles de la map

                - solids : Certains groupes de objects peuvent aussi appartenir, les test de collisions ne prennent que ce groupe

                - dangers : Les blocs qui kill le joueur (solides ou non)

                - projectiles : Tout types de projectiles

                - dangerProjectiles : Les projectiles dangereux au joueur

                - visuals : Effets visuels d'arrière-plans (principalement les particules)

                - forefront_visuals : Effets visuels de premier plan (rares)

                Les groupes sont utilisés distinctement, je pense que c'est bon là-dessus, ensuite je vais utiliser tes deux fonctions pour le ray casting des tourelles mais je pense aussi les utiliser à la création de projectiles, puisqu'ils ne changeront pas de direction, je n'ai qu'à faire qu'ils se détruisent au point d'intersection du ray cast (et si c'est un bloc cassable détruit avant, je fais un alive() et je recalcule si il n'est plus là)

                Dan737 a écrit:

                En regardant tes stats, as-tu remarqué que pygame\sprite.py:303(sprite) prenait 0.024 sec ? En allant voir le code, cette méthode est horriblement inefficace puisqu'elle retourne une liste des clé de spritedict. Depuis Python 3, il serait beaucoup plus efficace de retourner un itérateur. Ici à chaque appel de sprites(), Python va créer une liste en mémoire, la remplir avec les références vers les sprites du groupe, et puis une fois que l'itération est terminée, la liste va être détruite. C'est vachement inutile, alors que tout ce qu'on veut faire c'est itérer sur ces clés... Cette méthode est utilisée dans la méthode AbstractGroup.update(). Donc perso je remplacerais cette méthode update avec un simple

                for s in self.spritedict:
                    s.update(*args)

                J'ai pas trop compris, la méthode add des sprites créent des valeurs inutiles ? Après je ne m'est suis pas trop documenté sur les abstractGroup

                Dan737 a écrit:

                EDIT : j'ai oublié de demander : pour la partie graphique, tu utilises bien pygame.display.update(rects) en passant la liste des dirty rects ? Car si tu blit tout l'écran, c'est aussi une perte de perf. Pour ne plus te tracasser de la gourmandise graphique, passer sur pyglet permettrait d'utiliser OpenGL et donc d'alléger le CPU.


                J'ai pas vu non plus les dirty rects, les objets visuals sont des sprites "normaux" qui sont update et blit comme les autres, faudrait peut-être que je me mette à une documentation plus profonde de pygame...

                -
                Edité par ReFlix 20 mars 2018 à 21:14:11

                • Partager sur Facebook
                • Partager sur Twitter
                  21 mars 2018 à 9:42:50

                  Salut,

                  clemozoir a écrit:

                  Salut, tout d'abord merci beaucoup pour ces fonctions du ray casting ! :) (j'avais pensé à utiliser une méthode comme ça au codage des tourelles mais je suis pas très très bon en maths)

                  Malheureusement tu vas avoir besoin de faire des maths si tu veux faire des jeux. :) Mais c'est fun, une fois qu'on comprend mieux ce qu'on manipule. Tu devrais essayer de regarder quelques vidéos (Khan academy peut-être) sur la trigonométrie et le calcul vectoriel.

                  clemozoir a écrit:

                  Par rapport à l'algo de collision que tu as présenté, je l'avais déjà utilisé auparavant, mais cela causait des bugs comme un que j'ai schématisé : 

                  Le nouvel algo ne fait casi aucun bug, en fait je fais d'abord une prévisualisation des obstacles solides qui entreraient en collision avec le joueur pour celui déplacé en x seulement et déplacé en y seulement (pour être sûr de ne rien rater). Les obstacles concernés sont mis dans une liste et je teste les collision pixels pas pixels uniquement sur cette liste (qui ne comporte que 2-3 sprites max). Je ne fais pas la même chose pour les collisions avec les projectiles et les obstacles de type danger, le déplacement du joueur est le seul cas dans le programme où j'utilise cette méthode.

                   Je suis un peu étonné que le joueur se déplace d'une telle distance en une seule frame! Quel est ton FPS ? Quel est la distance max parcourue en une frame ?

                  Sinon si en effet tu as besoin de traiter de tels cas, perso j'essaierais quelque chose de moins coûteux. Comme tu le montres sur ton schéma, le joueur est repoussé dans l'obstacle du bas. Il suffirait d'avoir une boucle while qui après avoir sorti le joueur de son obstacle re-vérifie les collisions. Ceci le ressortirait de l'obstacle du bas. Donc au final ça ne coûterait qu'un calcul de 2 gestions de collisions, peut-être 3 dans le pire des cas. Hors avec ton système, si le joueur est à 5 px d'une collision, tu vas vérifier les collisions 5 fois. Bon d'un autre côté, cette fonction n'apparaissait pas dans tes stats de profiling, donc je n'y passerais pas trop de temps.

                  clemozoir a écrit:

                  [...]

                  Dan737 a écrit:

                  En regardant tes stats, as-tu remarqué que pygame\sprite.py:303(sprite) prenait 0.024 sec ? En allant voir le code, cette méthode est horriblement inefficace puisqu'elle retourne une liste des clé de spritedict. Depuis Python 3, il serait beaucoup plus efficace de retourner un itérateur. Ici à chaque appel de sprites(), Python va créer une liste en mémoire, la remplir avec les références vers les sprites du groupe, et puis une fois que l'itération est terminée, la liste va être détruite. C'est vachement inutile, alors que tout ce qu'on veut faire c'est itérer sur ces clés... Cette méthode est utilisée dans la méthode AbstractGroup.update(). Donc perso je remplacerais cette méthode update avec un simple

                  for s in self.spritedict:
                      s.update(*args)

                  J'ai pas trop compris, la méthode add des sprites créent des valeurs inutiles ? Après je ne m'est suis pas trop documenté sur les abstractGroup

                   Lorsque tu appelles mon_groupe.update(dt),  il va appeler la fonction AbstractGroup.update() qui n'est pas très optimisée puisqu'elle va créer une liste en mémoire (inutile) en recopiant les références vers les sprites du groupes. Le mieux est que tu aies regardé toi-même le code dans pygame. Je t'ai donné le nom du fichier et le numéro de ligne.

                  clemozoir a écrit:

                  Dan737 a écrit:

                  EDIT : j'ai oublié de demander : pour la partie graphique, tu utilises bien pygame.display.update(rects) en passant la liste des dirty rects ? Car si tu blit tout l'écran, c'est aussi une perte de perf. Pour ne plus te tracasser de la gourmandise graphique, passer sur pyglet permettrait d'utiliser OpenGL et donc d'alléger le CPU.


                  J'ai pas vu non plus les dirty rects, les objets visuals sont des sprites "normaux" qui sont update et blit comme les autres, faudrait peut-être que je me mette à une documentation plus profonde de pygame...

                  Ca me paraîtrait une bonne idée de se documenter sérieusement si tu espères obtenir des résultats satisfaisants. :p



                  • Partager sur Facebook
                  • Partager sur Twitter
                    23 mars 2018 à 15:58:08

                    Salut, j'ai implanté le ray casting dans le programme et ça marche parfaitement merci !

                    (Le seul petit souci c'est que ça ne marche pas avec les obstacles circulaires, mais en utilisant un rect à la place la différence n'est pas trop visible)

                    Maintenant il ne reste plus qu'à optimiser les projectiles et particules, j'avais cours cette semaine donc je ne n'ai pas encore eu le temps de me documenter sur tout les modules de pygame, mais je vais le faire.

                    Le shéma que j'ai montré n'est pas vraiment à l'échelle, le joueur ne peut se déplacer qu'à 9 pixels/frames au max, et je tourne en 60 fps, mais ce bug peut arriver, je préfère garder l'algo actuel car je n'ai aucun problème avec celui-ci.

                    J'ai aussi amélioré mon système de profilage, je peut choisir le nombre de frames à analyser donc si tu voulais plus de détails je peut t'en donner si tu veut.

                    • Partager sur Facebook
                    • Partager sur Twitter
                      23 mars 2018 à 17:46:41

                      Salut,

                      Content que le raycasting fonctionne bien. En effet les fonctions n'étaient prévues que pour des rectangles. J'ai un peu bidouiller pour avoir un ray casting avec ces cercles. Voici une petite démo. Bien entendu, la fonction raycasting qui prend les listes de rects et circles peut être modifiées selon tes besoins. Ce serait plus propre que ce soit les formes qui aille l'information de leur type (rect, cercle) et que raycasting dispatch à la bonne fonction selon le type.

                      from itertools import chain
                      import pygame
                      
                      
                      def dot(v1, v2):
                          "Dot product of vectors v1 and v2"
                          x1, y1 = v1
                          x2, y2 = v2
                          return x1 * x2 + y1 * y2
                      
                      
                      def raycast_rect(rect, orig, dest):
                          """Calculate the point of intersection between ray and rect.
                      
                          Ray start in `orig` and stops at `dest`. `rect` is the obstacle.
                          Args:
                              rect (pygame.Rect): obstacle
                              orig (tuple[float, float]): point of origin of the ray.
                              dest (tuple[float, float]): point of destination of the ray.
                      
                          Returns:
                              float or None: The percentage along the ray where the collision happens.
                                             So the return value is contrained between [0.0, 1.0].
                                             If there is no collision, returns None.
                          """
                          vertices, normals = ((rect.bottomleft, rect.bottomright, rect.topright, rect.topleft),
                                              ((0, 1), (1, 0), (0, -1), (-1, 0)))
                          lower, upper = 0.0, 1.0
                          x0, y0 = orig
                          x1, y1 = dest
                          ray = (x1-x0, y1-y0)
                          intersection = False
                       
                          for vertice, normal in zip(vertices, normals):
                              orig_to_vertice = (vertice[0]-x0, vertice[1]-y0)
                              n = normal
                       
                              numerator = dot(n, orig_to_vertice)
                              denominator = dot(n, ray)
                              if denominator == 0.0:
                                  if numerator < 0.:
                                      return None
                              else:
                                  if denominator < 0. and numerator < lower * denominator:
                                      intersection = True
                                      lower = numerator / denominator
                                  elif denominator > 0. and numerator < upper * denominator:
                                      upper = numerator / denominator
                       
                              if upper < lower:
                                  return None
                       
                          if intersection:
                              return lower
                          return None
                      
                      
                      def raycast_circle(circle, orig, dest):
                          """Calculate the point of intersection between ray and circle.
                       
                          Ray start in `orig` and stops at `dest`. `circle` is the obstacle.
                          Args:
                              circle (tuple[float, float, float]): x, y and r for the obstacle.
                              orig (tuple[float, float]): point of origin of the ray.
                              dest (tuple[float, float]): point of destination of the ray.
                           
                          Returns:
                              float or None: The percentage along the ray where the collision happens.
                                             So the return value is contrained between [0.0, 1.0].
                                             If there is no collision, returns None.
                          """
                          x0, y0 = orig
                          x1, y1 = dest
                          h, k, r = circle
                          a = (x1 - x0)**2 + (y1 - y0)**2
                          b = 2 * (x1 - x0) * (x0 - h) + 2 * (y1 - y0) * (y0 - k)
                          c = (x0 - h)**2 + (y0 - k)**2 - r**2
                          discriminant = b**2 - 4 * a * c
                          if discriminant < 0.0:
                              return None
                          t = 2 * c / (-b + discriminant**0.5)
                          if not 0 < t < 1:
                              return None
                          return t
                      
                      
                      def raycast(rects, circles, orig, dest):
                          """Raycast from orig to dest in the sequence of rects and circles.
                       
                          Returns:
                              float or None: the percentage of the ray where the closest collision happens.
                                             If no collisions, returns None.
                          """
                          fractions_r = (raycast_rect(rect, orig, dest) for rect in rects)
                          fractions_c = (raycast_circle(circle, orig, dest) for circle in circles)
                          fraction = min((fraction for fraction in chain(fractions_r, fractions_c) 
                                                   if fraction is not None), default=None)
                      
                          return fraction
                       
                      if __name__ == '__main__':
                          "Demo of raycasting"
                          from pygame.locals import *
                          import math
                          import random
                       
                          pygame.init()
                          taille_fenetre = (800, 600)
                          fenetre_rect = pygame.Rect((0, 0), taille_fenetre)
                          screen_surface = pygame.display.set_mode(taille_fenetre)
                          timer = pygame.time.Clock()
                          font = pygame.font.SysFont("Comic Sans Ms", 16)
                          fps = font.render("FPS : 0", True, (200, 200, 200))
                          fps_rect = fps.get_rect(bottomleft=(10, 600-10))
                       
                          rects = [pygame.Rect(i*200, j*200, 100, 100)
                                   for i in range(4)
                                      for j in range(3)
                                          if not (i == 2 and j == 1)]
                      
                          circles = [(600, 400, 100), (300, 150, 50), (100, 350, 120)]
                       
                          angle = 0
                          orig = (450, 250)
                      
                          pygame.time.set_timer(USEREVENT, 1000)
                       
                          continuer = True
                          while continuer:
                              for event in pygame.event.get():
                                  if event.type == QUIT:
                                      continuer = False
                                  elif event.type == USEREVENT:
                                      fps = font.render("FPS : {}".format(timer.get_fps()), 
                                                        True, (200, 200, 200))
                       
                              timer.tick(60)
                              angle += 0.005
                              angle = angle % (2 * math.pi)
                              dest = (450 + 300 * math.cos(angle), 250 + 300 * math.sin(angle))
                       
                              fraction = raycast(rects, circles, orig, dest)
                       
                              screen_surface.fill(0)
                              for rect in rects:
                                  pygame.draw.rect(screen_surface, (255, 0, 0), rect, 1)
                              for circle in circles:
                                  pygame.draw.circle(screen_surface, (0, 0, 255), circle[:2], circle[2], 1)
                              pygame.draw.line(screen_surface, (255, 0, 0), orig, dest)
                               
                              if fraction:
                                  xo, yo = orig
                                  xd, yd = dest
                                  intersection = (int(xo + fraction * (xd-xo)), int(yo + fraction * (yd-yo)))
                                  pygame.draw.circle(screen_surface, (255, 255, 0), intersection, 4)
                      
                              screen_surface.blit(fps, fps_rect)
                              pygame.display.flip()
                          pygame.quit()

                      S'il y a trop de cercles, ça pourrait ralentir l'algo. Comme toujours, c'est un choix entre précision et temps de calcul. :)

                      Bonne idée d'avoir améliorer l'outil de profilage. C'est pas nécessaire pour moi, mais pour toi. :)

                      • Partager sur Facebook
                      • Partager sur Twitter
                        25 mars 2018 à 0:48:05

                        Salut, encore une fois merci ta fonction fonctionne très bien :)

                        J'ai essayé d'optimiser les projectiles en ne calculant leur collision qu'avec les objets dont leur centre est à une distance inférieure à 100 pixels de la leur (aucun obstacle ne dépasse cette taille de toute façon), j'ai l'impression que c'est un petit peu mieux

                        Je suis entrain de refaire tout le système d'effets visuels là

                        -
                        Edité par ReFlix 25 mars 2018 à 0:48:50

                        • Partager sur Facebook
                        • Partager sur Twitter
                          7 avril 2018 à 23:49:13

                          Je met le topic en résolu !

                          J'ai beaucoup amélioré tout les systèmes, j'ai utilisé des structures de partitionnement : J'ai établi une grille sur l'écran avec des coordonnées pour chaque cases, chaque objet a des coordonnées dans la grille attribué à la création de la map (plusieurs coordonnées oui si l'objet est entre deux cases), pour le joueur et les projectiles, leur liste des cases est calculé aussi mais par rapport à leur case initiale, pas besoin de tester l'insertion dans toutes les cases. Et puis les collisions entre tout les objets vont être calculés seulement entre ceux qui ont une coordonnée en commun

                          Pour le graphique, j'ai remplacé les sprites des particules par mes propres objets, puis vu que ce ne sont que des détails, je les fais tourner qu'en 15fps au lieu de 60, elles sont supprimées si leur opacité est inférieure à 75 et j'ai définit une limite du nombre présent.

                          Résultat : Si je désactive les particules, je peut faire tourner mon jeu en 60fps avec plus de 500 projectiles sur la map !!!

                          Avec particules, un centaine seulement, mais je pense qu'il sera rare qu'il y en ait autant dans de vrai niveaux, de toute façon je vais permettre à l'utilisateur de pouvoir choisir ses niveaux de graphismes

                          Encore merci pour toute ton aide, j'aurai vraiment eu du mal de penser à tout ça sans toi ^^

                          • Partager sur Facebook
                          • Partager sur Twitter
                            8 avril 2018 à 9:28:22

                            Salut,

                            Content de lire que tu as pu améliorer ton jeu. Pour un système de particules, l'utilisation de numpy amènerait probablement un gain en vitesse pour la mise à jour des paramètres des particules (position, vitesse, accélération, couleur, etc). Je pense que tu vas toutefois vite être limité par le rendering, qui est fait avec le CPU. Le passage à OpenGL t'ouvrirait un autre monde de possibilité. Mais il y a beaucoup de choses à lire à ce sujet. :)

                            En tous cas, félicitation pour avoir persisté et réussi à améliorer ton jeu ! :) 

                            • Partager sur Facebook
                            • Partager sur Twitter

                            Mon jeu laggue énormément, mauvaise optimisation ?

                            × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                            × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                            • Editeur
                            • Markdown