Code source
Exécutable de l'éditeur de niveaux - exactement le même que la partie 21 (Windows 32bits)
Exécutable du jeu (Windows 32bits)
Avant d'essayer de compiler le code, allez dans l'onglet "Projets" dans le menu de gauche, séléctionnez l'onglet "Run" pour votre kit,
et mettez dans "Working directory" le chemin de "editor\data" pour l'éditeur ou "game\data" pour le jeu.
La base de données des objets
On va commencer par créer le fichier "items.xml" qu'on va utiliser dans le jeu.
Il va être assez différent de celui que j'ai écrit pour l'éditeur dans la dernière partie, qui contenait seulement
des noms.
Il faudra probablement qu'on change ça plus tard pour utiliser le même fichier aux 2 endroits, mais pour l'instant
on utilisera 2 fichiers différents pendant un petit moment.
Ce sera surement la plus grosse base de données du jeu, car elle va définir toutes les caractéristiques des 170
objets.
Mais on va commencer par une version simple et on ajoutera les différentes caractéristiques quand on en aura besoin
dans les parties futures.
Alors voyons de quelles données on a besoin pour afficher nos objets sur le sol:
<?xml version="1.0" encoding="utf-8"?>
<items>
<!-- ======================== Armes ======================== -->
<!-- OEIL DU TEMPS -->
<item name="ITEM001">
<category>Weapon</category>
<floor_image>00W_Ring.png</floor_image>
</item>
<!-- ROND D'ECLAIRS -->
<item name="ITEM002">
<category>Weapon</category>
<floor_image>00W_Ring.png</floor_image>
</item>
[...]
</items>
D'abord chaque objet a un nom. Je les ai ajoutés à "texts.xml" car ils apparaitront dans le jeu, mais on ne les
utilisera pas dans cette partie.
<?xml version="1.0" encoding="ISO-8859-1"?>
<texts>
[...]
<text id="ITEM001">OEIL DU TEMPS</text>
<text id="ITEM002">ROND D'ECLAIRS</text>
<text id="ITEM003">TORCHE</text>
<text id="ITEM004">FLAMITAINE</text>
[...]
</texts>
Ensuite, on trouve la catégorie de l'objet. Chaque objet appartient à une des 6 catégories suivantes:
- Weapon (Arme): Tous les objets que vous pouvez mettre dans votre main pour frapper les ennemis.
Ca ne contient pas seulement les épées et les haches, mais aussi les armes de lancer, les anneaux qui peuvent
lancer des sorts et les torches.
- Armor (Armure): Tout ce qu'il y a depuis les vêtements en tissu jusqu'aux armures de plaques.
Ca inclut aussi les casques et les boucliers.
- Scroll (Parchemin): Il n'y a qu'un type de parchemin dans ce jeu.
- Potion: Tout ce qui est dans une fiole. Ca peut être des potions, des bombes, et même des fioles
vides, ou remplies d'eau.
- Container (Contenant): Il n'y a qu'un type de contenant dans ce jeu: le coffre.
- Misc (Divers): Tous les autres objets, incluant les clés, les pièces, les colliers et plein de choses
diverses.
Ce champ catégorie ne sera pas utilisé dans cette partie, mais je l'ai ajouté parce que c'est plus clair pour trier
les objets.
A l'avenir, il pourrait être utile pour définir les paramètres additionnels. Par ex. une arme aura une valeur
d'attaque, une armure aura une valeur de défense, un parchemin aura un texte, etc...
Enfin, chaque objet dans la base de données contient le nom de l'image utilisée pour le dessiner sur le sol.
Vous pouvez les trouver dans "data/gfx/3DView/items_floor/", je les ai numérotées et triés par catégorie:
Les graphistes n'ont pas dessiné une image pour chaque objet, donc pour les 170 objets, il y a seulement 86 images
différentes.
Certains objets partagent la même image (par ex. les armures de plaque, la plupart des épées, la plupart des
potions, ...).
Ce fichier est lu par readObjectsDB() dans "objects.cpp", un nouveau fichier source pour toutes les fonctions
relatives aux objets.
Les données sont stockées dans le vecteur mObjectInfos.
Dans le jeu d'origine, les objets étaient placés bizarrement sur le sol:
Ils utilisaient probablement une grosse table de coordonnées pour chaque position possible.
Je ne voulais pas écrire une autre grosse table dans le code, alors j'ai trouvé une formule pour obtenir à peu près
les mêmes positions:
Ca m'a pris plus de temps que je le pensais.
Normalement, pour avoir un effet de perspective comme ça, vous devez diviser les coodonnées par la "profondeur" et
ajouter quelques coefficients pour redimensionner le tout.
Mais comme je l'ai dit auparavant, ce jeu est plein d'erreurs de perspective, alors j'ai fini avec une formule bizarre
avec un facteur de correction de 0.9 sorti de nulle part.
La voilà:
CVec2 CObjects::getObjectPos(CVec2 tablePos)
{
CVec2 pos;
pos.x = 250 * (tablePos.x - 4.5) / (8.5 - tablePos.y) + 112;
pos.y = 310 / (8.5 - 0.9 * tablePos.y) + 66;
return pos;
}
Néanmoins ça donne un résultat assez proche du jeu d'origine. Et cette fonction sera utile plus tard quand on
ajoutera les monstres.
Les rangées avant et arrière
Comme je l'ai dit quand on parlait des portes, les objets sur une case doivent être dessinés en 2 rangées séparées
parce que certains peuvent être cachés derrière une porte.
Donc on a 2 fonctions, une pour la rangée arrière (la plus loin de nous) et une pour la rangée avant (la plus
proche).
Voici la fonction pour la rangée arrière:
void CObjects::drawBackRow(QImage* image, CVec2 mapPos, CVec2 tablePos)
{
CVec2 pos1, pos2;
switch (player.dir)
{
case 0: // haut
pos1 = CVec2(0, 0);
pos2 = CVec2(1, 0);
break;
case 1: // gauche
pos1 = CVec2(0, 1);
pos2 = CVec2(0, 0);
break;
case 2: // bas
pos1 = CVec2(1, 1);
pos2 = CVec2(0, 1);
break;
case 3: // droite
pos1 = CVec2(1, 0);
pos2 = CVec2(1, 1);
break;
}
drawObjectsStack(image, mapPos * 2 + pos1, tablePos * 2 + CVec2(0, 0));
drawObjectsStack(image, mapPos * 2 + pos2, tablePos * 2 + CVec2(1, 0));
}
Vous pouvez voir que les coordonnées de la case sont multipliées par 2 parce qu'il y a 4 endroits pour poser les
tas d'objets dans une case.
Dans cette foncton, il y a 4 cas différents où on positionne les coordonnées en fonction de la direction dans
laquelle le joueur regarde.
Cette image va vous expliquer d'où viennent ces coordonnées:
La fonction pour la rangée de devant est la même à part pour les coordonnées.
Dessiner les objets est assez similaire aux décors des murs.
void CObjects::drawObjectsStack(QImage* image, CVec2 mapPos, CVec2 tablePos)
{
if (tablePos.y < 1 ||
tablePos.y > (WALL_TABLE_HEIGHT - 1) * 2)
return;
CObjectStack* stack = map.findObjectsStack(mapPos);
if (stack != NULL)
{
for (size_t i = 0; i < stack->getSize(); ++i)
{
int type = stack->getObject(i).getType();
if (type != 0)
{
// récupère l'image de l'objet
CObjectInfo object = mObjectInfos[type - 1];
QImage objectImage = fileCache.getImage(object.floorImage.toLocal8Bit().constData());
// récupère la position de l'objet et ajoute une valeur pseudo-aléatoire
CVec2 pos = getObjectPos(tablePos);
pos.x += ((mapPos.x - mapPos.y + i) * 53) % 7 - 3;
pos.y += (i < 4 ? i : 3);
// calcule la taille par rapport à la position de référence (la plus proche)
CVec2 pos0 = getObjectPos(CVec2(WALL_TABLE_WIDTH, (WALL_TABLE_HEIGHT - 1) * 2));
CVec2 pos1 = getObjectPos(CVec2(WALL_TABLE_WIDTH, tablePos.y));
float scale = (float)(pos1.x - 112) / (float)(pos0.x - 112);
// redimensionne l'objet dans une image temporaire
QImage scaledObject(objectImage.size() * scale, QImage::Format_ARGB32);
scaledObject.fill(QColor(0, 0, 0, 0));
graph2D.drawImageScaled(&scaledObject, CVec2(), objectImage, scale);
// assombrit l'objet en fonction de sa distance
float shadow = ((WALL_TABLE_HEIGHT - 1) * 2 - tablePos.y) * 0.13f;
graph2D.darken(&scaledObject, shadow);
// dessine l'objet à l'écran
pos -= CVec2(scaledObject.size().width() / 2, scaledObject.size().height() - 1);
graph2D.drawImage(image, pos, scaledObject, 0, false, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT));
}
}
}
}
On redimensionne l'image et on l'assombrit en fonction de la distance de l'objet.
Avant d'afficher l'objet, il on doit soustraire à la position la moitié de la largeur et toute le hauteur de l'image,
pour que la position qu'on a calculée dans getObjectPos() corresponde au milieu/bas de l'objet.
On fait ça parce que comme les objets sont posés sur le sol, 2 objets différents doivent être alignés par le bas.
Enfin, il y a encore une petite astuce que vous pouvez voir au début de la boucle: on ajoute un peu d'aléa à la
position pour éviter que les objets se superposent complètement quand il y a plusieurs fois le même objet dans un
tas.