JavaScript Object Notation
Un fichier JSON est un document texte léger qui sert à stocker et à échanger des données structurées entre des programmes informatiques.
Il est très utilisé pour le transfert de données entre un serveur et une application web (les API), et pour enregistrer les fichiers de configuration d'un logiciel. Il est lisible par un humain et facile à analyser par une machine : c'est donc un excellent format pour tout ce qui doit pouvoir changer sans recompiler — dialogues, statistiques d'entités, tables de loot, réglages graphiques.
- Aucun
Organisation d'un fichier à l'extension .json
Le format repose sur des paires de clé/valeur. Un objet s'écrit entre accolades {}, chaque clé est un texte entre guillemets doubles "", un deux-points : sépare la clé de sa valeur, et une virgule , sépare chaque élément. Une valeur peut être l'un des six types suivants :
- un texte : une chaîne de caractères entre guillemets doubles ;
- un nombre : entier ou décimal, jamais entre guillemets ;
- un booléen : true ou false ;
- la valeur null (vide) ;
- un objet : un autre bloc entre accolades ;
- un tableau : une liste entre crochets [].
Attention : le JSON strict n'accepte ni commentaire, ni virgule traînante après le dernier élément. C'est la première cause d'erreur de parsing.
/// JSON — Bible.json — update:30/09/26
{
"meta": { "version": 1, "game": "Spammy" },
"dialogue": [{
"id": "intro_gluant_01",
"speaker": "Gluant",
"lines": [{
"text": "Blooorp..",
"duration": 2.5},
{
"text": "Tu veux planter quelque chose ?", "duration": 3.0
}],
"next": "intro_pote_01"
}],
"entities": [{
"id": "ptipote_base",
"speed": 1.2,
"radius": 0.4,
"level": 1,
"actions": ["plant", "water", "explore"]
}]
}
Inclure l'en-tête nlohmann dans ton projet
nlohmann/json est une bibliothèque header-only : il n'y a rien à compiler, rien à linker, aucun .a ni .dylib à embarquer. Tu déposes le dossier nlohmann/ dans tes en-têtes externes, à côté de stb_image.h, et c'est terminé.
Structure du projet
MSLearn/
├── application/ …
│ └── macOS/
│ └── macOSInfo.plist
└── includes/ …
├── …
└── extern/
├── stb-image.h
└── nlohmann ← inclus le dossier ici
├── detail ← il contient les 8 dossiers de fichiers et fichiers
├── LICENSES
├── thirdparty
├── adl_serializer.hpp
├── byte_container_with_subtype.hpp
├── json.hpp
├── json_fwd.hpp
└── ordered_map.hpp
Aucune autre configuration sur Xcode ou par compilation Make supplémentaires n'est nécessaire.
Inclus le fichier à ta classe principale
/// C++ — Renderer.hpp — update:30/09/26 — pas de macro
#include "ParserJSON.hpp" // ton fichier ci-dessous
Le fichier …/nlohmann/json.hpp fait environ 25 000 lignes : chaque fichier qui l'inclut paie ce temps de compilation. Ne l'inclus que dans les fichiers qui parsent réellement du JSON — ici uniquement ParserJSON.hpp. Ailleurs, si tu as seulement besoin de déclarer un nlohmann::json& en paramètre, inclus <nlohmann/json_fwd.hpp>.
Dépôt GitHub & documentation (README) : nlohmann/json (à cloner, ou récupère l'archive include.zip de la dernière release).
Parser ce fichier .json
Le principe de la bibliothèque : tu écris une fonction libre from_json() par structure, et nlohmann la trouve tout seul par ADL (Argument-Dependent Lookup). À partir de là, get<T>() fonctionne sur T, mais aussi sur std::vector<T>, sans une ligne de plus. Les six types de données possibles sont parsés ci-dessous à titre d'exemple :
/// C++ — ParserJSON.hpp — update:30/09/26
#ifndef ParserJSON_hpp
#define ParserJSON_hpp
#include <string>
#include <vector>
#include <stdexcept>
#include <fstream>
#include <../includes/extern/nlohmann/json.hpp> // le fichier nlohmann
struct DialogueLine
{
std::string text;
float duration = 2.89f;
};
struct DialogueNode
{
std::string id;
std::string speaker;
std::vector<DialogueLine> lines;
std::string next;
};
struct EntityDef
{
std::string id;
float speed = 1.0f;
float radius = 0.5f;
int level = 1;
std::vector<std::string> actions;
};
struct Bible
{
int version = 0;
std::string gameName;
std::vector<DialogueNode> dialogues;
std::vector<EntityDef> entities;
};
inline void from_json(const nlohmann::json& j, DialogueLine& l)
{
j.at("text").get_to(l.text);
l.duration = j.value("duration", 2.0f); // value() = avec fallback
}
inline void from_json(const nlohmann::json& j, DialogueNode& n)
{
j.at("id").get_to(n.id);
j.at("speaker").get_to(n.speaker);
j.at("lines").get_to(n.lines);
n.next = j.value("next", std::string{});
}
inline void from_json(const nlohmann::json& j, EntityDef& e)
{
j.at("id").get_to(e.id);
e.speed = j.value("speed", 1.0f);
e.radius = j.value("radius", 0.5f);
e.level = j.value("level", 1);
e.actions = j.value("actions", std::vector<std::string>{});
}
class ParserJSON
{
public:
static Bible load(const std::string& path)
{
std::ifstream file(path);
if (!file.is_open())
throw std::runtime_error("[ParserJSON] Cannot open: " + path);
nlohmann::json root;
try {
root = nlohmann::json::parse(file);
} catch (const nlohmann::json::parse_error& e) {
throw std::runtime_error("[ParserJSON] Parse error: " + std::string(e.what()));
}
Bible bible;
if (root.contains("meta"))
{
bible.version = root["meta"].value("version", 0);
bible.gameName = root["meta"].value("game", std::string{});
}
if (root.contains("dialogue"))
bible.dialogues = root["dialogue"].get<std::vector<DialogueNode>>();
if (root.contains("entities"))
bible.entities = root["entities"].get<std::vector<EntityDef>>();
return bible;
}
};
#endif /* ParserJSON_hpp */
at("clé") lève une exception json::out_of_range si la clé manque : réserve-le aux champs sans lesquels la donnée n'a aucun sens (un dialogue sans id est inutilisable). value("clé", défaut) retourne le défaut silencieusement : utilise-le pour tout champ facultatif. Et n'utilise jamais j["clé"] sur un json non-const en lecture — l'opérateur crée la clé avec une valeur nulle au lieu de te signaler l'erreur.
Implémentation générale
/// C++ — Renderer.hpp — update:30/09/26 — pas de macro
private:
Bible m_bible;
const DialogueNode* findDialogue(const std::string& id) const;
const EntityDef* findEntity(const std::string& id) const;
/// C++ — Renderer.cpp — update:30/09/26 — initialisation
{
try {
m_bible = ParserJSON::load(resourcePath + "/Bible.json");
} catch (const std::exception& e) {
std::cerr << e.what() << "\n";
throw; // un jeu sans sa Bible ne démarre pas : on ne masque pas l'erreur
}
}
/// C++ — Renderer.cpp — update:30/09/26 — fonction()
const DialogueNode* Renderer::findDialogue(const std::string& id) const
{
for (const auto& n : m_bible.dialogues)
if (n.id == id) return &n;
return nullptr;
}
const EntityDef* Renderer::findEntity(const std::string& id) const
{
for (const auto& e : m_bible.entities)
if (e.id == id) return &e;
return nullptr;
}
Ces fonctions retournent un pointeur vers l'intérieur de m_bible.dialogues. Un seul push_back() sur ce vector après le chargement réalloue le tableau et transforme tous les pointeurs déjà distribués en pointeurs pendants. Règle simple : la Bible est chargée une fois au démarrage, puis traitée comme immuable.
Utiliser les données parsées
Le parsing ne sert à rien tant que les structures restent dans un coin. Voici les deux usages concrets que le fichier Bible.json alimente : jouer un dialogue et instancier une entité.
Jouer un dialogue
Le nœud est retrouvé par son id, chaque ligne reste affichée pendant sa duration, puis le champ next enchaîne automatiquement sur le nœud suivant.
/// C++ — Renderer.hpp — update:30/09/26 — état du dialogue
private:
const DialogueNode* m_currentNode = nullptr;
size_t m_currentLine = 0;
float m_lineTimer = 0.0f;
public:
void startDialogue(const std::string& id);
void updateDialogue(float dt);
const std::string* currentText() const;
/// C++ — Renderer.cpp — update:30/09/26 — lecture séquentielle
void Renderer::startDialogue(const std::string& id)
{
m_currentNode = findDialogue(id);
m_currentLine = 0;
m_lineTimer = 0.0f;
if (!m_currentNode)
std::cerr << "[Renderer] Dialogue introuvable: " << id << "\n";
}
void Renderer::updateDialogue(float dt)
{
if (!m_currentNode) return;
const auto& lines = m_currentNode->lines;
m_lineTimer += dt;
if (m_lineTimer < lines[m_currentLine].duration) return;
m_lineTimer = 0.0f;
++m_currentLine;
if (m_currentLine < lines.size()) return; // il reste des lignes dans ce nœud
if (!m_currentNode->next.empty())
startDialogue(m_currentNode->next); // enchaîne : "intro_pote_01"
else
m_currentNode = nullptr; // fin de la conversation
}
const std::string* Renderer::currentText() const
{
if (!m_currentNode) return nullptr;
return &m_currentNode->lines[m_currentLine].text;
}
Côté boucle de rendu, l'appel tient en deux lignes :
/// C++ — Renderer.cpp — update:30/09/26 — draw()
updateDialogue(deltaTime);
if (const std::string* line = currentText())
drawSubtitle(*line, m_currentNode->speaker);
Créer une entité depuis sa définition
L'EntityDef est le gabarit lu dans le fichier ; l'Entity est l'instance vivante en jeu. Le gabarit n'est jamais modifié, il est copié à chaque spawn — c'est ce qui te permet de faire apparaître cent ptipote_base et de rééquilibrer leur vitesse en éditant une seule ligne de JSON.
/// C++ — Entity.hpp — update:30/09/26 — l'instance runtime
struct Entity
{
std::string defId; // ← EntityDef::id d'origine
simd::float3 position = { 0, 0, 0 };
float speed = 1.0f;
float radius = 0.5f;
int level = 1;
bool canPlant = false;
};
/// C++ — Renderer.cpp — update:30/09/26 — nécessite <algorithm>
void Renderer::spawnEntity(const std::string& defId, simd::float3 position)
{
const EntityDef* def = findEntity(defId);
if (!def)
throw std::runtime_error("[Renderer] EntityDef inconnu: " + defId);
Entity e;
e.defId = def->id;
e.position = position;
e.speed = def->speed;
e.radius = def->radius;
e.level = def->level;
e.canPlant = std::find(def->actions.begin(), def->actions.end(), "plant")
!= def->actions.end();
m_entities.push_back(e);
}
Écrire un fichier .json
L'opération inverse s'écrit avec to_json(), la jumelle de from_json(). C'est ce qui te donne gratuitement une sauvegarde de partie, ou un éditeur de niveau qui réécrit la Bible depuis le jeu.
/// C++ — ParserJSON.hpp — update:30/09/26 — sérialisation
inline void to_json(nlohmann::json& j, const EntityDef& e)
{
j = nlohmann::json{
{ "id", e.id },
{ "speed", e.speed },
{ "radius", e.radius },
{ "level", e.level },
{ "actions", e.actions }
};
}
static void save(const Bible& b, const std::string& path)
{
nlohmann::json root;
root["meta"]["version"] = b.version;
root["meta"]["game"] = b.gameName;
root["entities"] = b.entities; // appelle to_json() sur chaque élément
std::ofstream out(path);
if (!out.is_open())
throw std::runtime_error("[ParserJSON] Cannot write: " + path);
out << root.dump(4); // 4 = indentation ; sans argument = tout sur une ligne
}
Le parsing est une opération de chargement, jamais de frame. Un nlohmann::json est un arbre de nœuds alloués sur le tas, et une recherche par clé y coûte une comparaison de chaînes : tu le lis une fois, tu le convertis en struct C++ plates, et tu ne gardes pas l'objet json vivant. Dans ta boucle de rendu, il ne doit plus rester que des float et des std::vector.
/// C++ — Renderer.hpp — update:30/09/26 — pas de macro
private :
Bible m_bible;
const DialogueNode* findDialogue(const std::string& id) const;
/// C++ — Renderer.cpp — update:30/09/26 — initialisation
{
m_bible = ParserJSON::load(resourcePath + "/Bible.json");
}
/// C++ — Renderer.cpp — update:30/09/26 — fonction()
const DialogueNode* Renderer::findDialogue(const std::string& id) const
{
for (auto& n : m_bible.dialogues)
if (n.id == id) return &n;
return nullptr;
}
Licences des dépendances pour les projets du site
Toutes les bibliothèques utilisées ici sont exploitables dans un projet commercial. Trois d'entre elles demandent en échange que leur licence et leur copyright soient reproduits quelque part dans le produit livré — un fichier CREDITS.txt dans le bundle, ou un écran « Crédits » dans le menu, suffit.
| Bibliothèque | Licence | Usage commercial | Mention obligatoire |
|---|---|---|---|
| nlohmann/json | MIT | ✓ Oui | Oui — copyright + texte MIT |
| Hedley (embarqué dans json.hpp) | CC0 1.0 / domaine public | ✓ Oui | Non |
| Assimp | BSD 3-Clause | ✓ Oui | Oui — dans docs/credits |
| stb_image | Public Domain / MIT | ✓ Oui | Non |
| metal-cpp | Apache 2.0 | ✓ Oui | Oui — fichier NOTICE |
| simd (Apple) | Xcode License | ✓ Oui | Non |
Le dossier nlohmann/LICENSES que tu as copié contient déjà les textes à reproduire (MIT pour la bibliothèque, CC0 pour Hedley). Ne le supprime pas de ton arborescence : c'est lui qui te met en règle. Ce chapitre décrit les termes usuels de ces licences, il ne remplace pas une lecture du fichier LICENSE de chaque dépôt.