Comment a été créé Pong

Projets

Dans cet article, nous allons nous interresser à la programmation et à l’architecture de la version 2 de Pong. Le jeu est écrit en Python pour numworks sous Oméga, car cette version offre plus de tas (100ko contre 32ko dans la version officielle).

Pourquoi une seconde version ?

La première version avait une architecture assez peu soignée car je ne l’avais pas pensé de manière globale. De plus, elle avais des manques de compression assez flagrants car elle fesait plus de 12ko. Pour le petite anecdote, j’avais du la compresser énormément pour pouvoir la faire tourner car elle souffrait d’un dépassement de mémoire. Paradoxalement, c’est un de mes jeux qui a le mieux marché, sans doute car il était très complet. Mais je ne voulais pas laisser à un programmeur qui tombe sur le jeu l’image que c’est un travail que je trouve a posteriori presque bâclé. J’ai donc entrepris de le réécrire.

Première étape : programmer les menus.

Moteur de menu

Pour une fois, j’ai voulu commencer par programmer le menu (j’ai appelé le moteur menulib). Je pourrais alors adapter l’architecture du moteur de jeu à l’architecture globale. Pour programmer ce moteur de menu, je suis parti d’une syntaxe de création de menu un peu alambiquée, qui rassemble touts les éléments iterractifs (le moteur ne s’occupe que des éléments interractifs, les titres ou autre doivent être dessinés à part): l’unique fonction prend en parametre un liste de tupple qui contiennent eux même le type, le titre et si besoin les valeurs de l’élément :

menu ( position x, position y, elements list , [color] , [background color] )

Les éléments on chacun un identifiant :

TypeIDSyntaxeInformation
Button« btn »["btn","name"]Simple button. Not intended to be used as a text
List« lst »["lst","name",("element1","element2"),current element index]Element that allow the user making a choice beetwen the different elements purposed
Slider« sld »["sld","name",(min val,max val),current value]Element that allow the user chosing a value bettwen min val and max val
(Extrait de la documentation, que j’avais écrite en anglais)

La fonction retourne alors le nom du bouton sélectionné et l’indexe de chaque liste ou slider.

De cette manière, j’ai pu créer facilement des menus et récupérer le résultat de l’interaction avec l’utilisateur. Exemple ci dessous avec le menu principal

def mainMenu(conf=base_conf):
  transition(conf) #petite animation

  drawSprite(LOGO,10,10,12,conf[Col2]) #dessiner le logo
  drawRect(10,80,300,142,conf[Col3]) #dessiner le fond
  
  act_el=[["btn","Play"],["lst","Mode",("Solo","2 Player","Vs Comp."),conf[Mode]],["btn","Game Options"],["btn","Graphics Options"]]
  #défini les éléments
  get_act=menu(20,100,act_el,conf[Col1],conf[Col3]) # appelle la fonction et affecte le résulat de l'action à get_act
  
  conf[Mode]=get_act[1]["Mode"] #défini la configuration du mode de jeu à la sélection.
  if get_act[0]=="Game Options":gameMenu(conf) #analyse quel bouton a été selectionné
  elif get_act[0]=="Graphics Options":graphMenu(conf)
  else:gameEngine(conf)

La configuration et la persistance des options

Vous avez peut-être remarqué que les fonctions mainMenu, graphMenu, gameMenu et transition prennent toutes en paramètre la variable conf. C’est une liste, en quelque sorte un fichier de configuration, qui contient toutes le informations et paramètres du programme. Cette liste est enregistrée dans un fichier sous forme de chaine grâce à la fonction str() elle est décompressé à partir de ce fichier de manière totalement transparente grâce à la fonction eval() :

def saveConf(conf):
  try : #fonction try utilisée pour éviter des arrêts suite à une potentielle erreur.
    with open("pong.conf","w") as f:
      f.truncate(0) #on vide le fichier ...
      f.write(str(conf)) #... et on réécrit la chaine
  except: print("Saving configuration failed.") #message d'erreur en cas d'échec, mais cela ne stop pas le programme

def loadConf():
  try: #fonction try utilisée pour éviter des arrêts suite à une potentielle erreur ou si la configuration n'a pas encore été enregistrée
    with open("pong.conf","r") as f:return eval(f.readline()) #comme la chaine correspond à une liste, elle est évaluée comme une liste par eval()
  except: #si le fichier n'existe pas
    print("Loading configuration failed.")
    return base_conf #on retourne la configuration par défaut

Pour plus de lisibilité du code, chaque élément de la configuration (qui rappelons-le, est une liste) n’est non pas atteint grace à un index mais grace à une constante qui a pour valeur l’index correspondant :

Mode,Diff,MaxPts,BallSpd,BallDetails,PadDetails,Col1,Col2,Col3,BgCol,Theme,Best=0,1,2,3,4,5,6,7,8,9,10,11

Gestion des thème

Pour gérer les theme, j’ai simplement utilisé la configuration. Les couleurs et le numéro du thème y sont contenus et mis à jour quand cela est nécessaire. Pour mettre à jour ces couleurs, une fonction récupère le numéro du thème dans la configuration et renvoie cette configuration avec les bonnes couleurs :

def setTheme(conf,nb):
  conf[Theme]=nb #définit le numéro du thème dans la configuration
  a,b,c,d=(255,255,255),(255,200,0),(100,100,100),(60,60,60) #défini les 4 couleurs du thème de base
  if nb==1:a,b,c,d=d,(200,150,60),(200,200,200),a #thème light : réarrange et modifie les couleurs
  elif nb==2:b=(220,50,50) #theme omega
  elif nb==3:b=(200,100,200) #theme nsiOs
  conf[Col1:BgCol+1]=a,b,c,d #on defini les couleurs dans la config
  return conf #et on renvoie la config

Le moteur de jeu

Architecture et déplacement

J’ai longtemps hésité sur le choix de l’architecture. J’ai bien évidemment directement pensé à une classe en orienté objet qui pourrait s’occuper à la fois des pads et de la balle. Mais cette option est assez très gourmande en mémoire et j’ai aussi pensé à une fonction « constructrice » qui retournerais une liste avec toutes les information de l’objet. Finallement, j’ai tout de même opté pour une classe Entity() qui contient différentes fonctions très utiles :

class Entity():
    def __init__(it,x,y,w,h,col,bg_col):
      #Fonction d'initialisation des attributs
      #Défini la position, la taille, la couleur, etc...
      it.x,it.y,it.w,it.h,it.col,it.bg_col=x,y,w,h,col,bg_col
      it.spd_x,it.spd_y=0,0
      it.last_draw=(int(it.x-it.w//2),int(it.y-it.h//2),int(it.w),int(it.h),it.bg_col)
    def hitBox(it,it2):
      #Fonction qui renvoie vrai si it (objet 1) touche it2 (objet 2)
      if it.x-it.w//2<it2.x+it2.w//2 and it.x+it.w//2>it2.x-it2.w//2 and it.y-it.h//2<it2.y+it2.h//2 and it.x+it.w>it2.x and it.y<it2.y+it2.h//2 and it.y+it.h//2>it2.y-it2.h//2:return True
      else: return False
    def applyVec(it):
      #Fonction qui applique la vitesse de l'objet à sa position
      it.x+=it.spd_x
      it.y+=it.spd_y
    
    def hideObj(it):
      #Fonction qui dessine un rectangle de la couleur du fond pour cacher le dernier affichage de l'objet
      drawRect(*it.last_draw)
    
    def drawObj(it,detail=0):
      #Fonction qui dessine un rectangle à la place de l'objet
      it.last_draw=[int(it.x-it.w//2),int(it.y-it.h//2),int(it.w),int(it.h),it.bg_col]
      if detail:
        for x2,y2 in zip((2,0),(0,2)):drawRect(int(it.x-it.w//2)+x2,int(it.y-it.h//2)+y2,int(it.w)-x2*2,int(it.h)-y2*2,it.col)
      else: drawRect(*it.last_draw[0:4]+[it.col])

Ensuite, j’ai amélioré le déplacement de la balle. Je lui ai ajouté un attribut ball.a qui représente son angle, et à chaque collision sa vitesse latérale et horizontale est redéfinie grace à une fonction :

def vec(s,a): #la fonction prend en parametre la vitesse s et l'angle a
      a=radians(a)#les fonctions trigo prennent l'angle en radians
      x=s*cos(a)
      y=s*sin(a)
      return x,y #retour du vecteur vitesse en 2D

Pour les collisions : c’est encore plus simple. J’utilise une fonction qui prend en paramètre l’angle de l’objet et de la surface, qui calcule l’angle d’incidence et qui retourne l’opposé :

def collide(a1,a2):
  return a2-simp(a1-a2)
#la fonctio simp() simplifie un angle pour qu'il soit compris entre 0° et 360°.

Apres, je « n’avais qu’a » associer des touches aux pad, gérer leur collision avec le haut et la bas et détecter quand la balle est en dehors.

Afficher le score

Il ne restait plus qu’a afficher le score (bon, en fait il restait pas mal de petits truc à faire mais je vais pas les détailler ici). Pour cela, j’ai utilisé la technique de Schraf : Math-infos que j’ai adapté. J’ai d’abord créé une fonction drawSprite() un peu compliquée, qui grosso modo lit un chiffre pour le décompresser en binaire; et lit chaque bit. Si le bit est de 1, un carré est dessiné. Si le bit est de 0, on laisse la case vide. La fonction drawNumber permet d’automatiser l’affichage d’un sprite pour chaque chiffre du nombre désiré.