Pourquoi le C++ pour les Tech Artists ?
En tant que Technical Artist, le C++ vous offre :
Performance : Le C++ est le langage qui fait tourner les moteurs de jeu. Quand les Blueprints sont trop lents, le C++ est la solution.
Accès à Unreal : Accès direct aux systèmes sous-jacents d’UE5 — des choses que vous ne pouvez tout simplement pas faire depuis les Blueprints.
Standard de l’industrie : Tous les grands studios de jeux vidéo utilisent le C++. C’est la lingua franca du développement de jeux.
**Rust, Zig, Carbon, Verse **? L’avenir nous dira quel sera le langage temps-réel qui remplacera C++. Rust semble avoir une belle longueur d’avance, à telle enseigne que Microsoft, Linus Torvald (Linux Kernel), Chromium, Android, et d’autres réécrivent une partie de leur vieux codebase en Rust. Yazi est un Terminal Based File manager extrêmement rapide écrit en Rust. Rust est le candidat le plus sérieux pour remplacer C++ dans ses bastions historiques (systèmes, embarqué, moteurs de jeu, navigateurs). Il résout le problème fondamental du C++ — la gestion mémoire — avec son borrow checker, sans sacrifier la performance. L’adoption par Linux kernel, Android, Windows, et Chromium est un signal fort. Mais sa courbe d’apprentissage est raide, et l’écosystème C++ existant est colossal. Go ne joue pas vraiment dans la même cour. Il excelle pour les services réseau, le cloud, les CLI — mais il a un garbage collector et n’offre pas le contrôle bas-niveau dont on a besoin là où C++ règne. Go remplace plutôt Java/Python pour le backend, pas C++. Verse (Epic Games / Tim Sweeney) est un pari intéressant et un peu à part. C’est un langage fonctionnel orienté vers le scripting de jeu dans UEFN/Fortnite. Pour l’instant il est trop jeune et trop niché pour menacer C++ — mais venant d’Epic, ça mérite d’être suivi, surtout dans l’écosystème Unreal qui t’intéresse directement.
Mon intuition : personne ne “remplacera” C++ à court terme. Ce qui va se passer, c’est une érosion progressive. Rust grignote les nouveaux projets systèmes. Zig attire ceux qui trouvent Rust trop complexe. Carbon (Google) tente l’interopérabilité directe avec le C++ existant. Et les projets C++ existants — qui représentent des milliards de lignes — resteront en C++ pendant des décennies, comme le COBOL avant eux.
Pour le TechArt, C++ restera central encore longtemps — Epic a trop investi dedans. Mais Verse pourrait devenir le layer de scripting dominant pour le gameplay, pendant que C++ reste le moteur sous le capot.
Bref historique du C++
Bjarne Stroustrup est le créateur du langage de programmation C++ (je l’ai « presque » rencontré à OOPSLA ‘95 (Object-Oriented Programming, Systems, Languages & Applications ) ! P.S., j’avais ceci dit rencontré Alan Kay, le fondateur du concept de programmation orienté objet à OOPSLA ‘97. Les deux ont profondément marqué ma vie; Alan Kay avait dit: “ I made up the term ‘object-oriented’, and I can tell you I did not have C++ in mind!”).
Au début, le langage se nommait C with Classes. Puis il est devenu C++, comme dans C incrémenté de 1.
Ne vous spécialisez pas trop. Ne soyez pas trop sûr de connaître l’avenir. Soyez flexible et n’oubliez pas que les carrières et les emplois s’inscrivent dans la durée. Trop de jeunes pensent qu’ils peuvent optimiser quelque chose, puis ils se rendent compte qu’ils ont passé quelques années ou plus à se spécialiser dans quelque chose qui n’était peut-être pas la bonne chose à faire. Et ce faisant, ils s’épuisent parce qu’ils n’ont pas passé assez de temps à nouer des amitiés et à avoir une vie en dehors de l’informatique.
Je rencontre beaucoup de personnes - je ne sais pas comment vous les appelez, des “geeks juniors” ? - qui pensent que la seule chose qui compte est la spécialité de l’informatique - la programmation ou l’IA ou le graphisme ou quelque chose comme ça. Et - eh bien, ce n’est pas le cas… Et s’ils ne font rien d’autre, eh bien - si vous ne communiquez pas vos idées, vous pouvez tout aussi bien faire du Sudoku… Il faut communiquer. Et beaucoup d’intellos caricaturaux l’oublient. Ils pensent qu’en écrivant le meilleur code, ils changeront le monde. Mais il faut savoir écouter. Il faut savoir communiquer avec les utilisateurs potentiels et apprendre d’eux. Et vous devez être en mesure de leur communiquer vos idées.
Vous ne pouvez donc pas vous contenter de faire du code. Il faut s’intéresser à la culture et à la manière d’exprimer les idées. Je n’ai jamais regretté le temps que j’ai consacré à l’histoire et aux mathématiques. Les mathématiques aiguisent votre esprit, l’histoire vous donne une idée de vos limites et de ce qui se passe dans le monde. Ne soyez donc pas trop sûrs de vous. Prenez le temps d’avoir une vie équilibrée.
Et soyez prêt à saisir l’occasion qui se présente. Je veux dire qu’une éducation diversifiée, un ensemble de compétences diversifiées - ce que vous construisez lorsque vous vous instruisez, vous construisez essentiellement un portefeuille de compétences - signifie que vous pouvez tirer parti d’une opportunité lorsqu’elle se présente. Vous pouvez parfois la reconnaître. Nous avons beaucoup d’opportunités. Mais pour beaucoup d’entre elles, nous ne pouvons pas en profiter ou nous ne les remarquons pas. C’est mon éducation assez large - j’ai fait de l’informatique standard, j’ai fait des compilateurs, j’ai fait plusieurs langages… Je crois que j’en connaissais deux douzaines à l’époque. J’ai également étudié l’architecture des machines et les systèmes d’exploitation. Et cet ensemble de compétences s’est avéré utile.
Il est difficile de donner des conseils - et il est au moins aussi difficile d’en recevoir.
Je suis devenu programmeur par erreur, en me trompant de mot au moment de choisir ce qu’il allait étudier après mes examens de fin d’études secondaires. Je pensais m’inscrire à un cours de mathématiques appliquées, qui s’est avéré être un cours d’informatique !
C++98 → Le standard originalC++11 → Le "C++ moderne" commence (auto, lambdas, smart pointers)C++14 → AmeliorationsC++17 → std::optional, structured bindingsC++20 → Concepts, ranges, coroutines, modulesC++23 → std::print, std::expected, plus de ranges
Cliquez sur le bouton “Download for Windows” (l’installeur détecte automatiquement votre architecture x64)
Lancez le fichier VSCodeUserSetup-x64-X.XX.X.exe téléchargé
1.2 — Options d’installation
Pendant l’installation, cochez les options suivantes (elles sont importantes) :
Ajouter l’action “Ouvrir avec Code” au menu contextuel de fichier Windows
Ajouter l’action “Ouvrir avec Code” au menu contextuel de répertoire Windows
Ajouter à PATH (permet d’ouvrir VS Code depuis un terminal avec la commande code)
Terminez l’installation et lancez VS Code pour vérifier qu’il fonctionne.
1.3 — Configurer la langue en français (optionnel)
Ouvrez VS Code
Appuyez sur Ctrl+Shift+P pour ouvrir la palette de commandes
Tapez Configure Display Language
Sélectionnez Français ou installez le pack de langue française
Installer le compilateur C++ (MSVC Build Tools)
C’est l’étape la plus importante. VS Code n’inclut pas de compilateur — il faut en installer un séparément. On utilise le compilateur officiel de Microsoft (MSVC), le même qu’Unreal Engine utilise.
Descendez jusqu’à la section “Outils pour Visual Studio” (ou “Tools for Visual Studio”)
Téléchargez “Build Tools pour Visual Studio 2022”
☝🏼
2026 ne fonctionne pas avec UE 5.7.4
- C’est un installeur léger (~1.5 Mo) qui téléchargera les composants nécessaires
☝🏼
Si vous ne trouvez pas la version 2022 (MS en ce moment ne montre que la version 2026), soit téléchargez-le via le lien https://aka.ms/vs/17/release/vs_community.exe ou téléchargez-le via Powershell ou GitBash via winget (attention, pas cmd !) :
cd $env:USERPROFILE/Downloadswinget install --id=Microsoft.VisualStudio.2022.Community -e
Note, l’option -e si vous utilisez winget va l’installer une fois téléchargé. Sinon, exécutez vs_community.exe à la main en double-cliquant dessus dans le dossier Downloads.
2.2 — Installer les composants C++
Le Visual Studio Installer se lance. Voici quoi sélectionner :
Dans l’onglet “Charges de travail” (Workloads), cochez :
Développement Desktop en C++ (“Desktop development with C++”)
Dans le panneau de droite, vérifiez que les composants suivants sont cochés :
Outils de build MSVC v145 (MSVC 2022 - 2026 ne fonctionne pas avec Unreal Engine)
SDK Windows 11 (la version la plus récente proposée)
Outils CMake C++ pour Windows
Fonctionnalités C++ de base
Cliquez sur “Installer” et patientez (environ 4 à 8 Go de téléchargement)
Redémarrez votre ordi
2.3 — Vérifier l’installation
Ouvrez le menu Démarrer
Cherchez “Developer Command Prompt for VS 2026”
Ouvrez-le et tapez :
cl
Vous devriez voir un message comme :
Microsoft (R) C/C++ Optimizing Compiler Version 19.xx.xxxxx
Si ce message apparaît, le compilateur est correctement installé.
Installer CMake
CMake est le système de build standard pour les projets C++ modernes. Il génère les fichiers de compilation à partir d’un fichier CMakeLists.txt.
Téléchargez le Windows x64 Installer (.msi) — version 4.2.3 ou plus récente
Lancez l’installateur
3.2 — Option cruciale pendant l’installation
Quand l’installateur vous le demande, Assurez-vous que “Add CMake to the system PATH for all users” est activé
C’est indispensable pour que VS Code puisse trouver CMake automatiquement.
3.3 — Vérifier l’installation
Ouvrez un nouveau terminal (PowerShell ou cmd) et tapez :
cmake --version
Vous devriez voir :
cmake version 4.2.3
Installer les extensions VS Code
Ouvrez VS Code et installez les extensions suivantes. Pour chaque extension, appuyez sur Ctrl+Shift+X (panneau Extensions) et cherchez le nom.
4.1 — Extensions essentielles
Extension
Éditeur
Rôle
C/C++
Microsoft
IntelliSense, débogage, navigation dans le code
C/C++ Extension Pack
Microsoft
Pack complet (inclut C/C++ Themes et autres)
CMake Tools
Microsoft
Intégration CMake dans VS Code (build, configure, etc.)
4.2 — Extensions recommandées
Extension
Éditeur
Rôle
Code Runner
Jun Han
Exécuter rapidement un fichier C++ avec un raccourci
Error Lens
Alexander
Affiche les erreurs directement dans le code, pas seulement dans le panneau
GitLens
GitKraken
Pour l’intégration Git (fortement recommandé)
Créer votre premier projet C++
5.1 — Créer la structure du projet
Créez un dossier sur votre disque, par exemple : C:\Projets\MonPremierCpp
Ouvrez VS Code
Fichier > Ouvrir le dossier... → sélectionnez MonPremierCpp
Oui je trust
5.2 — Créer les fichiers
Créez un fichier **CMakeLists.txt** à la racine du dossier :
cmake_minimum_required(VERSION 4.0)project(MonPremierCpp LANGUAGES CXX)# Utiliser le standard C++20set(CMAKE_CXX_STANDARD 20)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")add_executable(MonPremierCpp main.cpp)
Créez un fichier **main.cpp** à la racine du dossier :
Dans File → Preferences → Settings, cherchez pour “Auto Save” et mettre Auto Save à “After Delay” et Delay à 1ms
Assurez-vous que File → Auto Save est activé également.
Raccourcis clavier essentiels
Action
Raccourci
Palette de commandes
Ctrl+Shift+P
Compiler (Build)
F7
Exécuter sans débogage
Ctrl+F5
Déboguer
F5
Aller à la définition
F12
Voir les références
Shift+F12
Renommer un symbole
F2
Formater le fichier
Shift+Alt+F
Terminal intégré
`Ctrl+“
Chercher dans les fichiers
Ctrl+Shift+F
Dépannage courant
“cl n’est pas reconnu comme commande interne” — Vous n’êtes pas dans un Developer Command Prompt. Soit ouvrez-en un via le menu Démarrer, soit configurez VS Code pour utiliser le kit MSVC via CMake Tools.
“CMake Error: Could not find compiler” — Relancez VS Code, puis Ctrl+Shift+P → “CMake: Scan for Kits” pour que CMake Tools détecte votre compilateur.
“IntelliSense ne fonctionne pas” — Vérifiez que l’extension C/C++ est bien installée et que CMake a configuré le projet avec succès. L’IntelliSense utilise le fichier compile_commands.json généré par CMake.
Les erreurs de linking apparaissent — Vérifiez que vous avez bien sélectionné le kit x64 (et non x86) si vous développez en 64 bits.
Votre premier programme : Hello World
Décortiquons chaque ligne d'un programme C++ complet :
// A complete C++ program#include <iostream>int main(){ int answer {42}; // Defines the variable answer with value 42 std::cout << "The answer to life, the universe, and everything is " << answer << std::endl; return 0;}
Étape par étape : créer et exécuter ce programme
Ouvrez Visual Studio
Créez un nouveau projet “Console App”
Remplacez le code généré par le code ci-dessus
Appuyez sur Ctrl+F5 pour compiler et exécuter
Vous devriez voir : The answer to life, the universe, and everything is 42.
Types fondamentaux et variables
Types entiers
Type
Taille typique
Plage de valeurs
short
2 octets
-32 768 à 32 767
int
4 octets
-2 147 483 648 à 2 147 483 647
long
4 ou 8 octets
Au moins identique à int
long long
8 octets
±9,2 × 10^18
unsigned int
4 octets
0 à 4 294 967 295
Types à virgule flottante
Type
Taille
Précision
float
4 octets
~7 chiffres
double
8 octets
~15 chiffres
Un float 32 bits est découpé en trois parties selon la norme IEEE 754 :
[1 bit signe] [8 bits exposant] [23 bits mantisse]
Signe — 0 pour positif, 1 pour négatif
Exposant — détermine l’ordre de grandeur (la puissance de 2)
Mantisse — les chiffres significatifs
Le nombre se reconstitue ainsi : (-1)^signe × 1.mantisse × 2^(exposant - 127)
Pourquoi ce n’est pas précis :
Le problème fondamental est que la mantisse n’a que 23 bits, soit environ 7 chiffres décimaux de précision. Mais surtout, beaucoup de nombres décimaux simples n’ont pas de représentation binaire exacte.
Prenons 0.1 en base 10. En binaire, ça donne 0.000110011001100110011... qui se répète à l’infini, exactement comme 1/3 = 0.333... en décimal. Avec seulement 23 bits, on tronque, donc on stocke une approximation.
float f = 0.1f + 0.2f;std::cout << std::format("{:.20f}", f);// Affiche : 0.30000001192092895508// et non pas 0.3
Les conséquences pratiques sont importantes. Les comparaisons directes sont dangereuses :
if (f == 0.3f) // risque de ne jamais être vrai// Mieux : comparer avec une tolérance (epsilon)if (std::abs(f - 0.3f) < 0.0001f)
Et les erreurs s’accumulent à chaque opération. En additionnant des milliers de petites valeurs, l’écart peut devenir significatif.
C’est pour ça que double (52 bits de mantisse, ~15 chiffres de précision) est le choix par défaut. Et dans des domaines comme la finance, on évite complètement les flottants au profit d’arithmétique entière en centimes.
En game dev (Unreal, Blender), float reste courant pour les positions et couleurs parce que la performance mémoire prime et qu’une erreur au septième chiffre n’est pas visible à l’écran.
Autres types fondamentaux
Type
Taille
Utilisation
char
1 octet
Caractère unique (‘A’, ‘7’, ’!’)
bool
1 octet
true ou false
auto
variable
Le compilateur déduit le type
Déclaration et initialisation de variables
Le C++ offre plusieurs façons d'initialiser des variables. L'initialisation par accolades{} est la méthode moderne et recommandée :
int apple_count {15}; // Number of applesint orange_count {5}; // Number of orangesint total_fruit {apple_count + orange_count}; // Total number of fruit
Pourquoi l’initialisation par accolades **{}** ?
Empêche les conversions restrictives (ex. int x {3.14}; ne compilera pas — tant mieux !)
Fonctionne de manière cohérente pour tous les types
Montre clairement l’intention
unsigned a {}; est en fait l’option sûre — c’est une initialisation par valeur (value initialization), qui met a à 0.
Ce qu’il faut éviter, c’est :
unsigned a; // non initialisé — contient une valeur indéterminée
C’est une initialisation par défaut pour une variable locale de type primitif, ce qui en C++ signifie « aucune initialisation ». Lire a avant de lui assigner une valeur est un comportement indéfini (undefined behavior).
Résumé des options
unsigned a; // dangereux : valeur indéterminéeunsigned a {}; // sûr : initialisé à zéro (C++11, accolades vides)unsigned a = 0; // sûr : initialisation explicite classiqueunsigned a{0}; // sûr : initialisation directe par listeunsigned a(); // ATTENTION : ce n'est PAS une variable, // c'est une déclaration de fonction (most vexing parse)
En résumé
unsigned a {}; est parfaitement valide et considéré comme du C++ moderne idiomatique. Les accolades vides {} garantissent l’initialisation à zéro pour les types scalaires. C’est le style recommandé par les C++ Core Guidelines justement pour éviter les bugs liés à l’oubli d’initialisation.
Autres styles d’initialisation (à connaître, préférez **{}**) :
int a = 5; // C-style (allows narrowing — dangerous)int b(5); // Constructor-styleint c {5}; // Brace initialization (preferred!)int d {}; // Zero-initialized (preferred for default values)
const et constexpr
const int MAX_HEALTH {100}; // Cannot be changed after initializationconstexpr double PI {3.14159265}; // Computed at compile time (even better!)
const = “Je promets de ne pas modifier cette valeur” (définie à l’exécution ou à la compilation)
constexpr = “Cette valeur doit être connue à la compilation” (garantie plus forte)
Exercices
1. Explorer les tailles et limites des types
// Finding maximum and minimum values for data typesimport std;int main(){ std::println("The range for type short is from {} to {}", std::numeric_limits<short>::min(), std::numeric_limits<short>::max()); std::println("The range for type unsigned int is from {} to {}", std::numeric_limits<unsigned int>::min(), std::numeric_limits<unsigned int>::max()); std::println("The range for type long long is from {} to {}", std::numeric_limits<long long>::min(), std::numeric_limits<long long>::max()); std::println("The positive range for type float is from {} to {}", std::numeric_limits<float>::min(), std::numeric_limits<float>::max()); std::println("The full range for type float is from {} to {}", std::numeric_limits<float>::lowest(), std::numeric_limits<float>::max()); std::println("The positive range for type double is from {} to {}", std::numeric_limits<double>::min(), std::numeric_limits<double>::max());}
Ce que vous apprendrez : Chaque type a des limites. std::numeric_limits vous donne min(), max() et lowest() pour tout type numérique. C’est important quand vous devez choisir le bon type pour vos données.
2. Entrées/sorties basiques avec des variables
// Writing values of variables to the screenimport std;int main(){ int apple_count {15}; // Number of apples int orange_count {5}; // Number of oranges int total_fruit {apple_count + orange_count}; // Total number of fruit std::println("The value of apple_count is {}", apple_count); std::println("The value of orange_count is {}", orange_count); std::println("The value of total_fruit is {}", total_fruit);}
Sortie attendue :
The value of apple_count is 15The value of orange_count is 5The value of total_fruit is 20
Concepts clés :
std::println() affiche une ligne avec un retour à la ligne à la fin
{} est un espace réservé de formatage — remplacé par l’argument correspondant
Les variables stockent des valeurs qui peuvent être utilisées dans des expressionsTravail personnel — Semaine 1 (2 heures)
⚠️
Retirez la variable total_fruit, comment faire pour imprimer le total maintenant ?
3. Écrivez un programme qui affiche la taille de tous les types fondamentaux en utilisant sizeof :
import std;int main(){ std::println("Size of bool: {} bytes", sizeof(bool)); std::println("Size of char: {} bytes", sizeof(char)); std::println("Size of short: {} bytes", sizeof(short)); std::println("Size of int: {} bytes", sizeof(int)); std::println("Size of long: {} bytes", sizeof(long)); std::println("Size of long long: {} bytes", sizeof(long long)); std::println("Size of float: {} bytes", sizeof(float)); std::println("Size of double: {} bytes", sizeof(double));}
⚠️
On peut trouver “Size of string” vous pensez ?
Oui mais non quoi qu’à bien y penser en fait du coup
std::string str {"Hello World"}; std::println("Size of string (\"{}\") is {} bytes", str, sizeof(str));
4. Écrivez un programme qui :
Demande à l’utilisateur son nom et son âge
Calcule son année de naissance
Affiche : “Bonjour [nom], vous êtes né(e) en [année].”
Indice : Utilisez std::cin >> pour lire l’entrée et string une fois avoir #include <string> pour le nom.
Solutions
Solution de l'exercice 3 :
#include <iostream>#include <string>int main(){ std::string name; int age {}; std::cout << "Entrez votre nom : "; std::cin >> name; std::cout << "Entrez votre âge : "; std::cin >> age; const int current_year {2026}; int birth_year {current_year - age}; std::cout << "Bonjour " << name << ", vous êtes né(e) en " << birth_year << "." << std::endl;}
Travail personnel
Lecture
Lire Beginning C++23 Chapitre 1
Lire Tour of C++ Chapitre 1
Exercices
Exercice 1 : Écrivez un programme qui affiche la taille de tous les types fondamentaux en utilisant sizeof :
#include <iostream>#include <format>int main(){ std::cout << std::format("Size of bool: {} bytes", sizeof(bool)) << std::endl; std::cout << std::format("Size of char: {} bytes", sizeof(char)) << std::endl; std::cout << std::format("Size of short: {} bytes", sizeof(short)) << std::endl; std::cout << std::format("Size of int: {} bytes", sizeof(int)) << std::endl; std::cout << std::format("Size of long: {} bytes", sizeof(long)) << std::endl; std::cout << std::format("Size of long long: {} bytes", sizeof(long long)) << std::endl; std::cout << std::format("Size of float: {} bytes", sizeof(float)) << std::endl; std::cout << std::format("Size of double: {} bytes", sizeof(double)) << std::endl;}
Exercice 2 : Écrivez un programme qui :
Demande à l’utilisateur son nom et son âge
Calcule son année de naissance
Affiche : “Bonjour [nom], vous êtes né(e) en [année].”
Indice : Utilisez std::cin >> pour lire l’entrée et std::string pour le nom.
Exercice 3 (Trouvez les erreurs) : Trouvez et corrigez tous les bugs dans ce programme :
include <iostream> // Bug 1: missing #using namespace std // Bug 2: missing ;int main{ // Bug 3: missing () int 2numbers; // Bug 4: identifiers can't start with a digit std:cout << "Hello" // Bug 5: should be std::cout, also missing ; return 0;}
Solutions
Solution de l’exercice 1 : Le code de l’exercice est la solution - exécutez-le et observez les tailles.
Solution de l’exercice 2 :
#include <iostream>#include <format>#include <string>int main(){ std::string name; int age {}; std::cout << std::format("Entrez votre nom : "); std::cin >> name; std::cout << std::format("Entrez votre âge : "); std::cin >> age; const int current_year {2025}; int birth_year {current_year - age}; std::cout << std::format("Bonjour {}, vous êtes né(e) en {}.", name, birth_year) << std::endl;}
Solution de l’exercice 3 : Les bugs et corrections sont déjà annotés dans l’énoncé. Le programme corrigé :
#include <iostream> // Bug 1 corrigé: ajout du #using namespace std; // Bug 2 corrigé: ajout du ;int main() // Bug 3 corrigé: ajout des (){ int numbers; // Bug 4 corrigé: identifiant valide (pas de chiffre au début) std::cout << "Hello" << std::endl; // Bug 5 corrigé: std:: et ajout du ; return 0;}
Attention à la division entière !7 / 3 donne 2, et non 2.333.... Si vous voulez le résultat décimal, au moins un des opérandes doit être à virgule flottante : 7.0 / 3 donne 2.333...
Incrément et décrément : prefix vs postfix
int x {5};int a = ++x; // PREFIX: x becomes 6, then a gets 6 (increment first)int b = x++; // POSTFIX: b gets 6, then x becomes 7 (use first, then increment)// À éviter, il vaut mieux ceci, qui est plus clairint x {5};x++;int a {x};int b {x};x++;#Les 4 instructions font la même chose, i.e. ajouter 1 à x:++x;x++;x += 1;x = x + 1;
Opérateurs d'affectation composés
Opérateur
Équivalent
Exemple
+=
x = x + n
health += 10;
-=
x = x - n
health -= damage;
*=
x = x * n
score *= multiplier;
/=
x = x / n
speed /= 2;
%=
x = x % n
angle %= 360;
Priorité des opérateurs (simplifiée)
De la priorité la plus haute à la plus basse :
Priorité
Opérateurs
1
() (parenthèses)
2
++-- (préfixe), !
3
*/%
4
+-
5
<<=>>=
6
==!=
7
&&
8
||
9
=+=-= etc.
Bonne pratique : En cas de doute, utilisez des parenthèses pour clarifier l’intention.
Exemple : conversion de distances
// Converting distances#include <iostream>#include <format>int main(){ unsigned int yards {}, feet {}, inches {}; // Convert a distance in yards, feet, and inches to inches std::cout << "Enter a distance as yards, feet, and inches "; std::cout << "with the three values separated by spaces: " << std::endl; std::cin >> yards >> feet >> inches; const unsigned feet_per_yard {3}; const unsigned inches_per_foot {12}; unsigned total_inches {}; total_inches = inches + inches_per_foot * (yards * feet_per_yard + feet); std::cout << "This distance corresponds to " << total_inches << " inches." << std::endl; // Convert a distance in inches to yards, feet, and inches std::cout << "Enter a distance in inches: "; std::cin >> total_inches; feet = total_inches / inches_per_foot; inches = total_inches % inches_per_foot; yards = feet / feet_per_yard; feet = feet % feet_per_yard; // We can also use the std:format module (similar to Python's f-string formatting) std::cout << std::format("This distance corresponds to {} yards {} feet {} inches.", yards, feet, inches) << std::endl;}
Points clés à retenir :
std::cin >> lit à partir de l’entrée clavier
La division entière / donne le quotient (partie entière)
Le modulo % donne le reste
Ces deux opérateurs combinés sont parfaits pour les conversions d’unités
☝🏼
Écrivez un code similaire, cette fois-ci pour convertir mètres, centimètres et millimètres
Solution
// Converting distances#include <iostream>#include <format>int main(){ unsigned int meters {}, centimeters {}, millimeters {}; // Convert a distance in meters, centimeters, and millimeters to millimeters std::cout << "Enter a distance as meters, centimeters, and millimeters "; std::cout << "with the three values separated by spaces: " << std::endl; std::cin >> meters >> centimeters >> millimeters; const unsigned cm_per_meter {100}; const unsigned mm_per_cm {10}; unsigned total_millimeters {}; total_millimeters = millimeters + mm_per_cm * (meters * cm_per_meter + centimeters); std::cout << "This distance corresponds to " << total_millimeters << " millimeters." << std::endl; // Convert a distance in millimeters to meters, centimeters, and millimeters std::cout << "Enter a distance in millimeters: "; std::cin >> total_millimeters; centimeters = total_millimeters / mm_per_cm; millimeters = total_millimeters % mm_per_cm; meters = centimeters / cm_per_meter; centimeters = centimeters % cm_per_meter; // We can also use the std:format module (similar to Python's f-string formatting) std::cout << std::format("This distance corresponds to {} meters {} centimeters {} millimeters.", meters, centimeters, millimeters) << std::endl;}
int total {7};int count {3};double average = static_cast<double>(total) / count; // 2.333... (correct!)// Without cast: total / count = 2 (integer division!)
Ok, we know about the what of static_cast but not the why; meaning, why not just do this instead?
double average = (double)total / count; // C-style cast
Both produce the same result. The reason static_cast is preferred in modern C++ comes down to a few things:
It’s explicit about intent. The angle bracket syntax is verbose on purpose — it makes the cast impossible to miss during a code review. A C-style cast (double) can visually blend into surrounding code.
It’s safer. static_cast only performs conversions the compiler considers “reasonable” — like numeric type promotions. The C-style cast is a blunt instrument that will also attempt reinterpret_cast and const_cast behavior, meaning it can do things you didn’t intend without warning.
It’s searchable. If a bug is traced back to a cast, you can grep for static_cast in a large codebase. Good luck searching for (double) without tons of false positives.
It fails loudly when appropriate. If the conversion isn’t valid, static_cast will produce a compile error. The C-style cast will often find some interpretation and proceed silently.
Pourquoi les casts de style C sont dangereux
double pi {3.14159};int truncated = (int)pi; // C-style cast — compiles silently, loses dataint better = static_cast<int>(pi); // C++ cast — intent is clear, same result
**static_cast<>** est préférable car :
Il est recherchable dans le code (grep pour static_cast)
Il n’autorise que les conversions “logiques”
Il montre clairement l’intention — vous vouliez convertir
Exemple : calculateur de bassin (tout combiner)
// Sizing a pond for happy fishimport std;int main(){ // 0.2 square meters pond surface for every 15 cm of fish const double fish_factor { 0.2 / 0.15 }; // Area per unit length of fish (m² per m) double fish_count {}; // Number of fish double fish_length {}; // Average length of fish (cm) std::print("Enter the number of fish you want to keep: "); std::cin >> fish_count; std::print("Enter the average fish length in centimeters: "); std::cin >> fish_length; fish_length /= 100.0; // Convert cm to meters std::println(""); // Calculate the required surface area const double pond_area {fish_count * fish_length * fish_factor}; // Calculate the pond diameter from the area const double pond_diameter {2.0 * std::sqrt(pond_area / std::numbers::pi)}; // Convert to meters and centimeters const double pond_diameter_meters { std::floor(pond_diameter) }; const double pond_diameter_cm { std::round((pond_diameter - pond_diameter_meters) * 100.0) }; std::println("Pond diameter required for {} fish is {} m {} cm.", fish_count, pond_diameter_meters, pond_diameter_cm);}
Nouveaux concepts présentés :
std::sqrt() — fonction racine carrée
std::numbers::pi — constante C++20 pour pi
std::floor() — arrondit vers le bas
std::round() — arrondit au plus proche
/= affectation composée
Exercices
Exercice 1 : Énigmes de priorité, prédisez la valeur de chaque expression, puis vérifiez :
int a = 2 + 3 * 4; // ?int b = (2 + 3) * 4; // ?int c = 10 / 3; // ?int d = 10 % 3; // ?double e = 10 / 3; // ?double f = 10.0 / 3; // ?
Exercice 2 : Construire un convertisseur de température, écrivez un programme qui :
Demande à l’utilisateur une température en Celsius
Convertit en Fahrenheit : F = C * 9/5 + 32
Convertit en Kelvin : K = C + 273.15
Affiche les trois valeurs formatées à 1 décimale
Indice : Utilisez std::println("Temperature: {:.1f}", value) pour 1 décimale.
Exercice 3 : Calculatrice mixte, construisez une calculatrice qui :
Lit deux nombres (autorisez les entrées décimales)
Effectue +, -, *, / sur ces nombres
Affiche les résultats pour toutes les opérations
Gère correctement la division (pas de troncature entière)
Solutions
Solution de l'exercice 1 : Énigmes de priorité
int a = 2 + 3 * 4; // 14 (multiplication d'abord: 3*4=12, puis 2+12=14)int b = (2 + 3) * 4; // 20 (parenthèses d'abord: 5*4=20)int c = 10 / 3; // 3 (division entière, tronquée)int d = 10 % 3; // 1 (reste de la division: 10 - 3*3 = 1)double e = 10 / 3; // 3.0 (division entière d'abord → 3, puis converti en double)double f = 10.0 / 3; // 3.333... (division flottante car 10.0 est un double)
Solution de l'exercice 2 : Convertisseur de température
import std;int main(){ double a {}, b {}; std::print("Entrez le premier nombre : "); std::cin >> a; std::print("Entrez le deuxième nombre : "); std::cin >> b; std::println("{} + {} = {}", a, b, a + b); std::println("{} - {} = {}", a, b, a - b); std::println("{} * {} = {}", a, b, a * b); if (b != 0.0) std::println("{} / {} = {}", a, b, a / b); else std::println("Division par zéro impossible !");}
# ID Software, Quake II et Q_rsqrt : Ouate de Phoque ?
L’inverse de la racine carré d’un nombre, c’est l’une des opérations les plus fondamentales de toute la 3D temps réel.
La normalisation de vecteur
Normaliser un vecteur, c’est le ramener à une longueur entre 0.0 et 1.0 (un vecteur unitaire) tout en conservant sa direction.
La formule est :
v_normalisé = v / |v|
où |v| est la longueur du vecteur :
|v| = sqrt(x² + y² + z²)
Donc normaliser, c’est diviser chaque composante par cette longueur :
v_normalisé = v * (1 / sqrt(x² + y² + z²))
C’est exactement 1 / sqrt(value). L’inverse de la racine carrée n’est pas un truc de mathématicien — c’est la normalisation, exprimée de façon optimale.
Pourquoi a-t-on besoin de vecteurs unitaires partout ?
L’éclairage (Phong, Lambert, PBR)
C’est le cas le plus critique. Pour calculer si une surface est éclairée, on fait le produit scalaire (dot product) entre la normale de surface et la direction de la lumière :
intensité = dot(N, L) = N.xL.x + N.yL.y + N.z*L.z
Ce calcul ne donne un résultat correct que si N et L sont des vecteurs unitaires. Si la normale a une longueur de 2.5 au lieu de 1.0, ta surface sera 2.5× trop lumineuse. Dans Quake III, chaque pixel, chaque frame, pour chaque surface — on normalisait des vecteurs en permanence. C’est pour ça que la performance de 1/sqrt() importait autant.
Les normal maps
Chaque texel d’une normal map encode une normale. Au moment du rendu, le GPU doit normaliser ces normales après interpolation entre les vertices (l’interpolation brise la longueur unitaire). Des millions de normalisations par frame.
Les réflexions et réfractions
R = reflect(I, N) // nécessite N unitaire
Utilisé dans les shaders PBR, les ray tracers, les cubemaps.
L’animation et les quaternions
Les quaternions de rotation doivent rester normalisés pour représenter des rotations pures. Après des interpolations (SLERP, blending de poses), on renormalise pour éviter la dérive.
La physique — détection de collisions
Les normales de contact, les axes de séparation (SAT), les plans de collision — tous nécessitent des vecteurs unitaires pour que les calculs de distance et de projection soient corrects.
Le fait que Quake III faisait tourner des niveaux entiers avec des centaines d’entités éclairées en temps réel en 1999 sur du matériel 200 MHz, c’est en grande partie grâce à cette optimisation. Chaque personnage, chaque mur éclairé dynamiquement — des milliers de normalisations par frame, accélérées par ce hack de 6 lignes.
Dans Blender et Unreal aujourd’hui, c’est le GPU qui fait ces millions de normalisations en parallèle, mais le principe est identique — et les shaders GLSL/HLSL ont même une fonction intégrée normalize() qui fait exactement v * inversesqrt(dot(v,v)).
Quake’s Q_rsqrt (avec son WTF et son UB), une version équivalente moderne et un appel à la fonction sqrt standard:
// Benchmark: Original Q_rsqrt (UB) vs Modern bit_cast version// Requires C++20 with standard library modules support.import std;// ─────────────────────────────────────────────// Original Quake III version (undefined behavior)// NOTE: uses ‘long’ which was 32-bit on the original// target (MSVC/x86). On 64-bit Linux, long is 64-bit,// which BREAKS the bit trick — demonstrating real UB!// We preserve the original code exactly as written.// ─────────────────────────────────────────────float Q_rsqrt_original(float number){long i;float x2, y;const float threehalfs = 1.5F;x2 = number * 0.5F;y = number;i = * ( long * ) &y; // evil floating point bit level hackingi = 0x5f3759df - ( i >> 1 ); // what the fuck?y = * ( float * ) &i;y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration of Newton's methodreturn y;}// ─────────────────────────────────────────────// Modern C++20 version (defined behavior)// ─────────────────────────────────────────────float Q_rsqrt_modern(float number){const float x2 = number * 0.5F;const float threehalfs = 1.5F;std::uint32_t i = std::bit_cast<std::uint32_t>(number);i = 0x5f3759df - ( i >> 1 );float y = std::bit_cast<float>(i);y = y * ( threehalfs - ( x2 * y * y ) );return y;}// ─────────────────────────────────────────────// std::sqrt reference (for accuracy comparison)// ─────────────────────────────────────────────float rsqrt_reference(float number){return 1.0F / std::sqrt(number);}// ─────────────────────────────────────────────// Benchmark helper// ─────────────────────────────────────────────template<typename Func>double run_benchmark(Func func, const std::vector<float>& inputs, int iterations){// Warm upvolatile float sink {};for (auto v : inputs) sink = func(v);auto start = std::chrono::high_resolution_clock::now();for (int i = 0; i < iterations; ++i) for (auto v : inputs) sink = func(v);auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> elapsed = end - start;return elapsed.count();}int main(){// Build a set of test values (avoid 0)const int sample_count { 10000 };const int iterations { 1000 };std::vector<float> inputs(sample_count);for (int i = 0; i < sample_count; ++i) inputs[i] = 0.1F + static_cast<float>(i) * 0.01F;std::cout << std::fixed << std::setprecision(6);// ── Accuracy check ──────────────────────────────────────std::cout << "\n=== Accuracy Check (first 5 values) ===\n";std::cout << std::setw(10) << "Input" << std::setw(16) << "Reference" << std::setw(16) << "Original" << std::setw(16) << "Modern" << std::setw(14) << "Orig Error%" << std::setw(14) << "Mod Error%" << '\n';std::cout << std::string(86, '-') << '\n';for (int i = 0; i < 5; ++i){ float v = inputs[i]; float ref = rsqrt_reference(v); float orig = Q_rsqrt_original(v); float mod = Q_rsqrt_modern(v); double err_orig = std::abs((orig - ref) / ref) * 100.0; double err_mod = std::abs((mod - ref) / ref) * 100.0; std::cout << std::setw(10) << v << std::setw(16) << ref << std::setw(16) << orig << std::setw(16) << mod << std::setw(13) << err_orig << "%" << std::setw(13) << err_mod << "%" << '\n';}// ── Timing benchmark ────────────────────────────────────std::cout << "\n=== Benchmark (" << sample_count << " values × " << iterations << " iterations) ===\n";double time_orig = run_benchmark(Q_rsqrt_original, inputs, iterations);double time_mod = run_benchmark(Q_rsqrt_modern, inputs, iterations);double time_ref = run_benchmark(rsqrt_reference, inputs, iterations);std::cout << std::setprecision(3);std::cout << std::setw(20) << "Original (UB): " << time_orig << " ms\n";std::cout << std::setw(20) << "Modern (C++20): " << time_mod << " ms\n";std::cout << std::setw(20) << "std::sqrt ref: " << time_ref << " ms\n";double speedup = time_ref / std::min(time_orig, time_mod);if (speedup >= 1.0) std::cout << "\nFast inverse sqrt is ~" << speedup << "x faster than 1/std::sqrt()\n";else std::cout << "\n1/std::sqrt() is ~" << (1.0 / speedup) << "x faster than Fast inverse sqrt\n";std::cout << "\nNote: with modern CPUs the gap is much smaller than in 1999!\n" << " Hardware rsqrtss instruction often beats both.\n\n";return 0;}
Travail personnel
Lecture
Lire Beginning C++23 Chapitre 2
Exercices
Exercice 1 : Énigmes de priorité
Prédisez la valeur de chaque expression, puis vérifiez :
int a = 2 + 3 * 4; // ?int b = (2 + 3) * 4; // ?int c = 10 / 3; // ?int d = 10 % 3; // ?double e = 10 / 3; // ?double f = 10.0 / 3; // ?
Exercice 2 : Construire un convertisseur de température
Écrivez un programme qui :
Demande à l’utilisateur une température en Celsius
#include <iostream>#include <format>int main(){ double a {}, b {}; std::cout << std::format("Entrez le premier nombre : "); std::cin >> a; std::cout << std::format("Entrez le deuxième nombre : "); std::cin >> b; std::cout << std::format("{} + {} = {}", a, b, a + b) << std::endl; std::cout << std::format("{} - {} = {}", a, b, a - b) << std::endl; std::cout << std::format("{} * {} = {}", a, b, a * b) << std::endl; if (b != 0.0) std::cout << std::format("{} / {} = {}", a, b, a / b) << std::endl; else std::cout << std::format("Division par zéro impossible !") << std::endl;}
Banque de cours - Cours 3 - Flux de contrôle, décisions et boucles
# Cours 3 - Flux de contrôle, décisions et boucles
Où est Charlie le Bug ?
Trouvez tous les bugs dans ce code :
int health = 100;float damage = 25.5;int remaining = health - damage;cout << "Health: " health << endl;int x = 10 / 3.0;const int MAX = 100;MAX = 200;
Solution :
remaining devrait être double, ou utilisez static_cast<int>(health - damage) intentionnellement
Il manque le std:: devant le cout et devant le endl
Il manque << entre "Health: " et health
Utilisez double x = 10 / 3.0; si vous voulez le résultat décimal
Supprimez const ou ne réassignez pas — const signifie constant !
Prise de décisions
Opérateurs de comparaison
Opérateur
Signification
Exemple
==
Égal à
x == 5
!=
Différent de
x != 0
<
Inférieur à
x < 10
>
Supérieur à
x > 0
<=
Inférieur ou égal à
x <= 100
>=
Supérieur ou égal à
x >= 1
Opérateurs logiques
Opérateur
Signification
Exemple
&&
AND
x > 0 && x < 100
`
`
!
NOT
!gameOver
Comparer des valeurs
// Comparing data values#include <iostream>#include <format>int main(){ char first {}; // Stores the first character char second {}; // Stores the second character std::cout << std::format("Enter a character: "); std::cin >> first; std::cout << std::format("Enter a second character: "); std::cin >> second; std::cout << std::format("The value of the expression {} < {} is {}", first, second, first < second) << std:endl; std::cout << std::format("The value of the expression {} == {} is {}", first, second, first == second) << std::endl;}
Point clé : Les caractères sont comparés selon leurs valeurs ASCII. 'A' (65) < 'B' (66) est true.
if
#include <iostream>#include <format>int main(){ std::cout << std::format("Enter an integer between 50 and 100: "); int value {}; std::cin >> value; if (value) std::cout << std::format("You have entered a value that is different from zero.") << std::endl; if (value < 50) std::cout << std::format("The value is invalid - it is less than 50.") << std::endl; if (value > 100) std::cout << std::format("The value is invalid - it is greater than 100.") << std:endl; std::cout << std::format("You entered {}.", value) << std::endl;}
Mieux avec else if et else
En C++, else if permet d'enchaîner plusieurs conditions en évitant d'évaluer les suivantes dès qu'une est vraie. C'est plus efficace et plus lisible qu'une série de if indépendants.
if (value < 50){ std::cout << std::format("Too low!") << std::endl;}else if (value > 100){ std::cout << std::format("Too high!") << std::endl;}else{ std::cout << std::format("Perfect! You entered {}.", value) << std::endl;}
// Avec else if : dès qu'une condition est vraie, on sortif (score >= 90) grade = 'A';else if (score >= 80) grade = 'B';else if (score >= 70) grade = 'C';else grade = 'F';// Avec des if séparés : chaque condition est testée même si une précédente était vraieif (score >= 90) grade = 'A';if (score >= 80) grade = 'B'; // écrase 'A' si score >= 90 !if (score >= 70) grade = 'C'; // écrase encore...
Les avantages principaux sont la performance (moins de tests inutiles), la correction logique (pas d’écrasement accidentel comme dans le deuxième exemple), et la lisibilité (l’intention “une seule branche parmi plusieurs” est claire).
Instruction switch
Pour un grand nombre de valeurs discrètes, un switch peut être préférable,
L’opérateur spaceship<=>, introduit en C++20, effectue une comparaison tripartite en une seule opération. Au lieu d’écrire séparément <, ==, >, il retourne un résultat qui encode les trois cas à la fois.
#include <compare>auto result = a <=> b;// result < 0 → a est inférieur à b// result == 0 → a est égal à b// result > 0 → a est supérieur à b
L’intérêt principal est qu’en le définissant une seule fois, le compilateur génère automatiquement les six opérateurs de comparaison (<, >, <=, >=, ==, !=).
struct Point { int x, y; auto operator<=>(const Point&) const = default;};Point a{1, 2}, b{3, 4};// Tout ceci fonctionne automatiquement :a < b;a >= b;a == b;// etc.
Les trois types de retour reflètent la nature de la comparaison :
std::strong_ordering — pour les types où l’égalité signifie interchangeabilité (comme les entiers)
std::weak_ordering — pour les types où des valeurs “équivalentes” ne sont pas forcément identiques (comme un tri insensible à la casse)
std::partial_ordering — pour les types où certaines valeurs sont incomparables (comme les flottants avec NaN)
// Exemple concret avec un tri customstruct Student { std::string name; int grade; // Trier par note décroissante, puis nom croissant auto operator<=>(const Student& other) const { if (auto cmp = other.grade <=> grade; cmp != 0) return cmp; return name <=> other.name; } bool operator==(const Student&) const = default;};
Avant C++20, il fallait écrire manuellement chacun des six opérateurs (ou au minimum < et == avec std::rel_ops), ce qui était répétitif et source d’erreurs. Le spaceship simplifie tout ça en une seule déclaration.
Boucles
Boucle while
La boucle while répète un bloc de code tant qu'une condition est vraie. La condition est évaluée avant chaque itération.
Le piège classique est la boucle infinie — si la condition ne devient jamais fausse, le programme tourne indéfiniment :
while (true) { // tourne pour toujours, sauf si on utilise break}
Boucle do-while (s'exécute au moins une fois)
Il existe aussi la variante do...while, qui garantit au moins une exécution car la condition est testée après :
int input {};do { std::cout << std::format("Enter a positive number: "); std::cin >> input;} while (input <= 0); // Keep asking until valid
Boucle for
La boucle for en C++ existe sous trois formes principales.
1. La boucle **for** classique
Trois parties : initialisation, condition, incrémentation.
// Traditional for loopfor (int i {0}; i < 10; ++i){ std::cout << std::format("Iteration {}", i) << std::endl;}
2. La boucle range-based **for** (depuis C++11)
Parcourt directement les éléments d’un conteneur sans manipuler d’index.
std::vector<int> nums = {10, 20, 30};for (int n : nums) // par copie std::cout << n << std::endl;for (const auto& n : nums) // par référence constante (plus efficace) std::cout << n << std::endl;
3. Les nouveautés C++20
C++20 apporte deux ajouts importants à la boucle for :
L’instruction d’initialisation dans un range-based for, qui permet de déclarer une variable locale au scope de la boucle :
std::vector<int> getVector() { return {10, 20, 30};}for (auto v = getVector(); const auto& elem : v) { std::cout << elem << std::endl;}// v n'existe plus ici — pas de pollution du scope
Et les ranges de la bibliothèque <ranges>, qui permettent de composer des transformations de manière fluide :
#include <ranges>#include <vector>std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};// Filtrer les pairs, les multiplier par 3, prendre les 4 premiersfor (int n : nums | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * 3; }) | std::views::take(4)){ std::cout << n << " ";}// Affiche : 6 12 18 24
L’avantage des ranges est qu’ils sont lazy — chaque élément est traité à la demande sans créer de collections intermédiaires. C’est à la fois plus lisible et plus performant qu’enchaîner des boucles séparées.
Les ranges (<ranges>) sont une abstraction introduite en C++20 qui représente une séquence d’éléments sur laquelle on peut itérer. L’idée est de remplacer le pattern classique “passer deux itérateurs” par un objet unique, et de permettre la composition d’opérations avec l’opérateur pipe |.
À ce stade, vue ne contient aucun résultat. C’est juste une description du pipeline. Rien ne s’exécute.
Pourquoi c’est “lazy” :
Les views ne calculent rien au moment de leur création. Le travail se fait uniquement quand on demande un élément, typiquement dans une boucle :
for (int n : vue) { std::cout << n << " ";}// Affiche : 6 12 18
À chaque itération, le mécanisme est le suivant : on prend l’élément suivant de nums, on applique le filtre, s’il passe on applique la transformation, et on retourne le résultat. Tout se fait élément par élément, en un seul parcours et sans aucune allocation intermédiaire.
C’est particulièrement puissant avec std::views::take :
// Sur un vecteur d'un million d'élémentsfor (int n : nums | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * 3; }) | std::views::take(3)){ std::cout << n << " ";}
Ici, dès que trois éléments ont été produits, le parcours s’arrête. Avec l’approche classique STL, on aurait filtré et transformé le million d’éléments avant de n’en garder que trois.
En résumé, le lazy evaluation évite les allocations intermédiaires, fait tout en un seul parcours, et permet de court-circuiter dès qu’on a assez de résultats.
Portée, durée de vie et variables globales
// Demonstrating scope, lifetime, and global variables#include <iostream>#include <format>long count1{999L}; // Global count1double count2{3.14}; // Global count2int count3; // Global count3 - default initializationint main(){ int count1{10}; // Hides global count1 int count3{50}; // Hides global count3 std::cout << std::format("Value of outer count1 = {}", count1) << std::endl; std::cout << std::format("Value of global count1 = {}", ::count1) << std::endl; std::cout << std::format("Value of global count2 = {}", count2) << std::endl; { // New block scope starts here int count1{20}; // Hides the outer count1 int count2{30}; // Hides global count2 std::cout << std::format("\nValue of inner count1 = {}", count1) << std::endl; std::cout << std::format("Value of global count1 = {}", ::count1) << std::endl; std::cout << std::format("Value of inner count2 = {}", count2) << std::endl; std::cout << std::format("Value of global count2 = {}", ::count2) << std::endl; } // Block scope ends here std::cout << std::format("\nValue of outer count1 = {}", count1) << std::endl; std::cout << std::format("Value of outer count3 = {}", count3) << std::endl; std::cout << std::format("Value of global count3 = {}", ::count3) << std::endl;}
Concept clé :::count1 accède à la variable globale même quand une variable locale porte le même nom. Les variables sont détruites quand leur portée (le bloc {}) se termine.
Vous avez remarqué le suffixe L dans la déclaration de la variable count1? Celui-ci indique au compilateur que le littéral 999 doit être traité comme un long plutôt que comme un int.
Dans ce cas précis, ça ne change pas grand-chose puisque 999 tient largement dans un int. Mais c’est une bonne habitude pour la cohérence de type, et ça devient important avec de grandes valeurs :
long big = 3000000000L; // Sans le L, le compilateur pourrait interpréter // 3000000000 comme un int et provoquer un overflow
Les autres suffixes courants sont U pour unsigned, LL pour long long, et on peut les combiner (ULL pour unsigned long long).
break et continue
for (int i {0}; i < 100; ++i){ if (i % 2 == 0) continue; // Skip even numbers if (i > 20) break; // Stop after 20 std::cout << std::format("{}", i) << std::endl; // Only prints odd numbers 1-19}
break sert à sortir immédiatement du bloc dans lequel il se trouve.
Dans une boucle, il interrompt le parcours dès qu’une condition est remplie :
for (int i = 0; i < 100; i++) { if (i == 5) break; // sort de la boucle std::cout << i << " ";}// Affiche : 0 1 2 3 4
**Dans un ****switch**, son rôle est différent et essentiel. Sans break, l’exécution tombe en cascade dans les case suivants — c’est le fall-through :
int jour = 2;switch (jour) { case 1: std::cout << "Lundi"; break; case 2: std::cout << "Mardi"; break; // sans ce break, on continuerait dans case 3 case 3: std::cout << "Mercredi"; break;}// Affiche : Mardi
Sans les break :
int jour = 2;switch (jour) { case 1: std::cout << "Lundi "; case 2: std::cout << "Mardi "; case 3: std::cout << "Mercredi ";}// Affiche : Mardi Mercredi ← fall-through non voulu (contrairement au else if)
Parfois le fall-through est intentionnel, par exemple pour grouper des cas :
switch (jour) { case 1: case 2: case 3: case 4: case 5: std::cout << "Jour de semaine"; break; case 6: case 7: std::cout << "Weekend"; break;}// Avec des if, ça ressemblerait à ceciif (jour >= 1 || jour <= 5) std::cout << "Jour de semaine";else if ( jour <= 7) std::out << "Weekend";
En C++17, si le fall-through est voulu, on peut le rendre explicite avec l’attribut [[fallthrough]] pour éviter les warnings du compilateur :
switch (niveau) { case 3: std::cout << "Bonus niveau 3\n"; [[fallthrough]]; case 2: std::cout << "Bonus niveau 2\n"; [[fallthrough]]; case 1: std::cout << "Bonus niveau 1\n"; break;}
En résumé, dans une boucle break veut dire “j’arrête de boucler”, et dans un switch il veut dire “j’arrête de tomber dans les cases suivants”.
Exemple : jeu de devinette de nombre
#include <iostream>#include <format>int main(){ const int secret {42}; int guess {}; int attempts {0}; std::cout << std::format("I'm thinking of a number between 1 and 100...") << std::endl; do { std::cout << std::format("Your guess: "); std::cin >> guess; ++attempts; if (guess < secret) std::cout << std::format("Too low!") << std::endl; else if (guess > secret) std::cout << std::format("Too high!") << std::endl; else std::cout << std::format("Correct! You got it in {} attempts!", attempts) << std::endl; } while (guess != secret);}
Exercices
Exercice 1 : Menu textuel avec validation d'entrée
// Create a menu system that:// 1. Shows options (1-4) (1 = print a message, 2 = count until 10, 3 state current year - Use #include <ctime>, 4: Quit)// 2. Reads user choice// 3. Validates input (reject anything not 1-4)// 4. Executes the chosen option// 5. Loops until user selects "Quit"
Exercice 2 : Calculateur de factorielle
Écrivez un programme qui calcule n! (n factorielle) en utilisant une boucle for.
5! = 5 × 4 × 3 × 2 × 1 = 120
Exercice 3 : Suite de Fibonacci
Affichez les 20 premiers nombres de Fibonacci : 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
Solutions
Solution de l'exercice 1 : Menu textuel avec validation
#include <iostream>#include <ctime>void _print_menu() { std::cout << "\n=== MENU ===\n"; std::cout << "1. Afficher un message\n"; std::cout << "2. Compter jusqu'à 10\n"; std::cout << "3. Afficher la date\n"; std::cout << "4. Quitter\n"; std::cout << "Votre choix (1-4) : ";}int main() { int choice{}; do { _print_menu(); std::cin >> choice; switch (choice) { case 1: { std::cout << "\nCoucou, ceci est un message !\n"; break; } case 2: { for (int i{1}; i <= 10; ++i) std::cout << i; std::cout << std::endl; break; } case 3: { std::time_t now = std::time(0); std::cout << "Il est : " << std::ctime(&now) << std::endl; break; } case 4: { std::cout << "Au revoir !\n"; break; } default: { std::cout << "Choix invalide. Entrez un nombre entre 1 et 4.\n"; break; } } } while (choice != 4);}
Solution de l'exercice 2 : Calculateur de factorielle
#include <iostream>#include <format>int main(){ int n {}; std::cout << "Entrez un nombre entier positif : "; std::cin >> n; long long factorial {1}; for (long long i {2}; i <= n; ++i) { factorial *= i; } std::cout << std::format("{}! = {}", n, factorial) << std::endl;}
Solution de l'exercice 3 : Suite de Fibonacci
#include <iostream>#include <format>int main(){ int count {20}; long long a {0}, b {1}; std::cout << std::format("Les {} premiers nombres de Fibonacci :", count) << std::endl; for (int i {0}; i < count; ++i) { std::cout << a; if (i < count - 1) std::cout << ", "; long long next {a + b}; a = b; b = next; } std::cout << std::endl;}
Travail personnel
Lecture
Lire Beginning C++23 Chapitres 3-4
Exercices
Exercice 1 : Menu textuel avec validation d’entrée
// Create a menu system that:// 1. Shows options (1-4)// 2. Reads user choice// 3. Validates input (reject anything not 1-4)// 4. Executes the chosen option// 5. Loops until user selects "Quit"
Exercice 2 : Calculateur de factorielle
Écrivez un programme qui calcule n! (n factorielle) en utilisant une boucle for.
5! = 5 × 4 × 3 × 2 × 1 = 120
Exercice 3 : Vérificateur de nombres premiers
Écrivez une fonction qui vérifie si un nombre est premier.
Un nombre premier n’est divisible que par 1 et par lui-même
Testez avec : 2, 7, 13, 15, 97
Exercice 4 : Suite de Fibonacci
Affichez les 20 premiers nombres de Fibonacci : 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
Solutions
Solution de l’exercice 1 : Menu textuel avec validation
#include <iostream>#include <format>int main(){ int choice {}; do { std::cout << std::format("\\n=== MENU ===") << std::endl; std::cout << std::format("1. Afficher un message") << std::endl; std::cout << std::format("2. Compter jusqu'à 10") << std::endl; std::cout << std::format("3. Afficher la date") << std::endl; std::cout << std::format("4. Quitter") << std::endl; std::cout << std::format("Votre choix (1-4) : "); std::cin >> choice; switch (choice) { case 1: std::cout << std::format("Bonjour, Tech Artist !") << std::endl; break; case 2: for (int i {1}; i <= 10; ++i) std::cout << std::format("{} ", i); std::cout << std::endl; break; case 3: std::cout << std::format("Nous sommes en 2025.") << std::endl; break; case 4: std::cout << std::format("Au revoir !") << std::endl; break; default: std::cout << std::format("Choix invalide ! Entrez un nombre entre 1 et 4.") << std::endl; break; } } while (choice != 4);}
Solution de l’exercice 2 : Calculateur de factorielle
#include <iostream>#include <format>int main(){ int n {}; std::cout << std::format("Entrez un nombre entier positif : "); std::cin >> n; long long factorial {1}; for (int i {2}; i <= n; ++i) { factorial *= i; } std::cout << std::format("{}! = {}", n, factorial) << std::endl;}
Solution de l’exercice 3 : Vérificateur de nombres premiers
#include <iostream>#include <format>bool is_prime(int n){ if (n <= 1) return false; if (n <= 3) return true; if (n % 2 == 0 || n % 3 == 0) return false; for (int i {5}; i * i <= n; i += 6) { if (n % i == 0 || n % (i + 2) == 0) return false; } return true;}int main(){ int test_values[] {2, 7, 13, 15, 97}; for (int val : test_values) { std::cout << std::format("{} est {}", val, is_prime(val) ? "premier" : "non premier") << std::endl; }}
Solution de l’exercice 4 : Suite de Fibonacci
#include <iostream>#include <format>int main(){ const int count {20}; long long a {0}, b {1}; std::cout << std::format("Les {} premiers nombres de Fibonacci :", count) << std::endl; for (int i {0}; i < count; ++i) { std::cout << std::format("{}", a); if (i < count - 1) std::cout << std::format(", "); long long next {a + b}; a = b; b = next; } std::cout << std::endl;}
int i = 0;while (i < 10);{ cout << i << endl; i++;}if (x = 5) { }for (int j = 0; j < 10; j++) cout << j; cout << " done";switch (choice) { case 1: doSomething(); case 2: doOther();}
Solution :
Supprimer le ; après while (i < 10)( Sinon ça provoque une boucle infinie )
Remplacer = par == : if (x == 5)( = is assignment, == is comparison )
Ajouter des accolades : for (...) { cout << j; cout << " done"; }( only first line is in the loop (misleading indent )
Ajouter break; après doSomething(); ( missing break — falls through to case 2 )
Il manque un peu partout les std:: devant cout et endl
Fondamentaux des fonctions
Déclaration vs Définition de fonction
// Declaration (prototype) — tells the compiler "this function exists"double power(double x, int n);// Definition — provides the actual implementation. Note : a definition is also a declaration !double power(double x, int n){ double result {1.0}; if (n >= 0) { for (int i {1}; i <= n; ++i) result *= x; } else { for (int i {1}; i <= -n; ++i) result /= x; } return result;}
// This works — definition comes before the callint add(int a, int b) { return a + b;}int main() { add(2, 3); // compiler already knows about add()}
// This fails — compiler hasn't seen add() yetint main() { add(2, 3); // error: 'add' was not declared}int add(int a, int b) { return a + b;}
int add(int a, int b); // declaration only — no bodyint main() { add(2, 3); // now the compiler knows the signature}int add(int a, int b) { // definition comes later return a + b;}
Tutoriel : Calcul de puissances
// Calculating powers#include <iostream>#include <format>double power(double x, int n){ double result{ 1.0 }; if (n >= 0) { for (int i{ 1 }; i <= n; ++i) result *= x; } else // n < 0 { for (int i{ 1 }; i <= -n; ++i) result /= x; } return result;}int main(){ // Calculate powers of 8 from -3 to +3 for (int i{ -3 }; i <= 3; ++i) std::cout << std::format("{:10g}", power(8.0, i)); std::cout << std::endl;}
Sortie :0.00195312 0.015625 0.125 1 8 64 512
Pourquoi les prototypes de fonctions sont importants
// This WON'T compile — power() is used before it's defined!#include <iostream>#include <format>int main(){ for (int i {-3}; i <= 3; ++i) std::cout << std::format("{:10g}", power(8.0, i)); // ERROR: 'power' not declared std::cout << std::endl;}double power(double x, int n){ // ... definition comes too late}
Règle : Soit vous définissez les fonctions avant main(), soit vous déclarez un prototype avant main() et définissez la fonction après. Règle plus réglée : utiliser des header files (.h ou .hpp ou .hxx) et les inclure, mais on n’a pas encore vu ça :)
Overload de fonctions
Plusieurs fonctions peuvent partager le même nom si elles ont des listes de paramètres différentes :
int maximum(int a, int b) { return a > b ? a : b; }double maximum(double a, double b) { return a > b ? a : b; }// The compiler picks the right one based on argument types
Ceci dit, si la seule différence est le type de retour de la fonction, ça ne compilera pas. C’est le type ou le nombre de paramètres à la fonction overloadée qui compte.
int compute(double x); // OKdouble compute(double x); // error: cannot overload on return type alone
Règle : Les paramètres par défaut doivent aller de droite à gauche :
void func(int a, int b = 5, int c = 10); // OK// void func(int a = 5, int b, int c); // ERROR!func(3) // valide, même chose que func(3,5,10) // même chose
Ma qué, pourquoi de droite à gauche me direz vous ?
Parce que les appels de fonction en C++ associent les arguments aux paramètres de gauche à droite, par position. Le compilateur a donc besoin de savoir sans ambiguïté à quel paramètre correspond chaque argument.
Si on pouvait sauter un paramètre au milieu, le compilateur ne saurait pas ce qu’on veut dire :
// hypothétiquement, si le C++ permettait ceci (ce n'est pas le cas)void f(int a = 1, int b, int c = 3);f(10, 20); // est-ce f(10, 20, 3) ou f(1, 10, 20) ? C'est ambigu
En imposant les valeurs par défaut de droite à gauche, chaque appel est sans ambiguïté — on remplit à partir de la gauche et les valeurs par défaut complètent à partir de la droite :
void f(int a, int b = 2, int c = 3);f(10); // f(10, 2, 3)f(10, 20); // f(10, 20, 3)f(10, 20, 30); // f(10, 20, 30)
Il n’y a jamais de trou. Le C++ n’a pas d’arguments nommés (contrairement à Python avec f(c=30, a=10)), donc la position est la seule information dont le compilateur dispose.
Overload, valeurs par défaut et name mangling :
Le compilateur traite les fonctions overloadées comme des fonctions complètement distinctes. En coulisses, le C++ utilise le "name mangling", i.e. il encode les types des paramètres dans le nom du symbole. Ainsi, compute(int) pourrait devenir quelque chose comme _Z7computei et compute(double) devient _Z7computed dans le code binaire compilé. On peut d'ailleurs observer cela avec nm ou c++filt sur un fichier objet compilé.
Évidemment, c’est du C++, donc c’est bourré d’exceptions et de subtilités pas évidents, comme :
const et les références comptent — f(int&) et f(const int&) sont des overloads distincts.
Cela dit, un const de premier niveau sur un paramètre passé par valeur ne compte pas : f(int) et f(const int) ont la même signature, puisque l’appelant ne peut pas faire la différence.
Les arguments par défaut peuvent créer des ambiguïtés. Si on a f(int x, int y = 0) et f(int x), un appel à f(5) est ambigu et ne compilera pas — même si techniquement ce sont deux fonctions distinctes.
Passage de paramètres
Passage par valeur
// Failing to modify the original value of a function argument#include <iostream>double changeIt(double it); // Function prototypeint main(){ double it {5.0}; double result {changeIt(it)}; std::cout << std::format("After function execution, it = {}", it) << std::endl; // Still 5.0! std::cout << std::format("Result returned is {}", result) << std::endl; // 15.0}// Function that attempts to modify an argument and return itdouble changeIt(double it){ it += 10.0; // This modifies the COPY std::cout << std::format("Within function, it = {}", it) << std::endl; // 15.0 return it;}
Sortie :
Within function, it = 15After function execution, it = 5 ← Original unchanged!Result returned is 15
Passage par référence (&)
void doubleValue(int& x) // Note the & — this is a reference{ x = x * 2; // This modifies the ORIGINAL}int main(){ int num {5}; doubleValue(num); std::cout << std::format("{}", num) << std::endl; // 10 — the original was modified!}
Passage par référence constante (const &)
void printName(const std::string& name) // Can read, but cannot modify{ std::cout << std::format("Name: {}", name) << std::endl; // name = "Changed"; // ERROR: name is const!}
En résumé
Quand utiliser quoi :
Méthode
À utiliser quand…
Exemple
Passage par valeur
Types simples (int, double, bool) que vous n’avez pas besoin de modifier
void print(int x)
Passage par référence &
Vous devez modifier l’original
void swap(int& a, int& b)
Passage par const ref const&
Types volumineux que vous avez seulement besoin de lire
void print(const std::string& s)
Questions :
Est-ce que x, dans les cas 2 ou 3, est de type int ?
Quel est le “scope” de la variable x passée en paramètre à la fonction f?
Lab 1 : Fonction swap
#include <iostream>#include <format>void swap(int& a, int& b){ int temp {a}; a = b; b = temp;}int main(){ int x {10}, y {20}; std::cout << std::format("Before: x={}, y={}", x, y) << std::endl; swap(x, y); std::cout << std::format("After: x={}, y={}", x, y) << std::endl;}
Lab 2 : ColorLab
Fichiers multiples, Headers, struct et Passage de Paramètres
Un Laboratoire de Couleurs
On va construire un petit système de mélange de couleurs RGB. Il nous permettra de créer des couleurs, ajuster leurs composantes rouge, vert et bleu, mélanger deux couleurs ensemble, et afficher les résultats directement dans le terminal avec de la couleur. En chemin, on verra le pass-by-value, le pass-by-reference, et les const references en action.
Les couleurs RGB sont parfaites pour cet exercice : chaque composante (R, G, B) est un entier entre 0 et 255, ce qui nous oblige aussi à gérer le clamping (s’assurer qu’on ne dépasse jamais ces bornes).
Jusqu’ici, vous avez tout écrit dans un seul fichier main.cpp. Ça fonctionne bien pour les petits programmes, mais imaginez que vous avez 50 fonctions — faire défiler des milliers de lignes devient un cauchemar.
En développement C++ professionnel, on organise le code comme ceci :
Fichier
Rôle
main.cpp
Contient main() et la logique principale
colors.h
Header file — contient les déclarations (prototypes) des fonctions
colors.cpp
Source file — contient les définitions (implémentations) des fonctions
Pensez-y comme un atelier de peinture :
Le header file (.h) est le catalogue de l’atelier — il vous dit ce qui est disponible (noms des fonctions, paramètres, types de retour).
Le source file (.cpp) est l’arrière-boutique — c’est comment les choses sont réellement préparées (le corps des fonctions).
main.cpp est l’artiste — il consulte le catalogue et passe ses commandes.
Deux autres nouveaux concepts qu’on n’a pas encore vu
1 - Les struct en C++
Le problème
Sans struct, si on veut représenter une couleur avec seulement le types de base, on a, par exemple, besoin de plusieurs variables indépendantes :
Une struct regroupe des variables liées dans un type personnalisé
On accède aux membres avec .
Ça rend le code plus lisible et les fonctions plus simples
2 - Affichage de couleurs dans le terminal
On utilisera les ANSI escape codes — une fonctionnalité intégrée dans les terminaux Mac et Linux, sans aucune librairie externe. Ça nous permet d’afficher du texte coloré directement.
// \033 is interpreted as the octal escape for the ESC character (ASCII 27), which is what triggers ANSI color codes in the terminal// Exemple d'ANSI escape code pour afficher du texte en couleur RGB :int main(){ // --- Créer deux couleurs (pass by value) --- int r1 = 220, g1 = 60, b1 = 10; // Rouge Soleil std::cout << "\033[38;2;" << r1 << ";" << g1 << ";" << b1 << "m"; std::cout << "████████"; // Bloc de couleur std::cout << "\033[0m"; // Réinitialiser la couleur}
Ce que vous verrez dans le terminal cmd.exe :
☝🏼
CLion sur Windows malheureusement a un terminal qui ne supporte pas bien les couleurs. Pour bien voir le résultat, exécutez colorlab.exe, qui se trouve dans votre dosser cmake-build.debug, dans un command line (cmd.exe).
1 - Créer le projet dans CLion
Créez un nouveau dossier appelé ColorLab.
Ouvrez VSCode et “Open folder” de ColorLab
À l’intérieur de ce dossier, créez ces** fichiers** :
main.cpp
colors.h
colors.cpp
CMakeLists.txt
☝🏼
Dans CLion, ajoutez le toolchain MinGW (Default) dans Settings → Build, Execution, Deployments → Toolchains
Il se peut que vous ayez à faire un “Load CMake Project” si vous voyez le warning suivant “Project not configured” au-dessus de votre fichier CMakeLists.txt
à ce moment-là, cliquez sur Fix… et choisissez “Load CMake Project”
3 - Le Header File (colors.h)
Le header file ne contient que les déclarations (prototypes). Il dit au compilateur : « ces fonctions existent, et voici à quoi elles ressemblent. »
// colors.h// ========// Déclarations pour les fonctions du Laboratoire de Couleurs.#ifndef COLORS_H#define COLORS_H#include <string>// Une couleur RGB nommée.struct Color { std::string name; int r, g, b;};// Vérifie si les composantes sont dans la plage [0-255].// BY CONST REFERENCE — lecture seule, pas de copie de la struct.bool isValid(const Color& c);// Garde une valeur dans la plage [0, 255].// BY REFERENCE — on modifie la variable originale.void clamp(int& value);// ⚠️ PIÈGE — version cassée de clamp !// BY VALUE — la modification reste locale, l'original ne change PAS.void clampBroken(int value);// Augmente les composantes RGB d'une couleur.// 'c' est BY REFERENCE — on modifie la couleur originale.// Les amounts sont BY VALUE — un int est petit, pas besoin de référence.void add(Color& c, int redAmount, int greenAmount, int blueAmount);// Diminue les composantes RGB d'une couleur.// Même logique : BY REFERENCE pour 'c', BY VALUE pour les amounts.void remove(Color& c, int redAmount, int greenAmount, int blueAmount);// Mélange deux couleurs (version ints) — 9 paramètres !// Les 6 premiers sont BY VALUE — on ne veut pas altérer les originaux.// Les 3 derniers sont BY REFERENCE — c'est là qu'on écrit le résultat.void mixColors(int r1, int g1, int b1, int r2, int g2, int b2, int& outR, int& outG, int& outB);// Mélange deux couleurs (version struct) — OVERLOAD de la fonction ci-dessus.// Même nom, signature différente → le compilateur choisit la bonne version.// Entrées BY CONST REFERENCE — on ne modifie pas les originaux.// Retour PAR VALEUR — efficace grâce au RVO (Return Value Optimization).Color mixColors(const Color& a, const Color& b);// Affiche une couleur dans le terminal avec un bloc coloré ANSI.// BY CONST REFERENCE — efficace (évite la copie du string + ints),// et on promet de ne pas modifier la couleur.void displayColor(const Color& c);#endif // COLORS_H
C’est quoi ce #ifndef / #define / #endif ?
On appelle ça des include guards. Ils empêchent le header d’être traité plus d’une fois s’il est accidentellement inclus à plusieurs endroits. C’est une pratique standard — faites-le toujours dans vos header files.
Comment ça fonctionne :
La première fois que le compiler voit #include "colors.h", COLORS_H n’est pas encore défini.
Donc il le définit (#define COLORS_H) et traite tout ce qui se trouve à l’intérieur.
S’il rencontre #include "colors.h" une deuxième fois, COLORS_H est déjà défini, donc il saute tout. Pas de déclarations en double.
4 - Le Source File (colors.cpp)
Le source file contient les définitions (mises en œuvre). C’est ici que la vraie logique se trouve.
// colors.cpp// ==========// Définitions pour les fonctions du Laboratoire de Couleurs.#include "colors.h"#include <iostream>// ------------------------------------------------------------// isValid — vérifie que les composantes sont dans [0, 255]// 'c' est passé BY CONST REFERENCE — on lit sans copier// ------------------------------------------------------------bool isValid(const Color& c) { if (c.r < 0 || c.r > 255 || c.g < 0 || c.g > 255 || c.b < 0 || c.b > 255) { std::cout << " ATTENTION : " << c.name << " a des valeurs hors plage !" << std::endl; return false; } return true;}// ------------------------------------------------------------// clamp — garde une valeur entre 0 et 255// BY REFERENCE — modifie l'original// ------------------------------------------------------------void clamp(int& value) { if (value < 0) value = 0; if (value > 255) value = 255;}// ------------------------------------------------------------// clampBroken — ⚠️ PIÈGE : version cassée de clamp// BY VALUE — c'est une COPIE locale.// La modification ne sort jamais de cette fonction !// ------------------------------------------------------------void clampBroken(int value) { if (value < 0) value = 0; if (value > 255) value = 255; // 'value' est une copie → elle meurt ici, l'original est INCHANGÉ}// ------------------------------------------------------------// add — augmente les 3 composantes et applique le clamp// 'c' est BY REFERENCE — modifie la couleur originale// Les amounts sont BY VALUE — petit type, copie pas chère// ------------------------------------------------------------void add(Color& c, int redAmount, int greenAmount, int blueAmount) { c.r += redAmount; c.g += greenAmount; c.b += blueAmount; clamp(c.r); clamp(c.g); clamp(c.b); std::cout << " +RGB (" << redAmount << ", " << greenAmount << ", " << blueAmount << ") → R:" << c.r << " G:" << c.g << " B:" << c.b << std::endl;}// ------------------------------------------------------------// remove — diminue les 3 composantes et applique le clamp// Même logique que la fonction add// ------------------------------------------------------------void remove(Color& c, int redAmount, int greenAmount, int blueAmount) { c.r -= redAmount; c.g -= greenAmount; c.b -= blueAmount; clamp(c.r); clamp(c.g); clamp(c.b); std::cout << " -RGB (" << redAmount << ", " << greenAmount << ", " << blueAmount << ") → R:" << c.r << " G:" << c.g << " B:" << c.b << std::endl;}// ------------------------------------------------------------// mixColors (version ints) — 9 paramètres, pas très lisible...// Les 6 premiers BY VALUE — les originaux ne changent pas// Les 3 derniers BY REFERENCE — c'est là qu'on écrit le résultat// ------------------------------------------------------------void mixColors(int r1, int g1, int b1, int r2, int g2, int b2, int& outR, int& outG, int& outB) { std::cout << " Mélange (version ints)..." << std::endl; outR = (r1 + r2) / 2; outG = (g1 + g2) / 2; outB = (b1 + b2) / 2;}// ------------------------------------------------------------// mixColors (version struct) — OVERLOAD : même nom, signature différente// BY CONST REFERENCE → 2 paramètres au lieu de 9, bien plus clair// Retour PAR VALEUR — le compilateur optimise via RVO// ------------------------------------------------------------Color mixColors(const Color& a, const Color& b) { std::cout << " Mélange de " << a.name << " + " << b.name << "..." << std::endl; return Color{ a.name + " + " + b.name, (a.r + b.r) / 2, (a.g + b.g) / 2, (a.b + b.b) / 2 };}// ------------------------------------------------------------// displayColor — affiche un bloc coloré ANSI dans le terminal// BY CONST REFERENCE — évite la copie, garantit la lecture seule// ------------------------------------------------------------void displayColor(const Color& c) { std::cout << " [" << c.name << "] "; std::cout << "\033[38;2;" << c.r << ";" << c.g << ";" << c.b << "m"; std::cout << "████████"; std::cout << "\033[0m"; std::cout << " R:" << c.r << " G:" << c.g << " B:" << c.b << std::endl;}
Points importants à noter
On écrit #include "colors.h" (avec des guillemets, pas des chevrons). Les guillemets disent au compiler de chercher d’abord dans le dossier du projet. Les chevrons (< >) sont pour les headers système/standard library.
Chaque signature de fonction ici doit correspondre exactement à la déclaration dans le header.
La fonction clamp() est appelée à l’intérieur des autres fonctions — c’est un bon exemple de réutilisation de code.
5 - Le fichier principal contenant la fonction point d’entrée main() main.cpp
// main.cpp// ========// Le Laboratoire de Couleurs — là où la magie opère.#include "colors.h"#include <iostream>#ifdef _WIN32#include <windows.h>#endif#ifdef _WIN32void enableAnsiColors() { // Set console to UTF-8 SetConsoleOutputCP(CP_UTF8); SetConsoleCP(CP_UTF8); // Enable ANSI escape sequences HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); DWORD dwMode = 0; GetConsoleMode(hOut, &dwMode); SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);}#endifint main() { #ifdef _WIN32 enableAnsiColors(); #endif // --- Créer deux couleurs --- Color soleil{"Rouge Soleil", 220, 60, 10}; Color ocean{"Bleu Océan", 10, 80, 210}; displayColor(soleil); // const Color& → lecture seule, pas de copie displayColor(ocean); std::cout << std::endl; // --- Validation (const Color&) --- std::cout << "--- Test de validation ---" << std::endl; Color invalide{"Invalide", 300, -10, 50}; if (!isValid(invalide)) { std::cout << " Couleur rejetée !" << std::endl; } std::cout << std::endl; // --- PIÈGE : clampBroken vs clamp (by value vs by reference) --- std::cout << "--- clampBroken vs clamp ---" << std::endl; int testValue = 999; std::cout << " Avant : testValue = " << testValue << std::endl; clampBroken(testValue); // BY VALUE → ne modifie PAS testValue std::cout << " clampBroken : testValue = " << testValue << " (inchangé ! la copie est morte)" << std::endl; clamp(testValue); // BY REFERENCE → modifie testValue std::cout << " clamp : testValue = " << testValue << " (modifié via référence)" << std::endl; std::cout << std::endl; // --- Modifier une couleur (Color&) --- std::cout << "--- Modification de Rouge Soleil ---" << std::endl; add(soleil, 0, 80, 0); // Color& → modifie la struct originale remove(soleil, 0, 0, 5); displayColor(soleil); std::cout << std::endl; // --- Réinitialiser Rouge Soleil avant le mélange --- // Le mélange sera vraiment Rouge Soleil + Bleu Océan, pas la version modifiée soleil = Color{"Rouge Soleil", 220, 60, 10}; std::cout << "--- Rouge Soleil réinitialisé pour le mélange ---" << std::endl; displayColor(soleil); std::cout << std::endl; // --- Mélange avec ints : 9 paramètres, 3 out-references --- std::cout << "--- Mélange (version ints — 9 paramètres) ---" << std::endl; int outR, outG, outB; mixColors(soleil.r, soleil.g, soleil.b, ocean.r, ocean.g, ocean.b, outR, outG, outB); std::cout << " Résultat : R:" << outR << " G:" << outG << " B:" << outB << std::endl; std::cout << std::endl; // --- Mélange avec struct : même nom, 2 paramètres, bien plus clair --- std::cout << "--- Mélange (version struct — overload) ---" << std::endl; Color fusion = mixColors(soleil, ocean); // le compilateur choisit l'overload displayColor(fusion); std::cout << std::endl; // --- Vérifier que les originaux n'ont pas changé après les mélanges --- std::cout << "--- Originaux intacts (const& les protège) ---" << std::endl; displayColor(soleil); displayColor(ocean); std::cout << std::endl; return 0;}
Remarquez que main.cpp n’a besoin que de #include "colors.h" — il n’a pas besoin de savoir comment les fonctions fonctionnent, seulement leurs déclarations respectives.
6 - Compiler et exécuter
C’est le marteau dans CLion pour compiler, le “Play” vert pour exécuter
6.2 - Sortie attendue
☝🏼
CLion sur Windows malheureusement a un terminal qui ne supporte pas bien les couleurs. Pour bien voir le résultat, exécutez colorlab.exe, qui se trouve dans votre dosser cmake-build-debug, dans un command line (cmd.exe).
Pour ce faire, vous pouvez naviguer dans Windows Explorer vers le dossier cmake-build-debug, right-click et choisir “Open Terminal”.
Ensuite, il suffit d’exécuter colorlab.exe.
Si PowerShell se plain, exécutez en précédant colorlab.exe d’un “.\”, donc:
.\colorlab.exe puis enter
Banque de cours - Cours 5 - Tableaux (Arrays), Vecteurs (Vectors) et Chaînes de caractèr
# Cours 5 - Tableaux (Arrays), Vecteurs (Vectors) et Chaînes de caractères (Strings)
Où est Charlie le Bug ?
Sujets couverts : Variables, boucles, fonctions, passage de paramètres
void doubleValue(int x) { x = x * 2;}int add(int a, int b);int add(int a, int b) { int sum = a + b;}int calculate(int x = 5, int y) { }void process(bool condition) { int result; if (condition) { result = 10; } cout << result;}
Solution
void doubleValue(int x) { // Bug 1: should be reference (int& x) to modify original x = x * 2;}int add(int a, int b); // Declarationint add(int a, int b) { // Bug 2: missing return statement! int sum = a + b;}int calculate(int x = 5, int y) { } // Bug 3: default params must go right-to-leftvoid process(bool condition) { int result; if (condition) { result = 10; } cout << result; // Bug 4: result may be uninitialized if condition is false}
Tableaux fixes : std::array
Tableaux de style C (les connaître, les éviter)
int scores[5] {90, 85, 78, 92, 88};// Problems:// - Don't know their own size// - Decay to pointers when passed to functions// - No bounds checking// - Can't be returned from functions
std::array<T, N> : Le tableau moderne de taille fixe
#include <array>std::array<int, 5> scores {90, 85, 78, 92, 88};scores.size(); // Returns 5scores[0]; // Access first element (no bounds check)scores.at(0); // Access with bounds check (throws if out of range)
Tutoriel : Boucle for avec un tableau
// Using a for loop with an array#include <iostream>#include <format>#include <array>int main(){ std::array<unsigned, 6> height {26, 37, 47, 55, 62, 75}; // An array of heights unsigned total {}; for (std::size_t i {} ;i < height.size(); ++i) { total += height[i]; } const auto average{ static_cast<double>(total) / height.size() }; std::cout << std::format("The average height is {:.1f}.", average) << std::endl; unsigned count {}; for (std::size_t i {}; i < height.size(); ++i) { if (height[i] < average) ++count; } std::cout << std::format("{} people are below average height.", count) << std::endl;}
Concepts clésstd::array connaît sa propre taille — plus besoin d'une variable size séparée
static_cast<double>(total) empêche la division entière
{:.1f} formate un nombre à virgule flottante avec 1 décimale
mais justement, sa taille est fixe!
Obtenir la taille d'un tableau
// Obtaining the number of array elements#include <iostream>#include <format>#include <array>int main(){ std::array values {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; // CTAD: std::array<int, 10> std::cout << std::format("There are {} elements in the array.", values.size()) << std::endl; int sum {}; for (std::size_t i {}; i < values.size(); ++i) { sum += values[i]; } std::cout << std::format("The sum of the array elements is {}.", sum) << std::endl;}
Boucle for basée sur les intervalles (aperçu)
std::array values {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};for (int val : values) // Range-based for - iterates every element{ std::cout << std::format("{} ", val);}
Accéder à un élément (avec vérification des bornes)
front()
Premier élément
back()
Dernier élément
insert()
Insérer à l’index spécifié
std::string : Pas seulement des tableaux de char
// Concatenating strings#include <iostream>#include <format>#include <string>int main(){ std::string first; // Stores the first name std::string second; // Stores the second name std::cout << std::format("Enter your first name: "); std::cin >> first; // Read first name std::cout << std::format("Enter your last name: "); std::cin >> second; // Read second name std::string sentence {"Your full name is "}; // Create basic sentence sentence += first + " " + second + "."; // Augment with names std::cout << std::format("{}", sentence) << std::endl; std::cout << std::format("The string contains {} characters.", sentence.length()) << std::endl;}
Tutoriel : Compter les voyelles et les consonnes
// Accessing characters in a string#include <iostream>#include <format>#include <string>#include <cctype>int main(){ std::string text; std::cout << std::format("Enter a line of text:") << std::endl; std::getline(std::cin, text); // Read a line including spaces unsigned vowels {}; unsigned consonants {}; for (std::size_t i {}; i < text.length(); ++i) { if (std::isalpha(text[i])) // Check for a letter { switch (std::tolower(text[i])) // Convert to lowercase { case 'a': case 'e': case 'i': case 'o': case 'u': case 'y': ++vowels; break; default: ++consonants; break; } } } std::cout << std::format("Your input contained {} vowels and {} consonants.", vowels, consonants) << std::endl;}
std::cin >> texte
L'opérateur d'extraction >> lit les caractères depuis l'entrée standard jusqu'au premier espace blanc (espace, tabulation ou retour à la ligne). Les espaces blancs en début de saisie sont ignorés.
std::string texte;std::cin >> texte;// Saisie : "Bonjour le monde"// Résultat : texte == "Bonjour" ← le reste est perdu
std::getline(std::cin, texte)std::getline() lit toute la ligne jusqu'au caractère de retour à la ligne '\n'. Les espaces sont conservés. Le '\n' est consommé mais n'est pas stocké dans la chaîne.
std::string texte;std::getline(std::cin, texte);// Saisie : "Bonjour le monde"// Résultat : texte == "Bonjour le monde" ✓
Tableau comparatif
Comportement
std::cin >> texte
std::getline()
S’arrête à l’espace
✓
✗
Conserve les espaces
✗
✓
Ignore les espaces en début
✓
✗
Consomme le '\n' final
✗
✓
Le piège classique : mélanger les deux
Lorsqu'on utilise std::cin >>avantstd::getline(), un '\n' résiduel reste dans le buffer d'entrée (input buffer). std::getline() le lit immédiatement et retourne une chaîne vide.
int age;std::string nom;std::cout << "Entrez votre âge : ";std::cin >> age;// L'utilisateur tape "25" puis appuie sur Entrée.// Le '25' est lu, mais le '\n' reste dans le buffer.std::cout << "Entrez votre nom complet : ";std::getline(std::cin, nom);// std::getline() lit immédiatement le '\n' résiduel.// Résultat : nom == "" ← chaîne vide, la saisie est ignorée !
Pourquoi ça arrive ?
Buffer après std::cin >> age :[ '\n' ] ^ std::getline() s'arrête ici immédiatement
La solution : vider le buffer
Après un std::cin >>, il faut consommer le '\n' résiduel avant d'appeler std::getline().
Option 1 — std::cin.ignore() (simple)
std::cin >> age;std::cin.ignore(); // consomme un seul caractère (le '\n')std::getline(std::cin, nom);
#include <limits>std::cin >> age;std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');// Consomme tout ce qui reste dans le buffer jusqu'au '\n' inclusstd::getline(std::cin, nom);
Cette forme est préférable si l’utilisateur a pu saisir des caractères supplémentaires après la valeur (ex. : "25 abc\n").
Dès que std::cin >> et std::getline() coexistent dans un programme, il faut toujours vider le buffer entre les deux. C’est l’une des erreurs les plus fréquentes chez les débutants en C++.
☝🏼
Conseil: toujours utiliser std::getline(std::cin, my_string) plutôt que std::cin >> my_string lorsqu’il s’agit de lire une ligne de la console
ColorLab v2 - Palettes, Couleurs Web & Opérations sur les Chaînes
Rappel : où en sommes-nous ?
Dans le ColorLab v1 (semaine 4), on a introduit struct Color pour regrouper le nom et les composantes RGB, et on a vu comment les fonctions manipulent ces structs avec le passage par référence :
Color soleil {"Rouge Soleil", 220, 60, 10};Color ocean {"Bleu Océan", 10, 80, 210};Color fusion = mixColors(soleil, ocean); // 2 paramètres au lieu de 9 !displayColor(fusion); // 1 paramètre au lieu de 4 !
C’est bien plus propre qu’avec des int séparés. Mais on travaille toujours avec des couleurs individuelles. Et si on voulait gérer une palette de 12 couleurs web ? Les chercher par nom ? Les trier ? Il nous faut des collections.
L'évolution : des couleurs individuelles aux collections
On a déjà notre struct Color de la semaine 4. Ce qui change cette semaine, c'est qu'on peut maintenant les organiser en collections et exploiter les opérations sur les chaînes :
**std::vector<Color>** — une palette dynamique qui peut grandir à volonté
**std::string** — on exploite find(), npos et le formatage hex pour chercher et convertir les noms de couleurs
// colors.h - v2// ==============// Déclarations pour le ColorLab v2 : palettes, couleurs web, opérations sur les chaînes.#ifndef COLORS_H#define COLORS_H#include <string>#include <vector>// --- struct Color (défini en semaine 4 — rappel) ---struct Color { std::string name; // Nom de la couleur int r {}; // Rouge (0-255) int g {}; // Vert (0-255) int b {}; // Bleu (0-255)};// Garde une valeur dans la plage [0, 255].// Paramètre BY REFERENCE - modifie l'original.void clamp(int& value);// Crée une couleur avec validation et clamping automatique.// Retourne un Color par valeur.Color createColor(const std::string& name, int r, int g, int b);// Affiche une couleur avec un bloc ANSI coloré dans le terminal.// Paramètre BY CONST REFERENCE - lecture seule, efficace.void displayColor(const Color& color);// Calcule la luminosité moyenne pondérée d'une couleur (0–255).// Paramètre BY CONST REFERENCE.int brightness(const Color& color);// Mélange deux couleurs et retourne le résultat.// Les deux couleurs d'entrée sont BY CONST REFERENCE - on ne les modifie pas.Color mixColors(const Color& a, const Color& b, const std::string& mixName);// --- NOUVEAU v2 : fonctions qui utilisent std::vector et std::string ---// Affiche toutes les couleurs d'une palette.// Le vector est passé BY CONST REFERENCE - on ne modifie pas la palette.void displayPalette(const std::vector<Color>& palette);// Cherche une couleur par nom exact dans la palette.// Retourne l'index si trouvée, -1 sinon.int findByName(const std::vector<Color>& palette, const std::string& name);// Cherche toutes les couleurs dont le nom contient une sous-chaîne.// Utilise std::string::find() — retourne un vector d'indices.std::vector<int> findByPartialName(const std::vector<Color>& palette, const std::string& partial);// Convertit une couleur en chaîne hexadécimale CSS : "#RRGGBB".// Utilise std::format pour le formatage hexadécimal.std::string colorToHex(const Color& color);// Retourne une palette de couleurs web standard prédéfinies.std::vector<Color> getWebColors();#endif // COLORS_H
Ce qui a changé par rapport à v1
Aspect
v1 (semaine 4)
v2 (semaine 5)
struct Color
Introduit (name, r, g, b)
Réutilisé tel quel
Collections
Couleurs individuelles
std::vector<Color> — palettes dynamiques
Couleurs prédéfinies
Aucune
12 couleurs web (getWebColors())
Recherche
Non disponible
findByName(), findByPartialName()
Opérations sur chaînes
std::string pour les noms
string::find(), formatage hex
Format de sortie
std::cout << chaîné
std::format partout
Itération
Pas de collection à itérer
Range-based for sur la palette
2 - Le Source File (colors.cpp) - v2
// colors.cpp - v2// ================// Définitions pour le ColorLab v2 : palettes, recherche, hex.#include "colors.h"#include <iostream>#include <format>// ------------------------------------------------------------// clamp - inchangé depuis v1// ------------------------------------------------------------void clamp(int& value) { if (value < 0) value = 0; if (value > 255) value = 255;}// ------------------------------------------------------------// createColor - factory function avec clamping automatique// ------------------------------------------------------------Color createColor(const std::string& name, int r, int g, int b) { clamp(r); clamp(g); clamp(b); return Color{name, r, g, b};}// ------------------------------------------------------------// displayColor - affiche un bloc coloré ANSI// BY CONST REFERENCE - on ne modifie pas la couleur// ------------------------------------------------------------void displayColor(const Color& color) { std::cout << std::format(" [{:<20s}] ", color.name); std::cout << "\033[38;2;" << color.r << ";" << color.g << ";" << color.b << "m"; std::cout << "████████"; std::cout << "\033[0m"; std::cout << std::format(" R:{:<3d} G:{:<3d} B:{:<3d}", color.r, color.g, color.b); std::cout << std::endl;}// ------------------------------------------------------------// brightness - luminosité moyenne pondérée // The standard formula is a **weighted average**, most commonly the ITU-R BT.709 luma coefficient used in modern displays://// Y = 0.2126·R + 0.7152·G + 0.0722·B// The weights reflect the fact that the human eye is **far more sensitive to green** than to red, and barely sensitive to blue. This is due to the distribution of cone cells in the retina — the M (medium) cones responsible for green perception are the most numerous.// BY CONST REFERENCE - lecture seule// ------------------------------------------------------------int brightness(const Color& color) { return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;}// ------------------------------------------------------------// mixColors - prend 2 Color, retourne un Color// BY CONST REFERENCE pour les entrées// ------------------------------------------------------------Color mixColors(const Color& a, const Color& b, const std::string& mixName) { return Color{mixName, (a.r + b.r) / 2, (a.g + b.g) / 2, (a.b + b.b) / 2};}// ============================================================// NOUVEAU v2 : fonctions qui exploitent std::vector et std::string// ============================================================// ------------------------------------------------------------// displayPalette - affiche toutes les couleurs d'un vector// Le vector est passé BY CONST REFERENCE - efficace et sûr// On utilise un range-based for pour itérer// ------------------------------------------------------------void displayPalette(const std::vector<Color>& palette) { std::cout << std::format("=== Palette ({} couleurs) ===", palette.size()) << std::endl; for (const Color& c : palette) { displayColor(c); }}// ------------------------------------------------------------// findByName - recherche linéaire par nom exact// Retourne l'index ou -1 si non trouvée// ------------------------------------------------------------int findByName(const std::vector<Color>& palette, const std::string& name) { for (std::size_t i {0}; i < palette.size(); ++i) { if (palette[i].name == name) { return static_cast<int>(i); } } return -1;}// ------------------------------------------------------------// findByPartialName - recherche par sous-chaîne// Utilise string::find() pour chercher un fragment dans chaque nom.// Si find() retourne std::string::npos, le fragment n'a pas été trouvé.// Retourne un vector d'indices — peut être vide si rien ne correspond.// ------------------------------------------------------------std::vector<int> findByPartialName(const std::vector<Color>& palette, const std::string& partial) { std::vector<int> results; for (std::size_t i {0}; i < palette.size(); ++i) { if (palette[i].name.find(partial) != std::string::npos) { results.push_back(static_cast<int>(i)); } } return results;}// ------------------------------------------------------------// colorToHex - convertit une couleur en string hexadécimale CSS// Utilise std::format avec {:02X} pour formater chaque composante// en 2 chiffres hexadécimaux majuscules (00-FF).// Retourne une std::string par valeur.// ------------------------------------------------------------std::string colorToHex(const Color& color) { return std::format("#{:02X}{:02X}{:02X}", color.r, color.g, color.b);}// ------------------------------------------------------------// getWebColors - retourne une palette de couleurs web connues// Les noms viennent du standard CSS/SVG - vos étudiants les// connaissent peut-être déjà du web !// ------------------------------------------------------------std::vector<Color> getWebColors() { return { {"Teal", 0, 128, 128}, {"Salmon", 250, 128, 114}, {"Coral", 255, 127, 80}, {"Goldenrod", 218, 165, 32}, {"Orchid", 218, 112, 214}, {"SteelBlue", 70, 130, 180}, {"Tomato", 255, 99, 71}, {"MediumAquamarine", 102, 205, 170}, {"SlateGray", 112, 128, 144}, {"Crimson", 220, 20, 60}, {"LimeGreen", 50, 205, 50}, {"DarkOrange", 255, 140, 0} };}
Points importants à noter
**std::vector<Color>** peut contenir autant de couleurs qu’on veut, et on peut en ajouter dynamiquement avec push_back().
**Range-based ****for** : for (const Color& c : palette) est bien plus lisible que for (int i = 0; i < palette.size(); ++i).
**string::find()** retourne la position de la sous-chaîne trouvée, ou std::string::npos si elle n’existe pas. C’est le patron classique pour chercher dans une chaîne.
**std::format("{:02X}", value)** formate un entier en hexadécimal (X) sur au moins 2 caractères (02), avec des zéros à gauche. Parfait pour les codes couleur CSS.
**findByPartialName()** retourne un std::vector<int> — un vector qu’on construit dans la fonction avec push_back() et qu’on retourne par valeur. C’est un bon exemple d’utilisation dynamique d’un vector.
3 - main.cpp - v2
// main.cpp - ColorLab v2// =======================// Palettes, couleurs web, recherche par nom et codes hex.#include "colors.h"#include <iostream>#include <format>#ifdef _WIN32#include <windows.h>#endif#ifdef _WIN32void enableAnsiColors() { // Set console to UTF-8 SetConsoleOutputCP(CP_UTF8); SetConsoleCP(CP_UTF8); // Enable ANSI escape sequences HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); DWORD dwMode = 0; GetConsoleMode(hOut, &dwMode); SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);}#endifint main() { #ifdef _WIN32 enableAnsiColors(); #endif std::cout << "=== ColorLab v2 - Palettes & Couleurs Web ===" << "\n\n"; // --- Créer des couleurs avec notre struct (rappel semaine 4) --- Color soleil = createColor("Rouge Soleil", 220, 60, 10); Color ocean = createColor("Bleu Océan", 10, 80, 210); std::cout << "--- Nos couleurs personnalisées ---" << std::endl; displayColor(soleil); displayColor(ocean); std::cout << std::endl; // --- Mélanger deux couleurs --- Color fusion = mixColors(soleil, ocean, "Fusion"); std::cout << "--- Mélange ---" << std::endl; displayColor(fusion); std::cout << std::format(" Luminosité de {} : {}/255", fusion.name, brightness(fusion)) << "\n\n"; // --- Charger la palette de couleurs web --- std::vector<Color> palette = getWebColors(); displayPalette(palette); std::cout << std::endl; // --- Conversion en hexadécimal (std::string + std::format) --- std::cout << "--- Codes hexadécimaux ---" << std::endl; for (const Color& c : palette) { std::cout << std::format(" {:<20s} → {}", c.name, colorToHex(c)) << "\n"; } std::cout << std::endl; // --- Rechercher une couleur par nom exact --- std::string recherche {"Coral"}; int index = findByName(palette, recherche); if (index >= 0) { std::cout << std::format("'{}' trouvé à l'index {} :", recherche, index) << "\n"; displayColor(palette[index]); std::cout << std::format(" Code hex : {}", colorToHex(palette[index])) << "\n"; } else { std::cout << std::format("'{}' non trouvé dans la palette.", recherche) << "\n"; } std::cout << std::endl; // --- Recherche partielle (string::find + vector retourné) --- std::cout << "--- Recherche partielle : couleurs contenant 'Dark' ou 'Green' ---" << std::endl; std::vector<int> darkColors = findByPartialName(palette, "Dark"); std::vector<int> greenColors = findByPartialName(palette, "Green"); std::cout << std::format(" '{}' : {} résultat(s)", "Dark", darkColors.size()) << "\n"; for (int idx : darkColors) { displayColor(palette[idx]); } std::cout << std::format(" '{}' : {} résultat(s)", "Green", greenColors.size()) << "\n"; for (int idx : greenColors) { displayColor(palette[idx]); } std::cout << std::endl; // --- Ajouter nos couleurs personnalisées à la palette --- palette.push_back(soleil); palette.push_back(ocean); palette.push_back(fusion); std::cout << std::format("Palette mise à jour : {} couleurs au total.", palette.size()) << "\n\n"; // --- Afficher les luminosités de toute la palette --- std::cout << "=== Luminosités ===" << std::endl; for (const Color& c : palette) { std::cout << std::format(" {:<20s} : {}/255", c.name, brightness(c)) << "\n"; } std::cout << "\n=== Séance de Labo Terminée ===" << std::endl; return 0;}
Compiler et exécuter
Compilez dans CLion (Build), l’icône Marteau
Exécutez dans un cmd.exe
5 - Sortie attendue
☝🏼
CLion sur Windows malheureusement a un terminal qui ne supporte pas bien les couleurs. Pour bien voir le résultat, exécutez colorlab.exe, qui se trouve dans votre dosser cmake-build.debug, dans un command line (cmd.exe).
☝🏼
Remarque sur Windows, avez-vous remarqué qu’il y a un espace de trop dans l’espace réservé au nom “Rouge Soleil”?
Le piège UTF-8 et std::format en C++
Le problème
Quand on utilise std::format avec un padding fixe sur des chaînes contenant des caractères accentués (é, è, ê, etc.), l’alignement se décale sur Windows.
{:<20s} remplit jusqu’à 20 octets, pas 20 caractères visuels. En UTF-8, un caractère accentué comme é occupe 2 octets au lieu de 1. Résultat : std::format croit que la chaîne est plus longue qu’elle ne l’est visuellement, et ajoute moins d’espaces.
Exemple concret
Nom
Caractères visuels
Octets UTF-8
Décalage
Rouge Soleil
12
12
aucun
Bleu Océan
10
11 (é = 2 octets)
1 espace en moins
Sur Mac, le terminal compense souvent ce décalage. Sur Windows, non.
La solution
On compte les octets « en trop » dus aux caractères multi-octets, puis on augmente la largeur de padding en conséquence.
void displayColor(const Color& color) { // Compter la longueur visuelle (en caractères, pas en octets) int visualLen = 0; for (std::size_t i = 0; i < color.name.size(); ) { unsigned char ch = color.name[i]; if (ch < 0x80) { i += 1; } // ASCII : 1 octet else if (ch < 0xE0) { i += 2; } // 2 octets (accents latins) else if (ch < 0xF0) { i += 3; } // 3 octets (CJK, symboles) else { i += 4; } // 4 octets (emojis) visualLen++; } // Ajuster le padding : on ajoute la différence octets - caractères int padding = 20 + (static_cast<int>(color.name.size()) - visualLen); std::cout << std::format(" [{:<{}s}] ", color.name, padding); // ... reste de l'affichage}
Pourquoi ça marche
Pour "Bleu Océan" :
color.name.size() → 11 (octets)
visualLen → 10 (caractères)
padding → 20 + (11 - 10) = 21
std::format remplit jusqu’à 21 octets, ce qui donne visuellement 20 colonnes — alignement parfait.
Rappel : encodage UTF-8
Plage
Octets
Exemples
U+0000 – U+007F
1
A, z, 0, !
U+0080 – U+07FF
2
é, è, ñ, ü
U+0800 – U+FFFF
3
中, █, →
U+10000 – U+10FFFF
4
😀, 🎨
En résumé:
std::format avec {:<20s} remplit jusqu’à ce que la chaîne fasse 20 octets. Comme "Bleu Océan" fait déjà 11 octets (à cause du é), std::format n’ajoute que 9 espaces au lieu de 10. Visuellement, la chaîne ne fait que 10 caractères + 9 espaces = 19 colonnes au lieu de 20.
Donc "Bleu Océan" prend plus de place en mémoire (11 octets), mais moins de place à l’écran (19 colonnes) — c’est justement le piège. Le terminal affiche par colonnes visuelles, std::format compte en octets. Le é « mange » un espace de padding.
Récapitulatif : qu'avons-nous appris ?
Concept
Où on l’a vu
**std::vector<T>**
std::vector<Color> palette — palette dynamique
**push_back()**
Ajouter des couleurs à la palette
**.size()**
Nombre de couleurs dans la palette
**Range-based ****for**
for (const Color& c : palette)
**Retour d’un ****vector**
findByPartialName() construit et retourne un std::vector<int>
**string::find()**
Chercher une sous-chaîne dans le nom d’une couleur
**std::string::npos**
Valeur retournée par find() quand la sous-chaîne n’existe pas
**std::format("{:02X}")**
Formatage hexadécimal pour colorToHex()
Recherche linéaire
findByName() — parcourir un vector pour trouver un élément
Recherche par sous-chaîne
findByPartialName() — combiner string::find() et vector
Erreurs courantes à surveiller
1. Accéder à un index invalide dans un vector
std::vector<Color> palette = getWebColors();displayColor(palette[99]); // DANGER - dépasse la taille ! Comportement indéfini.// Utilisez .at(99) pour obtenir une exception au lieu d'un crash silencieux
2. Oublier **&** dans le range-based for
for (Color c : palette) { // COPIE chaque couleur à chaque itération !for (const Color& c : palette) { // Référence - pas de copie (préféré)
3. Comparaison de strings sensible à la casse
if (palette[i].name == "Coral") // OK - comparaison exacteif (palette[i].name == "coral") // FAUX - C++ est sensible à la casse !// Même chose pour find() : "dark" ne trouvera pas "DarkOrange"
**4. Oublier de vérifier **npos** après ****find()**
// FAUX - find() retourne std::string::npos (pas -1 !) si non trouvéif (palette[i].name.find("Dark") >= 0) { ... }// CORRECT - comparer avec nposif (palette[i].name.find("Dark") != std::string::npos) { ... }
5. Utiliser un vector retourné sans vérifier s’il est vide
std::vector<int> results = findByPartialName(palette, "Purple");displayColor(palette[results[0]]); // DANGER si results est vide !// CORRECT - vérifier d'abordif (!results.empty()) { displayColor(palette[results[0]]);}
Travail personnel
Lecture
Lire Beginning C++23 Chapitres 6-7
Défi 1 - Tri par luminosité
Ajoutez une fonction sortByBrightness qui trie une palette du plus sombre au plus lumineux. Vous pouvez utiliser un algorithme de tri simple (selection sort) - on verra std::sort dans les semaines suivantes.
// Déclaration dans colors.h :void sortByBrightness(std::vector<Color>& palette);
Affichez la palette avant et après le tri pour vérifier le résultat.
Défi 2 - Couleur la plus proche
Écrivez une fonction qui, étant donné des valeurs R, G, B quelconques, trouve la couleur la plus proche dans la palette (par distance de Manhattan) :
// Déclaration dans colors.h :int findClosest(const std::vector<Color>& palette, int r, int g, int b);
La « distance » entre deux couleurs se calcule comme : distance = |r1-r2| + |g1-g2| + |b1-b2|
Défi 3 - Gradient entre deux couleurs de la palette (avancé)
Ajoutez une fonction displayGradient qui affiche une transition progressive entre deux couleurs dans le terminal. Combinez-la avec findByName() pour chercher les couleurs dans la palette :
// Déclaration dans colors.h :void displayGradient(const Color& from, const Color& to, int steps);
Testez en affichant un gradient entre “Crimson” et “LimeGreen” de votre palette web. Utilisez std::lerp de <cmath> (C++20) pour interpoler chaque composante.
Solutions des défis
⚠️ Essayez d’abord par vous-même avant de regarder !
// ------------------------------------------------------------// sortByBrightness - selection sort par luminosité// La palette est passée BY REFERENCE - on la modifie directement// ------------------------------------------------------------void sortByBrightness(std::vector<Color>& palette) { for (std::size_t i {0}; i < palette.size(); ++i) { std::size_t minIdx {i}; for (std::size_t j {i + 1}; j < palette.size(); ++j) { if (brightness(palette[j]) < brightness(palette[minIdx])) { minIdx = j; } } if (minIdx != i) { Color temp = palette[i]; palette[i] = palette[minIdx]; palette[minIdx] = temp; } }}
Utilisation dans **main.cpp** :
std::cout << "=== Avant le tri ===" << std::endl;displayPalette(palette);sortByBrightness(palette);std::cout << "\\n=== Après tri par luminosité ===" << std::endl;displayPalette(palette);
Solution - Défi 2
Déclaration dans **colors.h** :
int findClosest(const std::vector<Color>& palette, int r, int g, int b);
Définition dans **colors.cpp** :
#include <cstdlib> // pour std::abs// ------------------------------------------------------------// findClosest - trouve la couleur la plus proche par distance de Manhattan// La palette est BY CONST REFERENCE - on ne la modifie pas// ------------------------------------------------------------int findClosest(const std::vector<Color>& palette, int r, int g, int b) { int bestIndex {-1}; int bestDistance {999999}; for (std::size_t i {0}; i < palette.size(); ++i) { int distance = std::abs(palette[i].r - r) + std::abs(palette[i].g - g) + std::abs(palette[i].b - b); if (distance < bestDistance) { bestDistance = distance; bestIndex = static_cast<int>(i); } } return bestIndex;}
Utilisation dans **main.cpp** :
std::cout << "--- Couleur la plus proche de (200, 50, 50) ---" << std::endl;int closest = findClosest(palette, 200, 50, 50);if (closest >= 0) { std::cout << std::format(" C'est '{}' !", palette[closest].name) << std::endl; displayColor(palette[closest]);}
Solution - Défi 3
Déclaration dans **colors.h** :
void displayGradient(const Color& from, const Color& to, int steps);
Définition dans **colors.cpp** :
#include <cmath> // pour std::lerp (C++20)// ------------------------------------------------------------// displayGradient - affiche une ligne de blocs colorés// interpolés entre deux couleurs// ------------------------------------------------------------void displayGradient(const Color& from, const Color& to, int steps) { std::cout << std::format(" [{}] → [{}]", from.name, to.name) << std::endl; std::cout << " "; for (int i {0}; i < steps; ++i) { float t = static_cast<float>(i) / (steps - 1); int r = static_cast<int>(std::lerp(static_cast<float>(from.r), static_cast<float>(to.r), t)); int g = static_cast<int>(std::lerp(static_cast<float>(from.g), static_cast<float>(to.g), t)); int b = static_cast<int>(std::lerp(static_cast<float>(from.b), static_cast<float>(to.b), t)); std::cout << "\\033[38;2;" << r << ";" << g << ";" << b << "m" << "██"; } std::cout << "\\033[0m" << std::endl;}
Point clé : Remarquez comment findByName() et displayGradient() travaillent ensemble : on cherche par nom dans la palette, puis on passe les couleurs trouvées au gradient. C’est la puissance des collections + fonctions combinées.
isPrimaryColor(const Color& c) — retourne true si la couleur est une couleur primaire pure (ex. : R:255 G:0 B:0).
isNeutralColor(const Color& c) — retourne true si R, G et B sont tous égaux (gris, blanc, noir).
Défi 2 — Gradients (avancé)
Un gradient est une transition progressive entre deux couleurs. Implémentez la fonction suivante :
// Dans colors.h :void displayGradient(const Color& a, const Color& b, int steps);
Elle devra afficher dans le terminal une ligne de blocs █ dont la couleur transite progressivement de la couleur A vers la couleur B, en steps étapes.
Exemple de sortie attendue pour un gradient Rouge → Bleu en 8 étapes :
(Chaque bloc étant légèrement plus bleu et moins rouge que le précédent.)
Indice : Utilisez std::lerp de <cmath> (disponible depuis C++20) pour interpoler chaque composante. N’oubliez pas que std::lerp travaille avec des float — pensez aux casts !
#include <cmath>std::lerp(a, b, t); // t entre 0.0 et 1.0 — retourne la valeur interpolée
Défi 1 - Tri par luminosité
Ajoutez une fonction sortByBrightness qui trie une palette du plus sombre au plus lumineux. Vous pouvez utiliser un algorithme de tri simple (selection sort) - on verra std::sort dans les semaines suivantes.
// Déclaration dans colors.h :void sortByBrightness(std::vector<Color>& palette);
Affichez la palette avant et après le tri pour vérifier le résultat.
Défi 2 - Couleur la plus proche
Écrivez une fonction qui, étant donné des valeurs R, G, B quelconques, trouve la couleur la plus proche dans la palette (par distance de Manhattan) :
// Déclaration dans colors.h :int findClosest(const std::vector<Color>& palette, int r, int g, int b);// Version qui retourne une Color plutôt que l'index dans le vecteur. Utiliser le range based for loop aussiColor findClosest(const std::vector<Color>&, int r, int g, int b);
La « distance » entre deux couleurs se calcule comme : distance = |r1-r2| + |g1-g2| + |b1-b2|
Défi Array
Qu’est-ce qui ne va pas avec ce code ?
// Using a for loop with an array#include <iostream>#include <format>#include <array>int main(){ std::array<unsigned, 6> height {26, 37, 47, 55, 62, 75}; // An array of heights unsigned total {}; for (unsigned i : height) { total += height[i]; } const auto average{ static_cast<double>(total) / height.size() }; std::cout << std::format("The average height is {:.1f}.", average) << std::endl; unsigned count {}; for (unsigned i : height) { if (height[i] < average) ++count; } std::cout << std::format("{} people are below average height.", count) << std::endl;}
Profitez-en pour consolider vos connaissances. Révisez les sujets où vous avez eu des difficultés à l’examen.
Lecture recommandée (optionnelle)
Relire les chapitres 1-6 de Beginning C++23 pour les sujets moins maîtrisés
Revoir les exercices « Où est Charlie » des semaines 1 à 7
Pratiquer la lecture de code sur papier pour les sujets où vous avez perdu des points
Banque de cours - Cours 9 - Classes & Programmation orientée objet
# Cours 9 - Classes & Programmation orientée objet
Où est Charlie le Bug ? (15 minutes)
#include <iostream>using namespace std;int main() { int scores[5] = {90, 85, 78, 92, 88}; for (int i = 0; i <= 5; i++) { cout << scores[i] << endl; } vector<int> nums; cout << nums[0]; string name = "Charlie"; char first = name[100]; cout << first; vector<int> data {1, 2, 3}; for (int i = 0; i < data.size(); i++) { cout << data[i] << endl; if (data[i] == 1) data.erase(data.begin() + i); } return 0;}
Solution
#includd <iostream>using namespace std;int main() { int scores[5] = {90, 85, 78, 92, 88}; for (int i = 0; i <= 5; i++) { // Bug 1: off-by-one! i <= 5 goes to index 5 (out of bounds) cout << scores[i] << endl; } vector<int> nums; //cout << nums[0]; // Bug 2: accessing empty vector - undefined behavior! string name = "Charlie"; char first = name[100]; // Bug 3: out of bounds - undefined behavior! cout << first; // Bug 4: Impression d'une valeur UB ! vector<int> data {1, 2, 3}; for (int i = 0; i < data.size(); i++) { cout << data[i] << endl; if (data[i] == 1) data.erase(data.begin() + i); // Bug 5: modifying container while iterating - skips elements } return 0;}
struct vs class
struct Point { double x; // public by default in struct double y;};class Point { double x; // private by default in class double y;public: // ... public interface};
Règle générale : Utiliser struct pour les simples conteneurs de données, class pour les types avec comportement.
Fondamentaux des classes
Classes et Objets en C++
En programmation orientée objet, les classes et les objets sont des concepts fondamentaux qui permettent de représenter des entités et des concepts du monde réel.
Une classe est un modèle (ou gabarit) servant à créer des objets ayant des propriétés et des comportements similaires. On peut dire qu’une classe est le plan de construction d’un objet.
Un objet est une instance d’une classe. Par exemple, le type animal Chien est une classe, tandis qu’un chien particulier nommé Tommy est un objet de la classe Chien.
Les Classes en C++
Une classe est un type de données défini par l'utilisateur, qui regroupe ses propres membres de données (data members) et fonctions membres (member functions), accessibles en créant une instance de cette classe. Une classe C++ est comme le plan de construction d'un objet.
Créer une classe : Une classe doit être définie avant d’être utilisée. En C++, on la définit avec le mot-clé class.
#include <iostream>using namespace std;// Définition de la classeclass GfG{ public: // Membre de données int val; // Fonction membre void show() { cout << "Valeur : " << val << endl; }};int main(){ // Les objets seront déclarés ici GfG myGfG; myGfG.val = 2; myGfG.show(); return 0;}
Dans l’exemple ci-dessus, la classe GfG est créée avec un membre de données val et une fonction membre show(). La fonction membre est ici définie à l’intérieur de la classe, mais elle peut aussi être simplement déclarée dans la classe puis définie à l’extérieur à l’aide de l’opérateur de résolution de portée::.
Ce qui précède s’appelle une définition de classe ou gabarit de classe.
Les Objets en C++
Quand une classe est définie, seules les spécifications (attributs et comportements) de l'objet sont établies. Aucune mémoire n'est allouée à la définition de la classe. Pour utiliser les données et accéder aux fonctions définies dans la classe, il faut créer des objets.
Les objets sont les entités réelles créées en tant qu’instances d’une classe. On peut créer autant d’objets d’une classe que l’on souhaite.
Créer un objet : Une fois la classe définie, on peut créer un objet de la même façon qu’on déclare une variable d’un type intégré :
NomClasse nomObjet;
Accès aux membres :
À l’intérieur de la classe, on accède aux membres directement par leur nom.
À l’extérieur, on utilise l’opérateur point (.) avec l’objet de la classe.
Les membres de données s’accèdent via objet.membre et les fonctions via objet.membre().
Classe Locale
Les classes sont généralement déclarées dans la portée globale et sont accessibles à toutes les fonctions ou autres classes une fois définies. Cependant, C++ permet aussi de définir une classe à l'intérieur d'une fonction. On parle alors de classe locale, qui n'est accessible qu'au sein de cette fonction.
Classe Imbriquée (Nested Class)
Une classe imbriquée est une classe définie à l'intérieur d'une autre classe (la classe englobante). En tant que membre de la classe englobante, elle dispose des mêmes droits d'accès que n'importe quel autre membre. Les membres de la classe englobante n'ont pas d'accès spécial aux membres de la classe imbriquée ; les règles d'accès standard s'appliquent.
#include <iostream>using namespace std;class Outer{ public: // Classe imbriquée à l'intérieur de Outer class Inner { public: void display() { cout << "Ceci est la classe Inner" << endl; } }; void show() { cout << "Ceci est la classe Outer" << endl; }};int main(){ // Création d'un objet de la classe Outer Outer outerObj; outerObj.show(); // Création d'un objet de la classe Inner via Outer Outer::Inner innerObj; innerObj.display(); return 0;}
Résultat :
Ceci est la classe OuterCeci est la classe Inner
Classe Enum (Enum Class)
Les classes enum en C++ offrent une façon plus sûre et plus organisée d'utiliser les énumérations. Elles permettent de regrouper des constantes liées tout en évitant les conflits de noms et en garantissant une meilleure sécurité de type.
Classe vs Objet
Le tableau suivant résume les principales différences entre classes et objets en C++ :
Classe
Objet
Un plan ou gabarit pour créer des objets.
Une instance d’une classe avec des valeurs réelles.
Aucune mémoire n’est allouée tant qu’un objet n’est pas créé.
La mémoire est allouée lors de la création d’un objet.
Entité conceptuelle décrivant structure et comportement.
Entité réelle créée à partir de la classe.
Définit les propriétés et fonctions communes à tous les objets du type.
Stocke des données spécifiques et les manipule via les fonctions de la classe.
Représente un concept ou un type général.
Représente une instance spécifique de la classe.
Définir une classe avec un constructeur
// Defining a class constructor#include <iostream>#include <format>class Box{public: // Constructor Box(double length, double width, double height) { std::cout << std::format("Box constructor called.") << std::endl; m_length = length; m_width = width; m_height = height; } // Function to calculate the volume of a box double volume() { return m_length * m_width * m_height; }private: double m_length {1.0}; double m_width {1.0}; double m_height {1.0};};int main(){ Box firstBox {80.0, 50.0, 40.0}; // Create a box double firstBoxVolume {firstBox.volume()}; // Calculate the box volume std::cout << std::format("Volume of Box object is {}", firstBoxVolume) << std::endl; // Box secondBox; // ERROR: no default constructor!}
Que se passe-t-il s'il n'y a pas de constructeur défini dans une classe C++ ?
Si aucun constructeur n'est défini, le compilateur en génère un automagiquement : le constructeur par défaut implicite.
class Chat {public: std::string nom; int age; float poids;};// Fonctionne : le compilateur a genere un constructeur par defautChat c; // Les membres sont initialises par defaut
C’est ici que se cache le piege. Le comportement depend de l’endroit ou l’objet est cree et du type du membre.
Type de membre
Stack (Chat c;)
Heap (new Chat)
Possède son propre constructeur
int, float, etc.
Valeur indéterminée
Valeur indéterminée
Non
std::string, std::vector, etc.
Initialise correctement
Initialise correctement
Oui
Classe ou struct imbriquée
Depend de cette classe
Depend de cette classe
Variable
Chat c;std::cout << c.age; // Comportement indéfini : valeur garbagestd::cout << c.nom; // Chaine vide : std::string a son propre constructeur
Le compilateur ne génère pas de constructeur par défaut si vous avez défini n’importe quel autre constructeur.
class Chien {public: // Un constructeur custom est defini : plus de constructeur par defaut Chien(std::string nom, int age) { ... }};Chien d; // Erreur de compilation : pas de constructeur par defautChien d("Rex", 3); // Correct
Vous pouvez le restaurer explicitement avec = default :
class Chien {public: Chien() = default; // Restaure le constructeur implicite Chien(std::string nom, int age) { ... }};
Ou l’interdire explicitement avec = delete :
class Singleton {public: Singleton() = delete; // Aucune construction par defaut autorisee};
Si votre classe gère une ressource (pointeur brut, descripteur de fichier…), le constructeur généré automagiquement ne sera pas suffisant. C’est là que s’applique la Règle de Trois / Cinq : destructeur, constructeur de copie, opérateur d’affectation par copie, et leurs variantes de déplacement (move) introduites en C++11. Ce sujet mérite son propre cours.
Le compilateur vous offre un constructeur gratuitement, mais les membres de types primitifs (int, float, etc.) contiennent des valeurs indéterminées.
☝🏼
Bref: Initialisez toujours vos membres explicitement.
Spécificateurs d'accès
class Character{public: // Anyone can access std::string getName() const { return m_name; } int getHealth() const { return m_health; } void takeDamage(int amount) { m_health -= amount; }protected: // Only this class and derived classes int m_level {1};private: // Only this class std::string m_name; int m_health {100};};
Listes d'initialisation (à privilégier !)
class Box{public: // Better: use initialization list Box(double length, double width, double height) : m_length{length}, m_width{width}, m_height{height} { // Constructor body (can be empty!) // m_length = length; // m_width = width; // m_height = height; }private: double m_length; double m_width; double m_height;};
Ce que tu vois s’appelle un member initializer list (ou simplement initializer list) en anglais.
En C++, les membres d’une classe sont construits avant que le corps du constructeur ne s’execute. Si tu fais ca :
Box(double length, double width, double height) { m_length = length; // Ce n'est PAS une initialisation, c'est une affectation}
m_length, m_width, m_height sont d’abord construits par défaut (valeur garbage pour un double)
Puis ecrases par l’affectation dans le corps
C’est un travail en double. Avec l’initializer list :
Les membres sont directement construits avec la bonne valeur, en une seule étape.
NomDuConstructeur(params) : membre1{val1}, membre2{val2}, membre3{val3}{ // corps optionnel}
Le : introduit la liste, et les membres sont séparés par des virgules.
class Moteur {public: const int cylindres; // const : ne peut etre affecté, seulement initialisé int& code_moteur; // reference : doit etre liée a la construction Turbo turbo; // classe sans constructeur par defaut Moteur(int cyl, int& code, Turbo t) : cylindres{cyl}, code_moteur{code}, turbo{t} // obligatoire ici {}};
Un membre const ou une référence ne peut pas être affecté après construction. L’initializer list est le seul endroit ou tu peux les initialiser.
L’ordre dans lequel les membres sont initialises est l’ordre de declaration dans la classe, pas l’ordre dans la liste. C’est un piège classique :
class Probleme { int a; int b;public: Probleme(int val) : b{val}, a{b} // a est initialisé AVANT b malgré l'ordre écrit {} // a reçoit une valeur garbage};
Bonne pratique : écrire la liste dans le même ordre que les déclarations de membres.
Préférer systématiquement l’initializer list au corps du constructeur. C’est plus efficace, c’est parfois obligatoire, et cela reflète mieux ce qui se passe réellement à l’exécution.
Fonctions membres const
class Circle{public: Circle(double radius) : m_radius{radius} {} double area() const // const = this function doesn't modify the object { return std::numbers::pi * m_radius * m_radius; } void setRadius(double r) // NOT const - it modifies the object { m_radius = r; }private: double m_radius;};
Règle : Marquez toute fonction membre qui ne modifie pas l’objet comme const.
Valeurs par défaut dans les constructeurs C++
Quand tu écris :
Le destructeur en C++ (Destructor)
Le ~ désigne le destructeur d'une classe. C'est une méthode spéciale appelée automagiquement quand un objet est détruit. C'est le pendant exact du constructeur : le constructeur est appelé à la naissance de l'objet, le destructeur à sa mort.
Enemy() { ++s_count; } // appele quand l'objet est cree~Enemy() { --s_count; } // appele quand l'objet est detruit
Quand est-ce qu'un objet est détruit
void maFonction() { Enemy e1; // constructeur appelé : s_count = 1 Enemy e2; // constructeur appelé : s_count = 2} // e1 et e2 sortent du scope, les deux destructeurs sont appelés : s_count = 0
Les objets sur le stack sont détruits automatiquement quand ils sortent de leur scope. Pour les objets sur le heap, le destructeur est appelé au moment du delete.
Enemy* e = new Enemy(); // constructeur : s_count = 1delete e; // destructeur : s_count = 0
Sa signature
Le destructeur a toujours la meme forme : le ~ suivi du nom de la classe, sans paramètres et sans type de retour. Il ne peut y en avoir qu'un seul par classe.
~Enemy() { /* code de nettoyage */ }
Quand doit-on en écrire un explicitement
Le compilateur génère un destructeur vide par défaut, ce qui est suffisant dans la majorité des cas. Tu dois en écrire un quand ta classe gère une ressource manuellement : un pointeur brut alloué avec new, un fichier ouvert, une connexion réseau. C'est la troisième pièce de la Règle de Trois mentionnée dans le section sur les constructeurs implicites.
Stack (pile) vs Heap (tas)
Le stack est une zone mémoire gérée automatiquement par le programme. Quand tu déclares une variable locale dans une fonction, elle va sur le stack. Quand la fonction se termine et que la variable sort de son scope, la memoire est libérée automatiquement. C'est rapide, c'est propre, mais la taille est limitée et la durée de vie est liée au scope.
void maFonction() { Enemy e1; // sur le stack, géré automatiquement} // e1 est détruit ici, destructeur appelé automatiquement
Le heap est une zone mémoire que tu gères toi-même. Tu alloues avec new, et tu dois libérer avec delete. La durée de vie n’est pas liée au scope, ce qui est utile quand tu veux qu’un objet survive a la fonction qui l’a créé. Mais si tu oublies le delete, la mémoire reste occupée jusqu’a la fin du programme : c’est une fuite memoire (memory leak).
void maFonction() { Enemy* e = new Enemy(); // sur le heap, tu gères la mémoire delete e; // destructeur appelé, mémoire libérée}
En C++ moderne, on évite le plus possible les new et delete manuels. On utilise des smart pointers comme std::unique_ptr ou std::shared_ptr qui gèrent le heap automatiquement pour toi, avec la garantie de ne pas oublier le delete. Mais ça, c’est un sujet à part entière.
Encapsulation & conception
L'encapsulation est le principe qui consiste a regrouper les données et les fonctions qui les manipulent dans une même unité (la classe), tout en contrôlant ce qui est accessible de l'extérieur.
En pratique, ca veut dire que les membres d’une classe sont divisés en deux categories : ce qui est privé (les donnÉes internes) et ce qui est public (l’interface que tu exposes au reste du programme).
// Pas de classe, tout est accessible directementdouble sante = 100.0;sante = -9999.0; // Rien ne t'en empeche, meme si c'est absurde
class Personnage {public: void setSante(double valeur) { if (valeur < 0.0) valeur = 0.0; // on valide avant d'accepter if (valeur > 100.0) valeur = 100.0; m_sante = valeur; } double getSante() const { return m_sante; }private: double m_sante{100.0}; // inaccessible directement de l'exterieur};Personnage p;p.m_sante = -9999.0; // erreur de compilation : membre privép.setSante(-9999.0); // accepte, mais corrige a 0.0 automatiquement
Les trois niveaux d’accès
class MaClasse {public: // accessible par tout le mondeprotected: // accessible par la classe elle-meme et ses classes derivees (heritage)private: // accessible uniquement par la classe elle-meme};
La validation des données. Comme dans l’exemple ci-dessus, tu peux imposer des règles metier directement dans le setter. Une santé négative n’a pas de sens, le setter le garantit.
La liberté de modifier l’implementation. Si tu changes la façon dont m_sante est stockée en interne (par exemple tu passes d’un double a un int), le reste du programme n’a pas besoin de changer tant que l’interface publique reste la meme.
La lisibilité et la maintenance. En lisant une classe bien encapsulée, tu sais immédiatement ce qu’elle expose et ce qu’elle cache. Tu n’as pas besoin de comprendre ses entrailles pour l’utiliser.
Tu as probablement remarqué le préfixe m_ sur les membres prives. C’est une convention très répandue en C++ pour distinguer visuellement les membres de la classe des variables locales. Ce n’est pas une obligation du langage, mais c’est une bonne habitude à prendre.
Membres static
class Enemy{public: Enemy() { ++s_count; } ~Enemy() { --s_count; } static int getCount() { return s_count; } // No object needed to callprivate: static int s_count; // Shared by ALL Enemy objects};int Enemy::s_count {0}; // Must be defined outside the class// Usage:Enemy e1, e2, e3;std::cout << std::format("Total enemies: {}", Enemy::getCount()) << std::endl; // 3
Lab :Concevoir une classe Vector2D
Le fichier Vector2D.h (La déclaration de la classe et ses membres)
Le fichier main.cpp (l’utilisation de la class Vector2D)
#include <iostream>#include <format>#include "Vector2D.h"int main(){ Vector2D a {3.0, 4.0}; Vector2D b {1.0, 2.0}; std::cout << std::format("Magnitude of vector a is : {}", a.magnitude()) << std::endl; Vector2D c = a + b; std::cout << std::format("Vector c is [{},{}] of magnitude {:.2f}", c.getX(), c.getY(), c.magnitude()) << std::endl; return 0;}
Travail personnel
Lecture
Lire Beginning C++23 Chapitres 8-9
Lire Tour of C++ Chapitres 4-5
Défi 1 - Surcharge d'opérateurs
Ajoutez deux opérateurs à la classe Color :
**operator==** - Compare deux couleurs par leurs composantes R, G, B (le nom n’est pas pris en compte).
**operator+** - Mélange deux couleurs en faisant la moyenne de leurs composantes et en combinant leurs noms avec " + ".
// Déclarations à ajouter dans la section public de Color (colors.h) :bool operator==(const Color& other) const;Color operator+(const Color& other) const;
Testez dans main.cpp :
Color a {"Rouge", 255, 0, 0};Color b {"Bleu", 0, 0, 255};if (a == b) { std::cout << "Identiques !\\n"; }else { std::cout << "Différentes !\\n"; }Color somme = a + b;somme.display(); // "Rouge + Bleu" R:127 G:0 B:127
Défi 2 - Setters avec validation
Ajoutez des setters qui démontrent la valeur de l'encapsulation :
// Déclarations à ajouter dans la section public de Color (colors.h) :void setR(int value);void setG(int value);void setB(int value);
Chaque setter doit clamper la valeur dans [0, 255] avant de l’assigner - exactement comme le constructeur. C’est l’avantage de l’encapsulation : on ne peut jamais mettre une valeur invalide !
Testez dans main.cpp :
Color c {"Test", 100, 100, 100};c.setR(999); // clampé à 255c.setG(-50); // clampé à 0c.display(); // R:255 G:0 B:100
Défi 3 - Couleur inversée (avancé)
Ajoutez une méthode qui retourne la couleur complémentaire (l'inverse) :
// Déclaration à ajouter dans la section public de Color (colors.h) :Color invert() const;
La couleur inversée se calcule comme : (255 - r, 255 - g, 255 - b). Le nom de la couleur inversée sera "Inv:" + m_name.
Testez en affichant des paires de couleurs avec leur inverse :
Color tomate {"Tomato", 255, 99, 71};Color inv = tomate.invert();std::cout << "--- Original et inverse ---\\n";tomate.display(); // R:255 G:99 B:71inv.display(); // R:0 G:156 B:184
Solutions des défis
⚠️ Essayez d’abord par vous-même avant de regarder !
Solution - Défi 1
Déclarations dans **colors.h** (section **public** de **Color**) :
// Compare deux couleurs par composantes RGB (le nom est ignoré).// other est BY CONST REFERENCE - on ne modifie pas l'autre couleur.// const - ne modifie pas l'objet courant.bool operator==(const Color& other) const;// Mélange deux couleurs (moyenne des composantes, concaténation des noms).// other est BY CONST REFERENCE.// const - ne modifie pas l'objet courant.// Retourne un nouveau Color BY VALUE.Color operator+(const Color& other) const;
Définitions dans **colors.cpp** :
// ------------------------------------------------------------// operator== - compare les composantes RGB uniquement// Le nom est intentionnellement ignoré : deux couleurs avec// des noms différents mais les mêmes composantes sont "égales".// ------------------------------------------------------------bool Color::operator==(const Color& other) const { return m_r == other.m_r && m_g == other.m_g && m_b == other.m_b;}// ------------------------------------------------------------// operator+ - mélange deux couleurs par moyenne// Retourne un nouveau Color avec le nom combiné// ------------------------------------------------------------Color Color::operator+(const Color& other) const { return Color{m_name + " + " + other.m_name, (m_r + other.m_r) / 2, (m_g + other.m_g) / 2, (m_b + other.m_b) / 2};}
Utilisation dans **main.cpp** :
Color a {"Rouge", 255, 0, 0};Color b {"Bleu", 0, 0, 255};Color c {"Rouge2", 255, 0, 0};std::cout << "--- Test operator== ---\\n";if (a == b) { std::cout << "a et b sont identiques\\n"; }else { std::cout << "a et b sont différentes\\n"; } // Différentesif (a == c) { std::cout << "a et c sont identiques\\n"; } // Identiques (mêmes RGB)else { std::cout << "a et c sont différentes\\n"; }std::cout << "\\n--- Test operator+ ---\\n";Color somme = a + b;somme.display(); // "Rouge + Bleu" R:127 G:0 B:127
Solution - Défi 2
Déclarations dans **colors.h** (section **public** de **Color**) :
// Modifie la composante rouge avec clamping automatique.// value est BY VALUE - petit int, copie triviale.void setR(int value);// Modifie la composante verte avec clamping automatique.void setG(int value);// Modifie la composante bleue avec clamping automatique.void setB(int value);
Définitions dans **colors.cpp** :
// ------------------------------------------------------------// Setters - chaque setter utilise clamp() pour garantir [0, 255]// C'est l'avantage de l'encapsulation : la validation est centralisée// ------------------------------------------------------------void Color::setR(int value) { m_r = clamp(value); }void Color::setG(int value) { m_g = clamp(value); }void Color::setB(int value) { m_b = clamp(value); }
Utilisation dans **main.cpp** :
std::cout << "--- Test setters avec clamping ---\\n";Color c {"Test", 100, 100, 100};c.display(); // R:100 G:100 B:100c.setR(999); // clampé à 255c.setG(-50); // clampé à 0c.setB(128);c.display(); // R:255 G:0 B:128std::cout << "Sans encapsulation, on aurait pu écrire c.m_r = -500 !\\n";std::cout << "Avec les setters, les valeurs sont toujours valides.\\n";
Solution - Défi 3
Déclaration dans **colors.h** (section **public** de **Color**) :
// Retourne la couleur complémentaire (255 - chaque composante).// const - ne modifie pas l'objet courant.// Retourne un nouveau Color BY VALUE.Color invert() const;
Définition dans **colors.cpp** :
// ------------------------------------------------------------// invert - retourne la couleur complémentaire// Calcul : (255 - r, 255 - g, 255 - b)// Le nom est préfixé par "Inv:" pour identifier l'inverse// ------------------------------------------------------------Color Color::invert() const { return Color{"Inv:" + m_name, 255 - m_r, 255 - m_g, 255 - m_b};}
Point clé : La méthode invert() est const - elle ne modifie pas l’objet courant, elle retourne un nouvel objet. C’est un patron de conception courant : les opérations qui transforment un objet retournent une copie modifiée plutôt que de modifier l’original.
Banque de cours - Cours 10 - Gym Colorlab v3 orienté objet
# Cours 10 - Gym Colorlab v3 : orienté objet
ColorLab v3 - La Classe Color
Prérequis : Vous devez avoir complété le ColorLab v2 (semaine 5). Nous allons transformer le struct Color en une véritable class Color avec encapsulation.
Rappel - Ce qu’on avait en v1/v2
Dans les semaines 4 et 5, notre Color était un simple struct (introduit en semaine 4, enrichi avec des palettes et des opérations sur les chaînes en semaine 5) :
// v1/v2 - struct avec membres publicsstruct Color { std::string name; int r {}; int g {}; int b {};};// Fonctions libres qui opèrent sur Colorvoid displayColor(const Color& color);int brightness(const Color& color);Color mixColors(const Color& a, const Color& b, const std::string& mixName);
N’importe qui pouvait écrire soleil.r = -500; et casser notre couleur. Les fonctions comme displayColor et brightness étaient séparées du type - elles auraient dû faire partie de Color lui-même.
L'évolution - Pourquoi une classe ?
Trois raisons majeures justifient le passage de struct à class :
Encapsulation - Les composantes R, G, B sont maintenant private. On ne peut plus les corrompre de l’extérieur. Toute modification passe par des méthodes contrôlées.
Validation dans le constructeur - Le clamping (0–255) est intégré directement dans le constructeur. Impossible de créer une couleur invalide.
Comportement attaché aux données - display(), brightness() et mix() deviennent des méthodes de la classe. On écrit soleil.display() au lieu de displayColor(soleil) - plus naturel et orienté objet.
défi : Modifiez colors.h, colors.cpp et main.cpp de manière à ce qu’une classe nommée Color soit utilisée
colors.h déclare la classe Color (prototype des méthodes publiques et privées, getters, setters, les variables privées, les constructeur, les valeurs par défaut, etc.
colors.cpp définit les méthodes de la classe Color, via le scope resolution operator ::, par exemple :
Dans colors.h :class Color{public: ... // Calcule la luminosité moyenne pondérée (BT.709). // const - ne modifie pas l'objet. int brightness() const; ...}Dans colors.cpp :// ------------------------------------------------------------// brightness - luminosité moyenne pondérée// ITU-R BT.709 luma : Y = 0.2126·R + 0.7152·G + 0.0722·B// const - ne modifie pas l'objet// ------------------------------------------------------------int Color::brightness() const { // Ici, le compilateur sait que la méthode brightness que l'on définit ici appartient à la class Color grâce au scope resolution operator :: return 0.2126 * m_r + 0.7152 * m_g + 0.0722 * m_b;}
Modifiez main.cpp également bien sûr. La sortie attendue devrait être la même que la v2 de Colorlab :
Solution
1 - Le Header File (colors.h) - v3
// colors.h - v3// ==============// Déclarations pour le ColorLab v3 : class Color avec encapsulation.#ifndef COLORS_H#define COLORS_H#include <string>#include <vector>// --- Notre type Color est maintenant une classe ---class Color{public: // Constructeur paramétré - clampe automatiquement les composantes dans [0, 255]. // name est passé BY CONST REFERENCE - lecture seule, pas de copie inutile. // r, g, b sont passés BY VALUE - ce sont de petits int, copie triviale. Color(const std::string& name, int r, int g, int b); // Constructeur par défaut - crée une couleur noire nommée "Default". Color(); // --- Getters (const - ils ne modifient pas l'objet) --- // Retourne le nom de la couleur. // BY VALUE pour std::string - retourne une copie. std::string name() const; // Retourne la composante rouge (0-255). int r() const; // Retourne la composante verte (0-255). int g() const; // Retourne la composante bleue (0-255). int b() const; // --- Méthodes (comportement attaché à l'objet) --- // Affiche la couleur avec un bloc ANSI coloré dans le terminal. // const - ne modifie pas l'objet. void display() const; // Calcule la luminosité moyenne pondérée (BT.709). // const - ne modifie pas l'objet. int brightness() const; // Mélange cette couleur avec une autre et retourne le résultat. // other est passé BY CONST REFERENCE - on ne modifie pas l'autre couleur. // mixName est passé BY CONST REFERENCE - lecture seule. // const - ne modifie pas l'objet courant non plus. Color mix(const Color& other, const std::string& mixName) const; // --- Membre static : compteur d'instances --- // Retourne le nombre total d'objets Color créés. // static - pas besoin d'un objet pour l'appeler. static int getCount();private: std::string m_name; // Nom de la couleur int m_r {}; // Rouge (0-255) int m_g {}; // Vert (0-255) int m_b {}; // Bleu (0-255) static int s_count; // Compteur d'instances partagé par tous les objets Color // Garde une valeur dans la plage [0, 255]. // static car elle n'a pas besoin d'accéder aux membres d'une instance. static int clamp(int value);};// --- Fonctions libres (opèrent sur des collections, pas sur une seule couleur) ---// Affiche toutes les couleurs d'une palette.// Le vector est passé BY CONST REFERENCE - on ne modifie pas la palette.void displayPalette(const std::vector<Color>& palette);// Cherche une couleur par nom dans la palette.// Retourne l'index si trouvée, -1 sinon.int findByName(const std::vector<Color>& palette, const std::string& name);// Retourne une palette de couleurs web standard prédéfinies.std::vector<Color> getWebColors();#endif // COLORS_H
Ce qui a changé par rapport à v2
Aspect
v2 (semaine 5)
v3 (semaine 6)
Type
struct Color (tout public)
class Color (private par défaut)
Accès aux données
Direct : color.r
Via getters : color.r()
Validation
createColor() - fonction libre
Constructeur avec clamping intégré
displayColor(c)
Fonction libre
Méthode : c.display()
brightness(c)
Fonction libre
Méthode : c.brightness()
mixColors(a, b, nom)
Fonction libre
Méthode : a.mix(b, nom)
clamp()
Fonction libre publique
Méthode statique privée
Compteur d’instances
Non disponible
Color::getCount() via static
2 - Le Source File (colors.cpp) - v3
// colors.cpp - v3// ================// Définitions pour le ColorLab v3.#include "colors.h"#include <iostream>#include <format>// ------------------------------------------------------------// Initialisation du membre static - obligatoire en dehors de la classe !// ------------------------------------------------------------int Color::s_count {0};// ------------------------------------------------------------// clamp - méthode statique privée// Retourne la valeur bornée dans [0, 255].// static : pas besoin d'un objet, c'est un utilitaire de la classe.// ------------------------------------------------------------int Color::clamp(int value) { if (value < 0) return 0; if (value > 255) return 255; return value;}// ------------------------------------------------------------// Constructeur paramétré - liste d'initialisation + clamping// Les composantes sont clampées automatiquement dès la création.// name est BY CONST REFERENCE - on copie dans m_name via l'init list.// ------------------------------------------------------------Color::Color(const std::string& name, int r, int g, int b) : m_name{name}, m_r{clamp(r)}, m_g{clamp(g)}, m_b{clamp(b)}{ ++s_count;}// ------------------------------------------------------------// Constructeur par défaut - couleur noire "Default"// Utilise la liste d'initialisation pour tout initialiser.// ------------------------------------------------------------Color::Color() : m_name{"Default"}, m_r{0}, m_g{0}, m_b{0}{ ++s_count;}// ------------------------------------------------------------// Getters - const car ils ne modifient pas l'objet// ------------------------------------------------------------std::string Color::name() const { return m_name; }int Color::r() const { return m_r; }int Color::g() const { return m_g; }int Color::b() const { return m_b; }// ------------------------------------------------------------// display - v3 : méthode de la classe au lieu de fonction libre// const - ne modifie pas l'objet// ------------------------------------------------------------void Color::display() const { std::cout << std::format(" [{:<20s}] ", m_name); std::cout << "\033[38;2;" << m_r << ";" << m_g << ";" << m_b << "m"; std::cout << "████████"; std::cout << "\033[0m"; std::cout << std::format(" R:{:<3d} G:{:<3d} B:{:<3d}", m_r, m_g, m_b); std::cout << std::endl;}// ------------------------------------------------------------// brightness - luminosité moyenne pondérée// ITU-R BT.709 luma : Y = 0.2126·R + 0.7152·G + 0.0722·B// const - ne modifie pas l'objet// ------------------------------------------------------------int Color::brightness() const { return 0.2126 * m_r + 0.7152 * m_g + 0.0722 * m_b;}// ------------------------------------------------------------// mix - mélange avec une autre couleur// other est BY CONST REFERENCE - on ne modifie pas l'autre couleur.// mixName est BY CONST REFERENCE - on ne copie pas la string inutilement.// const - ne modifie ni l'objet courant ni l'autre.// Retourne un nouveau Color BY VALUE.// ------------------------------------------------------------Color Color::mix(const Color& other, const std::string& mixName) const { return Color{mixName, (m_r + other.m_r) / 2, (m_g + other.m_g) / 2, (m_b + other.m_b) / 2};}// ------------------------------------------------------------// getCount - membre static, pas besoin d'objet// ------------------------------------------------------------int Color::getCount() { return s_count;}// ============================================================// Fonctions libres - elles opèrent sur des vectors, pas sur// une seule couleur, donc elles restent en dehors de la classe.// ============================================================// ------------------------------------------------------------// displayPalette - affiche toutes les couleurs d'un vector// Le vector est passé BY CONST REFERENCE - efficace et sûr// ------------------------------------------------------------void displayPalette(const std::vector<Color>& palette) { std::cout << std::format("=== Palette ({} couleurs) ===", palette.size()) << std::endl; for (const Color& c : palette) { c.display(); // v3 : on appelle la méthode, pas une fonction libre }}// ------------------------------------------------------------// findByName - recherche linéaire dans la palette// Retourne l'index ou -1 si non trouvée// On utilise le getter name() au lieu d'accéder directement au membre// ------------------------------------------------------------int findByName(const std::vector<Color>& palette, const std::string& name) { for (std::size_t i {0}; i < palette.size(); ++i) { if (palette[i].name() == name) { return static_cast<int>(i); } } return -1;}// ------------------------------------------------------------// getWebColors - retourne une palette de couleurs web connues// Chaque couleur est maintenant créée via le constructeur avec clamping.// ------------------------------------------------------------std::vector<Color> getWebColors() { return { {"Teal", 0, 128, 128}, {"Salmon", 250, 128, 114}, {"Coral", 255, 127, 80}, {"Goldenrod", 218, 165, 32}, {"Orchid", 218, 112, 214}, {"SteelBlue", 70, 130, 180}, {"Tomato", 255, 99, 71}, {"MediumAquamarine", 102, 205, 170}, {"SlateGray", 112, 128, 144}, {"Crimson", 220, 20, 60}, {"LimeGreen", 50, 205, 50}, {"DarkOrange", 255, 140, 0} };}
Points importants à noter
**class Color** utilise l’encapsulation : les données sont private, l’accès passe par des getters const.
Le constructeur appelle clamp() dans la liste d’initialisation - impossible de créer une couleur invalide.
**clamp()**** est **static** et ****private** - c’est un utilitaire interne de la classe, invisible de l’extérieur.
**s_count** est initialisé en dehors de la classe (int Color::s_count {0};) - c’est obligatoire pour les membres static.
Les fonctions libres (displayPalette, findByName, getWebColors) restent en dehors de la classe car elles opèrent sur des collections, pas sur une seule couleur.
3 - main.cpp - v3
// main.cpp - ColorLab v3// =======================// Classes, encapsulation et méthodes.#include "colors.h"#include <iostream>#include <format>#ifdef _WIN32#include <windows.h>#endifint main() { // Sur Windows, forcer la console à interpréter l'UTF-8#ifdef _WIN32 SetConsoleOutputCP(CP_UTF8);#endif std::cout << "=== ColorLab v3 - La Classe Color ===" << "\n\n"; // --- Créer des couleurs avec le constructeur --- Color soleil("Rouge Soleil", 220, 60, 10); Color ocean("Bleu Océan", 10, 80, 210); std::cout << "--- Nos couleurs personnalisées ---" << std::endl; soleil.display(); // v3 : méthode au lieu de displayColor(soleil) ocean.display(); std::cout << std::endl; // --- Luminosité - maintenant c'est une méthode --- std::cout << std::format(" Luminosité de {} : {}/255", soleil.name(), soleil.brightness()) << "\n"; std::cout << std::format(" Luminosité de {} : {}/255", ocean.name(), ocean.brightness()) << "\n\n"; // --- Mélanger - méthode de l'objet --- Color fusion = soleil.mix(ocean, "Fusion"); std::cout << "--- Mélange ---" << std::endl; fusion.display(); std::cout << std::format(" Luminosité de {} : {}/255", fusion.name(), fusion.brightness()) << "\n\n"; // --- Compteur d'instances (static) --- std::cout << std::format("Nombre de Color créés jusqu'ici : {}", Color::getCount()) << "\n\n"; // --- Charger la palette de couleurs web --- std::vector<Color> palette = getWebColors(); displayPalette(palette); std::cout << std::endl; // --- Compteur mis à jour après la palette --- std::cout << std::format("Nombre total de Color créés : {}", Color::getCount()) << "\n\n"; // --- Rechercher une couleur par nom --- std::string recherche {"Coral"}; int index = findByName(palette, recherche); if (index >= 0) { std::cout << std::format("'{}' trouvé à l'index {} :", recherche, index) << "\n"; palette[index].display(); } else { std::cout << std::format("'{}' non trouvé dans la palette.", recherche) << "\n"; } std::cout << std::endl; // --- Tester le clamping du constructeur --- Color extreme {"Extreme", 300, -50, 999}; std::cout << "--- Test clamping constructeur ---" << std::endl; extreme.display(); // Doit afficher R:255 G:0 B:255 std::cout << std::endl; // --- Couleur par défaut --- Color defaut; std::cout << "--- Couleur par défaut ---" << std::endl; defaut.display(); // "Default" R:0 G:0 B:0 std::cout << std::endl; std::cout << std::format("Nombre final de Color créés : {}", Color::getCount()) << "\n"; std::cout << "\n=== Séance de Labo Terminée ===" << std::endl; return 0;}
Récapitulatif : qu'avons-nous appris ?
Concept
Où on l’a vu
**class**
class Color - encapsulation des données
**private**
m_name, m_r, m_g, m_b - inaccessibles de l’extérieur
Le clamping est interne, les données sont protégées
Erreurs courantes à surveiller
**1. Accéder directement aux membres ****private**
Color c {"Rouge", 255, 0, 0};std::cout << c.m_r; // ERREUR - m_r est private !std::cout << c.r(); // OK - on passe par le getter
2. Oublier **const** sur les getters et méthodes de lecture
int r() { return m_r; } // Oubli de const - ne compilera pas sur un objet const !int r() const { return m_r; } // OK - promet de ne pas modifier l'objet
3. Oublier d’initialiser le membre **static** en dehors de la classe
class Color { static int s_count; // Déclaration seulement !};// ERREUR si on oublie cette ligne :int Color::s_count {0}; // Définition obligatoire dans le .cpp
4. Appeler une méthode non-**const**** sur un objet ****const**
void afficher(const Color& c) { c.display(); // OK seulement si display() est const ! // Si display() n'était pas const, ERREUR de compilation ici.}
5. Ne pas utiliser la liste d’initialisation
// Mauvais - initialise par défaut puis assigne :Color::Color(const std::string& name, int r, int g, int b) { m_name = name; // double travail : init par défaut + assignation m_r = clamp(r);}// Bon - initialise directement :Color::Color(const std::string& name, int r, int g, int b) : m_name{name}, m_r{clamp(r)}, m_g{clamp(g)}, m_b{clamp(b)}{ ++s_count;}
Défis
Défi 1 - Surcharge d'opérateurs
Ajoutez deux opérateurs à la classe Color :
**operator==** - Compare deux couleurs par leurs composantes R, G, B (le nom n’est pas pris en compte).
**operator+** - Mélange deux couleurs en faisant la moyenne de leurs composantes et en combinant leurs noms avec " + ".
// Déclarations à ajouter dans la section public de Color (colors.h) :bool operator==(const Color& other) const;Color operator+(const Color& other) const;
Testez dans main.cpp :
Color a {"Rouge", 255, 0, 0};Color b {"Bleu", 0, 0, 255};if (a == b) { std::cout << "Identiques !\n"; }else { std::cout << "Différentes !\n"; }Color somme = a + b;somme.display(); // "Rouge + Bleu" R:127 G:0 B:127
Défi 2 - Setters avec validation
Ajoutez des setters qui démontrent la valeur de l'encapsulation :
// Déclarations à ajouter dans la section public de Color (colors.h) :void setR(int value);void setG(int value);void setB(int value);
Chaque setter doit clamper la valeur dans [0, 255] avant de l’assigner - exactement comme le constructeur. C’est l’avantage de l’encapsulation : on ne peut jamais mettre une valeur invalide !
Testez dans main.cpp :
Color c {"Test", 100, 100, 100};c.setR(999); // clampé à 255c.setG(-50); // clampé à 0c.display(); // R:255 G:0 B:100
Défi 3 - Couleur inversée (avancé)
Ajoutez une méthode qui retourne la couleur complémentaire (l'inverse) :
// Déclaration à ajouter dans la section public de Color (colors.h) :Color invert() const;
La couleur inversée se calcule comme : (255 - r, 255 - g, 255 - b). Le nom de la couleur inversée sera "Inv:" + m_name.
Testez en affichant des paires de couleurs avec leur inverse :
Color tomate {"Tomato", 255, 99, 71};Color inv = tomate.invert();std::cout << "--- Original et inverse ---\n";tomate.display(); // R:255 G:99 B:71inv.display(); // R:0 G:156 B:184
Solutions des défis
Solution - Défi 1
Déclarations dans **colors.h** (section **public** de **Color**) :
// Compare deux couleurs par composantes RGB (le nom est ignoré).// other est BY CONST REFERENCE - on ne modifie pas l'autre couleur.// const - ne modifie pas l'objet courant.bool operator==(const Color& other) const;// Mélange deux couleurs (moyenne des composantes, concaténation des noms).// other est BY CONST REFERENCE.// const - ne modifie pas l'objet courant.// Retourne un nouveau Color BY VALUE.Color operator+(const Color& other) const;
Définitions dans **colors.cpp** :
// ------------------------------------------------------------// operator== - compare les composantes RGB uniquement// Le nom est intentionnellement ignoré : deux couleurs avec// des noms différents mais les mêmes composantes sont "égales".// ------------------------------------------------------------bool Color::operator==(const Color& other) const { return m_r == other.m_r && m_g == other.m_g && m_b == other.m_b;}// ------------------------------------------------------------// operator+ - mélange deux couleurs par moyenne// Retourne un nouveau Color avec le nom combiné// ------------------------------------------------------------Color Color::operator+(const Color& other) const { return Color{m_name + " + " + other.m_name, (m_r + other.m_r) / 2, (m_g + other.m_g) / 2, (m_b + other.m_b) / 2};}
Utilisation dans **main.cpp** :
Color a {"Rouge", 255, 0, 0};Color b {"Bleu", 0, 0, 255};Color c {"Rouge2", 255, 0, 0};std::cout << "--- Test operator== ---\n";if (a == b) { std::cout << "a et b sont identiques\n"; }else { std::cout << "a et b sont différentes\n"; } // Différentesif (a == c) { std::cout << "a et c sont identiques\n"; } // Identiques (mêmes RGB)else { std::cout << "a et c sont différentes\n"; }std::cout << "\n--- Test operator+ ---\n";Color somme = a + b;somme.display(); // "Rouge + Bleu" R:127 G:0 B:127
Solution - Défi 2
Déclarations dans **colors.h** (section **public** de **Color**) :
// Modifie la composante rouge avec clamping automatique.// value est BY VALUE - petit int, copie triviale.void setR(int value);// Modifie la composante verte avec clamping automatique.void setG(int value);// Modifie la composante bleue avec clamping automatique.void setB(int value);
Définitions dans **colors.cpp** :
// ------------------------------------------------------------// Setters - chaque setter utilise clamp() pour garantir [0, 255]// C'est l'avantage de l'encapsulation : la validation est centralisée// ------------------------------------------------------------void Color::setR(int value) { m_r = clamp(value); }void Color::setG(int value) { m_g = clamp(value); }void Color::setB(int value) { m_b = clamp(value); }
Utilisation dans **main.cpp** :
std::cout << "--- Test setters avec clamping ---\n";Color c {"Test", 100, 100, 100};c.display(); // R:100 G:100 B:100c.setR(999); // clampé à 255c.setG(-50); // clampé à 0c.setB(128);c.display(); // R:255 G:0 B:128std::cout << "Sans encapsulation, on aurait pu écrire c.m_r = -500 !\n";std::cout << "Avec les setters, les valeurs sont toujours valides.\n";
Solution - Défi 3
Déclaration dans **colors.h** (section **public** de **Color**) :
// Retourne la couleur complémentaire (255 - chaque composante).// const - ne modifie pas l'objet courant.// Retourne un nouveau Color BY VALUE.Color invert() const;
Définition dans **colors.cpp** :
// ------------------------------------------------------------// invert - retourne la couleur complémentaire// Calcul : (255 - r, 255 - g, 255 - b)// Le nom est préfixé par "Inv:" pour identifier l'inverse// ------------------------------------------------------------Color Color::invert() const { return Color{"Inv:" + m_name, 255 - m_r, 255 - m_g, 255 - m_b};}
Point clé : La méthode invert() est const - elle ne modifie pas l’objet courant, elle retourne un nouvel objet. C’est un Design Pattern courant : les opérations qui transforment un objet retournent une copie modifiée plutôt que de modifier l’original.
Banque de cours - Cours 11 - Pointeurs, référence et héritage
# Cours 11 - Pointeurs, référence et héritage
Où est Charlie le Bug ? (15 minutes)
class Character {public: Character(const std::string& name, int health, int maxHealth) { m_name = name; m_health = health; m_maxHealth = maxHealth; } std::string getName() { return m_name; } void heal(int amount) { m_health += amount; } static int getCount() { return s_count; }private: std::string m_name; int m_health; int m_maxHealth; static int s_count;};int main() { Character hero {"Aria", 80, 100}; hero.heal(50); std::cout << hero.m_health;}
Solution
class Character {public: Character(const std::string& name, int health, int maxHealth) { m_name = name; // Bug 1: pas de liste d'initialisation m_health = health; // (init par défaut puis assignatioo -> coûteux m_maxHealth = maxHealth; } std::string getName() { return m_name; } void heal(int amount) { m_health += amount; // Bug 3: aucune borne supérieure - m_health peut dépasser m_maxHealth } // (l'encapsulation existe pour valider, comme clamp() dans ColorLab) static int getCount() { return s_count; }private: std::string m_name; int m_health; int m_maxHealth; static int s_count; // Bug 4: déclaré mais jamais défini hors de la classe → erreur de linker};int main() { Character hero {"Aria", 80, 100}; hero.heal(50); std::cout << hero.m_health; // Bug 5: accès direct à un membre private - erreur de compilation}
Pointeurs, références et mémoire
Pile (Stack) vs Tas (Heap)
┌──────────────────────┐│ PILE (STACK) │ Rapide, automatique, taille limitée│ Variables locales │ Détruites à la fin de la portée│ Paramètres │├──────────────────────┤│ (libre) │├──────────────────────┤│ TAS (HEAP) │ Plus lent, manuel, grande taille│ Allocation dynamique│ Vous devez libérer (ou utiliser un smart pointer)│ new / delete │└──────────────────────┘
Idée clé : Tout ce que vous avez écrit jusqu’ici vit sur la pile. Le tas n’est utile que pour des objets dont la durée de vie dépasse la portée locale, ou dont la taille n’est connue qu’à l’exécution.
Qu'est-ce qu'un pointeur ?
Un pointeur est une variable qui stocke une adresse mémoire.
int value {42};int* ptr {&value}; // ptr stocke L'ADRESSE de value std::cout << std::format("value = {}", value) << std::endl; // 42 std::cout << &value << std::endl; // ex: 0x7fff... std::cout << ptr << std::endl; // même adresse std::cout << std::format("*ptr = {}", *ptr) << std::endl; // 42 (déréférencement)
Trois opérateurs à retenir :
Opérateur
Signification
Exemple
&x
”adresse de x”
&value
*ptr
”valeur à l’adresse de ptr”
*ptr = 100;
ptr->m
accès membre via pointeur
ptr->name()
nullptr
Un pointeur qui ne pointe vers rien doit valoir nullptr. Toujours vérifier avant de déréférencer.
Les références : un alias
Une référence est un autre nom pour une variable existante. Elle ne peut être ni nulle, ni réassignée.
int value {42};int& ref {value}; // ref EST valueint* ptr {&value};std::cout << value; // 100 - modifié à travers la référence
Pointeurs vs références (tableau de décision)
Caractéristique
Référence (T&)
Pointeur (T*)
Peut être nul ?
Non
Oui (nullptr)
Peut être réassigné ?
Non
Oui
Syntaxe d’accès
Direct : ref
Déréférencement : *ptr, ptr->
Quand l’utiliser
Cible toujours valide
Cible nullable ou changeante
Règle pratique : préférez la référence quand vous le pouvez ; utilisez le pointeur quand vous avez besoin d’optionnel ou de réassignation.
new / delete : le tas brut
int* ptr = new int {42}; // alloue sur le tasstd::cout << *ptr; // 42delete ptr; // libère la mémoireptr = nullptr; // bonne pratique
Trois pièges classiques :
Fuite mémoire : oublier delete (la mémoire n’est jamais libérée).
Pointeur pendant : utiliser ptr après delete ptr;.
Double **delete** : comportement indéfini.
C’est précisément pour éviter ces pièges que le C++ moderne recommande les smart pointers.
RAII et std::unique_ptr (l'essentiel)
RAII (Resource Acquisition Is Initialization) : un objet acquiert une ressource à sa construction et la libère automatiquement à sa destruction. std::vector fait déjà ça pour vous.
#include <memory>auto ptr = std::make_unique<int>(42); // alloue sur le tasstd::cout << *ptr << std::endl; // 42// pas de delete : ptr libère la mémoire en sortant de la portée
Règles minimales à retenir cette semaine :
std::make_unique<T>(args...) remplace new T(args...).
unique_ptr ne se copie pas (un seul propriétaire). Pour le déplacer : std::move(ptr).
std::shared_ptr existe pour la propriété partagée, mais on l’utilise rarement par défaut. Mention rapide seulement.
Pont vers Unreal (semaines 12-14) : Unreal fournit son propre système de gestion mémoire (UObject géré par le garbage collector, TObjectPtr<T>, UPROPERTY). Vous n’utiliserez quasiment jamais new/delete ni std::unique_ptr dans du code Unreal. Mais comprendre le principe RAII est essentiel pour lire le code Unreal.
RAII signifie Resource Acquisition Is Initialization. Le nom est un peu trompeur. Ce qu’il faut retenir, c’est l’idée centrale :
Un objet acquiert ses ressources dans son constructeur, et les libère dans son destructeur. La durée de vie de l’objet pilote la durée de vie de la ressource.
L’exemple classique : un fichier
void traiterFichier() { std::ifstream f("data.txt"); // constructeur : ouvre le fichier // ... lecture, traitement ...} // destructeur de f appelé automatiquement : ferme le fichier
Tu n’as jamais besoin d’écrire f.close(). Quand f sort de sa portée (la fin du bloc { }), son destructeur s’exécute et ferme le fichier. Cela arrive même si une exception est levée au milieu de la fonction, grâce au mécanisme de stack unwinding.
ptr détruit le Character et libère la mémoire automatiquement
Tout cela en ordre inverse de construction. Aucun delete, aucun unlock(), aucun free().
Pourquoi c’est important
En C, tu dois libérer manuellement chaque ressource sur chaque chemin de sortie de ta fonction. Si une fonction a 5 points de sortie possibles, tu dois te rappeler de tout libérer 5 fois. Une erreur, et tu as une fuite mémoire ou un fichier qui reste ouvert.
En C++ avec RAII, le compilateur insère les destructeurs pour toi. Le code devient exception-safe gratuitement.
La règle mentale
RAII transforme la gestion des ressources en problème de durée de vie, pas en problème de gestes à se rappeler.
Partie 2 : Le C++ d’Unreal Engine
Quand tu ouvres un projet UE5, tu vois ce genre de code :
Tout ça (UCLASS, UPROPERTY, UFUNCTION, GENERATED_BODY) ce ne sont pas des mots-clés C++. Ce sont des macros, parsées avant la compilation par un outil appelé Unreal Header Tool (UHT), qui génère du code supplémentaire dans des fichiers .generated.h.
Pourquoi Unreal fait ça
Le C++ standard n’a pas de système de réflexion (la capacité d’inspecter une classe à l’exécution). Or un moteur de jeu a besoin de :
Sérialiser des objets (sauvegardes, niveaux, réseau)
Exposer des propriétés à l’éditeur (les champs visibles dans le panneau Details)
Faire le pont avec Blueprint, un langage visuel
Gérer la mémoire automatiquement avec un garbage collector
Epic Games a donc construit un système de réflexion par-dessus C++ avec des macros.
Les conventions de nommage UE
Préfixe
Signification
Exemple
A
Acteur dans le monde
AActor, AMyCharacter
U
Objet géré par le GC
UObject, UStaticMeshComponent
F
Struct simple, sans GC
FVector, FString
I
Interface
IInteractable
E
Enum
EInputType
T
Template
TArray, TMap, TSharedPtr
UE n’utilise pas la library standard C++ (STL, Standard Template Library)
UE a ses propres équivalents :
STL
UE
std::vector<T>
TArray<T>
std::map<K,V>
TMap<K,V>
std::string
FString
std::shared_ptr<T>
TSharedPtr<T>
std::unique_ptr<T>
TUniquePtr<T>
Les raisons sont historiques (UE existe depuis avant C++11), liées à la portabilité console, et à l’intégration avec le système de réflexion et de GC.
Partie 3 : Le pont entre RAII et Unreal
C’est ici que ça devient intéressant. **UE casse partiellement RAII pour les ****UObject**.
Les UObject ne suivent pas RAII
Un UObject, et donc tout ce qui en hérite (AActor, UActorComponent, etc.), n’est pas géré par durée de vie automatique. Il est géré par un garbage collector intégré au moteur.
Tu ne fais jamais new AMyActor ni delete Actor. Tu fais :
// CréationAMyActor* Actor = GetWorld()->SpawnActor<AMyActor>(...);// Destruction (marquage pour le GC)Actor->Destroy();
Pour qu’une référence à un UObject soit “vue” par le GC et empêche sa destruction prématurée, elle doit être déclarée avec UPROPERTY() :
UPROPERTY()AEnemy* TargetEnemy; // le GC sait que cette référence existe
Sans UPROPERTY(), ton pointeur peut devenir pendant à tout moment, parce que le GC ne sait pas qu’il existe.
Tout le reste suit RAII normalement
Pour les types non-UObject, c’est-à-dire les structs F, les containers T, et les smart pointers T, RAII fonctionne exactement comme en C++ standard :
void AMyActor::DoSomething() { TArray<int32> Numbers; // RAII normal FScopeLock Lock(&CriticalSection); // RAII normal TUniquePtr<FMyData> Data = MakeUnique<FMyData>(); // RAII normal} // tout libéré automatiquement
FScopeLock est l’équivalent UE de std::lock_guard. TUniquePtr est l’équivalent de std::unique_ptr. Les noms changent, le comportement RAII est identique.
Partie 4 : Les règles à retenir
Quand tu passes du C++ pur à Unreal Engine :
Pour les **UObject** et descendants (Actor, Component, etc.)
Oublie RAII, le GC s’en occupe
UPROPERTY() pour les références
SpawnActor pour créer
Destroy() pour détruire
Pour tout le reste (structs F, containers T, smart pointers T)
RAII s’applique normalement, comme en C++ pur
Les macros UE ne sont pas magiques
Elles génèrent du code dans Intermediate/Build/.../*.generated.h
Tu peux l’inspecter si tu es curieux
La STL est bannie en pratique
Apprends les équivalents UE
TArray au lieu de std::vector, etc.
Métaphore pour conclure
Le C++ pur, c’est ta cuisine personnelle. Tu gères tout : achat, stockage, nettoyage, rangement. RAII te fournit des outils qui font une partie du travail automatiquement (le destructeur range les choses quand tu finis).
Unreal Engine, c’est une cuisine de restaurant industriel. Il y a un système qui gère le stock, le nettoyage, la rotation des produits, mais tu dois suivre ses règles : étiquetage (UPROPERTY), zones réservées (UCLASS), procédures de commande (SpawnActor).
Les deux sont du C++. Mais avec des contrats de durée de vie différents. La compétence à développer, c’est de savoir dans quelle cuisine tu te trouves à chaque instant.
Pour aller plus loin
Lire les fichiers .generated.h d’un projet UE pour démystifier les macros
Comparer std::vector et TArray côte à côte sur les mêmes opérations
Tester un cas où une référence sans UPROPERTY() devient pendante après un GC
En C++ moderne (C++20 et au-delà), explorer std::span, std::expected, et les concepts, qui n’existent pas encore dans la version C++ d’UE
Pointeurs vers objets en C++ moderne : cas d’usage et comparaison avec Rust, Java, C# et Python
En C++ moderne (depuis C++11), la règle générale est la suivante :
Préférer les valeurs.
Sinon, les références.
Sinon, les pointeurs intelligents (unique_ptr, shared_ptr).
Les pointeurs bruts (T*) en dernier recours, pour des raisons précises.
Cette hiérarchie peut sembler restrictive, mais les pointeurs vers objets restent indispensables dans plusieurs situations. Ce document détaille ces situations, puis compare l’approche du C++ avec celle d’autres langages que vous croiserez en production.
Les six cas d’usage légitimes en C++ moderne
1. Polymorphisme runtime avec conteneurs hétérogènes
Un std::vector<Animal> ne fonctionne pas si Animal est une classe de base : on perd la partie dérivée par “slicing”. Il faut donc stocker des pointeurs.
C’est probablement le cas le plus fréquent en code de jeu et d’outillage.
2. Observateurs non-propriétaires
Un T* brut signifie en C++ moderne : “je référence cet objet mais je ne gère pas sa durée de vie”. Une référence ne peut pas être nulle ni rebindable, donc dès qu’un observateur peut pointer vers A, puis B, puis rien, le pointeur brut est l’outil correct.
class Camera { Target* current_target_ = nullptr; // peut changer ou être nullpublic: void set_target(Target* t) { current_target_ = t; }};
Certaines bases de code utilisent gsl::not_null<T*> ou un observer_ptr<T> maison pour expliciter l’intention.
3. Paramètres optionnels d’objet
void draw(const Mesh& mesh, const Material* override = nullptr);// override = nullptr signifie "utilise le matériau par défaut"
std::optional<T&> n’existe pas dans la bibliothèque standard, donc le pointeur brut reste le moyen propre d’exprimer “passe-en un si tu veux”.
4. Références arrière dans arbres et graphes
Un noeud enfant ne possède pas son parent. Un unique_ptr causerait une double propriété, un shared_ptr créerait un cycle.
class Node { std::vector<std::unique_ptr<Node>> children_; Node* parent_ = nullptr; // observation, pas propriété};
5. Idiome Pimpl
// Headerclass Widget { class Impl; std::unique_ptr<Impl> pImpl_;};
Cela cache l’implémentation aux utilisateurs du header et accélère significativement la compilation. Pointeur propriétaire, mais toujours un cas de pointeur vers objet.
6. Interopérabilité avec API C
OpenGL, Vulkan, handles système, librairies anciennes : tout passe par des pointeurs bruts à la frontière.
Rust prend les concepts du C++ moderne et les rend obligatoires via le compilateur. L’analogue de unique_ptr est Box<T>, celui de shared_ptr est Rc<T> (mono-thread) ou Arc<T> (multi-thread).
Le compilateur garantit qu’aucun pointeur pendant ne peut exister. Le coût pédagogique : la courbe d’apprentissage du borrow checker. Pour un étudiant qui maîtrise les pointeurs C++, Rust paraît surtout strict, pas étrange.
Java : tout est référence implicite
En Java, toute variable d’objet est déjà une référence gérée par le ramasse-miettes (garbage collector). Les six cas d’usage du C++ disparaissent ou se transforment.
// Polymorphisme : trivial, pas de slicing possibleList<Animal> zoo = new ArrayList<>();zoo.add(new Lion());zoo.add(new Penguin());// Observateur non-propriétaire : juste une référenceclass Camera { private Target currentTarget; // null autorisé, GC gère la durée de vie}// Référence faible explicite pour caches et observateurs longue duréeimport java.lang.ref.WeakReference;WeakReference<Cache> ref = new WeakReference<>(cache);
Java n’a pas de pointeurs bruts. Le concept de “non-propriétaire” n’existe pas explicitement car le GC gère tout. Le revers : moins de contrôle sur la durée de vie précise et le placement mémoire.
C# : Java avec des échappatoires
C# ressemble à Java mais offre plus de flexibilité : struct (sémantique de valeur), mots-clés ref et out, et même des pointeurs bruts dans des blocs unsafe pour l’interop ou la performance.
// Polymorphisme : comme en JavaList<Animal> zoo = new List<Animal> { new Lion(), new Penguin() };// Référence par paramètre (équivalent partiel de T& en C++)void Modify(ref Vector3 v) { v.X += 1; }// Pointeurs bruts pour interop native ou hot pathunsafe void ProcessPixels(byte* data, int length) { for (int i = 0; i < length; i++) data[i] = 255;}// WeakReference pour observation longue duréeWeakReference<Parent> parentRef = new WeakReference<Parent>(parent);
C# est probablement le plus proche du C++ en termes d’options, tout en gardant un GC par défaut.
Python : tout est référence, GC en arrière-plan
En Python, chaque variable est une référence vers un objet dans le tas. Aucune notion syntaxique de pointeur. Le compteur de références plus un GC cyclique gèrent la mémoire.
# Polymorphisme : naturel, duck typingzoo = [Lion(), Penguin()]for animal in zoo: animal.speak()# Observateur non-propriétaire : assignation simpleclass Camera: def __init__(self): self.target = None # référence ou None# Référence faible pour éviter les cyclesimport weakrefclass Node: def __init__(self, parent=None): self.children = [] self._parent_ref = weakref.ref(parent) if parent else None @property def parent(self): return self._parent_ref() if self._parent_ref else None
Python privilégie la simplicité totale. Le coût : performance moindre et moins de contrôle sur la libération mémoire.
Synthèse pédagogique
La progression conceptuelle entre les langages est claire :
C++ expose tous les outils et laisse le développeur choisir. Puissance maximale, responsabilité maximale.
Rust prend les meilleures pratiques du C++ moderne et les impose au compile-time. Garanties fortes, courbe d’apprentissage exigeante.
C# propose un modèle managé par défaut avec des trappes d’évacuation pour la performance.
Java pousse l’abstraction plus loin : pas de pointeurs, modèle uniforme par référence pour les objets.
Python abstrait au maximum : tout est objet, tout est référence, le GC gère.
Pour bien apprendre les pointeurs en C++, retenir cette grille de décision :
Intention
Outil C++
Je possède cet objet, seul propriétaire
unique_ptr<T>
Je partage la propriété entre plusieurs entités
shared_ptr<T>
Je casse un cycle de propriété partagée
weak_ptr<T>
L’objet existe toujours et ne change jamais de cible
T& (référence)
L’objet peut être null ou changer de cible
T* (pointeur brut)
Interop avec API C ou code bas niveau
T* (pointeur brut)
Une fois cette grille intériorisée, la peur traditionnelle des pointeurs disparaît : un pointeur brut signifie simplement “non-propriétaire”, et la propriété est rendue explicite via les pointeurs intelligents. C’est exactement ce que Rust impose au compilateur, ce que Java et C# délèguent au GC, et ce que Python cache complètement.
Pour aller plus loin
C++ Core Guidelines, sections F (functions) et R (resource management).
“Effective Modern C++” de Scott Meyers, items 18 à 22.
“The Rust Book”, chapitres 4 et 15 (ownership et smart pointers).
Documentation java.lang.ref pour les références faibles côté JVM.
PEP 442 pour la finalisation des objets en Python.
protected est un troisième niveau de visibilité, entre public et private :
Mot-clé
Accessible depuis
Accessible depuis les classes filles
Accessible de l’extérieur
public
partout
oui
oui
protected
la classe et les classes filles
oui
non
private
la classe seulement
non
non
Dans notre exemple, m_length, m_width et m_height sont protected. Ça signifie que :
Carton peut accéder directement à m_length (par exemple dans une méthode)
Le code utilisateur ne peut pas faire monCarton.m_length (compilateur refuse)
Faut-il utiliser protected ?
C’est un choix de design discuté. Beaucoup de spécialistes (Stroustrup, Sutter) recommandent de garder les membres private même en présence d’héritage, et d’exposer des accesseurs protected si nécessaire. Pourquoi ? Parce que protected casse l’encapsulation : toutes les classes filles dépendent de la représentation interne de la classe parente.
La partie après les : est la liste d’initialisation des membres. C’est ici qu’on initialise les membres de la classe et qu’on appelle le constructeur de la classe de base.
Pourquoi c’est important
Quand un objet Carton est construit, il faut d’abord construire la “partie Box” de l’objet, puis la “partie Carton”. L’ordre est :
Le compilateur appelle le constructeur de Box avec l, w, h
Puis m_material est initialisé avec material
Puis le corps {} est exécuté (vide ici)
Si tu n’appelles pas explicitement Box{l, w, h}, le compilateur appelle le constructeur par défautBox(), ce qui te donnerait des valeurs 1.0 partout, pas ce que tu veux.
Schéma mental
Carton{30, 20, 15, "Cardboard"} | +--> Box{30, 20, 15} (construit la partie Box) | m_length = 30 | m_width = 20 | m_height = 15 | +--> m_material = "Cardboard" (initialise le membre propre à Carton)
explicit empêche le compilateur d’utiliser ce constructeur pour des conversions implicites.
Sans explicit : le piège
Imagine un constructeur à un seul argument :
class Carton {public: Carton(double size) : Box{size, size, size}, m_material{"Cardboard"} {}};void process(Carton c) { /* ... */ }process(42.0); // OK sans explicit : 42.0 est converti implicitement en Carton
Cette conversion implicite est rarement souhaitable et source de bugs. Avec explicit :
class Carton {public: explicit Carton(double size) : Box{size, size, size} {}};process(42.0); // ERREUR de compilationprocess(Carton{42.0}); // OK, conversion explicite demandée
La règle
Marquer **explicit** tous les constructeurs qui pourraient être utilisés comme conversion, c’est-à-dire ceux avec un seul argument ou ceux dont tous les arguments sauf un ont des valeurs par défaut.
Dans notre cas, le constructeur de Carton a 4 paramètres dont 1 avec valeur par défaut. Il pourrait théoriquement être appelé avec 3 arguments seulement. Le explicit empêche que le compilateur l’utilise pour des conversions surprenantes.
C’est un bon réflexe à développer : par défaut, tous les constructeurs sont *explicit*, sauf si tu veux explicitement permettre une conversion implicite (rare).
Le const après les parenthèses signifie que la méthode ne modifie pas l’état de l’objet. Le compilateur garantit que *this est traité comme un const Box.
Conséquences :
La méthode peut être appelée sur un const Box
À l’intérieur, toute tentative de modifier un membre déclenche une erreur de compilation
C’est un contrat avec l’appelant : “je promets de ne rien changer”
C’est ce qu’on appelle la const-correctness. Toute méthode qui ne modifie pas logiquement l’objet doit être marquée const.
7. Le mot-clé default
Box() = default;virtual ~Box() = default;
= default demande au compilateur de générer la version par défaut de cette fonction spéciale (constructeur, destructeur, etc.).
Pourquoi explicitement le demander ? Parce que dès que tu déclares un constructeur (comme Box(double, double, double)), le compilateur ne génère plus automatiquement le constructeur par défaut. Si tu veux quand même qu’on puisse écrire Box b;, tu dois le redemander explicitement avec = default.
C’est plus expressif et plus efficace que d’écrire un corps vide {}.
Récapitulatif des concepts
Concept
Signification
Quand l’utiliser
class B : public A
B EST-UN A
Quand la phrase “B est un A” est vraie
protected
accessible aux classes filles
Avec parcimonie, préférer private + accesseurs
virtual
dispatch dynamique
Sur les méthodes redéfinies dans les filles
virtual ~Class()
destructeur virtuel
Dès qu’on hérite et qu’on peut manipuler par pointeur de base
explicit
empêche conversion implicite
Sur tous les constructeurs convertibles, par défaut
: Base{...}, m_x{...}
liste d’initialisation
Toujours pour initialiser membres et base
const (méthode)
promesse de non-modification
Sur toute méthode qui ne modifie pas l’état
= default
génération par compilateur
Pour réactiver les fonctions spéciales
Pour aller plus loin
Étudier override et final (C++11) qui complètent virtual
Comparer héritage public, protégé, et privé (les deux derniers sont rares)
Explorer la composition comme alternative à l’héritage
Voir le polymorphisme statique avec les templates et les concepts (C++20)
Récapitulatif
Concept
Syntaxe
Idée clé
Adresse
&x
Adresse mémoire de x
Pointeur
T* ptr = &x;
Stocke une adresse
Déréférencement
*ptr, ptr->m
Accès à la valeur / au membre via pointeur
Référence
T& ref = x;
Alias permanent, jamais nul
nullptr
T* ptr = nullptr;
Pointeur qui ne pointe vers rien
new / delete
T* p = new T{}; delete p;
Allocation manuelle (à éviter)
std::unique_ptr<T>
auto p = std::make_unique<T>(args);
Propriétaire unique avec libération auto
Héritage
class D : public B { ... };
”D est un B”
Chaînage de constructeur
D(...) : B{...}, m_x{...} {}
Initialise la base avant les membres
virtual
virtual void f();
Fonction redéfinissable, résolue à l’exécution
override
void f() override;
Indique et vérifie la redéfinition
Destructeur virtuel
virtual ~Base() = default;
Obligatoire dans toute classe de base
Erreurs courantes
#
Erreur
Conséquence
Correction
1
Déréférencer un nullptr
Crash
Vérifier if (p != nullptr) avant *p
2
Oublier delete après new
Fuite mémoire
Préférer std::make_unique<T>()
3
Utiliser un pointeur après delete
Comportement indéfini
Mettre ptr = nullptr; après delete
4
Confondre . et ->
Erreur de compilation
obj.m pour un objet, ptr->m pour un pointeur
5
Oublier virtual sur le destructeur de la base
Destructeur de la dérivée non appelé
virtual ~Base() = default;
6
Oublier override sur la redéfinition
Bug silencieux si la signature diffère
Toujours écrire override
7
Ne pas chaîner le constructeur de base
Membres de base non initialisés correctement
Liste d’init : Derived(...) : Base{...}
8
Object slicing (passage par valeur d’une dérivée)
Perte de la partie dérivée
Passer par référence ou pointeur
Les trois types d’héritage en C++ : public, protected, private
En C++, le mot-clé qui précède la classe parente détermine la visibilité des membres hérités. Ce document explique les trois variantes et leurs usages.
Les trois syntaxes
class B : public A { /* ... */ }; // heritage publicclass B : protected A { /* ... */ }; // heritage protegeclass B : private A { /* ... */ }; // heritage prive
Le mot-clé entre : et le nom de la classe parente détermine comment les membres de A sont visibles depuis l’extérieur de B. Il ne change rien à ce que B peut voir en interne : B voit toujours les public et protected de A, jamais les private.
Ce qui change, c’est le niveau de visibilité maximal des membres hérités vu depuis l’extérieur.
Tableau de transformation des visibilités
Pour chaque type d’héritage, voici comment les membres de A apparaissent dans B :
Membre dans A
public A
protected A
private A
public
reste public
devient protected
devient private
protected
reste protected
reste protected
devient private
private
inaccessible
inaccessible
inaccessible
Règle mentale
Le mot-clé d’héritage est un plafond de visibilité. Aucun membre ne peut être plus visible que ce plafond une fois hérité.
Plafond public : aucune restriction supplémentaire
Plafond protected : tout devient au mieux protected
Plafond private : tout devient au mieux private
Héritage public : la relation EST-UN
class Carton : public Box { };
C’est la relation classique d’héritage. **Un **Carton** EST-UN ****Box**. On peut substituer un Carton partout où un Box est attendu (principe de substitution de Liskov).
void process(Box& b);Carton c{30, 20, 15, "Cardboard"};process(c); // OK : Carton EST-UN Box, le code exterieur le sait
C’est de loin le type d’héritage le plus utilisé en pratique. Quand on parle d’héritage sans préciser, on parle généralement d’héritage public.
Héritage protégé : “implémenté en termes de, partageable avec les filles”
class Carton : protected Box { };
Carton utilise Box pour son implémentation interne. L’extérieur ne sait pas qu’il y a un Box dedans, mais les classes qui hériteront de Carton le sauront.
void process(Box& b);Carton c{30, 20, 15, "Cardboard"};process(c); // ERREUR : la conversion Carton -> Box est inaccessible
Cas d’usage extrêmement rare. En pratique, on ne croise quasiment jamais ce type d’héritage dans du code de production.
Héritage privé : “implémenté en termes de”
class Carton : private Box { };
Carton utilise Box pour son implémentation interne, et personne d’autre ne le sait, pas même les classes filles. C’est presque équivalent à une composition (un Box comme membre privé), avec quelques différences techniques mineures.
Pourquoi presque toujours public
L’héritage non-public exprime une relation “est implémenté en termes de”, qui correspond en réalité à une relation de composition. Et la composition est presque toujours préférable :
La composition est plus claire, plus flexible (on peut changer le membre interne sans toucher à l’interface publique), et plus facile à raisonner. La règle de Sutter et Alexandrescu
Dans C++ Coding Standards, Herb Sutter et Andrei Alexandrescu énoncent :
Préférer la composition à l’héritage. Quand on hérite, hériter publiquement.
C’est un bon principe à graver dans la tête de tes étudiants.
Quand l’héritage non-public a-t-il un sens ?
Les cas légitimes sont rares et relèvent du C++ avancé :
**Accès aux membres ****protected** : quand on a besoin d’accéder à des membres protected de la classe parente, ce que la composition ne permet pas (la composition ne donne accès qu’aux membres public).
Redéfinition de méthodes virtuelles : quand on veut redéfinir une méthode virtuelle de la classe parente sans exposer la relation d’héritage à l’extérieur.
Empty Base Optimization (EBO) : un membre composé d’une classe vide occupe quand même 1 octet en mémoire, alors qu’une base vide peut occuper 0 octet. Optimisation utilisée dans certaines bibliothèques.
Ces cas sont rares. Pour un cours d’introduction à la POO, on peut sans problème dire : héritage = **public**, point.
Comparaison avec d’autres langages
Langage
Modèle
Java, C#
Un seul type d’héritage, implicitement public
Rust
Pas d’héritage du tout, uniquement composition et traits
Python
Pas de distinction syntaxique, convention de noms (_membre)
C++
Trois types d’héritage explicites
C++ est presque le seul langage à offrir cette flexibilité, et c’est largement considéré comme un excès historique du langage. Les langages plus récents ont préféré simplifier ou éliminer cette dimension.
Synthèse pour le cours
Type
Phrase mentale
Fréquence
Recommandation
public
“B EST-UN A”
omniprésent
utilisation normale
protected
“B est-un A pour ses filles uniquement”
quasi inexistant
éviter
private
“B est implémenté avec A”
rare
préférer la composition
Message à retenir
Dans 99% des cas réels, on utilise l’héritage public. Les étudiants doivent savoir que protected et private existent comme types d’héritage, comprendre le tableau de transformation, mais aussi savoir que ce sont des cas particuliers à investiguer si jamais ils en croisent dans du code existant.
Exercices suggérés
Écrire trois petites classes filles d’une même classe A avec les trois types d’héritage, et tester en ligne sur godbolt.org quels appels passent ou échouent depuis l’extérieur.
Réécrire un héritage privé en utilisant une composition équivalente, et comparer la lisibilité du code.
Explorer la directive using qui permet de réintroduire un membre spécifique dans la visibilité publique :
class Carton : private Box {public: using Box::volume; // volume redevient public dans Carton};
Pour aller plus loin
Lire le chapitre sur l’héritage dans Effective C++ de Scott Meyers (item sur composition vs héritage privé)
Comparer avec le système de traits de Rust, qui résout les mêmes problèmes différemment
Étudier le pattern PIMPL (Pointer to Implementation), qui utilise la composition pour cacher l’implémentation
Exercice et pont vers Unreal
Mini-exercice (à faire en classe)
Créez une petite hiérarchie :
class Shape { // cette classe devient abstraite à cause de la fonction virtuelle pure area()public: virtual double area() const = 0; // ou retourner 0.0 si on évite les virtuelles pures virtual void describe() const; // affiche "Shape" virtual ~Shape() = default;};class Square : public Shape {public: explicit Square(double side); double area() const override; // side * side void describe() const override; // affiche "Square de côté X"private: double m_side;};
Fonctions virtuelles pures et classes abstraites en C++
Ce document explique les concepts de fonction virtuelle pure, de classe abstraite, et les mots-clés override et final, à travers une hiérarchie Shape / Square / Circle.
Le = 0 à la fin n’est pas une affectation. C’est une syntaxe spéciale qui déclare la méthode comme virtuelle pure (pure virtual function).
Ça signifie deux choses :
La méthode n’a pas d’implémentation dans cette classe
Toute classe fille doit la redéfinir
Pourquoi cette syntaxe bizarre
Historiquement, Bjarne Stroustrup voulait éviter d’ajouter un nouveau mot-clé comme pure ou abstract. Il a réutilisé une syntaxe libre. Ce n’est pas élégant, mais c’est le standard.
Tous ces langages expriment la même idée : cette méthode existe dans le contrat, mais doit être implémentée ailleurs.
2. La classe abstraite
Une classe qui contient au moins une fonction virtuelle pure devient automatiquement abstraite. On ne peut pas l’instancier directement.
Shape s; // ERREUR : Shape est abstraiteauto s = std::make_unique<Shape>(); // ERREUR : Shape est abstraiteShape* p = new Square{3.0}; // OK : pointeur vers ShapeShape& r = *p; // OK : reference vers Shapestd::unique_ptr<Shape> u = std::make_unique<Square>(3.0); // OK
C’est exactement ce qu’on veut pour Shape : ça n’a aucun sens d’instancier une “forme abstraite”. On veut toujours une forme concrète : Square, Circle, etc.
Une classe abstraite peut quand même
Avoir des données membres
Avoir des méthodes non virtuelles (utilitaires)
Avoir des méthodes virtuelles non pures (avec implémentation par défaut)
Avoir un constructeur (appelé par les filles via la liste d’initialisation)
Avoir un destructeur (souvent virtuel)
Ce qu’elle ne peut pas faire, c’est exister seule comme objet en mémoire.
override indique au compilateur : “je redéfinis une méthode virtuelle de la classe parente”. C’est une sécurité, pas une obligation.
Pourquoi c’est utile
Sans override, ce code compile silencieusement mais ne fait pas ce que tu crois :
class Shape {public: virtual double area() const = 0;};class Square : public Shape {public: double area() { return m_side * m_side; } // BUG : pas de const};
Le area() de Square n’est pas une redéfinition de celui de Shape (signatures différentes : un est const, l’autre non). C’est une nouvelle méthode qui cache l’autre. Square hérite donc de la version pure de Shape et reste abstraite. Le compilateur ne dit rien, et tu te retrouves avec une erreur incompréhensible plus tard.
Avec override :
double area() override { return m_side * m_side; }// ^^^^^^^^// ERREUR : aucune methode virtuelle correspondante dans la classe parente
Le compilateur t’arrête immédiatement.
Règle pratique
Toujours mettre **override** quand tu redéfinis une méthode virtuelle. C’est gratuit en performance, c’est une garantie de correction.
4. Le mot-clé final
final est l’inverse de override : il interdit toute redéfinition future.
Sur une méthode
class Square : public Shape {public: double area() const override final { return m_side * m_side; }};class FancySquare : public Square {public: double area() const override; // ERREUR : Square::area est final};
Sur une classe entière
class Square final : public Shape { // ...};class FancySquare : public Square { // ERREUR : Square est final // ...};
Quand l’utiliser
final est utile quand :
Tu veux empêcher l’extension d’une classe pour des raisons de design
Tu veux permettre au compilateur d’optimiser les appels (la méthode étant finale, le compilateur peut éviter le dispatch dynamique)
À utiliser avec parcimonie. La plupart du temps, on laisse la porte ouverte à l’extension.
5. Le polymorphisme dynamique en action
L’intérêt de tout ce mécanisme est dans la boucle du main :
std::vector<std::unique_ptr<Shape>> shapes;shapes.push_back(std::make_unique<Square>(3.0));shapes.push_back(std::make_unique<Circle>(2.0));for (const auto& s : shapes) { s->describe();}
Le vector contient des pointeurs vers Shape, mais à l’exécution, chaque pointeur pointe vers un type concret différent. L’appel s->describe() :
Le compilateur ne sait pas quelle version appeler à la compilation
À l’exécution, le programme regarde le type réel de l’objet pointé
Il appelle la bonne version : Square::describe ou Circle::describe
C’est le dispatch dynamique, rendu possible par virtual. Sortie attendue :
Square de cote 3, aire = 9Cercle de rayon 2, aire = 12.5664
6. Comparaison : virtuelle pure vs implémentation par défaut
Sémantique honnête : il n’y a pas de réponse par défaut sensée
Avec implémentation par défaut
Shape peut être instanciée
Les filles peuvent oublier de redéfinir, et hériteront du comportement par défaut
Plus permissif, mais source de bugs silencieux
Sémantique discutable : retourner 0.0 pour une forme inconnue est un mensonge
Quand choisir l’un ou l’autre
Virtuelle pure quand il n’existe pas de comportement par défaut sensé. C’est le cas typique des classes de base abstraites qui définissent une interface.
Virtuelle simple quand un comportement par défaut a du sens, et que les filles peuvent éventuellement le surcharger. Exemple : une méthode log() qui par défaut écrit sur std::cout, mais qu’une fille peut rediriger ailleurs.
Dans le doute pour une interface, préférer la virtuelle pure.
7. Le destructeur virtuel (rappel)
virtual ~Shape() = default;
Toute classe destinée à servir de base polymorphique doit avoir un destructeur virtuel. Sans ça :
std::unique_ptr<Shape> s = std::make_unique<Square>(3.0);// quand s est detruit, sans destructeur virtuel, seul ~Shape est appele// ~Square ne s'execute jamais : fuite potentielle, comportement indefini
Avec un destructeur virtuel, la chaîne complète de destruction se déroule correctement.
Récapitulatif des syntaxes
Syntaxe
Nom
Effet
virtual f();
virtuelle simple
peut être redéfinie, a une implémentation
virtual f() = 0;
virtuelle pure
doit être redéfinie, rend la classe abstraite
f() override;
redéfinition explicite
sécurité de compilation
f() final;
méthode finale
empêche toute redéfinition ultérieure
class C final
classe finale
empêche toute classe fille
virtual ~C() = default;
destructeur virtuel
indispensable pour héritage polymorphique
Exercices suggérés
Ajouter une classe Triangle à la hiérarchie qui prend la base et la hauteur.
Ajouter une méthode virtuelle pure perimeter() à Shape et l’implémenter dans toutes les filles.
Essayer de retirer override et introduire une faute de frappe (area() const devient aera() const). Observer le message du compilateur.
Créer une classe ColoredShape qui hérite de Shape mais ajoute un membre m_color sans implémenter area(). Vérifier que ColoredShape reste abstraite.
Marquer Square comme final et essayer d’en hériter. Lire le message d’erreur.
Pour aller plus loin
Étudier les interfaces pures : classes ne contenant que des virtuelles pures, équivalentes aux interfaces de Java
Comparer avec les concepts de C++20, qui offrent du polymorphisme statique
Explorer le pattern Strategy qui utilise abondamment l’héritage avec virtuelles pures
Regarder comment Unreal Engine utilise virtual et = 0 dans ses interfaces (préfixe I)
Pont vers Unreal (semaines 12-14)
Tout ce qu'on vient de voir, vous le retrouverez dans Unreal sous une forme légèrement différente :
C++ standard (cette semaine)
Équivalent Unreal
class Derived : public Base
class AMyActor : public AActor
virtual void f() override
virtual void BeginPlay() override
virtual ~Base() = default
Géré par le moteur via UObject et le GC
std::unique_ptr<T>
TObjectPtr<T>, UPROPERTY()
new / delete
NewObject<T>(), SpawnActor<T>()
Pointeur brut T*
Pointeur brut, mais marqué UPROPERTY pour le GC
Travail personnel
Lecture
Beginning C++23, Chapitre 10 (sections pointeurs et références uniquement)
Défi 1 - Pointeur ou référence ?
Pour chacun des cas suivants, indiquez si vous utiliseriez une référence, un pointeur brut, ou un **std::unique_ptr**, et pourquoi :
Un paramètre de fonction qui doit lire une grosse std::string sans la copier.
Un membre d’une classe Player qui peut avoir ou ne pas avoir d’arme équipée.
Une fonction qui crée et retourne un nouvel Enemy alloué dynamiquement.
Un paramètre de fonction qui doit modifier un int appartenant à l’appelant.
Défi 2 - Mini hiérarchie Vehicle
Implémentez une classe de base Vehicle avec :
Défi 3 (bonus) - Détecter le bug du destructeur
Reprenez la classe Vehicle du défi 2, mais retirez le mot-clé virtual du destructeur de Vehicle. Ajoutez un message dans les destructeurs de Vehicle, Car, et Bike (par exemple std::cout << "~Car\\n";).
Exécutez le programme et observez la sortie quand le vector se vide en fin de portée. Quels destructeurs sont appelés ? Lesquels manquent ? Pourquoi ?
Remettez ensuite virtual et observez la différence.
Banque de cours - Cours 12 - Unreal C++ Suite de Fibonacci
# Cours 12 - Unreal C++ : Suite de Fibonacci
Où est Charlie le Bug ?
class Shape {public: void draw() { cout << "Shape"; } ~Shape() { }};class Circle : public Shape {public: void draw() { cout << "Circle"; }};Shape* s = new Circle();s->draw();delete s;class Animal {public: virtual void speak() = 0;};class Dog : public Animal { };void process(Shape shape) { shape.draw();}
Solution
class Shape {public: void draw() { cout << "Shape"; } // Bug 1: pas virtual - pas de polymorphisme ! ~Shape() { } // Bug 2: destructeur pas virtual !};class Circle : public Shape {public: void draw() { cout << "Circle"; }};Shape* s = new Circle();s->draw(); // Affiche "Shape" au lieu de "Circle" !delete s; // Seul ~Shape() est appelé !class Animal {public: virtual void speak() = 0;};class Dog : public Animal { }; // Bug 3: n'implémente pas speak() - toujours abstrait !void process(Shape shape) { // Bug 4: slicing - passé par valeur, la partie Circle est perdue ! shape.draw();}
Environnement Unreal C++
Préparer MSVC 2022
Démarrez, dans la barre de recherche, “Visual Studio Installer”
Si vous voyez “Visual Studio Community 2026” dans l’onglet “Installed”, il faut le désinstaller
Si vous n’avez pas “Visual Studio Community 2022”, il faut l’installer (allez dans la section “Available).
Si on vous suggère de mettre a jour Visual Studio Community 2022, go for it
Appuyer sur “Modify” sur Visual Studio Community 2022
Dans les Workloads, cochez Destop development with C++ dans la section “Desktop & Mobile
Cochez également Game development with C++ dans la section Gaming et assurez-vous de tout cocher sauf Windows 10 SDK et suivants dans Installation Details
Cliquez sur le bouton Modify pour installer ce qui manque
Installer UE 5.7.x si ce dernier n’est pas installé (5.7.4 au moment de ce document)
Allez dans Options
Ajoutez MetaHuman Creator Core Data, Editor symbols for debugging et au moins Windows ARM64 comme Target Platform)
Cliquer sur ”Apply”
Vous avez 30 GB à installer
Ouvrir Unreal Engine 5.7.x
Créer un nouveau projet → Choisir un modèle (e.g. First-Person)
Important** :** Sélectionner C++ (et non “Blueprint”) dans la section ProjectDefaults
Target Platform → Desktop et Quality Preset → Maximum
Nommer votre projet “MyCpp” par exemple, choisir l’emplacement (ou laisser celui par défaut)
☝🏼
Vos machines manquent de mémoire et il est possible que UE ne puisse pas compiler durant la phase “Compiling C++ Project” ;
Le cas échéant, n’ouvrez pas Visual Studio comme vous le suggère Unreal, faites plutôt ceci :
Appuyez sur Win + R et tapez…:
SystemPropertiesAdvanced
…puis Enter.
De là:
Sous Performance, clickez Settings
Allez à l’onglet Advanced
Sous Virtual memory, clickez Change
Déselectionnez Automatically manage paging file size for all drives
Si vous un disque rapide, choisissez celui-là,. sinon restez sur le drive C:.
Choisir Custom size
Initial size: 65536 MB
Maximum size: 65536 MB
Clickez sur Set, puis OK
Rebootez votre ordi
…
…
Vous avez rebooté ?
Redémarrer Epic Game Launcher
Dans la section Library Unreal Engine, double-cliquez sur votre projet MyCpp
Cliquez “Yes” lorsque UE vous dit “Missing MyCpp Modules”, Would you like to rebuild them now?
☝🏼
Vos machines vont prendre énormément de temps à Compiler les shaders d’Unreal la première fois.
Installer et configurer JetBrains Rider (édition non commerciale)
Rider est l'IDE recommandé pour Unreal C++ : meilleure indexation que Visual Studio sur les gros projets Unreal, support natif des macros (UCLASS, UPROPERTY, UFUNCTION), navigation intégrée vers les Blueprints, et débogueur complet. Depuis octobre 2024, JetBrains offre Rider gratuitement pour usage non commercial (apprentissage, projets personnels, contenu open source). C'est notre cas pour ce cours.
⚠️
Très important : si vous avez d’anciennes installations de JetBrains Rider (par exemple our Unity et C#, désintallez d’abord, sinon la configuration et l’intégration avec Unreal Engine risque d’échouer !
S’inscrire avec votre courriel étudiant Rubika (recommandé) ou personnel
Vérifier votre courriel pour activer le compte
2. Télécharger Rider
☝🏼
N’oubliez pas de tout cocher dans la fenêtre “Installation Options” de JetBrains Rider Setup
3. Activer la licence non commerciale
Au premier lancement de Rider :
Choisir “Log in to JetBrains Account”
Se connecter avec le compte créé à l’étape 1
Dans la fenêtre de licence, sélectionner “Non-Commercial Use”
Cocher la case confirmant l’usage non commercial (cours, projets personnels, etc.)
Cliquer “Start Trial / Activate”
La licence non commerciale envoie des données d’usage anonymisées à JetBrains (c’est la condition de la gratuité). Aucun code source n’est transmis.
4. Configuration spécifique pour Unreal Engine
Ouvrir l’Unreal Engine Editor 5.7.4
Menu Edit → Editor Preferences
Chercher “Source Code”
Dans Source Code Editor, sélectionner Rider Uproject (Attention, pas “Rider”, mais bien “Rider Uproject”)
Redémarrez UE Editor
Le menu Tools affiche maintenant Refresh Rider Uproject Project et Open Rider Uproject
Cliquer Tools → Refresh Rider Uproject Project pour générer/mettre à jour les fichiers de projet
Rider va prendre un peu de temps à s’installer, va vous poser quelques questions. Puis, à la fin (soyez patients, ça peut prendre du temps), Rider va vous demander “RiderLink plugin is recommended”:
Faites “Install plugin in Engine”.
☝🏼
Vous avez fait “Install plugin in Engine ? Sinon, refaites-le en suivant les instructions de la section ci-après (Plugin RiderLink):
Plugin RiderLink
RiderLink est le plugin qui connecte Rider à l'éditeur Unreal : logs en direct dans Rider, navigation Blueprint vers C++, points d'arrêt dans le PIE, etc. Il existe deux endroits où l'installer.
Emplacement
Où le plugin est installé
Quand choisir
Install RiderLink in Game Plugins
<Projet>/Plugins/Developer/RiderLink/
Le plugin suit le projet. Si vous partagez le projet, chaque collaborateur le récompile. Pratique si vous avez plusieurs versions d’UE en parallèle.
Install RiderLink in Engine Plugins
<UE_5.7>/Engine/Plugins/Developer/RiderLink/
Disponible pour tous les projets UE 5.7.x de cette machine. Recommandé pour ce cours (une seule machine, une seule version).
☝🏼
Fermez Autant Rider que Unreal Engine Editor !
5. Vérification
Dans Rider, vous devriez voir :
Le projet listé dans l’arborescence avec les dossiers Source/, Plugins/, Content/
Les macros Unreal colorées correctement (UCLASS, UPROPERTY, UFUNCTION en jaune/orange selon le thème)
L’autocomplétion qui propose les types Unreal (AActor, UStaticMeshComponent, FVector, etc.)
Un bouton “Build” et un bouton “Run” en haut à droite avec une configuration DebugGame Editor ou Development Editor
Une icône Unreal (avec le cercle) au bas du sidebar de Rider (c’est le Unreal Link), duquel vous pouvez voir en temps réel les logs d’Unreal Engine Editor !
Si l’autocomplétion ne fonctionne pas immédiatement : laisser Rider terminer l’indexation (barre de progression en bas), puis File → Invalidate Caches → Restart au besoin.
Test rapide de la liaison Rider <-> Unreal (RiderLink)
Pour valider que RiderLink communique bien avec l'éditeur Unreal :
Dans Unreal Editor, vérifier que le plugin est actif : Edit → Plugins, chercher “Rider Integration” (c’est le nom d’affichage de RiderLink), la case doit être cochée.
Garder l’éditeur Unreal ouvert et lancer Rider sur le même projet (Tools → Open Rider Uproject)
Dans Rider, ouvrir la fenêtre Unreal Log : View → Tool Windows → UnrealLink (ou l’icône Unreal dans le sidebar). Les logs de UE doivent défiler en direct quand on agit dans l’éditeur (compilation, sélection d’un actor, lancement du PIE, etc.).
Ferme UE et Rider. Redémarre ton projet MyCpp
Tools → Open Rider Unreal
Si Rider te propose d’installer le plugin, choisis Install RiderLink in Engine Plugins
Tu devrais voir deux plugins en cherchant pour Rider maintenant dans Edit → Plugins dans UE
Si ces deux signaux (logs en direct + “Open in Rider”) fonctionnent, la liaison est opérationnelle.
Pour enfin tester que l’intégration C++ BluePrint fonctionne :
Dans UE : Tools > New C++ Class > Actor, donne-lui un nom (ex. MyNode), crée-le.
UE va lancer la compilation et Rider devrait s’ouvrir (ou prendre le focus) sur MyNode.h.
Créer ensuite un Blueprint Class qui hérite de MyNode (clic droit dans Content Browser > Blueprint Class > chercher MyNode dans “All Classes”).
Double-clique sur ce Blueprint.
En haut à droite, tu devrais voir “Parent Class” et si tu cliques, le code va s’ouvrir dans Rider.
Pareillement, dans Rider, juste en dessous de UCLASS dans le fichier .h de ta classe (MyNode.h), Rider devrait t’indiquer “1 derived blueprint class…”. Si tu clic dessus, le blueprint editor va s’ouvrir dans Unreal.
☝🏼
Si vous ne voyez pas « 1 derived blueprint class… », c’est que, sans doute, le Blueprint n’a pas été sauvegardé dans Unreal (une étoile devant l’icône du blueprint devrait être présent dans le Content Browser. Right-click → Save sur cet icône !
Pourquoi des macros ? Le système de réflexion
En C++ standard, le compilateur "oublie" presque tout après la compilation. Le binaire final ne sait pas, à l'exécution :
Cette absence d’introspection est un choix de design du C++ (performance, simplicité (sérieux, simplicité ?!?!?)),/ mais elle pose un vrai problème pour un moteur de jeu comme Unreal :
Comment sauvegarder un Actor sur disque sans connaître ses champs ?
Comment afficher ses propriétés dans le panneau Details de l’éditeur ?
Comment laisser un Blueprint appeler une fonction C++ qu’il ne connaît qu’à l’exécution ?
Comment le garbage collector sait-il quels objets sont encore vivants ?
Comment répliquer un champ à travers le réseau en multijoueur ?
Pour répondre à tout cela, Unreal ajoute une couche de réflexion par-dessus le C++ : un système qui décrit, à l’exécution, la structure de votre code. C’est exactement ce que font les macros UCLASS, USTRUCT, UPROPERTY, UFUNCTION et compagnie. Sans elles, votre code C++ existe, mais le moteur ne le voit pas.
Comment ça marche en coulisses : l'Unreal Header Tool
Avant chaque compilation C++ classique (MSVC ou Clang), Unreal lance un outil maison : l'Unreal Header Tool (UHT - Pas le lait Ultra Haute Température). Cet outil scanne tous vos fichiers .h, repère les macros U-quelque-chose, et génère automatiquement du code C++ supplémentaire dans des fichiers nommés MaClasse.generated.h. Ce code généré contient toute la "plomberie" qui décrit votre classe au moteur : tables de noms, accesseurs réflexifs, code de sérialisation, hooks pour le GC, etc.
Le flux complet :
.h écrit par vous │ ▼UHT lit les macros UCLASS / UPROPERTY / UFUNCTION... │ ▼.generated.h créé automatiquement (le "pont" vers le moteur) │ ▼Compilateur C++ compile votre code + le code généré ensemble │ ▼Binaire final qui contient les métadonnées Unreal │ ▼Editeur, Blueprint, GC, save, réseau peuvent enfin "voir" votre code
C’est pour cette raison que vous verrez toujours ces deux conventions dans tout fichier Unreal :
#include "MaClasse.generated.h" // DOIT etre le DERNIER include du .h
Et dans le corps de chaque classe ou struct exposée :
GENERATED_BODY() // PREMIERE ligne du corps de la classe
GENERATED_BODY() est l’emplacement où UHT injecte le code généré. Sans elle, le pont n’est pas branché et la compilation échoue avec une erreur cryptique. Mais bon, les erreurs en C++ sont en général toujours cryptiques 😞
Convention : préfixes obligatoires
Avant de voir les macros une par une, retenez la convention de nommage Unreal. UHT vérifie le préfixe de chaque classe et refuse de compiler si vous l'oubliez.
Préfixe
Hérite de…
Exemple
Garbage Collected ?
A
AActor ou un descendant (objets placés dans le monde)
AMyActor, APawn, ACharacter
Oui
U
UObject ou un descendant non-Actor (composants, libraries)
Aucun héritage UObject (struct simple, type valeur)
FVector, FRotator
Non (type valeur, peut contenir des UPROPERTY)
T
Template
TArray, TMap<K,V>, TSubclassOf
Non (mais voir note sur UPROPERTY)
I
Interface C++ pure, implémentée par les classes UObject
IMonInterface
Non (le jumeau UInterface l’est)
E
Enum
EColorMode
Non
S
SWidget (UI Slate)
SButton, SCompoundWidget
Non (TSharedPtr, comptage de références)
b
Variable booléenne (convention de nommage, pas une classe)
bIsAlive, bCanJump
s/o
Précisions importantes
UCLASS() - Marque une classe
UCLASS()class AMyActor : public AActor{ GENERATED_BODY() // ...};
Ce que ça débloque :
La classe devient visible dans l’éditeur (menu “Add C++ Class”, possibilité de créer un Blueprint enfant qui en hérite).
Les instances sont gérées par le garbage collector d’Unreal.
La classe peut être sérialisée (sauvegarde dans un .umap ou un .uasset).
Spécificateurs utiles :Blueprintable (autorise la création de Blueprint enfant), BlueprintType (autorise le type comme variable Blueprint), Abstract (interdit l’instanciation directe). Pour la plupart des cas, UCLASS() sans option suffit.
USTRUCT() - Marque un struct
USTRUCT(BlueprintType)struct FMyColor{ GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) float Value = 5.0f;};
Ce que ça débloque :
Le struct peut être utilisé comme type de UPROPERTY dans une autre classe.
Le struct est sérialisable (sauvegardé avec l’objet qui le contient).
UPROPERTY() - Expose une variable membre
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")int32 Health = 100;UPROPERTY(VisibleAnywhere, BlueprintReadOnly)UStaticMeshComponent* Mesh;
Ce que ça débloque :
La variable apparaît dans le panneau Details de l’éditeur.
La variable est sauvegardée avec l’objet (sérialisation).
La variable est visible ou manipulable depuis Blueprint (selon les spécificateurs).
Si c’est un pointeur vers un UObject, il devient suivi par le GC. Très important : un UObject* SANS UPROPERTY peut être détruit sous vos pieds par le garbage collector au moment le plus inattendu.
Spécificateurs courants (combinables) :
Spécificateur
Effet
EditAnywhere
Modifiable dans le Details panel, sur l’instance comme sur le default
EditDefaultsOnly
Modifiable uniquement sur les valeurs par défaut de la classe (pas sur l’instance placée dans le monde)
EditInstanceOnly
Modifiable uniquement sur les instances placées dans le monde
VisibleAnywhere
Visible mais NON modifiable
BlueprintReadWrite
Lisible ET assignable depuis Blueprint (Get + Set)
BlueprintReadOnly
Lisible mais pas assignable depuis Blueprint (Get seulement)
Category = "Nom"
Regroupe la propriété sous “Nom” dans le Details panel
Règle pratique :Edit* contrôle l’éditeur, Blueprint* contrôle le Blueprint. Les deux familles sont indépendantes. On peut combiner EditAnywhere, BlueprintReadOnly (modifiable dans l’éditeur, lecture seule depuis Blueprint), ou VisibleAnywhere, BlueprintReadWrite (juste affichée dans l’éditeur, mais lisible et assignable depuis Blueprint).
La fonction peut être appelée depuis Blueprint (devient un noeud).
La fonction peut être liée à un événement réseau (RPC) ou à une animation.
Permet aussi BlueprintImplementableEvent et BlueprintNativeEvent pour des fonctions implémentées (ou surchargées) côté Blueprint.
Spécificateurs courants :
Spécificateur
Effet sur le noeud Blueprint
BlueprintCallable
Noeud avec pin d’exécution (entrée et sortie blanches). Pour les fonctions qui ont un effet de bord (modifient l’état).
BlueprintPure
Noeud SANS pin d’exécution (uniquement des pins de données). Pour les fonctions qui calculent et retournent une valeur sans rien modifier (souvent const).
BlueprintImplementableEvent
Pas d’implémentation C++. La fonction sera écrite en Blueprint par les designers.
BlueprintNativeEvent
Implémentation C++ par défaut, mais peut être surchargée en Blueprint. Idéal pour donner un comportement par défaut modifiable.
Category = "Nom"
Catégorie dans le menu contextuel (clic droit) du graph Blueprint. La barre verticale `
Règle de pouceBlueprintCallable vs BlueprintPure : si votre fonction a const à la fin et ne modifie rien, c’est probablement une candidate à BlueprintPure. Sinon, BlueprintCallable.
GENERATED_BODY() - Le pont vers le code généré
À placer en première ligne du corps de toute classe UCLASS ou struct USTRUCT. Ce n'est pas une option : sans elle, l'objet est invisible pour le moteur et la compilation échoue.
UCLASS()class AMyActor : public AActor{ GENERATED_BODY() // <- TOUJOURS en premier, juste apres l'accolade ouvrantepublic: // votre code ici};
UENUM() et UINTERFACE() (mention)
UENUM(BlueprintType) : expose un enum à Blueprint.
UINTERFACE() + IInterface : déclare une interface utilisable à la fois en C++ et en Blueprint. Plus avancé, on n’en aura pas besoin pour le ColorLab.
À retenir
Une variable ou une fonction sans macro U* n'existe pas pour Unreal. Elle reste du C++ "pur", invisible pour l'éditeur, les Blueprints, le garbage collector et le système de save. Les macros sont la frontière entre votre code et le moteur.
Dans la suite du cours, presque chaque classe que vous écrirez aura cette structure :
#include "CoreMinimal.h"// autres includes...#include "MaClasse.generated.h" // toujours en dernierUCLASS()class VOTREPROJET_API UMaClasse : public UneClasseDeBase{ GENERATED_BODY() // toujours en premier dans la classepublic: UPROPERTY(...) Type MaVariable; UFUNCTION(...) Type MaFonction(...);};
Votre premier nœud Blueprint en C++
Le super-pouvoir du Tech Artist
Le rôle du Tech Artist dans une équipe : écrire des outils en C++ que les designers utilisent en Blueprint sans jamais toucher au code. Vous écrivez la logique complexe une fois, vous l'exposez comme un noeud Blueprint avec les bonnes macros, et les artistes l'utilisent comme n'importe quel autre noeud du moteur.
Pour cette première démonstration, on va construire un noeud qui génère une spirale de Fibonacci (le motif des graines de tournesol) à partir de trois paramètres : nombre de points, espacement, hauteur. Les designers pourront ensuite faire spawner ce qu’ils veulent (sphères, fleurs, ennemis…) à chaque position retournée.
Qu'est-ce qu'une Blueprint Function Library (BFL) ?
Une Blueprint Function Library, souvent abrégée BFL, est un type de classe Unreal qui sert de boîte à outils de fonctions statiques exposées comme noeuds Blueprint. Caractéristiques :
Hérite de UBlueprintFunctionLibrary.
Toutes ses fonctions sont **static** : on ne crée jamais d’instance de la classe, on appelle directement les fonctions.
Les fonctions sont accessibles depuis n’importe quel Blueprint (Level Blueprint, Actor Blueprint, Widget Blueprint…) via clic droit dans le graph.
C’est exactement le mécanisme derrière les noeuds Blueprint que vous connaissez déjà : Get All Actors of Class, Print String, les fonctions math (Abs, Lerp, Clamp), les fonctions de chaîne (Append, Contains)… toutes viennent de BFL fournies par Epic.
Quand utiliser une BFL plutôt qu’une classe normale ?
Vous avez des fonctions utilitaires sans état (calculs, conversions, génération de données).
Vous voulez des noeuds Blueprint disponibles partout sans avoir à instancier quoi que ce soit.
Le code n’a pas besoin de stocker de variables membres entre appels.
Si votre code a besoin d’état (variables qui survivent entre appels), il faut un Actor ou un UObject, pas une BFL.
Premier noeud en classe : "Hello from C++" avec ChannelTo255
Avant d'écrire un noeud algorithmique complexe, on commence par valider le workflow complet avec une fonction plus simple : convertir une valeur de canal couleur normalisée (0.0 à 1.0) en valeur 8-bit (0-255). C'est exactement le genre de conversion qu'un Tech Artist fait en permanence quand il jongle entre des couleurs FLinearColor (UE, valeurs 0-1) et des valeurs 8-bit (UI, export, hex).
Étape 1 - Créer la BFL ColorLabLibrary depuis Unreal
Dans l’éditeur Unreal, d’abord ouvrir Rider (Tools → Open Rider Uproject)
Toujours dans l’éditeur Unreal, Tools -> New C++ Class…
Cliquer sur le bouton Public pour s’assurer que la classe est dans le dossier publique.
Nommer ColorLabLibrary (sans préfixe : Unreal ajoute U automatiquement).
Cliquer Create Class
Unreal génère les deux fichiers, compile une première fois, et ouvre Rider sur ColorLabLibrary.h.
Étape 2 - Compléter le header
Dans ColorLabLibrary.h (qui se trouve dans le dossier Private de votre projet MyCpp dans Rider), le squelette est déjà là. Ajouter dans le corps de la classe (après GENERATED_BODY()) :
Étape 3 - Implémenter la fonction
Dans ColorLabLibrary.cpp (qui lui se trouve dans le dossier Public), sous l'include du header, ajouter :
#include "Public/ColorLabLibrary.h" // Attention, il se peut que Rider/UE aient ajouté ce include mais sans le parent "Public/", il faut le mettre, car le .h n'est pas au même endroit que le .cppint32 UColorLabLibrary::ChannelTo255(float NormalizedValue){ return FMath::Clamp(static_cast<int32>(NormalizedValue * 255.0f), 0, 255);}
Décortiquons cette ligne unique :
NormalizedValue * 255.0f : la conversion mathématique de base. Si l’entrée vaut 0.5, on obtient 127.5.
static_cast<int32>(...) : tronque la partie décimale pour passer de float à int32. 127.5 devient 127.
FMath::Clamp(..., 0, 255) : borne le résultat à [0, 255] au cas où l’entrée serait hors de [0.0, 1.0] (ex. -0.2 ou 1.3 par accident).
Étape 4 - Compiler
Dans Rider : menu **Build -> Build and Reload (Le marteau avec une petite feuille, **Ctrl+F9 sur PC ou Cmd+F9 sur Mac). Vérifier la fenêtre Build en bas : elle affiche Build successful quand tout passe. En cas d'erreur, double-cliquer sur la ligne d'erreur pour sauter à la ligne fautive.
Unreal Editor a aussi un bouton “Compile” / “Live Coding” dans sa toolbar, mais il est déplacé ou masqué selon les versions de UE 5.x. Compiler depuis Rider est plus fiable et donne de meilleurs messages d’erreur. Si UE Editor est ouvert pendant le build Rider et que Live Coding est activé, le hot-reload se fait automatiquement.
Étape 5 - Tester dans le Level
Plutôt que d'utiliser le Level Blueprint (dont le bouton d'accès change de place selon les versions de UE 5.x), on crée un Blueprint Actor dédié et on le glisse dans le viewport. C'est aussi le workflow standard en production.
Dans le Content Browser,, clic droit sur le dossier “Content”, puis “New folder” → “MyCpp”
À partir de maintenant, ce qu’on va créer, ce sera dans ce dossier donc cliquez dessus pour qu’il devienne le dossier activé.
Dans le Content Browser, à l’intérieur du dossier vide MyCpp, clic droit -> Blueprint Class -> Actor. Le nommer BP_TestColorLab.
Double-cliquer pour l’ouvrir.
Onglet Event Graph. Sur Event BeginPlay, tirer le pin d’exécution -> chercher Channel To 255, placer (le noeud apparaît dans la catégorie ColorLab -> First Steps).
Sur le noeud, mettre Normalized Value = 0.5.
Du pin Return Value (vert/turquoise, int), tirer un fil et lâcher dans le vide. Dans le menu contextuel, décocher “Context Sensitive” en haut à droite : sinon Print String n’apparaît pas, car il attend une string et le pin source est un int. Une fois décoché, chercher Print String et placer. UE insère automatiquement un noeud de conversion To String (Integer) entre les deux pins.
Connecter les pins d’exécution des deux noeuds (le pin blanc d’exécution du Channel To 255 vers celui du Print String).
☝🏼
Context Sensitive est utile (filtre du bruit) quand vous cherchez un noeud, mais cache des noeuds qui peuvent quand même être utilisés. Ici, si Context sensitive est coché, Ue voit que Return Value est un Int, et ne montre donc pas le noeud “Print String” ; cela va vous pousser à utiliser d’abord un “To String (int)” après-lequel, seulement, apparaîtra le noeud “Print String”. Deux noeuds au lieu d’un (pas trop grave). Sachez que le noeud “Print String” est capable de convertir les types de base en string.
Compile + Save dans la barre d’outils du Blueprint (en haut à gauche).
Fermer la fenêtre du Blueprint, glisser BP_TestColorLab du Content Browser dans le viewport (n’importe où), puis lancer Play.
À l’écran (en haut à gauche, typiquement en vert), vous voyez 127. Vous venez d’écrire votre premier noeud Blueprint en C++.
Ce qui vient de se passer
Vous avez :
Créé une classe C++ héritant de UBlueprintFunctionLibrary.
Marqué une fonction avec UFUNCTION(BlueprintCallable, ...) pour la rendre visible côté Blueprint.
Compilé depuis Rider (avec hot-reload via Live Coding si UE Editor est ouvert).
Appelé la fonction depuis un Blueprint Actor et vu le résultat à l’écran.
C’est le workflow complet C++ -> Blueprint, validé. Si quelque chose ne marche pas pour ChannelTo255 (3 lignes utiles), c’est que la chaîne tooling/macros/compilation a un problème, pas l’algorithme. Avec ce repère, vous saurez où chercher quand un exercice plus complexe ne marchera pas.
Variantes rapides (si le temps le permet en classe)
**Linear255To1(int32 Value) -> float** : la fonction inverse, retourne Value / 255.0f clampé à [0.0, 1.0].
**MakeRGBColor(int32 R, int32 G, int32 B) -> FLinearColor** : retourne un FLinearColor à partir de 3 entiers 0-255 (en utilisant Linear255To1 sur chaque canal). Tester en passant le résultat à un Print String (le texte affiché prend la couleur, vous obtenez un debug visuel naturel).
Ces deux variantes ajoutent ~10 lignes et préparent le terrain pour la Spirale (devoir) en consolidant le workflow.
La Spirale de Tournesol
On passe maintenant à un cran au-dessus du ChannelTo255 validé en classe : générer une spirale de Fibonacci (motif des graines de tournesol) comme noeud Blueprint, puis spawner des sphères à chaque position retournée. Tout le pas-à-pas (création de la classe, code complet, test depuis un Blueprint Actor avec For Each Loop et Spawn Actor From Class) est détaillé dans les sections qui suivent.
L’objectif : appliquer le workflow validé en classe à un cas plus riche, qui retourne un TArray<FVector> au lieu d’un simple int32, et qui se branche à plusieurs noeuds Blueprint en cascade. Le code de la Spirale est aussi un classique en Tech Art (placement procédural d’objets, distribution naturelle), donc bien le maîtriser ouvre la porte à beaucoup d’outils de level design.
Étape 0 - Créer un dossier Fibo (pour Fibonacci) sous Content
Dans Unreal Editor, cliquez droit sur le dossier Content, → New Folder et crééz un dossier nommé “Fibo”
Étape 1 - Créer la classe depuis Unreal Editor
On laisse Unreal créer la BFL pour nous, plutôt que de créer les fichiers à la main. Cela garantit que le système de build connaît les nouveaux fichiers et que les noms respectent les conventions.
Assurez-vous d’être dans le dossier Fibo dans le Content Browser
Dans l’éditeur Unreal, menu Tools -> New C++ Class…
La fenêtre “Choose Parent Class” s’ouvre. En haut, cocher** All Classes**.
Dans le champ de recherche, taper BlueprintFunctionLibrary. Sélectionner BlueprintFunctionLibrary dans la liste, puis cliquer Next.
Dans “Name”, saisir ProceduralLibrary (sans préfixe : Unreal ajoutera U tout seul, comme on l’a vu dans la convention de nommage).
Vérifier que la classe est placée dans Public en choisissant “Class Type” = Public (ainsi, header dans Source/VotreProjet/Public/, implémentation dans Source/VotreProjet/Private/).
Cliquer Create Class.
Unreal va :
Créer ProceduralLibrary.h dans Source/VotreProjet/Public/.
Créer ProceduralLibrary.cpp dans Source/VotreProjet/Private/.
Lancer une première compilation (peut prendre 30 à 60 secondes).
Ouvrir Rider sur ProceduralLibrary.h (grâce à votre configuration Rider Uproject de la Période 1).
☝🏼
Si Rider ne s’ouvre pas tout seul : ouvrir Rider manuellement, ouvrir le .uproject correspondant, puis naviguer dans l’arborescence vers Source/VotreProjet/Public/ProceduralLibrary.h.
Étape 2 - Compléter le header (ProceduralLibrary.h)
Dans Rider, ouvrir ProceduralLibrary.h. Vous voyez le squelette généré par Unreal :
UFUNCTION(BlueprintCallable, ...) : on veut un noeud Blueprint avec pin d’exécution (la fonction crée un tableau, c’est une opération avec effet, pas un simple getter).
Category = "Procedural|Patterns" : la barre verticale | crée une sous-catégorie. Le noeud apparaitra sous Procedural -> Patterns dans le menu contextuel Blueprint.
static : obligatoire pour une BFL.
TArray<FVector> : le type de retour. TArray est l’équivalent Unreal de std::vector, et FVector est un point ou vecteur 3D (X, Y, Z floats).
Trois paramètres, dont ZHeight qui a une valeur par défaut de 0 (donc optionnel quand on appelle la fonction).
Sauvegarder le fichier (Ctrl+S).
Étape 3 - Implémenter l'algorithme (ProceduralLibrary.cpp)
Ouvrir ProceduralLibrary.cpp. Le squelette généré contient juste l'include du header. Ajouter l'implémentation :
#include "Public/ProceduralLibrary.h"TArray<FVector> UProceduralLibrary::GenerateSunflowerSpiral( int32 NumPoints, float Spacing, float ZHeight){ TArray<FVector> Points; const float GoldenAngle = 137.5f * (PI / 180.f); for (int32 i = 0; i < NumPoints; i++) { float Angle = i * GoldenAngle; float Radius = Spacing * FMath::Sqrt(static_cast<float>(i)); float X = Radius * FMath::Cos(Angle); float Y = Radius * FMath::Sin(Angle); Points.Add(FVector(X, Y, ZHeight)); } return Points;}
Détails importants :
Pas de **UFUNCTION** ici : la macro est déclarée sur la signature dans le .h uniquement. Dans le .cpp on écrit la signature normale C++.
PI est une constante définie par UE (pas besoin d’inclure <cmath>).
FMath::Sqrt, FMath::Cos, FMath::Sin sont les versions Unreal des fonctions math. **Toujours utiliser ****FMath::*** plutôt que std::* dans du code Unreal : c’est plus rapide sur certaines plateformes (consoles), et c’est la convention du moteur.
Points.Add(...) est l’équivalent de std::vector::push_back.
static_cast<float>(i) convertit l’int32 en float avant le Sqrt (sinon ambiguïté de surcharge).
Sauvegarder.
Étape 4 - Compiler
Dans Rider, menu Build -> Build Solution (raccourci Ctrl+F9). La fenêtre Build en bas affiche la progression et, en cas de succès, Build successful. En cas d'erreur, double-cliquer sur la ligne d'erreur pour sauter directement à la ligne fautive.
Si la compilation échoue :
“missing GENERATED_BODY()” : vérifier que GENERATED_BODY() est bien en première ligne du corps de classe.
“Cannot find generated.h” ou “fatal error: file not found” : vérifier l’ordre des includes (.generated.h doit être le dernier#include).
“redefinition of class” : vérifier qu’il n’y a qu’un seul UCLASS() et un seul class UProceduralLibrary dans le fichier.
“unresolved external symbol” sur la fonction : vous avez déclaré la fonction dans le .h mais oublié son implémentation dans le .cpp.
Une compilation réussie affiche Build successful dans la fenêtre Build de Rider.
Étape 5 - Tester le noeud avec un Blueprint Actor
Étape 5.1 - Créer un Blueprint qui porte le mesh à spawner
Pourquoi cette étape avant le spawner ? Si on demande à Spawn Actor From Class de spawner un StaticMeshActor brut (la classe nue d’UE), les acteurs spawnent bel et bien (on les voit apparaître dans l’Outliner pendant le Play), mais leur composant Static Mesh est vide : aucun mesh assigné par défaut, donc rien de visible dans le viewport. On crée donc d’abord une sous-classe Blueprint avec une sphère assignée, qu’on utilisera comme classe à spawner à l’étape suivante.
En production, on aurait souvent un acteur métier (un BP_Pickup, un BP_FoliageInstance, etc.) à spawner. Ici, BP_SphereChip joue ce rôle de placeholder visible.
Dans le Content Browser (Dans le dossier Fibo), clic droit -> Blueprint Class. Dans la fenêtre qui s’ouvre, déplier All Classes (en bas), taper StaticMeshActor, sélectionner la classe et cliquer Select.
Nommer le Blueprint BP_SphereChip.
Double-cliquer pour l’ouvrir.
Dans le panneau Components (en haut à gauche), sélectionner le composant StaticMeshComponent (le seul présent par héritage).
Dans le panneau Details (à droite), section Static Mesh, cliquer le dropdown et choisir Engine/BasicShapes/Sphere (taper “Sphere” dans la barre de recherche du dropdown si besoin).
☝🏼
Si la liste des contenus du moteur n’apparaît pas, activer dans le Content Browser le filtre Settings -> Show Engine Content.
Changer le Scale (dans Transform) à 0.2 sur les 3 axes
7. Cliquer Compile, puis Save. Fermer le Blueprint.
Étape 5.2 - Créer le spawner et brancher les noeuds
Dans le Content Browser, dans le dossier Fibo, clic droit -> Blueprint Class -> Actor. Le nommer BP_SunflowerSpawner. Double-cliquer pour l’ouvrir.
Onglet Event Graph. Sur le noeud Event BeginPlay (déjà présent ; sinon clic droit dans le graph, chercher “Begin Play”), faire glisser le pin d’exécution blanc (à droite du noeud) vers la droite et lâcher dans le vide. Un menu contextuel
s’ouvre.
Dans la barre de recherche, taper Generate Sunflower Spiral. Le noeud doit apparaître sous la catégorie Procedural -> Patterns. Cliquer dessus pour le placer.
Sur le noeud Generate Sunflower Spiral, régler les paramètres directement :
Num Points : 500
Spacing : 50
Z Height : 500 (pour que les sphères apparaissent au-dessus du sol et du joueur)
Du pin de sortie Return Value (un tableau de Vector, pin bleu avec icône grille), tirer un fil et chercher For Each Loop. Placer le noeud.
Connecter le pin d’exécution (blanc) du Generate Sunflower Spiral à l’entrée d’exécution du For Each Loop.
Du pin Array Element du For Each Loop (chaque position retournée par le générateur), tirer un fil et chercher Spawn Actor From Class. Placer le noeud.
Sur le Spawn Actor From Class :
Dans le dropdown Class, choisir BP_SphereChip (la classe créée à l’étape 5a). C’est un dropdown : rien à connecter au pin Class, juste sélectionner dans la liste. Si on choisit ici Static Mesh Actor (la classe nue), les acteurs spawnent
mais sont invisibles parce qu’ils n’ont aucun mesh assigné.
Le pin Spawn Transform attend un FTransform. Faire clic droit sur ce pin -> Split Struct Pin. On voit maintenant Location, Rotation, Scale séparés.
Connecter Array Element (du For Each Loop) à Spawn Transform Location.
Connecter le pin d’exécution Loop Body du For Each Loop à l’entrée d’exécution du Spawn Actor From Class.
Cliquer Compile puis Save dans la barre d’outils du Blueprint (en haut à gauche).
Fermer la fenêtre du Blueprint. Dans le Content Browser, glisser BP_SunflowerSpawner dans le viewport (n’importe où dans le niveau).
Appuyer sur Play (bouton triangulaire en haut du viewport).
Vous devriez voir 500 sphères apparaître en spirale de Fibonacci, centrées sur la position où vous avez placé BP_SunflowerSpawner dans le niveau. Si la scène est noire, ajouter une lumière (clic droit dans le viewport -> Place Actor -> Lights ->
Directional Light), puis refaire Play.
Si rien n’apparaît dans le Level pendant le Play :
Ouvrir l’Outliner pendant que le jeu tourne. Si on voit 500 entrées BP_SphereChip apparaître : le spawn fonctionne, c’est un problème de visibilité (mesh manquant, échelle nulle, caméra mal placée). Si on ne voit aucune entrée : c’est le branchement
du graph qui est en cause (Compile pas refait, fil oublié, BP_SunflowerSpawner pas glissé dans le niveau avant Play).
Vérifier que le Blueprint a bien été compilé après les branchements (bouton Compile en haut à gauche, pas juste Save).
Vérifier que BP_SunflowerSpawner est bien glissé dans le viewport avant d’appuyer sur Play. Sans instance dans le niveau, BeginPlay ne se déclenche jamais.
Si on voit des StaticMeshActor (et pas BP_SphereChip) dans l’Outliner, c’est qu’on a oublié de changer le dropdown Class du Spawn Actor From Class après avoir créé BP_SphereChip. Repasser à l’étape 8.
Changements clés :
Étape 5 scindée en 5a (créer BP_SphereChip qui hérite de StaticMeshActor avec sphère assignée) et 5b (le spawner avec son graph).
Note explicite à l’étape 8 : le pin Class est un dropdown, on n’y branche pas de fil, et on choisit BP_SphereChip pas Static Mesh Actor brut (la classe nue spawne des acteurs invisibles).
Bloc de dépannage final qui distingue “spawn marche mais pas visible” vs “spawn ne marche pas du tout” via l’Outliner.
Comprendre l'algorithme
Pour chaque point i de 0 a N: angle = i x 137.5° (angle d'or, converti en radians) rayon = spacing x √i (les points s'éloignent du centre) x = rayon x cos(angle) y = rayon x sin(angle) z = hauteur fixe
Pourquoi 137.5° ? C’est 360° / phi² (où phi = nombre d’or ≈ 1.618). Cet angle “irrationnel” garantit qu’aucun point ne tombe jamais sur un alignement radial avec un autre point déjà placé. Résultat : empilement maximal, aucun trou. La nature a “trouvé” cet angle dans les graines de tournesol, les pommes de pin, l’ananas, les pétales.
Pourquoi **√i** pour le rayon ? Pour que la densité de points reste constante quand on s’éloigne du centre. Si le rayon était linéaire (i x spacing), les points seraient denses au centre et clairsemés en périphérie. Avec √i, l’aire couverte par chaque “anneau” de points reste constante. C’est le même principe qui régit la croissance des plantes.
Pourquoi c'est puissant
Vous venez d'écrire ~20 lignes de C++ qui deviennent un outil utilisable par n'importe quel designer du studio, dans n'importe quel Blueprint, sans qu'ils aient à comprendre le code. Et vous pouvez maintenant :
Changer l’algorithme (spirale carrée, distribution Poisson, fractale…) en modifiant le .cpp et en recompilant. Tous les Blueprints qui utilisent le noeud bénéficient immédiatement du changement.
Ajouter d’autres fonctions à la même BFL (placement sur grille, dispersion aléatoire, distribution de Voronoï…).
Donner cette BFL à toute l’équipe, qui l’utilisera dans leurs propres niveaux sans dupliquer le code.
Travail personnel
Variante 1 : changer le motif en passant 60.0f au lieu de 137.5f pour le GoldenAngle. Observer le résultat (pas de spirale, alignements visibles). Comprendre pourquoi 137.5° est si particulier.
**Variante 2 : ajouter un paramètre ****FLinearColor TintColor** à la fonction. Pour le moment ce paramètre n’a aucun effet sur l’algorithme, mais vous verrez son pin coloré apparaître dans le noeud Blueprint. C’est l’occasion de tester d’autres types de paramètres exposés à Blueprint.
Variante 3 : créer une deuxième fonction dans la même BFL, par exemple GenerateGridPoints(int32 Rows, int32 Cols, float Spacing) qui retourne un TArray<FVector> représentant une grille régulière. Une seule BFL peut contenir autant de fonctions utilitaires que vous voulez.
Banque de cours - Cours 13 - Unreal C++ Hiérarchie de classes & premier Actor
# Cours 13 - Unreal C++ : Hiérarchie de classes & premier Actor
Hiérarchie de classes UE
Pourquoi une hiérarchie de classes ?
Unreal n'est pas qu'un moteur de rendu : c'est un framework complet où tout objet du jeu hérite d'une classe de base qui lui donne automatiquement certaines capacités (mémoire gérée par le GC, sérialisation, exposition à Blueprint, etc.). Pour écrire votre propre classe, vous devez choisir la bonne classe parente selon ce que votre objet doit faire.
Au sommet : UObject. À mesure qu’on descend dans l’arbre, chaque classe ajoute des capacités (présence dans le monde, transform, possession par un controller, mouvement intégré, etc.). Le tradeoff : plus de capacités = plus de mémoire et de coût CPU. On choisit la classe parente la plus haute qui contient déjà ce dont on a besoin, sans plus.
Vue d'ensemble : les classes de base UE
UObject <- Base de tout, géré par le garbage collector├── AActor <- Existe dans le monde, a un Transform│ ├── APawn <- Peut etre "possédé" par un Controller (joueur ou IA)│ │ └── ACharacter <- Ajoute marche/saut, capsule de collision, mesh squelettique│ ├── APlayerController <- Reçoit l'input du joueur, possède un Pawn│ ├── AGameModeBase <- Règles du jeu, spawn du joueur, conditions de victoire│ └── Vos Actors custom...├── UActorComponent <- "Briques" attachées à un Actor, peuvent ticker│ ├── USceneComponent <- A un Transform (peut etre placé dans l'espace)│ │ ├── UStaticMeshComponent <- Affiche un mesh statique│ │ └── ULightComponent <- Émet de la lumière│ └── Vos Components custom...└── UBlueprintFunctionLibrary <- Fonctions statiques exposées comme noeuds Blueprint
UObject - la racine de tout
Ce que c'est : la classe la plus basique d'Unreal. Tout objet géré par le moteur en hérite, directement ou indirectement.
Ce que ça donne :
Garbage collection automatique (pas besoin de delete).
Sérialisation (sauvegardable sur disque).
Reflection (visible par les macros UPROPERTY, UFUNCTION).
Ce qui manque : aucune notion de position dans le monde. Un UObject pur ne peut pas être “placé” dans un niveau.
Quand l’utiliser : pour des conteneurs de données purs, des gestionnaires (managers), de la logique sans présence physique. Exemples : un USaveGame, un UDataAsset, un système d’inventaire.
AActor - tout ce qui existe dans le monde
Ce que c'est : un UObject qui a en plus un Transform (position, rotation, échelle) et peut donc être placé dans un Level.
Ce que ça ajoute :
Position dans l’espace 3D.
Cycle de vie complet (Constructor, BeginPlay, Tick, EndPlay).
Peut contenir des UActorComponent pour ajouter des fonctionnalités (mesh, lumière, son, collision…).
Quand l’utiliser : pour tout ce qui existe physiquement dans la scène. Exemples : un coffre, une porte, une lumière, un déclencheur, un cube qui flotte devant le joueur (Semaine 14).
APawn - un Actor "possédable"
Ce que c'est : un AActor qui peut être contrôlé par un APlayerController (humain) ou un AAIController (IA).
Quand l’utiliser : pour les entités contrôlables (joueur, ennemi piloté par IA, véhicule).
ACharacter - un Pawn humanoïde
Ce que c'est : un APawn avec une capsule de collision, un mouvement marche/saut/chute intégré, et un mesh squelette.
Quand l’utiliser : pour un personnage humanoïde standard (joueur FPS, PNJ qui se déplace au sol).
UActorComponent et USceneComponent - les briques d'un Actor
Un Actor seul est juste une coquille vide. Ce sont les composants attachés qui lui donnent du contenu : un UStaticMeshComponent pour afficher un mesh, un ULightComponent pour émettre de la lumière, un UAudioComponent pour jouer un son, etc.
UActorComponent : composant pur (pas de transform). Pour de la logique sans présence visuelle.
USceneComponent : composant avec transform. Peut être positionné relativement à l’Actor parent.
UBlueprintFunctionLibrary - on connaît !
Vu en Semaine 12 : classe de fonctions statiques exposées comme noeuds Blueprint. Pas d'instance, pas de transform, juste des outils utilitaires. C'est le pattern utilisé en Semaine 12 pour la Spirale de Tournesol.
Tableau de décision : quelle classe choisir ?
Si je veux…
J’utilise
Un objet placé dans le monde, avec position/rotation
AActor
Une entité contrôlable par le joueur ou l’IA
APawn
Un personnage humanoïde avec marche/saut intégrés
ACharacter
Une brique réutilisable à attacher à un Actor
UActorComponent ou USceneComponent
Un conteneur de données ou gestionnaire sans présence dans le monde
UObject
Des fonctions utilitaires accessibles partout en Blueprint
UBlueprintFunctionLibrary
Pour la suite du parcours Unreal :
Cette semaine (S13) :AMyTestActor : public AActor - un premier Actor concret qui génère son mesh en C++ (votre prénom en lettres voxel, qui tourne), avec un UProceduralMeshComponent et un Tick. Zéro asset, zéro chemin /Engine/ : il s’affiche à l’identique sur toutes les machines.
Semaine 14 : un cube flottant tenu devant le joueur : un AActor qui lit la caméra, suit le joueur avec inertie, et ballotte avec une physique de ressort (mouvement secondaire), le tout simulé en C++.
Semaine 15 : révision et examen final (QCM).
Introduction à la classe Actor
La Période 1 ci-dessus a posé la carte des classes UE (UObject, AActor, APawn, etc.) et le tableau de décision pour choisir la bonne. Cette section zoome sur la classe que vous allez le plus manipuler dans le reste du cours : **AActor**. Tout objet placé dans un Level Unreal est un Actor (ou en hérite) : une lumière, un coffre, un déclencheur, un personnage, un projectile, une caméra, et bientôt le cube flottant que vous ferez suivre le joueur. Comprendre ce que c'est, comment lui ajouter des composants et comment manipuler sa position dans l'espace est la fondation de toute la suite du cours, en particulier de la Semaine 14.
Cette section est volontairement théorique et préparatoire. Mais juste après, la Période 2 vous fait créer un premier AMyTestActor concret qui génère son mesh en C++ (votre prénom en lettres voxel, qui tourne) pour que les concepts s’ancrent dans les mains avant le cube flottant de la Semaine 14.
Qu'est-ce qu'une classe Actor ?
Un AActor est la classe de base de tout ce qui peut être placé dans un Level. C'est l'unité fondamentale du gameplay Unreal. Si vous voyez un objet dans le viewport, en dehors de l'éclairage et du sky, c'est presque certainement un Actor.
Position dans la hiérarchie :
UObject <- racine, géré par le GC, pas de présence dans le monde└── AActor <- ajoute Transform + cycle de vie + composants ├── APawn <- Actor possédable par un Controller (vu en S13) ├── APlayerController ├── AGameModeBase └── vos Actors custom (AMyActor, AMyTestActor, votre cube flottant...)
Ce qui distingue un AActor d’un UObject “nu” :
Un Transform (position, rotation, échelle) : il existe à un endroit dans le monde 3D.
Un cycle de vie complet (Constructor, BeginPlay, Tick, EndPlay) que vous pouvez surcharger.
La capacité de contenir des composants (mesh, lumière, son, collision…) qui lui donnent sa forme et son comportement visuel.
La capacité d’être spawné dynamiquement (créé en cours de jeu via SpawnActor) ou placé manuellement dans le Level via l’éditeur.
Convention de nommage : une classe qui hérite de AActor (directement ou indirectement) commence par **A**. C’est UHT qui vérifie ; sans le préfixe, la compilation échoue.
Generated files, garbage collection et UHT (rappel ciblé Actor)
Toutes les notions de la section "Les macros Unreal" vue en Semaine 12 s'appliquent aux Actors, mais trois points méritent un rappel spécifique parce qu'ils sont à l'origine de bugs cryptiques quand on débute :
1. Le **.generated.h** est obligatoire dans tout header d’Actor
Chaque classe UCLASS() que vous écrivez doit avoir un header qui se termine par #include "MaClasse.generated.h" en dernier include. UHT lit la macro UCLASS(), génère ce fichier, et la compilation l’inclut automatiquement. Sans cet include en dernier, la compilation casse avec un message obscur du type Cannot find generated.h.
**2. Le garbage collector surveille et suit les pointeurs **UObject*** marqués ****UPROPERTY()**
C’est le piège le plus fréquent quand on commence avec les Actors. Chaque fois que votre Actor a un pointeur vers un autre objet géré par UE (un autre Actor, un composant, un material, une texture, etc.), ce pointeur doit être marqué UPROPERTY() :
// MAUVAISUStaticMeshComponent* Mesh; // Le GC ne sait pas qu'on tient à ce pointeur. // Il peut le détruire sous nos pieds n'importe quand.// CORRECTUPROPERTY()UStaticMeshComponent* Mesh; // Le GC sait que cet Actor possède une référence. // Le pointeur reste valide tant que l'Actor existe.
Sans UPROPERTY, le pointeur peut devenir invalide silencieusement et vous obtenez un crash random. Avec, vous êtes en sécurité.
3. UHT s’exécute AVANT le compilateur C++
Quand vous compilez (depuis Rider ou depuis le bouton Compile d’UE), UHT scanne d’abord tous vos .h, génère les fichiers .generated.h correspondants, puis le compilateur C++ classique passe. Si vous voyez une erreur “missing GENERATED_BODY()” alors que vous l’avez bien mis, c’est souvent que UHT n’a pas pu lire votre fichier (ex. encodage corrompu, accolades mal fermées). Re-générer les fichiers projet (clic droit sur .uproject → Generate project files) ou faire un Clean + Rebuild résout généralement le problème.
Tick et BeginPlay
Tous les Actors traversent le même cycle de vie. Les deux fonctions qu'on surcharge le plus souvent et qu'il faut connaître sont : BeginPlay() et Tick().
**BeginPlay()**** : initialisation au démarrage**
Appelé une seule fois, juste avant la première frame, quand l’Actor est complètement initialisé et que le monde est prêt. C’est ici qu’on initialise l’état de gameplay, qu’on cherche des références à d’autres Actors, qu’on démarre des timers, etc.
void AMyActor::BeginPlay(){ Super::BeginPlay(); // OBLIGATOIRE : appelle BeginPlay de la classe parente // votre code d'initialisation ici UE_LOG(LogTemp, Log, TEXT("AMyActor a démarré !"));}
Oublier Super::BeginPlay() est un bug subtil : tout compile, mais des comportements de la classe parente sont silencieusement cassés (timers non démarrés, replication non initialisée, etc.).
**Tick(float DeltaTime)**** : appelé chaque frame**
Appelé à chaque frame du jeu. DeltaTime est le temps écoulé depuis la frame précédente (en secondes), utilisé pour rendre les calculs indépendants du framerate (ex. Position += Vitesse * DeltaTime).
Tick est désactivé par défaut sur tous les Actors. Pour l’activer, dans le constructeur :
AMyActor::AMyActor(){ PrimaryActorTick.bCanEverTick = true; // Active le Tick}
Si vous n’avez pas besoin de Tick, laissez-le désactivé. Chaque Tick a un coût CPU même s’il ne fait rien, et avec des centaines d’Actors qui tickent dans une scène, ça compte.
void AMyActor::Tick(float DeltaTime){ Super::Tick(DeltaTime); // OBLIGATOIRE // votre logique par frame ici, multipliée par DeltaTime pour l'indépendance framerate}
Récapitulatif rapide quoi mettre où
Tâche
Constructor
BeginPlay
Tick
Créer un composant (mesh, light, audio)
OUI
non
non
Charger un asset avec ConstructorHelpers
OUI
non
non
Initialiser une variable par défaut
OUI
non
non
Spawner un autre Actor
non
OUI
rare
Chercher une référence à un autre Actor
non
OUI
non
Logique par frame (rotation, mouvement, animation custom)
non
non
OUI
Démarrer un timer ou jouer un son d’intro
non
OUI
non
Le piège du Constructor : dans le constructeur, le monde n’existe pas encore. GetWorld() retourne nullptr, on ne peut pas spawner d’autres Actors, on ne peut pas chercher de références. On y fait UNIQUEMENT : initialiser des UPROPERTY, créer des composants avec CreateDefaultSubobject, charger des assets avec ConstructorHelpers. Tout le reste va dans BeginPlay().
Hooks moins fréquents : OnConstruction et PostInitializeComponentsBeginPlay, Tick et le constructeur couvrent 90% des cas. Mais Unreal expose deux autres hooks utiles dans des cas plus pointus, qui méritent au moins d'être connus.
**OnConstruction(const FTransform& Transform)**** : reconstruction en éditeur ET au spawn**
Appelé chaque fois que l’Actor est (re)construit : à chaque mouvement dans l’éditeur, à chaque modification d’une propriété dans le Details panel, et au spawn runtime. Idéal pour de la génération procédurale visualisable en temps réel dans l’éditeur, sans avoir à lancer Play.
void AMyProcGenActor::OnConstruction(const FTransform& Transform){ Super::OnConstruction(Transform); // Régénère un truc procédural à chaque modif éditeur : // ex. recalculer la longueur d'un mur selon une UPROPERTY MurLength, // repeupler un HISM, redimensionner un mesh... RegenerateMyMesh();}
Tech Art classique : un Actor “wall builder” qui reconstruit son mur de N briques quand vous changez WallLength dans l’éditeur, sans relancer Play. C’est le hook qui rend ça possible. À utiliser avec parcimonie côté gameplay (peut être appelé souvent en éditeur).
**PostInitializeComponents()**** : tous les composants sont prêts**
Appelé après que tous les composants ont été initialisés et enregistrés, mais avantBeginPlay. Utile quand un composant doit configurer un autre composant avant le démarrage du gameplay (ex. brancher un délégué entre deux composants frères). Rare en pratique, mais on le voit parfois dans le code de plugins ou de gameplay framework custom.
void AMyComplexActor::PostInitializeComponents(){ Super::PostInitializeComponents(); // Brancher des composants entre eux ici, avant BeginPlay HealthComponent->OnDeath.AddDynamic(this, &AMyComplexActor::HandleDeath);}
Tableau récap actualisé :
Hook
Quand
Cas d’usage typique
Constructor
Création de l’objet (très tôt, monde absent)
Créer composants, init valeurs par défaut
OnConstruction
À chaque (re)construction (éditeur + spawn)
Génération procédurale visible en éditeur
PostInitializeComponents
Après init des composants, avant BeginPlay
Brancher composants entre eux
BeginPlay
Une fois, juste avant la 1ère frame
Init gameplay, spawner, chercher références
Tick
À chaque frame (si activé)
Logique par frame (mouvement, animation custom)
EndPlay
À la destruction
Libérer ressources, sauver état
Pour le cube flottant de la Semaine 14, vous utiliserez surtout Constructor (créer le mesh) + Tick (lire la caméra et appliquer la physique de ressort à chaque frame). OnConstruction et PostInitializeComponents restent du contexte culturel pour comprendre le code que vous croiserez plus tard dans des plugins UE ou des projets pros.
→ Montrer un Control Rig dans UE (Pinel ?) qui utilise Construction dans le Blueprint
UPROPERTY et UFUNCTION sur un Actor (rappels ciblés)
Vous avez vu ces macros en détail en Semaine 12. Trois rappels spécifiques aux Actors qui reviennent en pratique tout le temps :
1. Exposer une variable au panneau Details de l’éditeur
MaxHealth apparaît dans le Details panel modifiable (designers peuvent le tweaker par instance), CurrentHealth apparaît mais en lecture seule (informatif). Les deux sont accessibles depuis Blueprint.
2. Garder le GC content : tout pointeur UObject a une UPROPERTY*
UPROPERTY()UStaticMeshComponent* MeshComponent; // composant : doit être UPROPERTYUPROPERTY()AAnotherActor* ReferenceActor; // référence vers autre Actor : idemUPROPERTY()UMaterialInstanceDynamic* DynamicMat; // material instance : idem
Pas besoin de spécificateurs (EditAnywhere, etc.) si la variable est privée et juste pour usage interne. Le simple UPROPERTY() suffit pour activer le tracking GC.
BlueprintCallable crée un noeud avec pin d’exécution (pour des fonctions qui modifient l’état). BlueprintPure crée un noeud sans pin d’exécution (pour des getters purs, souvent const).
Composants : ajouter un Static Mesh à un Actor
Un Actor sans composants est une coquille vide (un Transform sans rien à afficher). Ce sont les composants qui lui donnent du contenu : un mesh, une lumière, une collision, un son, etc.
**Le composant le plus courant : ****UStaticMeshComponent**
UStaticMeshComponent affiche un mesh statique (un modèle 3D rigide, par opposition à un Skeletal Mesh animé) déjà fabriqué : un asset présent dans le Content Browser. C’est ce qu’on utilise pour la plupart des objets de décor et des props.
Pattern standard pour ajouter un mesh à un Actor :
// MyActor.cpp#include "MyActor.h"AMyActor::AMyActor(){ PrimaryActorTick.bCanEverTick = false; // 1. Créer le composant mesh dans le constructeur CubeMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("CubeMesh")); // 2. En faire le RootComponent (la racine du Transform de l'Actor) RootComponent = CubeMesh; // 3. Charger le mesh par chemin avec ConstructorHelpers (UNIQUEMENT dans le constructeur) static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeAsset( TEXT("/Engine/BasicShapes/Cube")); // Note : cette méthode marche une fois sur deux sur vos ordis // on va plutôt créer un mesh de toute pièce ! if (CubeAsset.Succeeded()) { CubeMesh->SetStaticMesh(CubeAsset.Object); }}
Décortiquons les concepts importants :
**CreateDefaultSubobject<T>(TEXT("Nom"))** : la seule façon valide de créer un composant attaché à un Actor depuis le constructeur. Ça alloue le composant dans la mémoire UE, l’enregistre comme appartenant à cet Actor, et lui donne un nom unique ("CubeMesh") qui sert d’identifiant interne. **Ne JAMAIS faire ****MeshComponent = new UStaticMeshComponent();** : UE refuse ou crashe.
**RootComponent = CubeMesh;** : tout AActor a un RootComponent, le composant racine qui définit la position de l’Actor dans le monde. Tous les autres composants sont attachés à lui (directement ou via une chaîne). Ici, le mesh cube devient la racine, donc déplacer l’Actor = déplacer le mesh. Si vous ne définissez pas de RootComponent, UE en crée un par défaut (un USceneComponent vide).
**ConstructorHelpers::FObjectFinder<T>** : helper qui cherche un asset par chemin (ex. /Engine/BasicShapes/Cube) au moment de la création de la classe. Ne fonctionne QUE dans le constructeur, ailleurs il crashe. Le static est obligatoire (sinon le helper est recréé à chaque construction d’instance, c’est très coûteux). Le chemin /Engine/... correspond aux assets du moteur, /Game/... à votre dossier Content/.
Le piège des chemins d’asset (et pourquoi on ne s’en sert pas pour notre mesh).ConstructorHelpers::FObjectFinder charge un asset par son chemin texte. Ça marche… tant que l’asset est exactement là où le chemin l’indique. En classe, on a vu le cube /Engine/BasicShapes/Cube se charger sur certaines machines et échouer sur d’autres, avec pourtant la même version d’Unreal : selon les plugins activés et les réglages hérités d’autres cours, le contenu du moteur n’est pas monté de façon identique partout. Le symptôme est un Actor sans mesh, un bug non reproductible, impossible à déboguer en plein cours. On supprime cette classe de bugs en générant notre géométrie nous-mêmes en C++.
Générer un mesh en C++ : UProceduralMeshComponent
Un mesh, au fond, c'est deux listes : des sommets (des points FVector dans l'espace) et des triangles (des triplets d'indices qui disent quels sommets relier en surface). On y ajoute des normales (la direction vers laquelle chaque face regarde, pour l'éclairage) et des UV (les coordonnées de texture). Là où UStaticMeshComponent affiche un mesh déjà fabriqué, UProceduralMeshComponent vous laisse construire ces listes en code et les afficher directement, sans aucun asset ni chemin.
Mise en place (une fois par projet). Le module fait partie du moteur. Ajoutez "ProceduralMeshComponent" à PublicDependencyModuleNames dans le Build.cs du projet :
Puis clic droit sur le .uproject → Generate project files, et #include "ProceduralMeshComponent.h" dans vos fichiers. (Si le compilateur ne trouve pas le module, activez le plugin Procedural Mesh Component dans Edit → Plugins, redémarrez, régénérez.)
La brique de base : une boîte. En Période 2, on écrit une fonction AppendBox(centre, demi-dimensions) qui ajoute les sommets et triangles d’un cube aux listes du mesh. Avec cette seule brique on construit ensuite n’importe quelle forme faite de cubes : un cube isolé (le cube flottant de la Semaine 14), ou des dizaines empilés pour dessiner des lettres (votre prénom, juste après).
Sens des triangles (winding). Une face n’est visible que si ses trois sommets sont donnés dans le bon ordre : en Unreal, horaire vu de l’extérieur. Dans le mauvais ordre, la face est back-face culled (invisible, on voit à travers le mesh). C’est le réglage classique à connaître dès qu’on génère de la géométrie. Notre AppendBox respecte cette convention pour ses 6 faces ; si jamais un mesh apparaît creux, c’est presque toujours le winding à inverser.
Composants utiles pour la suite du cours et au-delà :
Composant
Rôle
Quand l’utiliser
UStaticMeshComponent
Affiche un mesh statique déjà fabriqué (un asset)
La majorité des objets de décor et de props
UProceduralMeshComponent
Affiche une géométrie générée en code (sommets + triangles)
Nos meshs de S13-S14, zéro asset
USceneComponent
Composant avec Transform mais sans rendu (point d’attache)
Quand on veut un point pivot ou un parent invisible
UPointLightComponent
Lumière ponctuelle
Lampes, projectiles lumineux
UCameraComponent
Caméra
Pawns avec POV personnalisé
USkeletalMeshComponent
Mesh animé avec squelette
Personnages, créatures
UAudioComponent
Source sonore 3D
Sons attachés à des objets
UHierarchicalInstancedStaticMeshComponent
Spawn massif d’instances du même mesh
Scattering (feuillage, foule, props répétés)
Anatomie d’un Actor à plusieurs composants : un Actor peut avoir un seul composant (notre cube ci-dessus) ou plusieurs (un Root + un mesh + une lumière + un déclencheur). Quand il y en a plusieurs, le pattern est : un composant racine (RootComponent), et les autres attachés via SetupAttachment(RootComponent) dans le constructeur. C’est le cas par exemple d’un personnage qui porte une UCameraComponent attachée à son mesh, ou d’une arme qui porte une lumière et un émetteur de particules.
Get et Set la location et la rotation d'un Actor
Tout Actor a un Transform (hérité du RootComponent), composé de trois éléments : Location (FVector X/Y/Z), Rotation (FRotator Pitch/Yaw/Roll), Scale (FVector). UE fournit des getters et setters pour chacun.
Lecture (getters) :
FVector Pos = GetActorLocation(); // position dans le mondeFRotator Rot = GetActorRotation(); // rotation dans le monde (degrés)FVector Scale = GetActorScale3D(); // échelle dans le mondeFTransform Xform = GetActorTransform(); // les trois ensemble
Écriture (setters absolus, en coordonnées world) :
SetActorLocation(FVector(100.0f, 200.0f, 50.0f));SetActorRotation(FRotator(0.0f, 90.0f, 0.0f)); // Pitch=0, Yaw=90, Roll=0SetActorScale3D(FVector(2.0f)); // tout en doubleSetActorTransform(NouveauTransform);
Modification relative (adders, plus pratiques pour de l’animation) :
AddActorWorldOffset(FVector(10.0f, 0.0f, 0.0f)); // décale en world spaceAddActorWorldRotation(FRotator(0.0f, 5.0f, 0.0f)); // rotation en world spaceAddActorLocalOffset(FVector(0.0f, 0.0f, 50.0f)); // décale dans le repère localAddActorLocalRotation(FRotator(10.0f, 0.0f, 0.0f)); // rotation locale
World vs Local : “World” = par rapport à l’origine du monde (0,0,0). “Local” = par rapport à la rotation actuelle de l’Actor. Quand on dit “avance le personnage”, c’est typiquement “World offset selon sa direction locale”, ce qui se traduit par AddActorLocalOffset(FVector(VitesseAvant, 0, 0)).
Exemple typique : faire tourner un Actor à chaque frame
void AMyActor::Tick(float DeltaTime){ Super::Tick(DeltaTime); // Rotation de 90 degrés par seconde autour de l'axe Z (Yaw) AddActorWorldRotation(FRotator(0.0f, 90.0f * DeltaTime, 0.0f));}
Notez le * DeltaTime : sans lui, l’Actor tournerait de 90° par frame, donc à 60 fps il ferait 5400°/seconde (15 tours/seconde !). En multipliant par DeltaTime, on garantit 90°/seconde quel que soit le framerate. C’est la base des animations indépendantes du framerate.
Convention des axes UE : X = avant (forward), Y = droite (right), Z = haut (up). Les rotations en FRotator sont en degrés (pas en radians) : Pitch = autour de Y (haut/bas), Yaw = autour de Z (gauche/droite), Roll = autour de X (penché). Pour passer en radians (utile avec FMath::Cos/Sin), utilisez FMath::DegreesToRadians().
Spawner et détruire des Actors
Jusqu'ici on parle d'Actors placés manuellement dans le Level via drag-drop. Mais en pratique, beaucoup d'Actors sont créés dynamiquement au runtime : projectiles tirés par une arme, ennemis spawnés par un wave manager, items lâchés à la mort d'un ennemi. La fonction qui fait ça s'appelle SpawnActor.
SpawnActor<T> : créer un Actor en cours de jeu
L'API canonique est templatée :
AMyActor* NewActor = GetWorld()->SpawnActor<AMyActor>( AMyActor::StaticClass(), // la classe à instancier FVector(100.0f, 200.0f, 50.0f), // position dans le monde FRotator(0.0f, 90.0f, 0.0f) // rotation initiale);if (NewActor){ // OK, NewActor pointe vers l'instance fraîchement créée. // Sa BeginPlay() a déjà été appelée. NewActor->DoSomething();}
Points importants :
**SpawnActor**** ne fonctionne PAS dans le Constructor** (le monde n’existe pas encore). Toujours dans BeginPlay, dans un override de fonction, ou dans un callback runtime.
Le retour est **AMyActor*** typé grâce au template <AMyActor>. Toujours **vérifier ****if (NewActor)** avant de l’utiliser : le spawn peut échouer (collision, conditions de spawn refusées, etc.).
BeginPlay de l’Actor spawné est appelé immédiatement au moment du spawn.
Une variante avec FTransform au lieu de Location + Rotation existe : SpawnActor<T>(TSubclassOf<T> Class, const FTransform& SpawnTransform).
FActorSpawnParameters : paramètres avancés
Pour les cas plus pointus (qui possède l'Actor, comment gérer les collisions au spawn, etc.) :
FActorSpawnParameters Params;Params.Owner = this; // qui possède le nouvel ActorParams.Instigator = GetInstigator(); // qui est responsable (utile pour scoring)Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;AMyActor* NewActor = GetWorld()->SpawnActor<AMyActor>( AMyActor::StaticClass(), Position, Rotation, Params);
SpawnCollisionHandlingOverride est utile : par défaut UE peut refuser le spawn si l’Actor entre en collision avec un autre. Pour des objets positionnés mathématiquement et qui ne devraient jamais se chevaucher (mais bon…), AdjustIfPossibleButAlwaysSpawn garantit que le spawn passe.
SpawnActorDeferred : spawner puis configurer avant BeginPlay
Cas avancé : vous voulez modifier les UPROPERTY d'un Actor avant son BeginPlay, par exemple pour passer des données qui changeront son comportement initial. La séquence :
// 1. Spawn différé : l'Actor est créé mais BeginPlay n'a PAS encore tournéAMyActor* NewActor = GetWorld()->SpawnActorDeferred<AMyActor>( AMyActor::StaticClass(), SpawnTransform);if (NewActor){ // 2. Configurer les UPROPERTY avant BeginPlay NewActor->Health = 200; NewActor->Team = ETeam::Red; // 3. Finaliser le spawn (déclenche BeginPlay maintenant) NewActor->FinishSpawning(SpawnTransform);}
Le pattern courant : on fait un SpawnActor classique puis on configure l’instance juste après (par ex. sa couleur). Avec SpawnActorDeferred, on pourrait passer ces paramètres avantBeginPlay, ce qui permet à BeginPlay de les utiliser dans son init. Pour la plupart des cas, le pattern simple suffit.
Détruire un Actor : Destroy()
Symétrique de SpawnActor. Quand un Actor n'a plus de raison d'exister :
void AMyEnemy::Die(){ PlayDeathAnimation(); SpawnLoot(); Destroy(); // L'Actor est marqué pour destruction}
Destroy() n’efface pas l’objet immédiatement (il est marqué et nettoyé par le moteur au moment opportun, ce qui évite des bugs de “use after free”). Le EndPlay est appelé avant la destruction effective.
Un pattern de spawn courant : un spawner boucle sur une liste de positions, appelle GetWorld()->SpawnActor<...>(...) pour chacune, stocke les pointeurs dans un TArray<AActor*> (UPROPERTY pour le GC), et une fonction de nettoyage appelle Destroy() sur chacun. C’est le squelette de tout système de spawn (vagues d’ennemis, projectiles, items). Vous le pratiquerez au Défi 4 du travail personnel.
Autres APIs Actor utiles (référence rapide)
Pour ne pas alourdir le cours, voici les API restantes que vous croiserez en pratique, avec une ligne d'explication chacune. Pas besoin de les mémoriser maintenant : sachez qu'elles existent, vous y reviendrez quand le besoin se présentera.
Tags : étiqueter des Actors
Chaque Actor a une TArray<FName> Tags (héritée d'AActor) que vous pouvez remplir pour classifier vos Actors et les filtrer en jeu :
// Dans le constructeur ou via le panneau Details (champ Tags)Tags.Add(FName("Enemy"));Tags.Add(FName("Boss"));// Plus tard, filtrerif (OtherActor->ActorHasTag(FName("Enemy"))) { /* ... */ }
Très pratique pour des recherches rapides ou pour passer des indices entre Blueprint et C++ sans créer de UPROPERTY dédiée.
GetActorBounds(...) : récupérer la bounding box
Retourne les coordonnées du bounding box (origine + extension) d'un Actor, calculé sur tous ses composants visibles. Utile pour ajuster une caméra à la taille d'un objet, vérifier des chevauchements, calculer des espacements.
GetComponentByClass<T> et GetComponents<T> : retrouver ses composants
Si vous avez perdu la référence à un composant (ex. il a été créé en Blueprint, pas en C++) :
UGameplayStatics::GetAllActorsOfClass : trouver tous les Actors d'un type
Globalement utile : récupérer tous les Actors d'une classe donnée présents dans le monde.
TArray<AActor*> FoundActors;UGameplayStatics::GetAllActorsOfClass(GetWorld(), AMyTestActor::StaticClass(), FoundActors);// FoundActors contient maintenant tous les AMyTestActor placés dans le Level
C’est le genre de recherche qu’un gestionnaire utilise dans son BeginPlay pour retrouver les Actors qu’il doit piloter.
Distance et visibilité
float Dist = GetDistanceTo(OtherActor); // distance 3D entre 2 ActorsSetActorHiddenInGame(true); // cacher visuellement (l'Actor existe encore)SetActorEnableCollision(false); // désactiver collision globaleSetActorTickEnabled(false); // désactiver le Tick à la volée
Cycle de vie côté objet
bool IsValid(MyActor); // l'Actor existe-t-il encore (utile après un éventuel Destroy) ?MyActor->IsPendingKill(); // est-il marqué pour destruction ?
IsValid() est votre sauvegarde contre les pointeurs vers des Actors détruits. Toujours vérifier IsValid avant d’appeler une méthode sur un pointeur d’Actor stocké longtemps.
Hands-on - Votre premier Actor procédural (prénom qui tourne)
On consolide toute la théorie dans un Actor minimal qui génère son mesh en C++ : un UProceduralMeshComponent, un Tick qui le fait tourner, et des UPROPERTY exposées au Details panel. On avance en deux temps. D'abord un seul cube procédural, pour valider la fabrication de géométrie (c'est exactement la brique que vous réutiliserez en Semaine 14). Ensuite on boucle cette brique pour écrire votre prénom en lettres voxel. Aucun asset, aucun chemin /Engine/ : le résultat est identique sur toutes les machines.
Étape 0 - Activer le module ProceduralMeshComponent
Dans UE, si n’est pas activé, activez le plugin Procedural Mesh Component dans **Edit → Plugins puis **redémarrez si vous venez d’activer le module.
Dans Rider, dans MyCpp.Build.cs, ajoutez le module “ProceduralMeshComponent” à PublicDependencyModuleNames :
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "AIModule", "StateTreeModule", "GameplayStateTreeModule", "UMG", "Slate", // <-- N'oubliez pas d'ajouter une virgule ici puisque ce n'est plus le dernier élément "ProceduralMeshComponent" // <-- Ajoutez ceci });
Dans UE, Faites “Tools → Refresh Rider Uproject Project”
Recompilez.
Étape 1 - Créer la classe via Unreal
Dans l’éditeur Unreal, Tools → New C++ Class…
Sélectionner Actor comme classe parente, cliquer Next.
Nommer MyTestActor (sans le préfixe A : Unreal l’ajoute automatiquement).
Vérifier que la classe est dans Public, cliquer Create Class.
Unreal génère MyTestActor.h et MyTestActor.cpp, compile une première fois, et ouvre Rider.
Étape 2 - MyTestActor.h
On déclare le composant procédural, les paramètres exposés, et deux fonctions : BuildMesh (qui fabrique la géométrie) et AppendBox (la brique de base).
Remplacer tout le fichier MyTestActor.h par ceci :
// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h"#include "GameFramework/Actor.h"#include "ProceduralMeshComponent.h"#include "MyTestActor.generated.h" UCLASS() class MYCPP_API AMyTestActor : public AActor { GENERATED_BODY() public: AMyTestActor(); virtual void OnConstruction(const FTransform& Transform) override; virtual void Tick(float DeltaTime) override; // Texte à afficher en voxels (majuscules A-Z ; accents repliés, espaces gérés) UPROPERTY(EditAnywhere, Category = "Voxel") FString DisplayName = TEXT("KARIM"); // Taille d'un voxel, en cm UPROPERTY(EditAnywhere, Category = "Voxel") float VoxelSize = 20.f; // Vitesse de rotation (degrés/seconde), modifiable dans le Details panel UPROPERTY(EditAnywhere, Category = "Voxel") float RotationSpeed = 60.f; private: UPROPERTY(VisibleAnywhere) UProceduralMeshComponent* NameMesh; // (Re)génère toute la géométrie à partir de DisplayName void BuildMesh(); // Ajoute les 6 faces d'une boîte (centre + demi-dimensions) aux listes du mesh static void AppendBox(const FVector& Center, const FVector& HalfExtent, TArray<FVector>& Verts, TArray<int32>& Tris, TArray<FVector>& Normals, TArray<FVector2D>& UVs); };
On surcharge **OnConstruction** (vu en théorie) plutôt que BeginPlay : ainsi le mesh se régénère dans l’éditeur dès que vous changez DisplayName ou VoxelSize, sans lancer Play.
Étape 3 - MyTestActor.cpp : le squelette et un premier cube
On commence par afficher un seul cube, pour vérifier que AppendBox, le composant et le winding fonctionnent.
#include "Public/MyTestActor.h"AMyTestActor::AMyTestActor(){ PrimaryActorTick.bCanEverTick = true; // Le composant qui affichera notre géométrie générée NameMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("NameMesh")); RootComponent = NameMesh;}// Ajoute une boîte (24 sommets, 6 faces plates) aux listes passées par référence.void AMyTestActor::AppendBox(const FVector& C, const FVector& H, TArray<FVector>& Verts, TArray<int32>& Tris, TArray<FVector>& Normals, TArray<FVector2D>& UVs){ // Ajoute un quad : 4 sommets, 1 normale commune (face plate), 2 triangles. auto AddQuad = [&](const FVector& P0, const FVector& P1, const FVector& P2, const FVector& P3, const FVector& N) { const int32 Base = Verts.Num(); Verts.Add(P0); Verts.Add(P1); Verts.Add(P2); Verts.Add(P3); for (int32 k = 0; k < 4; ++k) { Normals.Add(N); } UVs.Add(FVector2D(0, 0)); UVs.Add(FVector2D(1, 0)); UVs.Add(FVector2D(1, 1)); UVs.Add(FVector2D(0, 1)); // Sens horaire vu de l'extérieur = face visible Tris.Add(Base); Tris.Add(Base + 2); Tris.Add(Base + 1); Tris.Add(Base); Tris.Add(Base + 3); Tris.Add(Base + 2); }; const float x = H.X, y = H.Y, z = H.Z; AddQuad(C+FVector(-x,-y, z), C+FVector( x,-y, z), C+FVector( x, y, z), C+FVector(-x, y, z), FVector(0,0, 1)); // haut AddQuad(C+FVector(-x,-y,-z), C+FVector(-x, y,-z), C+FVector( x, y,-z), C+FVector( x,-y,-z), FVector(0,0,-1)); // bas AddQuad(C+FVector( x,-y,-z), C+FVector( x, y,-z), C+FVector( x, y, z), C+FVector( x,-y, z), FVector( 1,0,0)); // +X AddQuad(C+FVector(-x,-y,-z), C+FVector(-x,-y, z), C+FVector(-x, y, z), C+FVector(-x, y,-z), FVector(-1,0,0)); // -X AddQuad(C+FVector(-x, y,-z), C+FVector(-x, y, z), C+FVector( x, y, z), C+FVector( x, y,-z), FVector(0, 1,0)); // +Y AddQuad(C+FVector(-x,-y,-z), C+FVector( x,-y,-z), C+FVector( x,-y, z), C+FVector(-x,-y, z), FVector(0,-1,0)); // -Y}void AMyTestActor::BuildMesh(){ TArray<FVector> Verts; TArray<int32> Tris; TArray<FVector> Normals; TArray<FVector2D> UVs; // Pour l'instant : un seul cube, centré sur l'origine. AppendBox(FVector::ZeroVector, FVector(VoxelSize), Verts, Tris, Normals, UVs); NameMesh->ClearAllMeshSections(); NameMesh->CreateMeshSection_LinearColor( 0, Verts, Tris, Normals, UVs, TArray<FLinearColor>(), TArray<FProcMeshTangent>(), false);}void AMyTestActor::OnConstruction(const FTransform& Transform){ Super::OnConstruction(Transform); BuildMesh(); // régénère le mesh à chaque (re)construction : visible en éditeur}void AMyTestActor::Tick(float DeltaTime){ Super::Tick(DeltaTime); AddActorWorldRotation(FRotator(0.f, RotationSpeed * DeltaTime, 0.f));}
Comprendre le code
Ce fichier .cpp se lit en quatre blocs : le constructeur (ce qui se passe à la création de l'acteur), AppendBox (la brique qui ajoute une boîte au tableau de sommets), BuildMesh (qui appelle AppendBox puis envoie le tout au GPU), et les surcharges **OnConstruction** et **Tick** (les hooks Unreal qui appellent BuildMesh et font tourner l'acteur).
Le constructeur AMyTestActor()
PrimaryActorTick.bCanEverTick = true;
Active la boucle Tick pour cet acteur. Par défaut, Unreal coupe le Tick des acteurs qui n’en ont pas besoin (économie CPU). Comme on veut faire tourner le cube image par image, on doit l’activer explicitement.
Crée le composant procédural qui hébergera notre géométrie. CreateDefaultSubobject est la façon standard Unreal de fabriquer un composant dans un constructeur d’acteur. Ne jamais utiliser new pour ça, sinon le système de réflexion (UPROPERTY, garbage collector, sérialisation) ne suit pas. TEXT("NameMesh") donne au composant un nom interne, visible dans l’arbre Details.
RootComponent = NameMesh;
Le RootComponent définit la position, la rotation et l’échelle de l’acteur dans le monde. En faisant de NameMesh la racine, faire tourner l’acteur (via AddActorWorldRotation dans Tick) fait directement tourner le mesh. Si on l’oubliait, Unreal créerait un SceneComponent racine invisible et le mesh serait attaché en dessous : ça marche, mais c’est un niveau de hiérarchie en trop.
C (Center) et H (HalfExtent) : le centre et la demi-taille de la boîte. Travailler en demi-taille rend les coordonnées symétriques (x, +x), ce qui simplifie la géométrie. Passés en const FVector& : pas de copie, et l’appelant a la garantie qu’ils ne sont pas modifiés.
Verts, Tris, Normals, UVs : les quatre listes du mesh, passées par référence non-**const**. La fonction y ajoute des éléments au lieu de reconstruire à chaque appel. C’est ce qui permettra de rappeler AppendBox plusieurs fois (une fois par voxel) dans la même BuildMesh, à l’Étape 4.
Pas de valeur de retour : la fonction modifie ses paramètres-sortie via les références.
Un lambda, c’est une mini-fonction définie sur place. AddQuad n’est utile qu’à l’intérieur d’AppendBox, donc on la déclare localement plutôt que de polluer l’en-tête. Le [&] signifie capture par référence : à l’intérieur du lambda, Verts, Tris, Normals et UVs désignent les mêmes tableaux que ceux reçus par AppendBox. Pas de copie, et tout ce que le lambda y ajoute est visible à l’extérieur.
const int32 Base = Verts.Num();
Base mémorise combien de sommets existent déjà avant d’ajouter ce quad. Les 4 prochains sommets occuperont donc les indices Base, Base+1, Base+2, Base+3. C’est ce qui permet d’enchaîner les quads sans collisions d’indices, même en accumulant 6 faces × N cubes.
Ajoute les 4 coins du quad dans l’ordre P0 → P1 → P2 → P3 (sens convenu par l’appelant pour que la face regarde vers l’extérieur).
for (int32 k = 0; k < 4; ++k) { Normals.Add(N); }
On répète la même normale N pour les 4 sommets. C’est ce qui rend la face plate : si on partageait les sommets entre faces voisines, leurs normales seraient moyennées et le cube paraîtrait lissé (« smooth shaded »). Pour un look cube franc avec arêtes nettes, on veut un saut de normale à chaque arête, donc 4 sommets dédiés par face (pas de partage entre faces).
Les UV sont les coordonnées de texture (0 à 1 sur chaque axe). On attribue le carré unité à chaque face, ce qui permettra plus tard à un matériau de plaquer une texture proprement. Même sans texture aujourd’hui, Unreal exige une entrée UV par sommet.
Un quad = 2 triangles, chacun décrit par 3 indices dans Verts. L’ordre 0, 2, 1 (et non 0, 1, 2) suit la convention horaire vue de l’extérieur d’Unreal : le moteur décide de quel côté de la face on regarde en fonction du sens dans lequel les indices tournent. Mauvais sens = face invisible (back-face culling), et on voit l’intérieur du cube.
Les 6 faces
const float x = H.X, y = H.Y, z = H.Z;AddQuad(C+FVector(-x,-y, z), C+FVector( x,-y, z), C+FVector( x, y, z), C+FVector(-x, y, z), FVector(0,0, 1)); // haut// ... 5 autres faces
On éclate H en x, y, z pour rendre les expressions lisibles. Chaque appel à AddQuad :
calcule les 4 coins de la face en partant du centre C, avec des signes choisis pour que les coins tournent dans le bon sens (horaire vu de l’extérieur),
passe la normale de la face : (0, 0, 1) pour le haut, (1, 0, 0) pour la face +X, etc.,
ce qui donne au total 6 faces × 4 sommets = 24 sommets et 6 × 2 = 12 triangles pour une boîte.
Un seul cube, centré sur l’origine de l’acteur, avec une demi-taille de VoxelSize cm sur chaque axe. FVector::ZeroVector est juste FVector(0, 0, 0) (constante Unreal, plus lisible). FVector(VoxelSize) est le constructeur scalaire qui construit (VoxelSize, VoxelSize, VoxelSize).
NameMesh->ClearAllMeshSections();
Vide les sections de mesh existantes. Indispensable parce que BuildMesh peut être rappelée plusieurs fois (à chaque ré-construction dans l’éditeur, voir plus bas) : sans nettoyage, chaque appel empilerait un cube par-dessus l’ancien.
OnConstruction est appelée chaque fois que l’acteur est (re)construit dans l’éditeur : au drop dans le niveau, à chaque changement de propriété dans le panneau Details, à chaque recompilation. En appelant BuildMesh ici, on voit le résultat sans lancer Play : changer VoxelSize ou DisplayName met le mesh à jour instantanément. L’appel à Super::OnConstruction(Transform) laisse la classe de base faire son travail interne : à prendre comme habitude sur toutes les surcharges Unreal.
Tick est appelée à chaque image, à condition d’avoir activé bCanEverTick dans le constructeur. DeltaTime est le temps écoulé depuis l’image précédente, en secondes. Multiplier RotationSpeed (en degrés par seconde) par DeltaTime donne l’angle à ajouter cette image pour avancer à la bonne vitesse, peu importe le framerate. FRotator(Pitch, Yaw, Roll) : ici on ne touche qu’au Yaw (rotation autour de l’axe vertical Z) pour faire tourner le cube comme un présentoir.
Compilez, glissez l’Actor Class dans le niveau (il se trouve dans “C++ Classes → MyCpp → Public”) : un cube apparaît directement dans l’éditeur (c’est OnConstruction qui l’a généré), et il tourne en Play.
DRAG AND DROP DE LA CLASSE
Étape 4 - Étendre à votre prénom
On garde tout le code précédent. Il suffit de **remplacer le corps de ****BuildMesh** par une boucle qui parcourt DisplayName et pose un cube pour chaque pixel allumé d'une police 5x7. Ajoutez d'abord, **au-dessus de ****BuildMesh**, la table de police et deux helpers :
// --- Police 5x7 : 7 lignes de 5 colonnes. '.' = vide, 'X' = voxel. ---namespace{ struct FGlyph5x7 { const char* Rows[7]; }; const FGlyph5x7* GetGlyph(TCHAR c) { static const FGlyph5x7 Font[27] = { /* A */ {{".XXX.","X...X","X...X","XXXXX","X...X","X...X","X...X"}}, /* B */ {{"XXXX.","X...X","X...X","XXXX.","X...X","X...X","XXXX."}}, /* C */ {{".XXX.","X...X","X....","X....","X....","X...X",".XXX."}}, /* D */ {{"XXXX.","X...X","X...X","X...X","X...X","X...X","XXXX."}}, /* E */ {{"XXXXX","X....","X....","XXXX.","X....","X....","XXXXX"}}, /* F */ {{"XXXXX","X....","X....","XXXX.","X....","X....","X...."}}, /* G */ {{".XXX.","X...X","X....","X.XXX","X...X","X...X",".XXX."}}, /* H */ {{"X...X","X...X","X...X","XXXXX","X...X","X...X","X...X"}}, /* I */ {{"XXXXX","..X..","..X..","..X..","..X..","..X..","XXXXX"}}, /* J */ {{"..XXX","...X.","...X.","...X.","X..X.","X..X.",".XX.."}}, /* K */ {{"X...X","X..X.","X.X..","XX...","X.X..","X..X.","X...X"}}, /* L */ {{"X....","X....","X....","X....","X....","X....","XXXXX"}}, /* M */ {{"X...X","XX.XX","X.X.X","X.X.X","X...X","X...X","X...X"}}, /* N */ {{"X...X","XX..X","X.X.X","X..XX","X...X","X...X","X...X"}}, /* O */ {{".XXX.","X...X","X...X","X...X","X...X","X...X",".XXX."}}, /* P */ {{"XXXX.","X...X","X...X","XXXX.","X....","X....","X...."}}, /* Q */ {{".XXX.","X...X","X...X","X...X","X.X.X","X..X.",".XX.X"}}, /* R */ {{"XXXX.","X...X","X...X","XXXX.","X.X..","X..X.","X...X"}}, /* S */ {{".XXXX","X....","X....",".XXX.","....X","....X","XXXX."}}, /* T */ {{"XXXXX","..X..","..X..","..X..","..X..","..X..","..X.."}}, /* U */ {{"X...X","X...X","X...X","X...X","X...X","X...X",".XXX."}}, /* V */ {{"X...X","X...X","X...X","X...X","X...X",".X.X.","..X.."}}, /* W */ {{"X...X","X...X","X...X","X.X.X","X.X.X","XX.XX","X...X"}}, /* X */ {{"X...X","X...X",".X.X.","..X..",".X.X.","X...X","X...X"}}, /* Y */ {{"X...X","X...X",".X.X.","..X..","..X..","..X..","..X.."}}, /* Z */ {{"XXXXX","....X","...X.","..X..",".X...","X....","XXXXX"}} }; if (c < TEXT('A') || c > TEXT('Z')) { return nullptr; } // espace, etc. return &Font[c - TEXT('A')]; } // Majuscule + accents repliés sur la lettre de base (É->E, Ç->C, À->A...) TCHAR FoldToAZ(TCHAR c) { c = FChar::ToUpper(c); switch (c) { case TEXT('À'): case TEXT('Â'): case TEXT('Ä'): return TEXT('A'); case TEXT('Ç'): return TEXT('C'); case TEXT('É'): case TEXT('È'): case TEXT('Ê'): case TEXT('Ë'): return TEXT('E'); case TEXT('Î'): case TEXT('Ï'): return TEXT('I'); case TEXT('Ô'): case TEXT('Ö'): return TEXT('O'); case TEXT('Û'): case TEXT('Ù'): case TEXT('Ü'): return TEXT('U'); default: return c; } }}
Enregistrez le .cpp en UTF-8 pour que les littéraux accentués (TEXT('É'), etc.) soient lus correctement par le compilateur.
La boucle est exactement le réflexe Tech Art : une grille 2D (la police) projetée dans l’espace 3D, un cube par cellule allumée. CenterCol recentre le texte sur l’origine pour qu’il tourne autour de son milieu.
Comprendre le code
L'Étape 4 ajoute deux nouveaux blocs au .cpp : un bloc police (un namespace anonyme contenant la table des 26 glyphes et deux helpers), suivi d'une **nouvelle version de ****BuildMesh** qui parcourt DisplayName et appelle AppendBox une fois par voxel allumé. Le constructeur, AppendBox, OnConstruction et Tick ne changent pas.
namespace { ... } sans nom est un namespace anonyme : tout ce qui est déclaré à l’intérieur est visible **uniquement dans ce fichier ****.cpp**. C’est l’équivalent C++ moderne du vieux static (visibilité fichier) et c’est l’idiome standard pour cacher les helpers qui n’ont pas à figurer dans le .h. Pas de pollution de l’espace de noms global, pas de risque de conflit de lien si un autre .cpp du projet a déjà une fonction GetGlyph ou un type FGlyph5x7.
La **structure ****FGlyph5x7** modélise un glyphe comme un tableau de 7 chaînes (les 7 lignes) de 5 caractères chacune : 'X' = voxel allumé, '.' = vide. On utilise const char* (pas FString) parce que c’est léger, parfaitement compatible avec une table static const placée en lecture seule par le compilateur, et qu’on n’a jamais à modifier ces données.
La **table ****Font[26]** est déclarée static const à l’intérieur de GetGlyph : elle est donc construite une seule fois (au premier appel) et survit pendant toute la durée du programme. Chaque entrée est un glyphe dessiné à la main, ce qui rend la police lisible et modifiable directement dans le code : changez un '.' en 'X' et le voxel correspondant apparaît à la prochaine compilation.
La recherche est ensuite triviale : si c n’est pas dans 'A'..'Z', on renvoie nullptr (l’appelant traitera ce cas avec if (!G) { continue; }). Sinon, on indexe la table par arithmétique de caractères : 'A' - 'A' = 0, 'B' - 'A' = 1, etc. Plus rapide et plus court qu’un switch.
FoldToAZ : majuscule et accents repliés
TCHAR FoldToAZ(TCHAR c){ c = FChar::ToUpper(c); switch (c) { case TEXT('À'): case TEXT('Â'): case TEXT('Ä'): return TEXT('A'); case TEXT('Ç'): return TEXT('C'); case TEXT('É'): case TEXT('È'): case TEXT('Ê'): case TEXT('Ë'): return TEXT('E'); // ... default: return c; }}
Deux étapes successives :
FChar::ToUpper(c) met le caractère en majuscule. Unreal fournit FChar (et pas le toupper du C) pour rester portable et gérer correctement les caractères Unicode.
Le switchreplie les accents sur leur lettre de base : É → E, Ç → C, À → A, Ï → I, etc. Comme la police 5x7 ne contient que A-Z, ce repli garantit que KÉVIN donne le même rendu que KEVIN. Les caractères inconnus (chiffres, ponctuation, espace) ressortent inchangés via le default, et GetGlyph renverra ensuite nullptr : la boucle de BuildMesh les sautera proprement.
C’est de là que vient la note UTF-8 obligatoire : les littéraux comme TEXT('É') ne signifient rien si le compilateur lit le fichier dans le mauvais encodage. Avec un .cpp sauvegardé en UTF-8, 'É' dans le code source et 'É' saisi dans DisplayName correspondent au même point de code Unicode, et le case matche.
La nouvelle BuildMesh : les constantes de mise en page
Cell : la taille d’une cellule de la grille, en cm. C’est l’écart d’un voxel à son voisin.
Half(Cell * 0.45f) : la demi-taille de chaque voxel. On prend 0.45f (et non 0.5f) pour laisser un petit espace entre cubes : les voxels deviennent distincts visuellement au lieu de fusionner en un bloc lisse.
Stride = 6 : 5 colonnes de lettre + 1 colonne d’espace inter-caractères. Chaque caractère consomme donc 6 colonnes sur l’axe horizontal.
Count : le nombre de caractères dans DisplayName.
CenterCol : la colonne centrale du texte entier, exprimée en cellules. La dernière colonne occupée est (Count - 1) * Stride + 4 (cellules 0 à 4 du dernier glyphe), donc son milieu vaut ((Count - 1) * Stride + 4) * 0.5f. On s’en sert plus bas pour décaler chaque voxel et centrer le nom sur l’origine de l’acteur, afin qu’il tourne autour de son milieu et pas autour de sa première lettre. Le ternaire évite une expression bizarre quand DisplayName est vide.
Les deux boucles imbriquées : un voxel par pixel allumé
for (int32 i = 0; i < Count; ++i){ const FGlyph5x7* G = GetGlyph(FoldToAZ(DisplayName[i])); if (!G) { continue; } for (int32 row = 0; row < 7; ++row) for (int32 col = 0; col < 5; ++col) { if (G->Rows[row][col] != 'X') { continue; } const FVector Center( 0.f, ((i * Stride + col) - CenterCol) * Cell, (3 - row) * Cell); AppendBox(Center, Half, Verts, Tris, Normals, UVs); }}
La boucle externe parcourt chaque caractère de DisplayName. On normalise via FoldToAZ, on cherche le glyphe avec GetGlyph, et si le résultat est nullptr (espace, chiffre, ponctuation) le continue saute le caractère sans poser de voxel. Comme Stride continue de s’appliquer pour le caractère suivant, l’espace reste bien un blanc dans le texte.
Les deux boucles internes balaient la grille 5x7 du glyphe, cellule par cellule. C’est le réflexe Tech Art classique : une grille 2D parcourue ligne par ligne, colonne par colonne. if (G->Rows[row][col] != 'X') { continue; } saute les cellules vides.
Pour chaque cellule allumée, on calcule la position 3D du voxel :
X = 0.f : tout le texte est plat dans le plan YZ (frontal). La rotation appliquée dans Tick autour de Z fera défiler le nom face caméra, puis de profil.
Y = ((i * Stride + col) - CenterCol) * Cell : la colonne devient la coordonnée horizontale. i * Stride + col donne la colonne absolue dans la chaîne complète, on soustrait CenterCol pour centrer, puis on multiplie par Cell pour convertir l’index en cm.
Z = (3 - row) * Cell : la ligne devient la coordonnée verticale. Le (3 - row)inverse le sens parce que +Z pointe vers le haut dans Unreal, mais les lignes du glyphe sont indexées du haut vers le bas. Sans cette inversion, le texte apparaîtrait à l’envers.
Enfin AppendBox(Center, Half, Verts, Tris, Normals, UVs) ajoute la boîte aux quatre tableaux partagés. C’est précisément pour ça qu’AppendBox recevait ses tableaux par référence à l’Étape 3 : l’accumulation dans les mêmes buffers permet de bâtir un seul gros mesh à partir de centaines de cubes, sans recopier ni reconstruire à chaque appel.
L'envoi au GPU : strictement identique à l'Étape 3
Aucune différence avec l’Étape 3 : ClearAllMeshSections purge le mesh précédent (indispensable parce qu’OnConstruction peut rappeler BuildMesh à chaque changement de propriété), puis CreateMeshSection_LinearColor envoie tous les voxels en une seule section. Le GPU dessine l’ensemble comme un unique objet, ce qui reste très efficace même avec plusieurs centaines de cubes : la différence de coût entre 24 et 2400 sommets est négligeable face au coût fixe d’un appel de rendu.
Étape 5 - Créer un Blueprint et tester dans le Level
Compilez (bouton Compile d’Unreal, ou Ctrl+F9 dans Rider).
Content Browser → C++ Classes → VotreProjet → Public, clic droit sur MyTestActor → Create Blueprint class based on MyTestActor… Nommez-le BP_MyTestActor, choisissez le dossier Content→MyCpp, Create.
L’éditeur Blueprint s’ouvre. (Variante : clic droit dans le Content Browser → Blueprint Class → dépliez All Classes, cherchez MyTestActor.)
Dans le Blueprint, panneau Details (Class Defaults), réglez Display Name sur votre prénom, ajustez Voxel Size et Rotation Speed. Compile puis Save le Blueprint.
Glissez BP_MyTestActor du Content Browser dans le viewport : votre prénom apparaît en voxels sans lancer Play (c’est OnConstruction). Vous pouvez encore surcharger les valeurs par instance dans le Details panel.
Lancez Play : le prénom tourne sur lui-même autour de l’axe vertical.
Pourquoi passer par un Blueprint ? Le C++ définit le comportement (la classe AMyTestActor) ; le Blueprint fixe les valeurs par défaut, peut ajouter des composants ou du visuel, et se place dans le monde, le tout sans recompiler. On peut glisser la classe C++ brute (utile pour un test rapide, comme à l’Étape 3), mais en production on travaille presque toujours sur un Blueprint dérivé.
Étape 5 - Placer dans le Level et tester
Compilez (bouton Compile d’Unreal, ou Ctrl+F9 dans Rider).
Content Browser → C++ Classes → VotreProjet, glissez MyTestActor dans le viewport. Votre prénom apparaît en voxels sans lancer Play.
Dans le Details panel, changez Display Name pour votre propre prénom : le mesh se reconstruit en direct (c’est OnConstruction). Ajustez Voxel Size et Rotation Speed.
Lancez Play : le prénom tourne sur lui-même autour de l’axe vertical.
Caractères pris en charge. Le rendu n’est pas du texte Unicode complet : seules les lettres A-Z sont dessinées. Les minuscules passent en majuscules, et les accents français listés dans FoldToAZ sont repliés sur leur lettre de base (É → E). L’espace sert de séparateur. Tout autre caractère (chiffres, ponctuation, symboles, caractères non latins) ne dessine rien mais occupe quand même la largeur d’une lettre. Pour étendre la police (trait d’union, accents fidèles), voir le Défi 5.
Ce qui vient de se passer
Vous avez :
**Hérité de ****AActor** pour avoir un Transform, un cycle de vie et des composants.
**Ajouté un ****UProceduralMeshComponent** : au lieu d’afficher un asset, vous fabriquez la géométrie en code.
**Écrit ****AppendBox** : sommets, triangles et normales, la définition même d’un mesh. Un savoir Tech Art directement transférable (mêmes notions dans les Geometry Scripts, les outils de modeling, ou un mesh node de Blender).
**Utilisé ****OnConstruction** (le hook vu en théorie) pour régénérer le mesh à chaque modification dans l’éditeur, sans Play.
**Activé le ****Tick** avec DeltaTime pour une rotation indépendante du framerate.
**Exposé **DisplayName**, **VoxelSize**, ****RotationSpeed** au Details panel.
Le tout **sans aucun asset ni chemin ****/Engine/** : votre Actor s’affiche à l’identique sur toutes les machines de la classe.
C’est le squelette et la brique que vous étendrez en Semaine 14 : le même AppendBox, mais appelé une seule fois pour fabriquer un cube unique, qui flottera devant le joueur et le suivra avec une physique de ressort. Vous savez déjà fabriquer le cube ; en S14 vous lui donnerez vie.
Le raccourci de production. Unreal fournit déjà une boîte toute faite via UKismetProceduralMeshLibrary::CreateBoxMesh, qui remplit les listes de sommets/triangles/normales/UV pour vous. On écrit AppendBox à la main parce que le but est de comprendre ce qu’est un mesh ; une fois la notion acquise, l’helper moteur est trivial à utiliser. (Même logique qu’en S14 avec le ressort manuel face à VectorSpringInterp.)
💡
Defi : ajouter le character “:” affichable
Travail personnel
Tous les défis partent du AMyTestActor que vous venez de créer. Ils introduisent une à une des briques fondamentales d'Actor : paramètres exposés, animation par sin (qui pilotera le bob du cube flottant en S14), couleur via Dynamic Material Instance (couleur du cube en S14), et spawn en boucle (un classique du gameplay UE).
Documentation UE : “Actors and Components” (préparation Semaine 14)
Défi 1 - Plus de paramètres exposés
Ajoutez à AMyTestActor deux UPROPERTY(EditAnywhere) : un bool bRotate pour activer ou couper la rotation, et un FVector RotationAxis pour choisir l'axe de rotation. Modifiez Tick pour en tenir compte, puis testez en direct dans le Details panel.
Défi 2 - Faire flotter le cube (oscillation sinusoïdale)
Dans Tick, ajoutez un mouvement vertical avec FMath::Sin. Exposez FloatAmplitude et FloatSpeed, et accumulez le temps écoulé dans une variable membre. C'est exactement la brique d'animation qui pilotera le bob vertical du cube flottant en Semaine 14.
Défi 3 - Colorer le mesh (Dynamic Material Instance)
Créez un Material simple avec un paramètre vectoriel nommé BaseColor (un Material de votre projet, dans Content/, pas un asset du moteur). Assignez-le au slot Element 0 du NameMesh (Details panel, ou SetMaterial(0, ...)). Dans BeginPlay, créez un Dynamic Material Instance et appliquez une couleur exposée via UPROPERTY. Prépare la coloration du cube selon sa vitesse en Semaine 14.
Défi 4 (Avancé) - Spawner une rangée d'Actors
Créez un nouvel Actor ARowSpawner qui, dans BeginPlay, spawn Count Actors (vos panneaux-prénom, ou n'importe quelle classe) espacés de Spacing sur l'axe X via une boucle. C'est l'exercice classique de SpawnActor en boucle, le squelette de tout spawner de gameplay (vagues d'ennemis, projectiles, items).
Défi 5 (bonus) - Accents et caractères spéciaux
Notre police couvre A-Z et l'espace ; les accents sont repliés (É devient E). Étendez FoldToAZet la table Font pour gérer un caractère de plus : par exemple le trait d'union - (pour les prénoms composés comme Jean-Luc), ou un vrai glyphe accentué. Indice : un glyphe accentué dans une grille de seulement 7 lignes oblige à un compromis (l'accent mange une ligne), ce qui explique pourquoi les vraies polices passent à une grille plus haute.
Solutions
Essayez d’abord par vous-même avant de regarder.
Note : la classe de base n’override plus BeginPlay (on génère le mesh dans OnConstruction). Les défis 2 et 3 qui utilisent BeginPlay doivent donc en déclarer l’override dans le .h : protected: virtual void BeginPlay() override;.
Solution Défi 1 - Paramètres exposés
// MyTestActor.h - dans la section publicUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test")bool bRotate = true;UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test")FVector RotationAxis = FVector(0.f, 0.f, 1.f); // Z = Yaw par défaut
Le paramètre "BaseColor" doit exister dans le Material assigné au cube, sinon l’appel est ignoré silencieusement. C’est le même mécanisme de coloration par code que vous réutiliserez sur le cube flottant en Semaine 14.
Solution Défi 4 - ARowSpawner
// RowSpawner.h#pragma once#include "CoreMinimal.h"#include "GameFramework/Actor.h"#include "RowSpawner.generated.h"UCLASS()class VOTREPROJET_API ARowSpawner : public AActor{ GENERATED_BODY()public: ARowSpawner();protected: virtual void BeginPlay() override;public: UPROPERTY(EditAnywhere, Category = "Spawner") int32 Count = 5; UPROPERTY(EditAnywhere, Category = "Spawner") float Spacing = 200.f; // Quelle classe spawner : on choisira AMyTestActor dans l'éditeur UPROPERTY(EditAnywhere, Category = "Spawner") TSubclassOf<AActor> ActorToSpawn;};
Le même schéma (boucle + SpawnActor + stockage dans un TArray) se généralise à n’importe quelle liste de positions : c’est la base de tout spawner procédural (vagues, projectiles, débris).
Solution Défi 5 - Étendre la police (exemple : le trait d'union)
La police est indexée sur A-Z via c - 'A'. Pour un caractère hors de cette plage, on ajoute un cas explicite dans GetGlyphavant l'indexation :
const FGlyph5x7* GetGlyph(TCHAR c){ static const FGlyph5x7 Hyphen = {{".....",".....",".....","XXXXX",".....",".....","....."}}; if (c == TEXT('-')) { return &Hyphen; } static const FGlyph5x7 Font[26] = { /* ... A-Z comme avant ... */ }; if (c < TEXT('A') || c > TEXT('Z')) { return nullptr; } return &Font[c - TEXT('A')];}
Et il ne faut pas que FoldToAZ écrase ce caractère : FChar::ToUpper('-') renvoie '-', qui n’est dans aucun case, donc le default le laisse passer tel quel. Le trait d’union arrive donc intact jusqu’à GetGlyph.
Pour un vrai accent (par ex. É), retirez son repli dans FoldToAZ et donnez-lui son propre glyphe (un E avec une cellule allumée en haut). Comme la grille ne fait que 7 lignes, l’accent empiète sur le E : c’est le compromis de lisibilité évoqué dans l’énoncé. Une police accentuée propre passe en général à une grille plus haute (5x9, par exemple), ce qui veut dire élargir FGlyph5x7 et la boucle de BuildMesh en conséquence.
Banque de cours - Cours 14 - Unreal C++ Cube flottant tenu & physique de ressort
# Cours 14 - Unreal C++ : Cube flottant tenu & physique de ressort
Le Gravity gun de Half-Life 2 , ça vous dit quelque chose ? Il s’agit d’un dispositif qui permet de tenir des objets flottants à environ 1 mètre devant le joueur, à hauteur de torse. L’objet suit alors le joueur quand celui-ci se déplace, et réagit physiquement quand le joueur tourne, avec de l’inertie et un léger ballottement (wobble).
Rassurez-vous, on ira pas jusqu’à pouvoir lancer les objets, etc., mais au moins donner un mouvement secondaire à l’objet. C’est l’un des savoir-faire les plus précieux du Tech Art : le mouvement secondaire (secondary motion). En animation, le mouvement primaire est piloté (la caméra du joueur, ici). Le mouvement secondaire, c’est tout ce qui suit avec retard et rebond : les cheveux, les vêtements, une antenne, une chaîne, une arme qui balance. On ne l’anime pas à la main : on le simule avec un peu de physique. Si vous avez touché aux spring bones d’un rig, au jiggle deformer, ou aux noeuds “spring” de Blender, c’est exactement le même principe. Ici, vous écrivez la simulation vous-même en C++, ce qui vous donne l’intuition de ce qui se passe sous le capot de ces outils.
On construit AHeldCube : un Actor qui lit la caméra du joueur, calcule un point cible devant lui, et y amène le cube. En Période 1, on fait suivre le cube avec une interpolation simple (lag, inertie douce). En Période 2, on remplace cette interpolation par un vrai ressort-amortisseur qui produit le dépassement et l’oscillation (le wobble), puis on ajoute un bob vertical piloté par la vitesse pour que tourner fasse onduler le cube de haut en bas.
Le projet : un personnage première personne
Jusqu'ici (Semaines 12-13), vous n'avez créé que des Actors. Cette semaine, le cube doit suivre un joueur, donc il vous faut un personnage contrôlable. Comme en semaine 12 on a créé notre projet à partir d’un template First Person, tout est en place :
Ce template fournit déjà tout : un personnage première personne avec une caméra à hauteur des yeux, le déplacement clavier/souris, et l’input câblé (Enhanced Input). Vous n’avez aucun code d’input à écrire.
On laisse ce personnage tel quel. Notre cube sera un Actor indépendant qui se contente de lire la caméra du joueur, peu importe le personnage utilisé.
Pourquoi un Actor indépendant plutôt qu’un code dans le personnage ? Parce que le cube ne contrôle rien : il observe le joueur et réagit. En le gardant séparé, on réutilise exactement le squelette d’Actor de la Semaine 13 (Constructor + composant mesh + Tick), et le cube fonctionne avec n’importe quel pawn ou personnage, sans dépendre du template. C’est aussi plus proche de la réalité : un effet de mouvement secondaire est presque toujours un module qu’on attache, pas du code noyé dans le personnage.
Le cube qui suit le joueur (inertie simple)
Ce qu’on réutilise
De la Semaine 13 : le squelette d’Actor (AMyTestActor) avec un UProceduralMeshComponent racine et la fonction AppendBox, le Tick activé, les UPROPERTY exposées à l’éditeur, et le réflexe du DeltaTime pour être indépendant du framerate.
Des maths des Semaines 2-3 : un vecteur, sa direction (forward), une multiplication scalaire pour avancer le long d’une direction.
L’idée
À chaque frame, on veut placer le cube à un point cible : 1 mètre devant la caméra du joueur, un peu sous la ligne des yeux (hauteur de torse). Si on y téléporte le cube directement, il colle parfaitement à la caméra : rigide, sans vie. À la place, on le fait converger doucement vers la cible. Comme il met un petit temps à rattraper, il traîne quand on bouge : c’est l’inertie.
Créer la classe depuis Unreal
Dans l’éditeur Unreal, Tools → New C++ Class…
Classe parente Actor, Next.
Nommer HeldCube (sans le préfixe A, Unreal l’ajoute), vérifier Public, Create Class.
Unreal génère les fichiers, compile, et ouvre Rider.
Module procédural. Comme en Semaine 13, ce cube est généré en C++. Ajoutez "ProceduralMeshComponent" à PublicDependencyModuleNames dans le Build.cs du projet si ce n’a pas été fait au cours 13, puis régénérez les fichiers projet (Tools → Refresh Rider Uproject Project).
Fichier 1 : HeldCube.h
#pragma once#include "CoreMinimal.h"#include "GameFramework/Actor.h"#include "ProceduralMeshComponent.h"#include "HeldCube.generated.h"UCLASS()class MYCPP_API AHeldCube : public AActor{ GENERATED_BODY()public: AHeldCube(); virtual void OnConstruction(const FTransform& Transform) override; virtual void Tick(float DeltaTime) override; // Distance devant la caméra, en cm (100 = 1 m) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Hold") float HoldDistance = 100.f; // Décalage vertical par rapport aux yeux (négatif = plus bas, vers le torse) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Hold") float HoldHeightOffset = -20.f; // Vitesse de rattrapage en position (plus grand = moins d'inertie) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Hold") float FollowSpeed = 6.f; // Vitesse de rattrapage en rotation UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Hold") float RotationFollowSpeed = 8.f; // Demi-côté du cube, en cm (50 = cube de 1 m) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Hold") float CubeHalfSize = 10.f;private: UPROPERTY(VisibleAnywhere) UProceduralMeshComponent* CubeMesh; // Fabrique le cube en C++ (même brique AppendBox qu'en Semaine 13) void BuildCube(); static void AppendBox(const FVector& Center, const FVector& HalfExtent, TArray<FVector>& Verts, TArray<int32>& Tris, TArray<FVector>& Normals, TArray<FVector2D>& UVs);};
Fichier 2 : HeldCube.cpp
#include "Public/HeldCube.h"#include "Kismet/GameplayStatics.h" // pour GetPlayerCameraManager#include "Camera/PlayerCameraManager.h"AHeldCube::AHeldCube(){ // On bouge le cube à chaque frame : Tick activé. PrimaryActorTick.bCanEverTick = true; // Même squelette qu'en Semaine 13 : un mesh racine, mais généré en C++. CubeMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("CubeMesh")); RootComponent = CubeMesh; // Un objet "tenu magiquement" ne doit pas se cogner au joueur : // on coupe la collision pour cette première version. CubeMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);}// Identique à la Semaine 13 : ajoute les 6 faces d'une boîte aux listes du mesh.void AHeldCube::AppendBox(const FVector& C, const FVector& H, TArray<FVector>& Verts, TArray<int32>& Tris, TArray<FVector>& Normals, TArray<FVector2D>& UVs){ auto AddQuad = [&](const FVector& P0, const FVector& P1, const FVector& P2, const FVector& P3, const FVector& N) { const int32 Base = Verts.Num(); Verts.Add(P0); Verts.Add(P1); Verts.Add(P2); Verts.Add(P3); for (int32 k = 0; k < 4; ++k) { Normals.Add(N); } UVs.Add(FVector2D(0, 0)); UVs.Add(FVector2D(1, 0)); UVs.Add(FVector2D(1, 1)); UVs.Add(FVector2D(0, 1)); Tris.Add(Base); Tris.Add(Base + 2); Tris.Add(Base + 1); Tris.Add(Base); Tris.Add(Base + 3); Tris.Add(Base + 2); }; const float x = H.X, y = H.Y, z = H.Z; AddQuad(C+FVector(-x,-y, z), C+FVector( x,-y, z), C+FVector( x, y, z), C+FVector(-x, y, z), FVector(0,0, 1)); AddQuad(C+FVector(-x,-y,-z), C+FVector(-x, y,-z), C+FVector( x, y,-z), C+FVector( x,-y,-z), FVector(0,0,-1)); AddQuad(C+FVector( x,-y,-z), C+FVector( x, y,-z), C+FVector( x, y, z), C+FVector( x,-y, z), FVector( 1,0,0)); AddQuad(C+FVector(-x,-y,-z), C+FVector(-x,-y, z), C+FVector(-x, y, z), C+FVector(-x, y,-z), FVector(-1,0,0)); AddQuad(C+FVector(-x, y,-z), C+FVector(-x, y, z), C+FVector( x, y, z), C+FVector( x, y,-z), FVector(0, 1,0)); AddQuad(C+FVector(-x,-y,-z), C+FVector( x,-y,-z), C+FVector( x,-y, z), C+FVector(-x,-y, z), FVector(0,-1,0));}void AHeldCube::BuildCube(){ TArray<FVector> Verts; TArray<int32> Tris; TArray<FVector> Normals; TArray<FVector2D> UVs; // Un seul cube, centré sur l'Actor. AppendBox(FVector::ZeroVector, FVector(CubeHalfSize), Verts, Tris, Normals, UVs); CubeMesh->ClearAllMeshSections(); CubeMesh->CreateMeshSection_LinearColor( 0, Verts, Tris, Normals, UVs, TArray<FLinearColor>(), TArray<FProcMeshTangent>(), false);}void AHeldCube::OnConstruction(const FTransform& Transform){ Super::OnConstruction(Transform); BuildCube(); // cube généré à la (re)construction : visible dès l'éditeur}void AHeldCube::Tick(float DeltaTime){ Super::Tick(DeltaTime); // 1. Récupérer la caméra du joueur 0. N'existe qu'au runtime (en Play), // pas dans l'éditeur : on sort proprement si elle est absente. APlayerCameraManager* Cam = UGameplayStatics::GetPlayerCameraManager(this, 0); if (!Cam) { return; } const FVector CamLoc = Cam->GetCameraLocation(); const FRotator CamRot = Cam->GetCameraRotation(); // 2. Le point cible : 1 m devant la caméra (le long du forward), // décalé un peu vers le bas pour tomber à hauteur de torse. const FVector Forward = CamRot.Vector(); // vecteur unitaire "devant" const FVector TargetLoc = CamLoc + Forward * HoldDistance + FVector::UpVector * HoldHeightOffset; // 3. Inertie : au lieu de coller le cube sur la cible, on l'y amène // progressivement. VInterpTo rapproche Current de Target d'une // fraction proportionnelle à DeltaTime * Speed (ease-out). const FVector NewLoc = FMath::VInterpTo(GetActorLocation(), TargetLoc, DeltaTime, FollowSpeed); SetActorLocation(NewLoc); // 4. La rotation suit la caméra, elle aussi avec un léger retard. const FRotator NewRot = FMath::RInterpTo(GetActorRotation(), CamRot, DeltaTime, RotationFollowSpeed); SetActorRotation(NewRot);}
Comprendre le code
Tout ce qui touche au mesh (constructeur partiel, AppendBox, BuildCube, OnConstruction) reprend exactement la brique de la Semaine 13. Trois choses sont propres à W14 : la désactivation de la collision dans le constructeur, et surtout le Tick qui lit la caméra du joueur pour calculer un point cible devant lui, puis y amène le cube avec de l'inertie.
Les trois premières lignes sont strictement identiques à AMyTestActor de la Semaine 13 : activer Tick, créer le composant procédural, en faire le RootComponent pour que les rotations/déplacements de l’acteur portent directement sur le mesh.
SetCollisionEnabled(ECollisionEnabled::NoCollision) est nouveau : on coupe complètement la collision sur le cube. Comme l’objet est “tenu magiquement” devant le joueur, on ne veut pas qu’il pousse le joueur ni qu’il rebondisse sur les murs en arrivant. Le cube traverse tout (visuel pur). Si on activait la collision, il faudrait gérer les conflits avec la CapsuleComponent du Character, et l’intégration ressort de la Période 2 deviendrait instable.
GetPlayerCameraManager(this, 0) renvoie le gestionnaire de caméra du joueur d’indice 0 (le joueur local). Le this sert à donner accès au World dans lequel l’acteur vit. Le 0 est l’index du joueur (en split-screen, il pourrait y en avoir plusieurs).
L’avantage de passer par le PlayerCameraManager plutôt que par le Character directement : notre cube reste agnostique du personnage. Peu importe le type de pawn utilisé par le projet (FirstPersonCharacter, ThirdPersonCharacter, custom), tant qu’il y a une caméra, le cube fonctionne.
Le piège : ce gestionnaire n’existe qu’en Play. Dans l’éditeur (hors Play), il vaut nullptr parce qu’aucun joueur n’est encore instancié. Le if (!Cam) return; évite un crash et laisse le cube immobile à l’origine tant qu’on n’a pas appuyé sur Play.
Tick, étape 2 : calculer le point cible (1 m devant, à hauteur de torse)
GetCameraLocation() et GetCameraRotation() donnent exactement la position et l’orientation de la caméra à cette frame, c’est-à-dire ce que voit le joueur.
CamRot.Vector() convertit un FRotator (pitch/yaw/roll en degrés) en un vecteur unitaire pointant “devant” (la direction du regard). C’est l’axe X du repère local de la caméra exprimé en coordonnées monde : si le yaw vaut 0, le vecteur retourne (1, 0, 0).
CamLoc + Forward * HoldDistance : on part de la caméra, on avance de HoldDistance cm dans la direction du regard. Avec HoldDistance = 100, ça fait 1 m devant.
+ FVector::UpVector * HoldHeightOffset : on descend ensuite le point de HoldHeightOffset cm (la valeur par défaut est négative). Comme FVector::UpVector vaut (0, 0, 1) et que +Z pointe vers le haut dans Unreal, multiplier par -20 donne un décalage vers le bas, à hauteur de torse plutôt que de yeux.
Rappel des axes UE (gaucher) : X = avant, Y = droite, Z = haut.
Tick, étape 3 : l’inertie en position avec VInterpTo
VInterpTo(Current, Target, DeltaTime, Speed) est la brique d’interpolation amortie d’Unreal. À chaque frame, elle calcule une nouvelle position entreCurrent et Target, avec un pas proportionnel à DeltaTime * Speed.
Comportement : quand le cube est loin de la cible, le pas est grand (rattrapage rapide) ; quand il s’en approche, le pas devient petit (ralentissement). C’est un ease-out : le cube n’atteint jamais la cible instantanément mais s’en approche de plus en plus lentement.
Avec FollowSpeed = 6, le cube traîne d’environ 1/6 de seconde derrière la cible : assez pour qu’on perçoive le retard, assez court pour qu’il ne décroche pas visuellement. Plus FollowSpeed est grand, plus l’inertie est faible (le cube colle).
SetActorLocation(NewLoc) applique la nouvelle position au cube via son RootComponent (donc directement sur CubeMesh).
Tick, étape 4 : l’inertie en rotation avec RInterpTo
Exactement la même chose, mais sur un FRotator. Le cube oriente progressivement ses faces dans le même sens que la caméra. Sans cette ligne, le cube garderait toujours la même orientation monde, ce qui briserait l’illusion : il faut qu’il “regarde” dans la même direction que vous pour vendre le sentiment qu’il est tenu devant vous.
RotationFollowSpeed = 8 (un peu plus rapide que FollowSpeed = 6) parce qu’un retard en rotation se voit plus qu’un retard en position : le cube semblerait “vissé au monde” si la rotation traînait trop.
Pourquoi DeltaTime est déjà dans VInterpTo
Contrairement au Tick de la Semaine 13 où on multipliait explicitement par DeltaTime, ici on passeDeltaTime à VInterpTo / RInterpTo. La multiplication se fait à l’intérieur. C’est ce qui garde l’inertie indépendante du framerate : à 30 fps comme à 120 fps, le cube met le même temps physique à rattraper sa cible (et pas un temps proportionnel au nombre de frames).
Test dans l'éditeur
Compiler (bouton Compile d’Unreal, ou Ctrl+F9 dans Rider).
Content Browser → C++ Classes → VotreProjet, glisser HeldCube dans le niveau, n’importe où.
Lancer Play. Un cube apparaît à ~1 m devant vous, légèrement sous la ligne des yeux.
Déplacez-vous, tournez la caméra : le cube vous suit, mais traîne un peu avant de se recaler. C’est l’inertie.
Dans le Details panel de l’Actor, jouez avec Follow Speed : 2 donne un cube très mou (gros retard), 20 un cube presque collé (peu d’inertie).
Lag n'est pas wobble
Augmentez FollowSpeed, baissez-le : le cube traîne plus ou moins, mais il ne dépasse jamais sa cible et ne rebondit pas. VInterpTo est un rattrapage amorti qui s'arrête net sur la cible, sans oscillation. Or le ressenti "gravity gun" a un vrai ballottement : l'objet dépasse, revient, oscille une ou deux fois. Ce comportement-là demande de la vraie physique : un ressort. C'est tout l'objet de la Période 2.
On imagine le cube relié au point cible par un ressort, et plongé dans un fluide qui freine son mouvement (amortisseur). Deux forces s’appliquent à chaque instant :
Le ressort tire le cube vers la cible, avec une force proportionnelle à l’écart : F_ressort = Raideur * (Cible - Position). Plus le cube est loin, plus ça tire fort.
L’amortisseur freine le cube, avec une force opposée à sa vitesse : F_amort = -Amortissement * Vitesse. C’est ce qui finit par calmer l’oscillation.
En prenant une masse de 1, l’accélération est simplement la somme des deux :
C’est un oscillateur harmonique amorti, exactement la physique d’une masse au bout d’un ressort. Le comportement dépend du rapport entre Raideur et Amortissement :
Beaucoup d’amortissement (par rapport à la raideur) : le cube rejoint la cible sans la dépasser. C’est le régime “sur-amorti”, proche de VInterpTo. Pas de wobble.
Peu d’amortissement : le cube dépasse la cible, revient, dépasse de l’autre côté, et oscille en s’atténuant. Régime “sous-amorti” : c’est le wobble.
Régler Raideur et Amortissement, c’est donc choisir à quel point le cube est nerveux et combien de fois il rebondit. C’est l’outil de base de tout mouvement secondaire.
Le raccourci de production : Unreal fournit déjà ce calcul tout fait, via UKismetMathLibrary::VectorSpringInterp (avec un état FVectorSpringState). On l’écrit ici à la main parce que le but est de comprendre la physique sous le capot. Une fois que vous l’avez vue, utiliser la version moteur en production est trivial (c’est le Défi 3).
HeldCube.h : ajouts
On remplace l'inertie simple par le ressort. On garde HoldDistance, HoldHeightOffset, RotationFollowSpeed, et on ajoute :
// Raideur du ressort : plus grand = rattrape plus vite et plus fortUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring")float Stiffness = 200.f;// Amortissement : plus grand = moins de rebond. Trop bas = ça oscille longtemps.UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring")float Damping = 12.f;private: // Vitesse courante du cube, intégrée d'une frame à l'autre. FVector CubeVelocity = FVector::ZeroVector;
CubeVelocity est une variable d’état : contrairement à VInterpTo qui ne regarde que la position, le ressort a besoin de mémoriser la vitesse du cube entre les frames. Pas besoin de UPROPERTY (usage interne, pas un pointeur UObject).
HeldCube.cpp : le Tick version ressort
void AHeldCube::Tick(float DeltaTime){ Super::Tick(DeltaTime); APlayerCameraManager* Cam = UGameplayStatics::GetPlayerCameraManager(this, 0); if (!Cam) { return; } const FRotator CamRot = Cam->GetCameraRotation(); // 1. Point cible : 1 m devant la caméra, à hauteur de torse (inchangé) const FVector TargetLoc = Cam->GetCameraLocation() + CamRot.Vector() * HoldDistance + FVector::UpVector * HoldHeightOffset; // 2. Ressort-amortisseur (masse = 1) const FVector Current = GetActorLocation(); const FVector Displacement = TargetLoc - Current; // écart au repos const FVector Accel = Displacement * Stiffness - CubeVelocity * Damping; // 3. Intégration d'Euler : vitesse d'abord, puis position CubeVelocity += Accel * DeltaTime; SetActorLocation(Current + CubeVelocity * DeltaTime); // 4. La rotation suit toujours avec un simple lag (pas de ressort ici) SetActorRotation( FMath::RInterpTo(GetActorRotation(), CamRot, DeltaTime, RotationFollowSpeed));}
Comprendre ce code
Le Tick de Période 2 remplace VInterpTo par un ressort manuel : à chaque frame, on calcule une accélération à partir de l'écart cube/cible, on l'intègre en vitesse, puis en position. Le bob vertical, ajouté juste après, module la cible elle-même selon la vitesse du regard, et c'est le ressort qui se charge de lisser le tout. La caméra et le TargetLoc se calculent exactement comme en Période 1.
C’est la traduction directe de la formule physique a = k·(cible - pos) - c·v (vue dans la section théorique) en code.
Displacement = TargetLoc - Current : le vecteur d’écart entre la cible et le cube, en cm. Si le cube est à la cible, Displacement vaut zéro et la force de ressort s’annule.
Displacement * Stiffness : la force de rappel du ressort (loi de Hooke linéaire). Plus l’écart est grand, plus la force est grande. Avec Stiffness = 200, un écart de 1 m (100 cm) génère une accélération de 100 * 200 = 20 000 cm/s².
CubeVelocity * Damping : la force de freinage de l’amortisseur, proportionnelle à la vitesse, opposée à elle (d’où le moins). Sans ce terme, le cube oscillerait éternellement autour de la cible (mouvement perpétuel d’un ressort idéal).
La somme des deux donne l’accélération totale à appliquer cette frame. On suppose une masse de 1, donc F = m·a se simplifie en a = F.
C’est la méthode d’intégration numérique la plus simple. On a une accélération, on veut une position : il faut passer par la vitesse.
CubeVelocity += Accel * DeltaTime : on met à jour la vitesse. DeltaTime est le temps écoulé depuis la frame précédente (en secondes), donc Accel * DeltaTime est le changement de vitesse sur cet intervalle.
Current + CubeVelocity * DeltaTime : on met à jour la position avec la nouvelle vitesse. Même principe : vitesse * temps = distance parcourue.
CubeVelocity est mémorisée comme membre de classe (déclarée dans le .h), donc elle persiste d’une frame à l’autre. C’est ce qui fait toute la différence avec VInterpTo : le cube a une mémoire de mouvement. Il peut donc dépasser la cible (parce qu’il arrive lancé) et osciller (parce que la force le ramène, mais qu’il repart de l’autre côté).
On met à jour la vitesse en premier, puis on l’utilise pour la position. C’est la convention standard, plus stable numériquement que l’ordre inverse.
Volontairement inchangée par rapport à la Période 1. Le ressort apporte son ballottement sur la position (c’est là que le mouvement secondaire est le plus visible) ; la rotation reste un simple lag. Mettre un ressort sur la rotation aussi est un excellent défi, mais ça multiplie les paramètres à régler et complique le débogage.
Le bob, étape 1 : mesurer la vitesse du point cible
On veut un bob qui ne s’active que quand le joueur bouge ou tourne. On mesure donc à quelle vitesse la cible (TargetLoc) se déplace d’une frame à l’autre.
(TargetLoc - PrevTargetLoc).Size() : la distance parcourue par la cible depuis la frame précédente, en cm. Size() calcule la norme du vecteur (sa longueur).
/ DeltaTime : on divise par le temps écoulé pour obtenir une vitesse en cm/s, indépendante du framerate.
FMath::Max(DeltaTime, KINDA_SMALL_NUMBER) : protection contre la division par zéro. KINDA_SMALL_NUMBER est une constante Unreal valant environ 1e-4. Si DeltaTime est nul ou minuscule (cas pathologique, comme un breakpoint qui bloque la frame), on évite le NaN ou l’infini qui propagerait du grain dans tout le code derrière.
PrevTargetLoc = TargetLoc : on mémorise la cible courante pour la prochaine frame. PrevTargetLoc est un membre de classe.
Le bob, étape 2 : lisser la vitesse pour éviter les sauts d’amplitude
FInterpTo est la version scalaire de VInterpTo (F pour float). Même principe : on rapproche SmoothedSpeed de TargetSpeed d’un pas proportionnel à DeltaTime * 5.f.
Pourquoi lisser ? TargetSpeed brut est très bruité d’une frame à l’autre : à 60 fps, un petit mouvement de souris fait bondir la valeur. Sans lissage, l’amplitude du bob sauterait visiblement de 0 à plein régime puis inversement, ce qui se verrait à l’écran. Le lissage donne une rampe douce : quand on commence à tourner, l’amplitude monte progressivement ; quand on s’arrête, elle redescend progressivement.
Le bob, étape 3 : oscillation sin modulée par la vitesse
BobTime += DeltaTime * BobFrequency : on fait avancer la phase de l’oscillation. Avec BobFrequency = 9.f, la phase avance de 9 radians par seconde (environ 1,4 cycle complet par seconde). Comme BobTime est un membre de classe, elle s’accumule entre les frames : la phase n’est jamais remise à zéro.
SpeedFactor = Clamp(SmoothedSpeed / BobSpeedRef, 0.f, 1.f) : on normalise la vitesse lissée entre 0 et 1, avec saturation. Avec BobSpeedRef = 300.f, à 300 cm/s SpeedFactor vaut 1 (amplitude max) ; à 150 cm/s il vaut 0,5 ; à l’arrêt il vaut 0.
Sin(BobTime) * BobAmplitude * SpeedFactor : l’oscillation finale. Sin(BobTime) oscille entre -1 et 1, BobAmplitude (12 cm) la met à l’échelle, et SpeedFactormodule l’amplitude par la vitesse : à l’arrêt le bob est nul, à pleine vitesse il vaut ±12 cm.
C’est exactement le principe du head bob ou du weapon sway des FPS : un sin dont l’amplitude suit l’activité du joueur.
Le bob, étape 4 : décaler la cible et laisser le ressort lisser
const FVector BobbedTarget = TargetLoc + FVector::UpVector * Bob;// Puis le ressort travaille sur BobbedTarget au lieu de TargetLocconst FVector Displacement = BobbedTarget - Current;
L’astuce élégante : au lieu d’ajouter le bob à la position finale du cube, on l’injecte dans la cible que le ressort doit poursuivre. Trois conséquences :
Le ressort vise une cible qui ondule verticalement, et le cube suit avec son inertie : si le bob change vite, le ressort lisse naturellement les hautes fréquences.
Pas de double couche d’animation à débugger : tout passe par le même mécanisme (ressort sur une cible).
À l’arrêt, SpeedFactor = 0 donc Bob = 0, et BobbedTarget égale TargetLoc : on retrouve le comportement de Période 2 sans bob.
L’envoi au cube se fait ensuite avec le même code de ressort qu’avant (Accel, CubeVelocity += ..., SetActorLocation(...)), mais avec BobbedTarget à la place de TargetLoc.
Lancez Play et tournez vite la caméra : le cube part en retard, dépasse, revient, oscille une ou deux fois avant de se stabiliser. C’est le wobble. Baissez Damping à 4 : il oscille beaucoup plus longtemps (trop “gélatineux”). Montez-le à 30 : presque pas de rebond (retour à un comportement type VInterpTo). Montez Stiffness : le cube devient plus nerveux et colle mieux.
Le bob vertical : tourner fait onduler de haut en bas
Le ressort produit déjà un ballottement, mais surtout dans la direction du mouvement. Quand vous tournez à gauche/droite (yaw), la cible se décale horizontalement, donc le wobble est surtout horizontal. Pour obtenir le "haut/bas quand on tourne" demandé, on ajoute un bob vertical piloté par la vitesse : plus le point cible bouge vite (parce que vous tournez ou avancez vite), plus le cube ondule verticalement. C'est exactement le principe du head bob ou du weapon sway des FPS.
On mesure la vitesse du point cible (combien il se déplace par seconde), on la lisse pour éviter les à-coups, et on s’en sert pour moduler l’amplitude d’une oscillation sin (le sin vu en Semaine 13, Défi 2).
HeldCube.h, ajouts :
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Bob")float BobAmplitude = 12.f; // hauteur max du bob, en cmUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Bob")float BobFrequency = 9.f; // vitesse de l'oscillationUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Bob")float BobSpeedRef = 300.f; // vitesse cible (cm/s) pour un bob à pleine amplitudeprivate: FVector CubeVelocity = FVector::ZeroVector; FVector PrevTargetLoc = FVector::ZeroVector; // cible de la frame précédente float SmoothedSpeed = 0.f; // vitesse cible lissée float BobTime = 0.f; // phase de l'oscillation
Dans le Tick, juste après avoir calculé TargetLoc et avant le ressort, on calcule le bob et on l’ajoute à la cible (ainsi le ressort lisse aussi le bob) :
// Vitesse du point cible : grand quand on tourne/avance vite const float TargetSpeed = (TargetLoc - PrevTargetLoc).Size() / FMath::Max(DeltaTime, KINDA_SMALL_NUMBER); PrevTargetLoc = TargetLoc; // On lisse pour éviter les sauts d'amplitude SmoothedSpeed = FMath::FInterpTo(SmoothedSpeed, TargetSpeed, DeltaTime, 5.f); // Amplitude proportionnelle à la vitesse (0 à l'arrêt, max au-delà de BobSpeedRef) BobTime += DeltaTime * BobFrequency; const float SpeedFactor = FMath::Clamp(SmoothedSpeed / BobSpeedRef, 0.f, 1.f); const float Bob = FMath::Sin(BobTime) * BobAmplitude * SpeedFactor; // On décale la cible verticalement du bob const FVector BobbedTarget = TargetLoc + FVector::UpVector * Bob;
Puis on fait travailler le ressort sur BobbedTarget au lieu de TargetLoc :
Maintenant, à l’arrêt le cube est calme (amplitude 0). Dès que vous tournez ou marchez, il se met à onduler de haut en bas, d’autant plus fort que vous bougez vite, et le ressort adoucit le tout. Combiné au dépassement du ressort, on obtient le ressenti complet : inertie, ballottement, et bob vertical à la rotation.
Stabilité de l’intégration d’Euler : si vous montez Stiffness très haut sans monter Damping, le cube peut “exploser” (partir à l’infini). C’est une limite connue de l’intégration d’Euler explicite : quand Stiffness * DeltaTime devient trop grand, chaque pas dépasse la cible de plus en plus, et ça diverge. En pratique, gardez des valeurs raisonnables (Stiffness quelques centaines, Damping autour de 10-20), ou utilisez la version moteur (VectorSpringInterp) qui gère ça proprement.
Résultat attendu
Un cube unique flotte à 1 m devant vous, à hauteur de torse. Quand vous restez immobile, il est calme. Quand vous marchez, il traîne légèrement (inertie). Quand vous tournez, il part en retard, dépasse, oscille, et ondule de haut en bas le temps que vous tourniez. Vous avez simulé du mouvement secondaire entièrement en C++, à partir d'une force de ressort et d'une intégration manuelle, sans aucun système d'animation.
Le cube ne bouge pas du tout : vous regardez la scène hors Play. GetPlayerCameraManager vaut nullptr dans l’éditeur ; il faut lancer Play pour qu’il existe.
Crash au lancement :Cam non vérifié avant usage. Toujours if (!Cam) return;.
Le cube est collé à la caméra, sans inertie : vous faites SetActorLocation(TargetLoc) directement au lieu de passer par VInterpTo ou le ressort.
Le cube part à l’infini / disparaît :Stiffness trop élevé par rapport à Damping, l’intégration d’Euler diverge. Baissez la raideur ou montez l’amortissement.
Aucun rebond, même avec le ressort :Damping trop élevé (régime sur-amorti). Baissez-le pour voir l’oscillation.
Le cube est invisible ou creux (on voit à travers) : géométrie non générée (vérifiez l’appel à BuildCube), ou winding des faces inversé (voir la note AppendBox de la Semaine 13).
Le mouvement dépend du framerate : un DeltaTime a été oublié dans l’intégration.
Le bob saute brutalement :SmoothedSpeed non lissé (on utilise TargetSpeed brut), ou DeltaTime non protégé par FMath::Max(..., KINDA_SMALL_NUMBER).
Relire votre Tick du Défi 2 de la Semaine 13 (oscillation sin) et comparer avec le bob d’ici
Défi 1 - Pencher dans les virages (banking)
Quand on tourne vite, un objet tenu a tendance à pencher dans le virage. Ajoutez un Roll au cube proportionnel à la vitesse de rotation horizontale de la caméra (la différence de Yaw entre deux frames). C'est du mouvement secondaire de rotation, complémentaire du bob de position.
Défi 2 - Colorer selon la vitesse (Dynamic Material Instance)
Réutilisez la technique du Défi 3 de la Semaine 13 : créez un Material avec un paramètre BaseColor, un Dynamic Material Instance dans BeginPlay, et faites varier la couleur du cube selon SmoothedSpeed (calme = bleu, rapide = orange) avec un FMath::Lerp.
Défi 3 (Avancé) - Passer au ressort du moteur
Remplacez votre intégration d'Euler manuelle par UKismetMathLibrary::VectorSpringInterp, en gardant un membre FVectorSpringState. Comparez le ressenti et la stabilité aux valeurs extrêmes (Stiffness très haut) : la version moteur ne diverge pas. C'est le pont entre "je comprends la physique" et "j'utilise l'outil de production".
// BeginPlay (à déclarer/override comme en S13)void AHeldCube::BeginPlay(){ Super::BeginPlay(); if (UMaterialInterface* Base = CubeMesh->GetMaterial(0)) { DynMat = UMaterialInstanceDynamic::Create(Base, this); CubeMesh->SetMaterial(0, DynMat); }}// fin de Tickif (DynMat){ const float T = FMath::Clamp(SmoothedSpeed / BobSpeedRef, 0.f, 1.f); DynMat->SetVectorParameterValue(TEXT("BaseColor"), FMath::Lerp(SlowColor, FastColor, T));}
Assignez d’abord votre Material au slot Element 0 du Procedural Mesh (Details panel ou SetMaterial(0, ...)), et il doit exposer un paramètre vectoriel BaseColor, sinon l’appel est ignoré silencieusement (même mécanisme qu’en Semaine 13).
// dans Tick, en remplacement de l'intégration manuelle :const FVector NewLoc = UKismetMathLibrary::VectorSpringInterp( GetActorLocation(), // Current BobbedTarget, // Target SpringState, // état du ressort (vitesse interne mémorisée) Stiffness, // Stiffness Damping, // CriticalDampingFactor (1 = critique, <1 = wobble) DeltaTime, 1.f, // Mass 0.f); // TargetVelocityAmountSetActorLocation(NewLoc);
La version moteur encapsule l’état (SpringState) et reste stable même à raideur élevée. Notez que CriticalDampingFactor y est normalisé (1 = amortissement critique, sans dépassement ; en dessous de 1 = wobble), une paramétrisation plus pratique que le Damping brut de notre version manuelle.