Retour à l'accueil
Erin apprend le développement de jeu, du croquis au code puis au monde jouable

De zéro au jeu · Carnet d'apprentissage

Publié le 27/08/2026

DevLog #9 : C++ — Construire un inventaire pour spécialiser une classe de personnage

C++ — Construire un inventaire pour spécialiser une classe de personnage

Transformer un exercice en petit système de jeu

Je poursuis mon apprentissage du C++ en développant progressivement un petit système de combat inspiré des besoins que je pourrais rencontrer dans Embers of Fog.

Mon programme possédait déjà une classe Personnage, capable de combattre, de recevoir des dégâts et de se soigner. J’avais également créé une classe Mage héritant de Personnage, avec sa propre réserve de mana et une attaque magique.

Cette fois, je voulais aller un peu plus loin :

Comment donner un équipement et des consommables particuliers à chaque type de personnage ?

Un mage pourrait commencer avec des fioles de mana, un bâton et une épée de secours. Une future classe Guerriere pourrait recevoir une armure, des potions et différentes armes.

L’objectif n’était donc plus seulement de modifier quelques statistiques, mais de commencer à construire un véritable système d’inventaire réutilisable.


Ce que j’ai réussi à mettre en place

J’ai créé une classe Item contenant trois informations :

  • le nom de l’objet ;
  • sa quantité ;
  • sa valeur ou sa puissance.

J’ai ensuite créé une classe Inventaire, qui conserve plusieurs objets dans un std::vector<Item>.

Chaque personnage possède maintenant son propre inventaire. La classe Mage peut donc recevoir automatiquement son équipement lors de sa création :

cpp
inventaire.ajouterItem(Item("Fiole de Mana", 5, 20));
inventaire.ajouterItem(Item("Potion de Soin", 2, 10));
inventaire.ajouterItem(Item("Baton de Mage", 1, 0));
inventaire.ajouterItem(Item("Epee de Mage", 1, 0));

Le mage peut rechercher une fiole par son nom, vérifier sa quantité, récupérer sa valeur puis la consommer :

cpp
Item *fiole = inventaire.getItem("Fiole de Mana");

if (fiole && fiole->quantite > 0) {
    mana += fiole->valeur;
    fiole->quantite -= 1;
}

Ce petit morceau de code m’a permis de travailler plusieurs notions importantes :

  • la composition entre plusieurs classes ;
  • l’utilisation d’un std::vector ;
  • la recherche d’un élément dans une collection ;
  • les pointeurs et nullptr ;
  • la modification d’un objet trouvé dans un inventaire ;
  • l’héritage entre Personnage et Mage ;
  • la redéfinition d’une méthode avec virtual et override.

Et surtout, j’ai commencé à comprendre comment plusieurs petits systèmes peuvent communiquer pour produire un comportement de jeu plus crédible.


À votre tour : équipez le mage

Je vous propose de repartir d’une version simplifiée de mon programme.

Dans cette version, le système d’inventaire a volontairement disparu. Le mage sait encore utiliser sa magie, mais il ne possède aucun objet.

Votre mission consiste à reconstruire son inventaire.

Objectif principal

À la fin de l’exercice, un mage devra commencer la partie avec :

  • 5 fioles de mana restaurant 20 points chacune ;
  • 2 potions de soin restaurant 10 points chacune ;
  • 1 bâton de mage ;
  • 1 épée de secours.

Lorsqu’il n’a plus assez de mana, il doit pouvoir utiliser son épée pour effectuer une attaque physique.


Code de départ

cpp
#include <algorithm>
#include <iostream>
#include <string>

class Personnage {
public:
    std::string nom;
    int maxHp;
    int hpActuels;
    int attaque;

    Personnage(
        const std::string &nomDepart,
        int hpMaxDepart,
        int attaqueDepart
    )
        : nom(nomDepart),
          maxHp(hpMaxDepart),
          hpActuels(hpMaxDepart),
          attaque(attaqueDepart) {
        std::cout << "[Personnage] " << nom
                  << " a ete cree avec "
                  << maxHp << " HP et "
                  << attaque << " ATK."
                  << std::endl;
    }

    virtual ~Personnage() = default;

    void recevoirDegats(int degats) {
        hpActuels = hpActuels - degats;

        if (hpActuels <= 0) {
            hpActuels = 0;
            std::cout << "[!] " << nom
                      << " est mort..."
                      << std::endl;
        } else {
            std::cout << "[i] " << nom
                      << " a " << hpActuels
                      << " HP restants."
                      << std::endl;
        }
    }

    void seSoigner(int soin) {
        hpActuels = std::min(hpActuels + soin, maxHp);

        std::cout << "[+] " << nom
                  << " remonte a "
                  << hpActuels << " HP."
                  << std::endl;
    }

    virtual void attaquer(Personnage &cible) {
        std::cout << "\n[Attaque Basique] "
                  << nom << " frappe "
                  << cible.nom << " !"
                  << std::endl;

        cible.recevoirDegats(attaque);
    }
};

class Mage : public Personnage {
public:
    int mana;

    Mage(
        const std::string &nomDepart,
        int hpMaxDepart,
        int attaqueDepart,
        int manaDepart
    )
        : Personnage(
              nomDepart,
              hpMaxDepart,
              attaqueDepart
          ),
          mana(manaDepart) {
        std::cout << "[Specialisation] "
                  << nom
                  << " canalise la magie avec "
                  << mana << " MP !"
                  << std::endl;
    }

    void lancerSort(Personnage &cible) {
        if (mana >= 10) {
            std::cout << "\n[Magie] "
                      << nom
                      << " sacrifie 10 MP !"
                      << std::endl;

            mana -= 10;
            cible.recevoirDegats(attaque * 2);
        } else {
            std::cout << "\n[Epuise] "
                      << nom
                      << " manque de mana."
                      << std::endl;
        }
    }

    void attaquer(Personnage &cible) override {
        if (mana >= 10) {
            lancerSort(cible);
        } else {
            std::cout << "\n[Attaque Physique] "
                      << nom
                      << " frappe sans magie !"
                      << std::endl;

            cible.recevoirDegats(attaque);
        }
    }
};

int main() {
    std::cout << "--- Initialisation du Monde ---\n"
              << std::endl;

    Personnage erin(
        "Erin la Guerriere",
        100,
        25
    );

    Mage frieren(
        "Frieren la Mage",
        100,
        25,
        100
    );

    Mage mageEpuise(
        "Serie la Mage",
        100,
        25,
        0
    );

    Personnage gobelin(
        "Skrag le Gobelin",
        100,
        15
    );

    std::cout << "\n--- Debut de l'action ---"
              << std::endl;

    gobelin.attaquer(erin);
    erin.attaquer(gobelin);
    frieren.attaquer(gobelin);

    std::cout << "\n--- Mage sans mana ---"
              << std::endl;

    mageEpuise.attaquer(gobelin);

    return 0;
}

Étape 1 — Créer un objet

Créez une classe Item.

Elle doit contenir :

cpp
std::string nom;
int quantite;
int valeur;

Ajoutez également un constructeur permettant d’initialiser ces trois propriétés.

Question

Pourquoi est-il préférable de créer une classe Item plutôt que trois variables séparées pour chaque objet ?


Étape 2 — Créer l’inventaire

Créez une classe Inventaire contenant :

cpp
std::vector<Item> items;

Vous devrez donc réintroduire l’en-tête suivant :

cpp
#include <vector>

Ajoutez ensuite une méthode permettant d’insérer un objet :

cpp
void ajouterItem(const Item &item);

Indice

std::vector possède une méthode appelée push_back.


Étape 3 — Rechercher un objet

Ajoutez cette méthode à la classe Inventaire :

cpp
Item *getItem(const std::string &nom);

Elle doit parcourir le tableau et retourner l’adresse de l’objet correspondant.

Si aucun objet ne possède ce nom, elle doit retourner :

cpp
nullptr

Question

Pourquoi faut-il vérifier le pointeur avant d’utiliser l’objet retourné ?


Étape 4 — Donner un inventaire aux personnages

Ajoutez un inventaire dans la classe Personnage :

cpp
Inventaire inventaire;

Grâce à la composition, tous les personnages disposeront désormais d’un inventaire, même si son contenu dépendra de leur spécialisation.


Étape 5 — Créer l’équipement du mage

Dans le constructeur de Mage, ajoutez :

  • 5 fioles de mana d’une valeur de 20 ;
  • 2 potions de soin d’une valeur de 10 ;
  • un bâton ;
  • une épée.

Vous pouvez utiliser une instruction de cette forme :

cpp
inventaire.ajouterItem(
    Item("Nom de l'objet", quantite, valeur)
);

Étape 6 — Consommer une fiole de mana

Créez une méthode :

cpp
void utiliserFioleMana();

Elle devra :

  1. rechercher une fiole de mana ;
  2. vérifier que l’objet existe ;
  3. vérifier que sa quantité est supérieure à zéro ;
  4. ajouter sa valeur à la réserve de mana ;
  5. diminuer sa quantité d’une unité ;
  6. afficher le nombre de fioles restantes.

Si aucune fiole n’est disponible, affichez un message adapté.


Bonus — Vérifier réellement l’équipement

Lorsqu’un mage n’a plus de mana, ne supposez pas automatiquement qu’il possède une épée.

Recherchez d’abord l’objet :

cpp
Item *epee = inventaire.getItem("Epee de Mage");

Vérifiez ensuite que epee n’est pas égal à nullptr.

Si l’épée n’existe pas, le mage pourrait :

  • attaquer à mains nues ;
  • infliger moins de dégâts ;
  • passer son tour ;
  • chercher un autre objet dans son inventaire.

Cette vérification évite qu’un pointeur invalide provoque l’arrêt brutal du programme.


Pour aller plus loin

Une fois l’inventaire fonctionnel, plusieurs améliorations deviennent possibles :

  • regrouper automatiquement deux objets portant le même nom ;
  • empêcher les quantités négatives ;
  • supprimer les objets épuisés ;
  • afficher tout le contenu de l’inventaire ;
  • définir une quantité maximale ;
  • créer différents types d’objets ;
  • équiper ou déséquiper une arme ;
  • utiliser une potion de soin ;
  • créer une véritable classe Guerriere héritant de Personnage.

Cette dernière étape permettrait de transformer Erin en spécialisation complète, comme le mage :

cpp
class Guerriere : public Personnage {
    // Armure, endurance et compétences propres
};

Erin ne serait alors plus simplement un Personnage portant le nom « Guerrière ». Elle posséderait réellement les comportements, les statistiques et l’équipement propres à cette classe.


Ma solution

Ma solution complète sera ajoutée sous cette section.

Avant de l’ouvrir, essayez de réaliser au moins les quatre premières étapes. Même si le programme ne compile pas immédiatement, les erreurs font partie de l’exercice : apprendre à les lire est aussi important qu’obtenir le résultat final.

La solution présentée correspond à mon propre parcours d’apprentissage. Ce n’est pas l’unique manière de construire un inventaire en C++, mais c’est celle qui m’a permis de comprendre progressivement la relation entre objets, collections, pointeurs, composition et héritage.

Voir la solution
cpp
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

class Item {
public:
  std::string nom;
  int quantite;
  int valeur;

  Item(const std::string &nomDepart, int quantiteDepart, int valeurDepart)
      : nom(nomDepart), quantite(quantiteDepart), valeur(valeurDepart) {
    std::cout << "[Item] " << nom << " a ete cree avec succes (" << quantite
              << " quantite, " << valeur << " valeur) !" << std::endl;
  }
};

class Inventaire {
public:
  std::vector<Item> items;

  void ajouterItem(const Item &item) { items.push_back(item); }

  Item *getItem(const std::string &nom) {
    for (auto &item : items) {
      if (item.nom == nom) {
        return &item;
      }
    }
    return nullptr;
  }
};

class Personnage {
public:
  // Les Attributs
  std::string nom;
  int maxHp;
  int hpActuels;
  int attaque;
  Inventaire inventaire;
  // --- LE CONSTRUCTEUR ---
  // Il prend les valeurs de départ en paramètres et les attribue aux variables
  // de l'objet
  Personnage(const std::string &nomDepart, int hpMaxDepart, int attaqueDepart)
      : nom(nomDepart), maxHp(hpMaxDepart), hpActuels(hpMaxDepart),
        attaque(attaqueDepart) {
    std::cout << "[Moule] " << nom << " a ete cree avec succes (" << maxHp
              << " HP, " << attaque << " ATK) !" << std::endl;
  }

  // Les Méthodes
  void recevoirDegats(int degats) {
    hpActuels = hpActuels - degats;
    if (hpActuels <= 0) {
      hpActuels = 0;
      std::cout << "[!] " << nom << " est mort..." << std::endl;
    } else {
      std::cout << "[i] " << nom << " a " << hpActuels << " HP restants."
                << std::endl;
    }
  }

  void seSoigner(int potion) {
    hpActuels = std::min(hpActuels + potion, maxHp);
    std::cout << "[+] " << nom << " remonte a " << hpActuels << " HP."
              << std::endl;
  }
  // En ajoutant "virtual", on permet aux classes filles de réécrire cette
  // action !
  virtual void attaquer(Personnage &cible) {
    std::cout << "\n[Attaque Basique] " << nom << " frappe " << cible.nom
              << " !" << std::endl;
    cible.recevoirDegats(attaque);
  }

  void volerVie(Personnage &cible) {
    std::cout << "\n[Alerte] " << nom << " vole la vie de " << cible.nom << " !"
              << std::endl;
    cible.recevoirDegats(attaque);
    seSoigner(attaque / 2); // On récupère la moitié des dégâts infligés
  }
};

// Le ": public Personnage" signifie que Mage hérite de TOUT ce que possède
// Personnage
class Mage : public Personnage {
public:
  // Nouvel attribut exclusif au Mage
  int mana;

  // LE CONSTRUCTEUR DU MAGE
  // Il prend les stats de base + le mana.
  // Il demande ensuite au constructeur "Personnage" de faire le travail de base
  // !
  Mage(const std::string &nomDepart, int hpMaxDepart, int attaqueDepart,
       int manaDepart)
      : Personnage(nomDepart, hpMaxDepart, attaqueDepart), mana(manaDepart) {
    std::cout << "[Specialisation] " << nom << " canalise la magie avec "
              << mana << " MP !" << std::endl;

    inventaire.ajouterItem(Item("Fiole de Mana", 5, 20));
    inventaire.ajouterItem(Item("Potion de Soin", 2, 10));
    inventaire.ajouterItem(Item("Baton de Mage", 1, 0));
    inventaire.ajouterItem(Item("Epee de Mage", 1, 0));
  }

  void utiliserFioleMana() {
    Item *fiole = inventaire.getItem("Fiole de Mana");
    if (fiole && fiole->quantite > 0) {
      mana += fiole->valeur; // 20
      fiole->quantite -= 1;
      std::cout << "[+] " << nom << " boit une fiole (+" << fiole->valeur
                << " mana). Restantes : " << fiole->quantite << std::endl;
    } else {
      std::cout << "[!] " << nom << " n'a pas de fiole de mana."
                << " Il utilise alors une attaque physique ! Avec son "
                << inventaire.getItem("Epee de Mage")->nom << " !" << std::endl;
    }
  }

  // Nouvelle méthode exclusive
  void lancerSort(Personnage &cible) {
    if (mana >= 10) {
      std::cout << "\n[Magie] " << nom << " incante un sort et sacrifie 10 MP !"
                << std::endl;
      mana = mana - 10;
      cible.recevoirDegats(attaque *
                           2); // Le sort fait très mal (dégâts doublés)
    } else {
      std::cout << "\n[Epuise] " << nom
                << " essaie de lancer un sort, mais manque de mana..."
                << std::endl;
    }
  }

  void attaquer(Personnage &cible) override {
    if (mana >= 10) {
      std::cout << "\n[Attaque Magique] " << nom << " utilise son baton sur "
                << cible.nom << " !" << std::endl;
      lancerSort(cible);
    } else {
      std::cout << "\n[Attaque Physique] " << nom
                << " n'a plus de mana et frappe avec son Epee de Mage !"
                << std::endl;
      cible.recevoirDegats(attaque);
    }
  }
};

int main() {
  std::cout << "--- Initialisation du Monde ---\n" << std::endl;

  Personnage joueurErin("Erin la Guerriere", 100, 25);
  Mage mageFrieren("Frieren la Mage", 100, 25, 100);
  Mage mageEpuise("Serie la Mage", 100, 25, 0); // 0 mana → épée
  Personnage gobelin("Skrag le Gobelin", 100, 15);

  std::cout << "\n--- Debut de l'action ---" << std::endl;

  gobelin.attaquer(joueurErin);
  joueurErin.attaquer(gobelin);
  mageFrieren.attaquer(gobelin); // magie (assez de mana)

  std::cout << "\n--- Test : mage sans mana ---" << std::endl;
  mageEpuise.attaquer(gobelin); // épée

  return 0;
}

Ce que je retiens de cet exercice

Ce système reste volontairement simple, mais il représente une étape importante dans mon apprentissage.

Je ne manipule plus seulement des variables isolées. Je commence à réfléchir en termes de responsabilités :

  • Item décrit un objet ;
  • Inventaire conserve et recherche les objets ;
  • Personnage possède un inventaire ;
  • Mage décide quels objets correspondent à sa spécialisation ;
  • les méthodes de combat utilisent ces objets pour modifier leur comportement.

C’est précisément cette manière de découper les responsabilités qui permet ensuite de construire des systèmes plus grands sans transformer tout le programme en un énorme bloc impossible à maintenir.

La prochaine étape pourrait être de créer une véritable classe Guerriere pour Erin, avec son propre équipement et ses propres capacités.

Et, petit à petit, l’exercice commence à ressembler à un jeu. 💜

🔥 Défi pour le prochain devlog : l’objet fantôme

Notre inventaire fonctionne, mais une ligne mérite toute votre attention :

cpp
inventaire.getItem("Epee de Mage")->nom

Tant que l’épée existe, tout se passe correctement.

Mais que se produirait-il si nous retirions "Epee de Mage" de l’inventaire du mage ?

cpp
// inventaire.ajouterItem(Item("Epee de Mage", 1, 0));

Le programme pourrait-il continuer normalement ? Que retourne getItem() lorsque l’objet demandé n’existe pas ? Et surtout : est-il prudent d’utiliser directement ->nom sans vérifier le résultat ?

Votre mission

Essayez de rendre l’attaque physique suffisamment sûre pour que le programme ne plante jamais, même si le mage :

  • ne possède pas d’épée ;
  • possède une épée en quantité nulle ;
  • cherche un objet dont le nom est incorrect.

Vous devrez également décider ce que fait le mage lorsqu’il n’a ni mana ni arme :

  • attaquer à mains nues ;
  • infliger moins de dégâts ;
  • passer son tour ;
  • afficher un message particulier.

Je présenterai ma solution dans le prochain devlog.

Et puisque nous commençons à parler d’équipement propre à chaque spécialisation, nous tenterons ensuite de transformer Erin en une véritable classe Guerriere, avec son inventaire et ses capacités particulières. 😈

Petit indice : avant de suivre une flèche, assurez-vous toujours qu’elle pointe réellement vers quelque chose.


Rédigé par Erin.

Commentaires (0)

Rejoins la guilde pour laisser un commentaire !

Se connecter

Cet article n'a pas encore de commentaires. Ouvre le bal !