
Comment a été créé Pong
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 :
Type | ID | Syntaxe | Information |
---|---|---|---|
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 |
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é.
Étudiant en classe de seconde au lycée Antoine de Saint-Exupéry à Lyon.
J’aime la programmation en python, les jeux vidéos et l’aérospatiale.