PureBasic:Realiser un RPG2D/Combats, fondus enchaînés et coffre
PureBasic:Realiser_un_RPG2D
<< Précédent | Sommaire | Suivant >>
Difficultée estimée de cette étape: moyenne
Pré-requis:
- Bonne connaissance du PureBasic (allocations dynamiques, pointeurs et gestion des entrées/sorties)
- Avoir compris le fonctionnement du système d'événements.
Cet article va apporter une toute nouvelle fonctionnalité: la gestion des combats. Il s'agira de la plus grosse partie de cette étape. Toutefois, d'aures petits ajouts viendront s'y ajouter comme les fondus enchaînés, la gestion des coffres et de l'écran de fin de jeu.
Sommaire |
[modifier] Introduction
Ce tutoriel est incomplet, il manque la gestion des fondues enchainées et des combats. Je n'ai pas le temps de faire l'adaptation complète, aussi si quelqu'un souhaite poursuivre qu'il ne s'en prive pas.
Je voulais tout de même faire l'adaptation du code pour la version 4.0 de PureBasic, et le tester avec les six cartes que M. Cool a créé.
Je poste les sources des fois que quelqu'un souhaite se plonger dedans pour poursuivre le tutoriel.
[modifier] Fondus enchaînés
Cette partie n'est pas encore faite. (Un volontaire ? )
Ce système va être réutilisé lors de la conception des combats afin de fournir une transition dynamique entre la carte et le combat lui-même.
[modifier] Coffre
Avant de commencer, demandons-nous ce qu'est un coffre dans un jeu: il s'agit d'un élément du décor qui est dynamique: il s'agit d'une tuile qui change dans notre carte lorsque le joueur appuie sur la touche d'action face à ce dernier.
Pour réaliser un coffre, il nous faut donc un nouvel événement: EVENT_CHANGETILE. Cet événement permettra de modifier la carte. Il sera très utile lors de la conception du jeu afin de fournir une carte plus dynamique: ouvrir un passage sur la carte, faire disparaître ou apparaître un décor est désormais à notre portée!
[modifier] Ajout d'un nouvel événement
L'ajout de l'événement s'effectue comme d'habitude:
On ajoute le nouvel événement au type énumérés contenant la liste des événements gérés (dans events.pbi):
;Type d'évènement : e_event_type Enumeration #EVENT_NULL #EVENT_TELEPORT #EVENT_DIALOG #EVENT_BATTLE #EVENT_SHOP #EVENT_INN #EVENT_CHANGETILE EndEnumeration
On ajoute plus bas le type paramètre de notre événement:
Structure s_event_param_changetile x.l y.l newtile.l EndStructure
Les paramètres sont simples à comprendre: lorsque l'événement est activé, la tuile à la position (x,y) prend la valeur newtile.
Profitons-en pour ajouter le prototype de la fonction associée à l'événement:
Declare DoEventChangetile(*event.s_event, *map.s_map)
Il faut ensuite modifier les fonctions d'écriture et lecture afin de pouvoir sauvegarder et charger notre nouvel événement (dans events.pb):
Procedure ReadEvent(*event.s_event)
Define.s_event_param_teleport *Ptparam_telport
Define.s_event_param_dialog *Ptparam_dialog
Define.s_event_param_battle *Ptparam_battle
Define.s_event_param_shop *Ptparam_shop
Define.s_event_param_inn *Ptparam_inn
Define.s_event_param_changetile *Ptparam_changetile
Define *buffer
Define.long *Ptr
Define.s FichierSon, Fichier, dialogue
Define.l i, j
InitEvent(*event)
*event\type = ReadLong(0)
If *event\type = #EVENT_NULL : ProcedureReturn : EndIf
*event\onaction = ReadLong(0)
*event\proba = ReadByte(0)
*event\player_anim = ReadLong(0)
*event\is_unique = ReadByte(0)
FichierSon = Space(256)
ReadData(0, @FichierSon, 63)
*event\sound=LoadSound(#PB_Any, FichierSon)
Select *event\type
Case #EVENT_NULL
Case #EVENT_TELEPORT
*event\param = AllocateMemory(SizeOf(s_event_param_teleport))
*Ptparam_telport = *event\param
Fichier = Space(256)
ReadData(0, @Fichier, 63)
If Len(Trim(Fichier)) = 0
*Ptparam_telport\filename = #NULL$
Else
*Ptparam_telport\filename = Fichier
EndIf
*Ptparam_telport\startX = ReadLong(0)
*Ptparam_telport\startY = ReadLong(0)
Case #EVENT_DIALOG
*event\param = AllocateMemory(SizeOf(s_event_param_dialog))
*Ptparam_dialog = *event\param
dialogue = Space(256)
ReadData(0, @dialogue, 255)
*Ptparam_dialog\dialog = dialogue
Case #EVENT_BATTLE
*event\param = AllocateMemory(SizeOf(s_event_param_battle))
*Ptparam_battle = *event\param
*Ptparam_battle\n_monstre = ReadLong(0)
*buffer = AllocateMemory(SizeOf(byte) * 63)
ReadData(0, *buffer, 63)
;If buffer[0] = 0
; ((struct s_event_param_battle*)event->param)->fond=NULL;
;Else
; ((struct s_event_param_battle*)event->param)->fond=strdup(buffer);
;EndIf
*buffer = AllocateMemory(SizeOf(byte) * 63)
ReadData(0, *buffer, 63)
;If buffer[0] = 0
; ((struct s_event_param_battle*)event->param)->fondu=NULL;
;Else
; ((struct s_event_param_battle*)event->param)->fondu=strdup(buffer);
;EndIf
*Ptparam_battle\monstres = AllocateMemory(*Ptparam_battle\n_monstre * SizeOf(s_monstre))
For i = 0 To *Ptparam_battle\n_monstre - 1
With *Ptparam_battle\monstres
\hp = ReadLong(0)
\hpmax = ReadLong(0)
\mp = ReadLong(0)
\mpmax = ReadLong(0)
\vitesse = ReadLong(0)
\force = ReadLong(0)
\dexterite = ReadLong(0)
\precision = ReadLong(0)
\intelligence = ReadLong(0)
\n_attaques = ReadLong(0)
\attaques = AllocateMemory(\n_attaques * SizeOf(long))
For j = 0 To \n_attaques - 1
ReadLong(0) ; stocker les valeurs lues dans \attaques
;fread(&((struct s_event_param_battle*)event->param)->monstres[i].attaques[i],SizeOf(unsigned int), 1, f);
Next j
EndWith
*buffer = AllocateMemory(SizeOf(byte) * 63)
ReadData(0, *buffer, 63)
;If(buffer[0]==0)
; ((struct s_event_param_battle*)event->param)->monstres[i].surf=NULL;
;Else
; ((struct s_event_param_battle*)event->param)->monstres[i].surf=SDL_LoadBMP(buffer);
; SDL_SetColorKey(((struct s_event_param_battle*)event->param)->monstres[i].surf, SDL_SRCCOLORKEY,SDL_MapRGB(((struct s_event_param_battle*)event->param)->monstres[i].surf->format,255,255,255));
Next i
Case #EVENT_SHOP
*event\param = AllocateMemory(SizeOf(s_event_param_shop))
*Ptparam_shop = *event\param
*Ptparam_shop\n_item = ReadLong(0)
*Ptparam_shop\itemId = AllocateMemory(*Ptparam_shop\n_item * SizeOf(LONG))
*Ptparam_shop\coutItem = AllocateMemory(*Ptparam_shop\n_item * SizeOf(LONG))
For i = 0 To *Ptparam_shop\n_item - 1
*Ptr = *Ptparam_shop\itemId + i * SizeOf(LONG)
*Ptr\l = ReadLong(0)
*Ptr = *Ptparam_shop\coutItem + i * SizeOf(LONG)
*Ptr\l = ReadLong(0)
Next i
*Ptparam_shop\prixVente = AllocateMemory(#MAX_ITEMS * SizeOf(LONG))
For i = 0 To #MAX_ITEMS-1
*Ptr = *Ptparam_shop\prixVente + i * SizeOf(LONG)
*Ptr\l = ReadLong(0)
Next i
Case #EVENT_INN
*event\param = AllocateMemory(SizeOf(s_event_param_inn))
*Ptparam_inn = *event\param
*Ptparam_inn\cout = ReadLong(0)
Case #EVENT_CHANGETILE
*event\param = AllocateMemory(SizeOf(s_event_param_changetile))
*Ptparam_changetile = *event\param
*Ptparam_changetile\x = ReadLong(0)
*Ptparam_changetile\y = ReadLong(0)
*Ptparam_changetile\newtile = ReadLong(0)
EndSelect
EndProcedure
Procedure WriteEvent(*event.s_event, bg_sound.s)
Define.s_event_param_dialog *Ptparam_dialog
Define.s_event_param_teleport *Ptparam_telport
Define.s_event_param_inn *Ptparam_inn
Define.s_event_param_shop *Ptparam_shop
Define.long *Ptr
Define *buffer
Define i
*buffer = AllocateMemory(256)
WriteLong(0, *event\type)
If *event\type = #EVENT_NULL : ProcedureReturn : EndIf
WriteLong(0, *event\onaction)
WriteByte(0, *event\proba)
WriteLong(0, *event\player_anim)
WriteByte(0, *event\is_unique)
WriteData(0, @bg_sound, Len(bg_sound))
WriteData(0, *buffer, 63 - Len(bg_sound))
Select *event\type
Case #EVENT_NULL
Case #EVENT_TELEPORT
*Ptparam_telport = *event\param
If *Ptparam_telport\filename <> #NULL$
WriteData(0, @*Ptparam_telport\filename, Len(*Ptparam_telport\filename))
EndIf
WriteData(0, *buffer,63 - Len(*Ptparam_telport\filename))
WriteLong(0, *Ptparam_telport\startX)
WriteLong(0, *Ptparam_telport\startY)
Case #EVENT_DIALOG
*Ptparam_dialog = *event\param
If *Ptparam_dialog\dialog <> #NULL$
WriteData(0, @*Ptparam_dialog\dialog, Len(*Ptparam_dialog\dialog))
EndIf
WriteData(0, *buffer, 255 - Len(*Ptparam_dialog\dialog))
Case #EVENT_BATTLE
; fwrite(&((struct s_event_param_battle*)event->param)->n_monstre, SizeOf(unsigned int), 1, f);
;
; memset(&buffer,0,SizeOf(buffer));
; If(((struct s_event_param_battle*)event->param)->fond!=NULL)
; strcpy(buffer,((struct s_event_param_battle*)event->param)->fond);
; fwrite(&buffer,63,SizeOf(char),f);
;
; memset(&buffer,0,SizeOf(buffer));
; If(((struct s_event_param_battle*)event->param)->fondu!=NULL)
; strcpy(buffer,((struct s_event_param_battle*)event->param)->fondu);
; fwrite(&buffer,63,SizeOf(char),f);
;
; For(i=0;i<((struct s_event_param_battle*)event->param)->n_monstre;i++)
; {
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].hp,
; SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].hpmax,
; SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].mp,
; SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].mpmax,
; SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].vitesse,
; SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].force,
; SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].dexterite,
; SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].precision,
; SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].intelligence,
; SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].n_attaques,
; SizeOf(unsigned int), 1, f);
; For(j=0;j<((struct s_event_param_battle*)event->param)->monstres[i].n_attaques;j++)
; fwrite(&((struct s_event_param_battle*)event->param)->monstres[i].attaques[i],
; SizeOf(unsigned int), 1, f);
;
; memset(&buffer,0,SizeOf(buffer));
; strcpy(buffer,"battlers/051-Undead01.bmp");
; fwrite(&buffer,63,SizeOf(char),f);
; }
; Break;
Case #EVENT_SHOP
*Ptparam_shop = *event\param
WriteLong(0, *Ptparam_shop\n_item)
For i = 0 To *Ptparam_shop\n_item - 1
*Ptr=*Ptparam_shop\itemId + i * SizeOf(LONG)
WriteLong(0, *Ptr\l)
*Ptr=*Ptparam_shop\coutItem + i * SizeOf(LONG)
WriteLong(0, *Ptr\l)
Next i
For i = 0 To #MAX_ITEMS - 1
*Ptr=*Ptparam_shop\prixVente + i * SizeOf(LONG)
WriteLong(0, *Ptr\l)
Next i
Case #EVENT_INN
*Ptparam_inn = *event\param
WriteLong(0, *Ptparam_inn\cout)
Case #EVENT_CHANGETILE
; fwrite(&((struct s_event_param_changetile*)event->param)->x, SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_changetile*)event->param)->y, SizeOf(unsigned int), 1, f);
; fwrite(&((struct s_event_param_changetile*)event->param)->newtile, SizeOf(unsigned int), 1, f);
EndSelect
EndProcedure
Il ne s'agit donc que d'ajouter 3 fread()/fwrite() dans nos fonctions afin de charger les paramètres. Rien de compliqué jusqu'à présent!
Il faut également penser à libérer les paramètres dans la fonction FreeEvent()
Procedure FreeEvent(*event.s_event)
Define.s_event *suivant
*Suivant.s_event
If *event = 0 : ProcedureReturn : EndIf
*event = *event\Next
While *event
*Suivant = *event\Next
If *event\sound : FreeSound(*event\sound) : EndIf
Select *event\type
Case #EVENT_NULL
Case #EVENT_TELEPORT
FreeMemory(*event\param)
Case #EVENT_DIALOG
FreeMemory(*event\param)
Case #EVENT_BATTLE
FreeMemory(*event\param)
Case #EVENT_SHOP
FreeMemory(*event\param)
Case #EVENT_INN
FreeMemory(*event\param)
Case #EVENT_CHANGETILE
FreeMemory(*event\param)
EndSelect
FreeMemory(*event)
*event = *Suivant
Wend
EndProcedure
Désormais, écrivons la fonction appellée lors du déclenchement de l'événement:
Procedure DoEventChangetile(*event.s_event, *map.s_map) Define.s_event_param_changetile *Ptparam_s_changetile *Ptparam_s_changetile = *event\param DataMap(GET_TILE(*Ptparam_s_changetile\x, *Ptparam_s_changetile\y, *map)) = *Ptparam_s_changetile\newtile EndProcedure
Il ne reste plus qu'à modifier DoEvent pour appeler la fonction que nous venons d'écrire lorsqu'un événement de type CHANTILE se déclenche:
Procedure DoEvent(*event.s_event, *libevent.s_lib_event, *map.s_map, *libsprite.s_lib_sprite, *player.s_player,*gui.s_gui)
Define.s_event_param_shop *Ptparam_shop
Define.s_event_param_inn *Ptparam_inn
If *event = #Null : ProcedureReturn 0 : EndIf
If *event\type = #EVENT_NULL : ProcedureReturn 0 : EndIf
If *event\sound : PlaySound(*event\sound) : EndIf
Select *event\type
Case #EVENT_TELEPORT
DoEventTeleport(*event, *libevent, *map, *libsprite)
ProcedureReturn 1
Case #EVENT_DIALOG
DoEventDialog(*event, *gui, *map, *libsprite)
Case #EVENT_BATTLE
DoEventBattle(*event, *libevent, *map, *libsprite)
Case #EVENT_SHOP
DoEventShop(*event, *map, *libsprite, *player, *gui)
Case #EVENT_INN
DoEventInn(*event, *map, *libsprite, *player, *gui)
Case #EVENT_CHANGETILE
DoEventChangetile(*event, *map)
EndSelect
If *event\is_unique
;suppr. de l'événement
Select *event\type
Case #EVENT_NULL
Case #EVENT_TELEPORT
FreeMemory(*event\param)
Case #EVENT_DIALOG
FreeMemory(*event\param)
Case #EVENT_BATTLE
FreeMemory(*event\param)
Case #EVENT_SHOP
*Ptparam_shop = *event\param
FreeMemory(*Ptparam_shop\itemId)
FreeMemory(*Ptparam_shop\coutItem)
FreeMemory(*Ptparam_shop\prixVente)
FreeMemory(*event\param)
Case #EVENT_INN
FreeMemory(*event\param)
Case #EVENT_CHANGETILE
FreeMemory(*event\param)
EndSelect
*event\type = #EVENT_NULL
*event\onaction = #EVENT_ACTION_AUTO
;*event\param = #Null
EndIf
ProcedureReturn 0
EndProcedure
Il est également nécessaire d'ajouter un cas au switch supprimant les événements s'ils sont uniques.
Une fois ces modifications effectuées, vous pouvez utiliser RPG 2D Generator afin de générer des niveaux comportant ce type d'événement.
[modifier] Conception du coffre (carte et événement)
Pour concevoir le coffre, il faut donc réaliser le travail suivant:
- on place un tile de coffre à un endroit de la carte
- on place un événement de type changetile afin de pouvoir l'ouvrir.
L'événement devrait ressembler à cela dans le générateur:
code à créer
Il s'agit d'un événement associé à un coffre situé à la position (2,3).
[modifier] Game Over
Dès lors que l'on implémente un système de combat, notre joueur peut mourir. Nous allons prévoir cela en implémentant une petite fonction affichant l'image de fin à l'écran et en jouant une musique.
Code a créer
[modifier] Système de combat tour par tour
[modifier] Principe général
Le principe du combat est le suivant: chaque opposant choisit chacun son tour une attaque et la lance.
Il s'agit d'une version très simplifiée des systèmes présents dans les RPG actuels, libre à vous de l'améliorer. Les combats opposants plus de deux combatants ne sont pas non plus gérés même si les types le permette. Libre à vous d'améliorer le système!
[modifier] Ajout des fichiers fight.pb et fight.pbi
Il est nécessaire pour commencer d'ajouter deux fichiers à notre projet: fight.pb et fight.pbi qui contiendront les fonctions associées au système de combat.
;/* ;** fight.pb ;** ;** Système de combat tour par tour. ;** ;*/ XIncludeFile "fight.pbi"
;/* ;** fight.pbi ;** ;** Système de combat tour par tour. ;** ;*/ A compléter
L'en-tête du fichier contient également quelques macros définissant la position des sprites des monstres et des joueurs selon leur n° dans l'équipe et leur taille. Il n'y a pas grand chose à comprendre ici, ces formules ayant été trouvées de manière empirique après quelques essais.
[modifier] Gestion des attaques
Nous allons devoir concevoir un système gérant:
- la liste des attaques du jeu avec leurs caractéristiques
- la liste des attaques disponibles pour chaque joueur
Note: les attaques des monstres seront gérés à la section suivante.
Dans ce but, nous allons ajouter les types nécessaires dans le fichier player.pbi:
Code a créer
Les prototypes des fonctions que nous allons écrire sont:
Declare InitLibAttaque(*lib_attaque.s_lib_attaque) Declare FreeLibAttaque(*lib_attaque.s_lib_attaque) Declare ChargeAttaques(*lib_attaque.s_lib_attaque)
Il reste à définir ces fonctions dans player.pb:
Code a créer
Nous pouvons charger et libérer les attaques, il faut désormais à chaque joueurs leurs attaques. Deux champs sont nécessaires dans les statistiques de chaque joueur (dans events.pbi):
structure s_stat_player A compléter EndStructure
[modifier] Gestion des monstres
La gestion des monstres est équivalente à celle des joueurs à quelques exceptions près puisque nous n'avons pas à gérer leur évolution lors de la partie.
Le type définissant un monstre est (dans player.pbi):
structure s_monstre A compléter EndStructure
Les prototypes nécessaires sont:
Declare InitMonstre(*monstre.s_monstre) Declare FreeMonstre(*monstre.s_monstre)
Les monstres sont initialisés et libérés par ces fonctions (dans player.pb)
Code a créer
[modifier] Affichage des animations
L'affichage des animations fonctionne grâce à un système de variables statiques: à chaque fois que la fonction est appellée, on vérifie si l'animation a changé depuis la dernière fois. Si c'est le cas, on initialise la nouvelle animation. Dans le cas contraire l'image suivante de l'animation est affichée.
La fonction renvoit le numéro de l'image affichée ou -1 si l'animation est finie.
Code a créer
[modifier] Affichage des dégâts
L'affichage des dégâts fonctionne d'une façon similaire à la fonction précédente. Dans ce cas, il faut mettre reinit à vrai pour que la fonction soit réinitialisée. Sinon les dégâts continuent d'être affichés à l'écran. Les trois premiers paramètres ne sont pas pris en compte si l'on n'est pas en train de réinitialiser la fonction.
Code a créer
[modifier] Affichage des joueurs et des monstres
L'affichage des joueurs et des monstres et une simple boucle for qui effectue une copie des surfaces en mémoire.
Code a créer
Code a créer
[modifier] Réaliser la fonction gérant le combat
La fonction gérant le combat est assez complexe car il faut:
- afficher la scène via les fonctions écrites juste avant
- gérer le menu
- gérer les tours (est-ce à l'ia ou au joueur de jouer?)
- vérifier si le combat est fini
Si le joueur gagne la partie, il obtient 100 pièces d'or. Il s'agit d'une récompense inclue dans le code et qui n'est pas paramétrable actuellement.
Code a créer
La fonction DoFight() gère le combat et le fait précéder du rendu spécifier dans les paramètres de l'événement.
Code a créer
[modifier] Gestion de l'événement associé
Les lecteurs les plus perspicaces auront remarqué que le code de la première section ne modifie pas que l'événement CHANGETILE. Le code concernant l'événement BATTLE a également changé afin de concorder avec le type ci-dessous:
Structure a créer
Le fondu contient le chemin du schéma de transition (cf fondus enchaînés). Le fond contient le chemin vers le décor de fond durant la bataille. Le nombre de monstres et un tableau contenant les monstres est également fourni.
Je reprécise ci-dessous quelques morceaux de code concernant cet événement particulier. Cependant, ces changements étaient déjà présent au début de l'article lors de la description du système permettant la gestion des coffres.
Dans ReadEvent():
Code a créer
Dans WriteEvent():
Code a créer
[modifier] Modification de la fonction principale
La seule modification dans la fonction principale est l'ajout du chargement des attaques ainsi que leur libération.
Compléter le fichier main.pb
[modifier] Notes concernant la mise à niveau pour la version 4.0
Les codes des tutoriels précédents sont réalisés avec la version 3.94, celui ci est mis à jour pour fonctionner avec la version 4.0.
Les principales modifications pour fonctionner avec la version 4.0 sont :
- ClearScreen() : Il n'y a plus qu'un paramètre couleur
- TransparentSpriteColor() : Il n'y a plus qu'un paramètre couleur
- FrontColor() : Il n'y a plus qu'un paramètre couleur
- Locate() : Cette commande est supprimée
- DrawText() : Cette commande intègre la position qui était autrefois dans Locate()
- TextHeight() : Remplace l'API GetTextExtentPoint32_()
- TextWidth() : Remplace la commande TextLengh(), c'est la même chose, il n'y a que le nom qui change
- Toutes les commandes de la bibliothèque File ont un paramètre supplémentaire. Ce nouveau paramètre permet de préciser le numéro du fichier concerné.
- Utilisation de With EndWith , pour montrer que ça existe :)
- Utilisation des macros
- EnableExplicit : Cette commande n'était pas indispensable, mais elle est conseillée.
Et j'en ai profité pour remplacer certaines couleurs par les constantes déjà définies dans les résidents , comme la couleur noire = #Black , ou blanche = #White ou encore la couleur rouge = #Red.
[modifier] Démo n°6: combats tour par tour, fondus enchaînés et coffre
Cette nouvelle démo apporte un monde bien plus étendu (6 cartes au lieu de 2), des combats contre deux types de monstres, la gestion des fondus enchaînés et un coffre. Enfin pas encore, seulement si un volontaire trouve du temps pour finir l'adaption du code original.



