Projet Blitzmax EPU 2009

De $1

Version de 21:06, 3 Déc 2024

cette version.

Revenir à liste des archives.

Voir la version actuelle

 


Tutoriels Blitzmax 

 

Nous avons créé un jeu-vidéo complet pour découvrir autant que possible les fonctionnalités qu’offre Blitzmax en matière de programmation orientée jeux-vidéo. Notre jeu, Vibz, est un shoot them up 2D vertical. Nous aspirons à partager notre expérience à travers ce tutoriel. Celui-ci est le premier que nous écrivons et espérons qu’il est adapté. Tout conseil pour l’améliorer est bienvenu !  

Nous allons vous présenter les aspects que l’on retrouve dans beaucoup de jeux tels que la création d’un avatar pour le joueur, son déplacement, sa capacité à agir sur son environnement, etc. Cette présentation se fera à chaque fois sur plusieurs niveaux : d’abord la base, ce que Blitzmax permet de faire en quelques lignes de code, puis ce que nous avons implémenté dans notre jeu. 

Tout au long du tutoriel, des concepts de base de la programmation seront abordés de manière assez simplifiée. Les lecteurs ayant une bonne expérience de la programmation pourront passer ces rappels et se concentrer sur les exemples de code. Le meilleur moyen d’apprendre étant d’expérimenter, il ne faut pas hésiter à passer les bouts de code indépendants dans BlitzMax pour en éprouver le comportement.  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Les bases de la programmation 

 

 

Cette partie du tutoriel est un peu à part. Elle traite surtout de la syntaxe et de la manière de programmer en BlitzMax. Il n’est pas nécessaire de la connaître en entier avant de faire son premier jeu en BlitzMax, même si c’est conseillé. Cependant, de nombreux exemples du tutoriel feront référence à des notions présentes dans cette section.  

 

BlitzMax est un langage mixte : pour utiliser les termes techniques, il est possible de l’utiliser de manière procédurale et orientée objet à la fois. Il est donc possible de définir des variables, des fonctions, des tests et des objets comme dans de nombreux langages de programmation haut-niveau. Voyons cela au cas par cas. 

 

Le concept de machine à états 

 

BlitzMax (comme OpenGL), peut se voir comme une machine à états. En clair, on travaille en permanence avec un certain nombre d’états (la couleur, le mode et le coefficient de transparence, la rotation, l’échelle, la police de caractère …) qui s’appliquent à chaque objet affiché. Nous les verrons en détail plus loin dans ce tutoriel. 

 

Ces états ne changent que si on le demande explicitement et ne sont jamais remis automatiquement à leur valeur par défaut. Il faut donc toujours garder en tête dans quel état se trouve la machine avant chaque affichage et optimiser les changements d’états, qui sont coûteux pour le processeur graphique.  

La syntaxe 

 

Pour que l’ordinateur comprenne les instructions et informations qu’on lui donne, il faut respecter une certaine grammaire. Avant de voir le nécessaire au cas par cas, voici un aperçu de cette syntaxe, pour commencer sans se perdre. 

 

Tout d’abord, à la fin de chaque action, il faut soit un retour à la ligne (dans la plupart des cas), soit un point-virgule, lorsque l’on veut enchaîner plusieurs actions sur une même ligne. 

 

Comme en mathématiques, il est possible de regrouper certaines informations à l’aide de parenthèses.  

 

Il est possible d’écrire des commentaires dans notre code, c’est-à-dire des informations qui ne seront pas interprétées par l’ordinateur. Elles ne servent qu’aux programmeurs, c’est-à-dire vous ou ceux qui vous succéderont, pour prendre des notes ou autre. Les commentaires peuvent s’écrire sur une ligne à l’aide d’une apostrophe ou sur plusieurs lignes à l’aide des mots clé  Rem et End Rem. 

 

Afin de rendre le code plus facile à lire et à comprendre, il est fortement conseillé de l’indenter correctement. Indenter signifie décaler l’écriture du code, un peu comme les alinéas lorsque l’on écrit un texte. Le décalage se fait en général à l’aide de tabulations. Par convention, on décale le code d’une tabulation à chaque fois que l’on rentre dans un contexte indépendant. Par exemple, dans une fonction, une déclaration de classe, dans un test ou dans une boucle. A noter que les contextes tels que les boucles, les tests et autres définitions de classes ou de méthodes sont fermés non pas par des accolades mais par des End suivis du type de contexte (If, While, Type, Function, Method ...) en général. En effet, certaines exceptions sont à noter, comme nous le verrons par la suite. 

 

Une particularité très importante de BlitzMax est qu’il n’est PAS case sensitive. Cela veut dire que vous pouvez écrire exactement le même code avec des majuscules n’importe où, il fonctionnera quand même. Il est toutefois conseillé de respecter les conventions classiques d’écriture de code pour qu’il soit plus simple à relire. 

 

Voici un exemple de code valide regroupant ces différentes notions : 

 

Rem   

Voici un exemple de commentaire   

sur plusieurs lignes  

End Rem  

  

' commentaire sur une ligne  

action1() ; action2(var1,var2,var3)   

If (test())  

action3(var4) ' un niveau d'indentation  

If (var1 = 3)  

action4(var1) ' deux niveaux d'indentation  

End If' fin de contexte avec ou sans espace  

EndIf ' les deux fonctionnent

Les mots clef / types de donnée 

 

Il existe des mots clef réservés en BlitzMax qui désignent des types ou des fonctions de base. Il est impossible de s’en servir pour désigner autre chose. Nous parlerons de certaines fonctions au cours du tutoriel. Il est tout de même important de connaître les types de donnée de base : 

 

Syntaxe 

Description 

Raccourci 

Byte 

Entier non signé de 8 bits 

@ 

Short 

Entier non signé de 16 bits 

@@ 

Int 

Entier signé de 32 bits 

% 

Long 

Entier signé de 64 bits 

%% 

Float 

Réel de 32 bits 

# 

Double 

Réel de 64 bits 

! 

String 

Chaîne de caractère unicode de 16 bits 

$ 

ElementType[] 

Tableau d’éléments d’ElementType 

 

TypeName 

Objet du type spécifié 

 

VariableType Ptr 

Pointeur sur une variable 

 

VariableType Var 

Variable 

 

 

Les variables et les constantes 

 

Une variable, comme en mathématique, peut se voir comme un nom représentant une information. Cette information peut être modifiée au cours de la vie du programme et la variable associée suivra. Cela peut servir, entre autres, à stocker des informations à une place où l’on pourra les retrouver facilement.  

 

Une variable peut être globale ou locale. Les variables globales sont « visibles » à n’importe quel endroit du programme alors que les variables locales ne le sont que dans le contexte où elles ont été définies. 

Il est également possible de définir des constantes. Cela permet de stocker une information qu’il sera impossible de modifier dans le programme sans erreur à la compilation. 

 

En BlitzMax, les variables ont un type qu’il faut préciser à leur création. Voici un exemple de création et d’utilisation de variables en BlitzMax : 

 

Const longueur:Float = 15.5 ' constante 

Global largeur:Float = 10 ' variable globale 

Local aire:Float = longueur*largeur ' variable locale 

Les classes 

 

BlitzMax, en tant que langage orienté objet, permet la définition de classes. Les classes représentent un concept, une catégorie d’objets. Les objets, ou instances de classe, peuvent correspondre à des objets du monde réel (par exemple notre bâtiment des Templiers de l’Ecole Polytech’Nice), les classes correspondent à une entité abstraite de niveau supérieur (en l’occurrence le concept de « bâtiment »). Les  classes rassemblent les attributs (définissant l’état) et les méthodes (définissant les comportements) communs à une catégorie d’objets. 

 

Les classes en BlitzMax peuvent être abstraites et dériver d’autres classes. Elles peuvent posséder des attributs et des méthodes qui leur sont propres. 

 

Par exemple, voici la classe TEnemyExplosion qui dérive de la classe TExplosion dans Vibz :  

 

Type TEnemyExplosion Extends TExplosion  

 

    Function Make(x,y,MaxRadius# = 10, growSpeed = 1) 

        Local Explosion:TEnemyExplosion = New TEnemyExplosion 

       explosion.x = x 

       explosion.y = y+60 

       IfRand (1,2) = 1  

           explosion.image = explosionAnimImage 

       Else  

           explosion.image = explosionAnimImage2 

       EndIf 

       explosionlist.AddLast (explosion)       

    EndFunction 

 

EndType

Les fonctions 

 

Les fonctions sont des programmes dans le programme. Elles servent en général à donner un résultat dépendant de la ou des valeur(s) entrée(s) dans celle-ci. Cela peut être explicite ou implicite. En effet, parfois, il n’y a pas de valeur d’entrée ni de sortie indiquée. Dans ces cas-là, le plus souvent, une action est appliquée aux valeurs globales du programme ou aux attributs de classe. 

 

En BlitzMax, de nombreuses fonctions de base existent déjà. Elles sont répertoriées et expliquées dans la documentation. 

 

Voici un code très simple de fonction valide (et inutile ; c’est pour l’exemple !). Elle retourne le résultat de l’addition des variables a, b et c.  

 

Function add%( a% , b% , c% = 0 ) 

    Return a + b + c 

EndFunction 

 

Pour l’utiliser dans le code, il est nécessaire de donner au moins deux arguments, et possible d’en donner un troisième. En effet, si l’on n’affecte pas de valeur à l’argument « c », il aura par défaut la valeur 0. Par exemple, result vaudra ici 16 : 

 

Local a%=1 

Local result% = add(a,15) 

Les tests 

 

On utilise un test lorsqu’on veut vérifier si une condition est bien remplie et effectuer une action en fonction du résultat. Avant d’écrire la première condition d’un test, il faut écrire If. Avant de tester d’autres conditions, on écrit Else If. Enfin, avant d’effectuer l’action par défaut, on écrit Else. Il peut être utile d’utiliser Then après une condition pour indiquer que l’on va effectuer une action par la suite (notamment si l’on veut le faire sur la même ligne). Pour fermer un test, on écrit EndIf ou End If. 

 

Pour simplifier, il n’est pas nécessaire de finir les tests comportant seulement un If par un EndIf. Voici par exemple comment l’on change la difficulté du jeu en appuyant sur une touche dans le menu : 

 

IfKeyHit(KEY_1) Then Difficulty = 1 

IfKeyHit(KEY_2) Then Difficulty = 2  

IfKeyHit(KEY_3) Then Difficulty = 3 

 

Les boucles 

 

Une boucle permet d’exécuter une action un certain nombre de fois. Elle est composée de la condition de sortie et de l’action elle-même. La condition de sortie peut se présenter de plusieurs manières et cela donne le type de boucle : 

 

a.     Les boucles for : 

 

Il y a trois manières de les utiliser : avec To, Until ou EachIn. Les deux premiers sont des conditions sur des valeurs numériques. Avec To, la condition de sortie est atteinte lorsque la valeur testée atteint la valeur de test. Avec Until, la sortie est atteinte à l’itération précédant la valeur de test. Il est possible de définir un pas (Step), un décalage (par défaut 1) pour ces boucles. Enfin, avec EachIn, l’action de la boucle s’effectue sur tous les membres d’une collection. 

 

ForLocal x% = 0 To 20 Step 2 

    Print x/2 

Next 

 

ForLocal x% = 0 Until 11 

    Print x 

Next

 

Local array[]=[0,1,2,3,4,5,6,7,8,9,10] 

ForLocal x% = EachIn array 

    Print x 

Next 

 

 

b.     Les boucles while : 

 

Les boucles while s’exécutent tant que leur condition n’est pas atteinte. 

 

Local x% = 0 

While x < 11 

    Print x 

    x:+1 

Wend 

 

 

c.      Les boucles repeat : 

 

Ce sont exactement les mêmes que les boucles while sauf que la condition de sortie s’écrit à la fin. A la place de la condition de sortie, on peut utiliser Forever pour que la boucle s’exécute indéfiniment. 

 

Local x%=0

Repeat  

    Print x 

    x:+1 

Until x = 11

 

Cependant, quelque soit le type de boucle, il est toujours possible de changer de type de boucle en réécrivant la condition de sortie pour obtenir exactement le même résultat, comme dans les exemples précédents. 

 

Pour finir sur les boucles, il faut savoir que dans un programme BlitzMax, on utilisera en général une boucle principale qui ne se terminera que lorsque l’utilisateur voudra fermer le programme. Cette boucle principale sera exécutée systématiquement en fonction de la fréquence de rafraîchissement de l’application. Nous parlerons de « tour de boucle » dans la suite du tutoriel pour représenter une exécution de cette boucle principale. 

Les collections 

 

Les collections servent à stocker plusieurs variables au même endroit. En BlitzMax, il existe trois collections de base : les tableaux, les listes et les maps. Leur utilisation reste très classique.  

 

Les tableaux se définissent et s’utilisent ainsi : 

 

Local nameList$["alpha","bravo","charlie","delta","echo"] ' Crée un tableau composé de 5 Strings 

Local a$ = nameList[0] ' La variable a aura la valeur alpha 

 

Comme souvent, les tableaux sont indexés à partir de 0. Les variables du tableau sont toutes du même type, défini selon le tableau. A noter qu’il peut également exister des tableaux à plusieurs dimensions. 

 

Les listes  se définissent et s’utilisent ainsi :  

 

Global alphabet:TList = CreateList() 

alphabet.addFirst("b") 

alphabet.addLast("c") 

alphabet.addFirst("a") 

alphabet.addLast("d") 

 

Cette liste aura dans l’ordre les caractères a, b, c et d. On peut entre autres supprimer les éléments d’une liste, vérifier qu’elle contient un élément donné et boucler facilement dessus avec EachIn.  

 

Les maps se définissent et s’utilisent ainsi théoriquement : 

 

Global alphabetMap:TMap = CreateMap() 

alphabet.insert(2,"b") 

alphabet.insert(3,"c") 

alphabet.insert(1,"a") 

alphabet.insert(4,"d")  

 

Nous avons précisé ici « théoriquement » car nous n’avons pas réussi à créer de maps bien qu’elles existent dans la documentation. Peut-être n’existent-elles que dans la dernière version de BlitzMax que nous ne possédons pas. 

 

 

 

 

Je veux créer ! 

 

Bien, maintenant que nous sommes d’accord sur la syntaxe et les détails du langage, passons aux choses sérieuses. Nous allons voir comment obtenir rapidement un aperçu de la puissance de BlitzMax par l’affichage simple de graphismes. 

Le premier affichage 

 

Commençons par afficher une fenêtre avec une résolution de 800 par 600 pixels. Nous voulons qu’elle reste affichée tant qu’on ne ferme pas le programme. Pour cela, trois lignes de code !  

 

Graphics 800,600 ' Affichage de la fenêtre 

 

Repeat' Boucle principale 

' ... Ici, les futurs affichages 

UntilAppTerminate() ' Condition de fin : lorsque l'utilisateur ferme la fenêtre 

 

A noter que la fonction Graphics peut avoir plus d’arguments : mode fenêtré ou non, taux de rafraîchissement des images ... 

 

Il faut également noter que le système de coordonnées est le suivant : l’origine se trouve dans le coin supérieur gauche de la fenêtre. L’abscisse augmente donc vers la droite et l’ordonnée vers le bas. 

Les formes basiques 

 

BlitzMax permet de dessiner des rectangles, des ovales, des lignes, des points et du texte, respectivement à l’aide des fonctions : DrawRect, DrawOval, DrawLine, Plot et DrawText. Il faut leur renseigner les dimensions et la position de la forme. 

 

Deux petites subtilités. Tout d’abord, pour afficher effectivement ces formes, nous allons, à chaque tour de boucle, effacer le contenu actuel de l’écran et afficher son nouveau contenu. Cela se fait respectivement à l’aide des fonctions Cls (pour clear screen) et Flip. 

 

Ensuite, nous allons utiliser une fonction intéressante de BlitzMax qui nous renvoie la position actuelle du curseur pour positionner notre rectangle par exemple. Voici comment tout cela fonctionne : 

 

Graphics 800,600 ' Affichage de la fenêtre 

 

Repeat' Boucle principale 

 

    Cls 

        

    DrawRectMouseX(), MouseY(), 20,20 ' Position puis dimension 

    DrawOval 20,200,50,100 ' Position puis dimension 

    DrawLine 0,0,800,600 ' Positions des deux extrémités 

    Plot 50,500 ' Position du point 

    DrawText"What's up World ?",30,400 ' Chaîne de caractères puis position 

     

    Flip 

 

UntilAppTerminate() ' Condition de fin 

Les images 

 

Pour afficher des images, il faut au préalable charger l’image concernée dans un objet de type TImage, puis l’afficher simplement avec DrawImage. 

 

Graphics 800,600 ' Affichage de la fenêtre 

 

Global image:TImage = LoadImage("music_tribal.jpg") ' Chargement de l'image dans un objet 

 

Repeat' Boucle principale 

 

    Cls 

        

       DrawImage image,300,200 ' Affichage de l'image 

     

    Flip 

 

UntilAppTerminate() ' Condition de fin 

 

Pour travailler l’affichage de ces images à l’aide de BlitzMax, de nombreuses fonctionnalités vous sont proposées. Faites un tour dans la section effets spéciaux pour y trouver votre bonheur ! 

Les animations 

 

Les animations en BlitzMax sont tout simplement des parties d’images indexées. Il n’y a pas de moyen simple en BlitzMax de faire des interpolations de mouvement ou de déformation. Leur utilisation est rigide mais cela a l’avantage d’être facile à comprendre et à gérer. 

 

Pour créer une animation, il suffit de charger une image d’un format quelconque (bmp, jpg, png …) en précisant les dimensions d’une « frame » de l’animation, l’index de la première frame qu’on utilisera dans cette image pour l’animation et l’index de la dernière.  

 

Pour l’utiliser, il suffit de dessiner l’image comme précédemment mais en précisant la frame que l’on utilise. Pour faire défiler les frames, il est possible d’utiliser de nombreuses astuces. Créer sa propre classe d’animation, gérer la fréquence de défilement en fonction des tours de boucles ou du temps de jeu, etc. 

 

Voici un exemple simple d’animation qui se répète : 

 

Graphics 800,600 ' Affichage de la fenêtre 

 

Global explosion:TImage = LoadAnimImage("explosion.png",200,250,0,12)  

' Chargement de l'animation dans un objet 

Global loopsCount% ' Compteur de tours de boucles 

 

Repeat' Boucle principale 

 

    Cls 

    loopsCount :+ 1  

    DrawImage explosion,300,200, loopsCount/6 Mod 12  

       ' Affichage de l'animation, défilement de frames tous les 6 tours de boucle 

    Flip 

 

UntilAppTerminate() ' Condition de fin 

 

La gestion du temps 

 

Elle est facilitée par la fonction Millisecs() qui renvoie le temps courant en millisecondes comme son nom l’indique. Cela peut être utile pour afficher la durée d’une partie, gérer des « timers », des délais d’action, et bien d’autres. Voyons tout de suite un exemple de gestion du temps dans le mini-jeu que nous vous proposons d’implémenter dans ce tutoriel. 

 

Mini-jeu – Premières créations et gestion du temps 

 

Essayons de créer des plateformes pour notre mini-jeu. Il va nous falloir une structure pour les stocker et les afficher à chaque tour de boucle. Pour les représenter, nous utiliserons simplement des rectangles. 

 

Pour gérer le temps, nous utiliserons une variable globale définie au lancement du jeu par la valeur de Millisecs(). Le temps courant de la partie sera donc le temps courant, Millisecs(), moins la valeur d’origine. Ajoutons également une variable pour garder le meilleur score du joueur au clavier. Nous les afficherons ensuite dans la boucle principale. 

 

Attention : il faut bien réfléchir à ce que l’on met dans la boucle principale pour ne pas surcharger la mémoire ou engendrer des comportements indésirables. Pour simplifier la lecture du code, nous allons placer au maximum les actions dans les classes utilisées. 

 

Graphics 800,600 

 

' Liste des plateformes 

Global platforms:TList = CreateList() 

'Gestion du temps 

Global timeOrigin= MilliSecs() 

Global survival = 0 

 

 

'Classe pour les plateformes 

Type Platform 

    Field x% 

    Field y% 

    Field height% = 20 

    Field width% = 200 

     

    ' Fonction de création des plateformes 

    Function create(x,y,w,h) 

       pf:Platform = New Platform 

       pf.x = x ; pf.y = y 

       pf.width = w ; pf.height = h 

       platforms.addLast(pf) 

    EndFunction 

 

    Function Update() 

       ForLocal pf:Platform = EachIn platforms 

           DrawRect (pf.x,pf.y,pf.width,pf.height) 

       Next  

    EndFunction   

     

EndType 

 

' Création des plateformes 

Platform.create(0,500,800,20) 

Platform.create(10,450,50,10) 

Platform.create(500,450,50,10) 

Platform.create(300,350,50,10) 

Platform.create(200,400,50,10) 

 

' Boucle principale 

Repeat 

 

    Cls 

 

    Platform.Update()' Affichage des plateformes 

     

    DrawText"Survival time : " + (MilliSecs() - timeOrigin)/1000 + " seconds", 20, 30 

    DrawText"Survival record : " + survival + " seconds", 20, 10 

 

    Flip 

 

UntilAppTerminate()  

La création dans Vibz 

 

Dans Vibz, les chargements de toutes les images et animations se font dans des fichiers spécifiques. La plupart sont stockées dans des variables globales, que l’on peut utiliser dans le code à tout moment. Ainsi, elles ne sont chargées qu’une fois. 

 

La plupart des objets possèdent leur propre méthode Update, appelée à chaque tour dans la boucle principale, qui régit leur comportement et leur affichage. Il existe différentes méthodes pour structurer un programme, n’hésitez pas à en tester plusieurs et à choisir celle qui vous correspond le mieux. 

 

 

Après un essai de gestion automatique des animations par une classe spéciale, il s’est avéré plus simple de gérer les animations « à la main » dans les classes qui les utilisaient. 

 

Voir les fichiers Effets.bmx et Classes.bmx. 

 

Je veux du mouvement ! 

 

Le déplacement du joueur 

 

Il existe encore une fois beaucoup de façons de gérer le mouvement dans BlitzMax. Voulez-vous utiliser le clavier ou la souris ? Ces événements sont extrêmement simples à gérer, voyons cela tout de suite.  

 

A la souris : comme vu plus haut, on peut connaître à tout moment la position du curseur à l’aide des fonctions MouseX() et MouseY(). Il est alors très simple de modifier la position d’un objet en modifiant son attribut en fonction de la position de la souris. Par exemple :  

 

DrawOvalMouseX(), MouseY(), 20,20 

 

Au clavier : le plus simple est d’effectuer un déplacement du joueur à chaque appui sur une touche. Par exemple, pour les touches directionnelles :

 

    IfKeyDown (Key_Right) Then player.x :+ player.speed 

    IfKeyDown (Key_Left) Then player.x :- player.speed 

    IfKeyDown (Key_Up) Then player.y :- player.speed 

IfKeyDown (Key_Down) Then player.y :+ player.speed 

 

 

La rotation 

 

La rotation dans Vibz se gère à l’aide de la machine à états. En d’autres termes, lorsque la rotation aura été modifiée, elle s’appliquera à tous les objets affichés jusqu’à ce qu’on la remette à zéro. Dans BlitzMax, la rotation s’exprime en degrés. Le rectangle dessiné ici sera incliné de 45 degrés. 

 

SetRotation 45 

DrawRectMouseX(), MouseY(), 20, 20 

SetRotation 0

Le scrolling (défilement) 

 

Ajoutons à présent un fond à notre écran. Il suffit de dessiner une image de la taille de la fenêtre en premier dans notre boucle principale pour ne pas cacher les autres objets. 

 

Nous pouvons ensuite le faire bouger par exemple en fonction du temps ou en fonction des déplacements du joueur. Cela s’appelle le scrolling. BlitzMax permet de réaliser le scrolling automatiquement en appelant simplement la fonction TileImage à la place de la fonction DrawImage. Par exemple, avec bg1 l’objet TImage du fond : 

 

TileImage bg1,200 + mapX,mapY 

 

 Pour que l’effet soit réussi, il faut que le fond d’écran soit raccordable, c’est-à-dire qu’il n’y ait pas de frontière visible lorsque l’on accole ce fond à lui-même, comme dans une mosaïque. La plupart des éditeurs graphiques possèdent des algorithmes pour rendre une image raccordable. 

Les trajectoires 

 

Les trajectoires des objets peuvent encore une fois être gérées de nombreuses manières. Nous verrons ici comment les implémenter de manière simple. 

 

Le principe d’implémentation de la trajectoire consiste à mettre à jour la position de l’objet concerné en fonction du temps (ou ici, des tours de boucle). On peut alors jouer avec les fonctions mathématiques pour donner des trajectoires linéaires, sinusoïdales, circulaires ou autres aux objets. 

Mini-jeu – Déplacement du joueur 

 

Il faut pour cela rajouter dans notre jeu une classe « joueur », une variable globale qui instancie cette classe, et sa fonction de mise à jour dans la boucle principale. Notre joueur sera représenté par un cercle blanc. 

 

Avant la boucle principale : 

 

Global player:TPlayer = New TPlayer 

 

' Classe pour le joueur 

Type TPlayer 

    Field x# = 400 

    Field y# = 300 

    Field speed = 15 

     

    Function Update() 

       IfKeyDown (Key_Right) Then player.x :+ player.speed 

       IfKeyDown (Key_Left) Then player.x :- player.speed 

       IfKeyDown (Key_Up) Then player.y :- player.speed 

       IfKeyDown (Key_Down) Then player.y :+ player.speed 

       DrawOval player.x,player.y,20,20 

    EndFunction   

 

EndType 

  

Dans la boucle principale : 

 

   TPlayer.Update() ' Gestion du joueur 

 

Le mouvement dans Vibz 

 

Dans Vibz, le joueur se déplace à l’aide du clavier, d’une manière similaire à ce que l’on vient de voir. Le vaisseau possède sa propre vitesse qui ne change jamais. 

 

Les ennemis utilisent des trajectoires complexes, les B-Splines. Ils suivent ces trajectoires à vitesse constante. Ces trajectoires sont calculées à partir de points donnés dans les fichiers des différents niveaux du jeu. 

 

Le fond d’écran défile en permanence à la verticale et défile horizontalement avec le déplacement du joueur. Le fond d’écran est en fait composé de deux fonds superposés dont les vitesses de défilement diffèrent, afin de donner un effet de profondeur à l’ensemble. 

 

Une des grandes originalités de Vibz est le système de ralenti porté au gameplay des shoot them up. En effet, en appuyant sur Shift gauche, on active le mode ralenti, ce qui ralentit tout l’environnement du joueur (projectiles et vaisseaux ennemis, défilement du paysage), mais pas le joueur, qui a alors un avantage conséquent pour une durée limitée. 

 

Voir les fichiers xml des niveaux ainsi que Vibz.bmx et Classes.bmx . 

 

 

Je veux de la puissance ! 

 

Les projectiles 

 

Pour gérer des projectiles, vous aurez besoin de types spécifiques. Ils auront au minimum leur position en attribut et une fonction pour calculer leur trajectoire et les afficher. Un timer est nécessaire si vous voulez que le joueur ne puisse tirer qu’avec un délai. A vous de voir si vous avez besoin de plus !  

 

Les stocker dans une liste facilite leur mise à jour, à l’aide d’une boucle. Les projectiles seront supprimés de la liste s’ils sortent de l’aire de jeu. Il faudra ensuite tester les collisions avec les cibles à chaque tour de boucle.   

 

Voici un exemple qui peut servir de base pour vos projectiles. Celui-ci crée un projectile avec une direction aléatoire et une trajectoire linéaire. N’oubliez pas de définir une liste qui contiendra ces projectiles. 

 

Global projectiles:TList = CreateList()

 

Type TProjectile 

 

    Field x# 

    Field y# 

    Field angle# 

    Field speed# = 10 

 

    Function create(x,y) 

       bulletTimer = MilliSecs() 

       proj:TProjectile = New TProjectile 

       proj.x = x 

       proj.y = y 

       proj.angle = Rand(0,8) * 45 

       projectiles.addLast(proj) 

    EndFunction 

     

    Function update() 

       For proj:TProjectile = EachIn projectiles 

           proj.x :+ proj.speed * Cos(proj.angle)  

           proj.y :+ proj.speed * Sin(proj.angle)  

           DrawOval proj.x,proj.y,20,20 

           If proj.x<0 Or proj.x>GraphicsWidth() Or proj.y<0 Or proj.y>GraphicsHeight() Then projectiles.remove(proj) 

       Next 

    EndFunction 

 

EndType 

 

Pour l’utiliser, il faudra appeler la fonction create à partir d’un événement clavier par exemple : 

 

IfKeyDown (Key_Space) And bulletTimer < MilliSecs()-1000 Then TProjectile.create(player.x, player.y) 

 

Ici, le projectile part du joueur et il faut attendre une seconde avant de pouvoir tirer à nouveau. Il faudra donc ajouter le bulletTimer quelque part, en variable globale par exemple. Il faudra ensuite appeler la fonction update de la classe dans la boucle principale : 

 

TProjectile.update()  ' Gestion des projectiles 

Les collisions 

 

Ce paragraphe aurait pu se trouver tout aussi bien dans la partie « Mouvement » car nous allons parler à présent de tous types de collisions : collisions entre le joueur et le paysage, collisions entre des projectiles et des cibles, etc. 

 

Comme à l’accoutumée, plusieurs façons de faire : 

 

A la main :  

 

Penchons-nous sur la collision la plus simple : lorsque deux rectangles se touchent ou se chevauchent. Voici une manière rudimentaire de l’implémenter :  

 

Function rectsOverlap(x0, y0, w0, h0, x2, y2, w2, h2) 

    If x0 > (x2 + w2) Or (x0 + w0) < x2 ThenReturnFalse 

    If y0 > (y2 + h2) Or (y0 + h0) < y2 ThenReturnFalse 

    ReturnTrue 

EndFunction 

 

Pour l’utiliser, il faut bien sûr donner la position et la dimension de chaque rectangle impliqué. Au passage, on voit avec cette fonction deux subtilités : quand on ne déclare pas de type en BlitzMax, le type par défaut est Int. Deuxième subtilité, un booléen en BlitzMax n’est autre qu’un Int avec False = 0 et True = autre chose.  

 

Un peu plus complexe, la collision entre un rectangle et un cercle :  

 

Function circRectsOverlap(x0, y0, w0, h0, cx, cy, r) 

    testX=cX 

    testY=cY 

    If TestX < x0 Then TestX=x0 

    If TestX > (x0+w0) Then TestX=(x0+w0) 

    If TestY < y0 Then TestY=y0 

    If TestY > (y0+h0) Then TestY=(y0+h0) 

 

    Return ((cX-TestX)*(cX-TestX)+(cY-TestY)*(cY-TestY))<r*r 

EndFunction 

 

Les tests à la main peuvent être utiles, soit pour expliciter le code (mais on peut considérer que les fonctions fournies par BlitzMax sont déjà assez explicites), soit pour faire des tests que BlitzMax ne fait pas de base, comme par exemple des tests de collision sur une seule arête d’un rectangle, etc. 

 

ImagesCollide :  

 

Cette fonction teste si deux images entrent en collision au pixel près ! Voici sa signature : 

 

FunctionImagesCollide(image1:TImage,x1,y1,frame1,image2:TImage,x2,y2,frame2) 

 

Pour l’utiliser, il suffit de donner une image, sa position et sa frame (0 pour les images simples) puis la même chose pour une deuxième image et la fonction renvoie True s’il y a collision et False sinon. 

 

CollideRect :  

 

BlitzMax propose des fonctions très avancées pour tester les collisions. Elles utilisent le concept de « masques de collision ». Il y a 32 masques différents que l’on peut utiliser. Attention, ce sont des masques un peu spéciaux, des « bitmasks ».  

 

Les étapes pour utiliser ce mécanisme sont les suivantes :  

 

a.     S’assurer que les masques de collision sont remis à zéro 

ResetCollisions() 

b.     Ecrire dans le masque de collision les images avec lesquelles on va tester la collision 

c.      Comparer une image avec les images présentes dans le masque de collision 

 

Ces deux dernières étapes seront réalisées ici à l’aide de la fonction CollideRect, qui teste la collision entre un rectangle et les objets d’un masque de collision donné. Voici sa signature :  

 

FunctionCollideRect:Object[](x,y,w,h,collidemask%,writemask%,id:Object=Null) 

 

x, y, w et h correspondent à la position et à la dimension du rectangle dont on va tester la collision. On indiquera le masque de collision avec lequel on veut tester le rectangle dans collidemask le cas échéant. On indiquera le masque dans lequel on veut inclure le rectangle dans writemask. En règle générale, on n’utilisera qu’un seul de ces deux arguments, l’autre sera mis à 0.  

 

Voyons à présent à quoi servent l’argument optionnel id et le tableau Object[] retourné par la function. Lorsqu’on veut tester une collision, il est souvent utile de savoir quel objet particulier dans un masque entre en collision avec l’objet testé. Ainsi, lorsqu’on écrit un objet dans un masque, on peut l’identifier à l’aide de l’argument id. Lorsqu’on effectuera un test de collision par la suite, les identifiants des objets en collision avec l’objet testé seront renvoyés dans le tableau Object[]. 

 

Voici un exemple d’utilisation de la fonction CollideRect avec au passage la syntaxe pour une sélection de cas :  

 

' Ecriture de deux rectangles dans les masques 

CollideRect(10,10,100,100,0,1,1) 

CollideRect(200,200,200,200,0,1,2) 

 

' Test de collision 

Local p:Object[]=CollideRect(MouseX(),MouseY(),100,100,1,0) 

ForLocal i=EachIn p 

   Select i 

    Case 1 

       Print ("Le premier rectangle a été touché") 

    Case 2 

       Print ("Le second rectangle a été touché") 

   EndSelect 

Next 

 

CollideImage :  

 

La fonction CollideImage s’utilise de la même manière que la fonction CollideRect, sauf qu’elle teste des collisions au pixel près entre des images et non des rectangles. Il est d’ailleurs possible de combiner des CollideImage et des CollideRect utilisant le même masque de collisions. Voici la signature de la fonction :  

 

FunctionCollideImage:Object[](image:TImage,x,y,frame,collidemask%,writemask%,id:Object=Null) 

 

Avec cette fois l’image, sa position et sa frame (0 pour les images simples) en arguments. Notez que cette fonction tient compte des états actuels de rotation et d’échelle ! 

Mini-jeu - Gameplay 

 

Rajoutons un système de leurre pour perdre le joueur à la souris ! Ce sera simplement la classe de projectile vue plus haut à rajouter dans le programme.  On ajoutera ensuite dans la fonction update de la classe TPlayer :  

 

IfKeyDown (Key_Space) And bulletTimer < MilliSecs()-1000 Then TProjectile.create(player.x, player.y) 

 

Et la mise à jour dans la boucle principale : TProjectile.update()   

 

Ainsi, toutes les secondes, le joueur au clavier pourra lancer un double de lui-même avec une direction aléatoire pour tromper l’autre joueur.  

 

Testons ensuite si le joueur à la souris arrive à cliquer sur le joueur au clavier. Nous utiliserons pour cela un test de collision « à la main » entre le point où la souris clique et un carré englobant le joueur. Nous placerons ce test dans la fonction update du joueur :  

 

IfMouseDown(1) 

           IfMouseX()<player.x+player.width AndMouseX()>player.x AndMouseY()<player.y+player.height AndMouseY()>player.y 

              DrawText"Caught in " + (MilliSecs() - timeOrigin)/1000 + " seconds", 20, 50 

              If (MilliSecs() - timeOrigin)/1000 > survival Then survival = (MilliSecs() - timeOrigin)/1000 

              timeOrigin = MilliSecs() 

              player.x = Rand(0,800) 

           EndIf 

EndIf 

La puissance dans Vibz 

 

Dans Vibz, il existe cinq principaux types de projectiles : le laser rouge, l’onde bleue, les missiles alliés rouges, les bombes et les tirs ennemis. Ces projectiles sont gérés différemment.  

 

Les bombes, l’onde bleue et les tirs ennemis ont un comportement très classique, avec une trajectoire linéaire et une vitesse constante.  

 

En revanche, le laser rouge a été très complexe à mettre en place. C’est en fait une avalanche de mini-projectiles animés lancés à grande vitesse en face du joueur. La position en x de tous les mini-projectiles qui composent le laser suit celle du joueur.  

 

Côté missiles alliés, ils ont trois originalités : une vitesse exponentielle, un angle donné en fonction de la direction du joueur, et ils rebondissent sur les bords du terrain de jeu.  Comme c’est notre jour de bonté, nous vous offrons la petite fonction magique pour gérer les trajectoires linéaires et les rebonds des projectiles, à placer avant l’affichage du projectile :  

 

 

' Arguments : le projectile, les coefficients de déplacements en x et y 

Function computeAngle(bullet:TBullet,coeffX#=1,coeffY#=1) 

    If bullet.bouncing ' Si le projectile rebondit ... 

       If bullet.x <= leftedge Then bullet.angle = 360-bullet.angle  ' Collision avec le bord gauche 

       If bullet.x >= rightedge Then bullet.angle = 360-bullet.angle ' Collision avec le droit 

    EndIf 

    bullet.x :+ Sin(bullet.angle)*coeffX ' Déplacement du projectile 

    bullet.y :+ Cos(bullet.angle)*coeffY 

    SetRotation bullet.angle ' Change l'angle de l'image 

EndFunction 

 

Toutes les collisions sont faites à la main dans Vibz. Nous utilisons des masques de collision rectangulaires adaptés au cas par cas.  Ainsi, on utilise moins de mémoire qu’avec des tests au pixel près. De plus, le vaisseau du joueur est assez large et il aurait été extrêmement difficile pour le joueur d’éviter les tirs ennemis sans un masque de collision réduit. 

 

Il y a deux types de collision gérés en fait de la même façon : les collisions entre vaisseaux et les collisions entre vaisseaux et projectiles. Si le joueur est touché par un vaisseau ou tir ennemi, il perd une vie. Si un ennemi est touché par un projectile du joueur, il perd des points de vie en fonction de sa « fréquence de résonance » représentée par sa couleur et de la fréquence du tir du joueur.  

 

 

Je veux des effets spéciaux ! 

Couleur, transparence, lumière 

 

Pour travailler les images, les textes ou les formes dans BlitzMax, quelques outils très puissants nous sont donnés à travers la machine à états.  

 

Ainsi, il est possible de définir une couleur, un coefficient et un mode de transparence courants respectivement à l’aide de : 

 

SetColor 255,255,255 

SetAlpha 1 

SetBlend ALPHABLEND 

 

Les valeurs données ici en arguments sont les valeurs par défaut pour ces états. Les trois arguments de SetColor sont les composantes rouge, verte et bleue de la couleur, de 0 à 255 pour chacune. L’argument de SetAlpha est le coefficient de transparence, entre 0 pour totalement transparent et 1 pour totalement opaque. L’argument de SetBlend est une constante qui doit faire partie de : 

 

Mode de transparence 

Effet 

MASKBLEND 

Les pixels sont dessinés uniquement si leur alpha est supérieur à 0.5. 

SOLIDBLEND 

Les pixels remplacent les pixels existants du backbuffer. 

ALPHABLEND 

Les pixels sont dessinés en transparence par-dessus ceux existants dans le buffer, en fonction de leur alpha. 

LIGHTBLEND 

Les composantes de couleur des pixels sont ajoutées à celles des pixels déjà existants dans le buffer. 

SHADEBLEND 

Les composantes de couleur des pixels sont multipliées avec celles des pixels déjà existants dans le buffer. 

Motion blur

 

Le motion blur est un effet intéressant qui consiste à laisser une trace du déplacement d’un objet. Il peut être utilisé pour représenter un effet de déformation du temps (ralenti, accélération). 

 

Nous avons testé deux moyens d’implémenter cet effet : soit en conservant dans une liste les dernières positions de l’objet, soit en les recalculant à partir de l’équation de sa trajectoire.  

 

Dans les deux cas, le concept est d’afficher le même objet avec un alpha de plus en plus faible au fur et à mesure qu’on remonte dans ses positions antérieures. 

 

Voici une fonction qui affiche cet effet à partir d’une liste de coordonnées et d’une image. En option : l’échelle, la frame et le nombre maximum d’images affichées.  

 

Function motionBlur(coordsList:TList, image:TImage, scale# = 1, frame#=0, maxImages# = 20) 

    Local c# = 0 

    ForLocal coord:coordinate = EachIn coordsList 

       If c Mod (2) = 0 

           SetBlend alphablend 

           Local csurmax# = c/maxImages 

           SetAlpha(1-csurmax) 

           SetColor (100,100,100) 

           SetScale scale,scale 

           DrawImage image, coord.a, coord.b,frame ' On affiche une fois sur deux   

       EndIf 

       c:+1 

       If c=maxImages ThenListRemove(coordsList,coord)' 

    Next 

    SetScale 1,1 

    SetAlpha 1 

EndFunction

Les particules 

 

Une particule n’est rien d’autre qu’un objet voué à être fabriqué et affiché dans notre jeu un grand nombre de fois, avec un comportement différent pour chaque instance de ce type d’objet. Il est alors pratique de créer une fonction pour les fabriquer à la chaîne automatiquement. Comme pour les autres objets, nous utiliserons deux étapes principales 

-        La création, qui consistera à créer une particule, lui donner une valeur initiale pour ses attributs et la placer dans une liste de particules.  

-        La mise à jour, qui consistera à les déplacer, les modifier, les afficher et les détruire au bout d’un moment.  

 

Voici un exemple de moteur à particule assez impressionnant, à essayer ! N’oubliez pas de rajouter la liste de particules et d’appeler la mise à jour sur ses éléments dans la boucle principale. 

 

Type TSpark Extends TEntity 

 

    Field x#,y#,xs#,ys# ' Position et vitesse 

    Field color[3],rot#,rots# ' Couleur, rotation et vitesse de rotation 

    Field image:TImage ' Image de la particule 

 

    Method Update() ' Méthode de mise à jour par objet 

 

       ys:+GRAVITY 

       x:+xs 

       y:+ys 

 

       If x<0 Or x>=GraphicsWidth() Or y>=GraphicsHeight() 

           remove 

           Return 

       EndIf 

 

       rot=rot+rots 

       SetHandle 8,8 

       SetRotation rot# 

       SetColor color[0],color[1],color[2] 

       DrawImage image,x,y 

       SetHandle 0,0 

       SetRotation 0 

       SetAlpha 1 

    EndMethod 

 

    Function CreateSpark:TSpark( x#,y#,color[],image:TImage ) ' Création et ajout d'une particule dans la liste "sparks" 

       Local spark:TSpark=New TSpark 

       Local an#=Rnd(360),sp#=Rnd(3,5) 

       spark.x=x 

       spark.y=y 

       spark.xs=Cos(an)*sp 

       spark.ys=Sin(an)*sp 

       spark.rots=Rnd(-15,15) 

       spark.color=color 

       spark.image = image 

       spark.AddLast sparks 

       Return spark 

    EndFunction 

 

EndType 

 

Function firepaint(centerX,centerY,image:TImage, sparksNumber = 3) 

 

    Local rgb[]=[Rand(0,255),Rand(0,255),Rand(0,255)] 

    ForLocal k=1 To sparksNumber 

       TSpark.CreateSpark centerX,centerY,rgb,image 

    Next 

 

EndFunction 

Les effets spéciaux dans Vibz 

D’autres moteurs à particules sont implémentés dans Vibz pour donner de nombreux effets spectaculaires (les particules flottantes dans le menu, les impacts sur les vaisseaux ennemis, l’apparition du vaisseau, etc.).  

 

Le motion blur est également implémenté de deux manières différentes, l’une pour les vaisseaux, dont les trajectoires ne dépendent pas d’une équation, l’autre pour les tirs ennemis qui en revanchent dépendent d’une équation simple. Le motion blur s’active sur la plupart des objets du jeu en mode ralenti.  

 

Voir les classes Effets.bmx et Firepaint.bmx 

 

Références pour les tutoriels 

 

 

La documentation officielle :  

http://www.blitzbasic.com/bmdocs/docs.php 

 

Le wiki officiel :  

http://blitzmax.org/index.php?title=Main_Page 

 

La page WikiBooks, très utile pour les bases et détails de syntaxe : 

http://en.wikibooks.org/wiki/BlitzMax/Language 

 

La page essentielle de tutoriels sur le forum de BlitzMax :  

http://www.blitzbasic.com/Community/topics.php?forum=112s 

 

Un tutoriel assez clair en Anglais pour débuter et sa traduction en Français :  

http://www.truplo.com/docs/BeginnersGuideToBlitzMax10.pdf 

http://ben.kicks-ass.net/BlitzMaxGuide.txt 

 

Un tutoriel assez complet sur certains aspects de BlitzMax : 

http://www.2dgamecreators.com/tutorials/gameprogramming/ 

 

Un tutoriel pour faire un jeu d’arcade :  

http://rveilleux.googlepages.com/blitzmaxarcadeemulatortutorial 

 

 

 

 

 

 

Comparaison avec les autres langages 

Performances 

 

Compilation 

 

Taille du code, conventions …