Les connaissances ne devraient pas être brevetées, elles sont faites pour être partagées

Karim Aktouf

Dernière mise à jour: 2026-03-19

# Où l’on apprendra le C++ standard 20

prendra C++20

Banque de cours - Cours 1 - Intro

# Cours 1 - Intro

Historique

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 original
C++11  →  Le "C++ moderne" commence (auto, lambdas, smart pointers)
C++14  →  Ameliorations
C++17  →  std::optional, structured bindings
C++20  →  Concepts, ranges, coroutines, modules
C++23  →  std::print, std::expected, plus de ranges

https://fr.wikipedia.org/wiki/C%2B%2B

Installation et configuration

Installer l’IDE

CLion (recommandé)

  1. Aller sur https://jetbrains.com/clion/download et téléchargez CLion. C’est gratuit.
  2. Installer via l’exécution de Clion-yyyy.x.x.exe dans Downloads
  3. Dans la fenêtre de CLion Setup intitulée “Installation Setup”, cochez tout:

  1. Démarrez CLion.
  2. Dans Settings → Build, Execution, Deployment → Toolchains, choisissez MinGW (default).
  3. Cliquez sur Next, puis sur Finish. C’est tout!

Démarrez CLion. Choisir WinGW comme toolchain (surtout pas VScode!):

☝🏼

Dans CLion, ajoutez le toolchain MinGW (Default) dans Settings → Build, Execution, Deployments → Toolchains

Configuer CMake (CMake est déjà bundled dans CLion)

Ou alors si vous voulez vivre dangereusement, Visual Studio Code (pas recommandé, compliqué)

1.1 — Télécharger VS Code

  1. Rendez-vous sur https://code.visualstudio.com
  2. Cliquez sur le bouton “Download for Windows” (l’installeur détecte automatiquement votre architecture x64)
  3. 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)

  1. Ouvrez VS Code

  2. Appuyez sur Ctrl+Shift+P pour ouvrir la palette de commandes

  3. Tapez Configure Display Language

  4. 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.

2.1 — Télécharger les Build Tools

  1. Rendez-vous sur https://visualstudio.microsoft.com/downloads/
  2. Descendez jusqu’à la section “Outils pour Visual Studio” (ou “Tools for Visual Studio”)
  3. 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/Downloads
winget 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 :

  1. Dans l’onglet “Charges de travail” (Workloads), cochez :
    • Développement Desktop en C++ (“Desktop development with C++”)
  2. 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
  3. Cliquez sur “Installer” et patientez (environ 4 à 8 Go de téléchargement)
  4. Redémarrez votre ordi

2.3 — Vérifier l’installation

  1. Ouvrez le menu Démarrer
  2. Cherchez “Developer Command Prompt for VS 2026”
  3. 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.

3.1 — Télécharger CMake

  1. Rendez-vous sur https://cmake.org/download/
  2. Téléchargez le Windows x64 Installer (.msi) — version 4.2.3 ou plus récente
  3. 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ÉditeurRôle
C/C++MicrosoftIntelliSense, débogage, navigation dans le code
C/C++ Extension PackMicrosoftPack complet (inclut C/C++ Themes et autres)
CMake ToolsMicrosoftIntégration CMake dans VS Code (build, configure, etc.)

4.2 — Extensions recommandées

ExtensionÉditeurRôle
Code RunnerJun HanExécuter rapidement un fichier C++ avec un raccourci
Error LensAlexanderAffiche les erreurs directement dans le code, pas seulement dans le panneau
GitLensGitKrakenPour l’intégration Git (fortement recommandé)

Créer votre premier projet C++

5.1 — Créer la structure du projet

  1. Créez un dossier sur votre disque, par exemple : C:\Projets\MonPremierCpp
  2. Ouvrez VS Code
  3. Fichier > Ouvrir le dossier... → sélectionnez MonPremierCpp
  4. 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++20
set(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 :

#include <iostream>
 
int main() {
    std::cout << "Hello World" << std::endl;
    return 0;
}

5.3 — Configurer le projet avec CMake Tools

  1. Appuyez sur Ctrl+Shift+P (Open Command Palette : View → Command Palette)
  2. Tapez “CMake: Quick Start” ou “CMake: Configure”
  3. VS Code vous demande de choisir un kit (compilateur). Sélectionnez :
    • Visual Studio Build Tools 2026 Release - amd64 (attention, ne pas choisir amd64_x86 !)
    • C’est le compilateur MSVC installé à l’étape 2
  4. La configuration se lance automatiquement. Vous verrez les messages CMake dans le panneau Output.

5.4 — Compiler et exécuter

  • Compiler : Cliquez sur le bouton “Build” dans la barre d’état en bas à gauche, ou appuyez sur F7
  • Exécuter sans débogage : Appuyez sur Ctrl+F5 (ou Run → Run Without Debugging)

☝🏼

Si on vous demande quel debugger utiliser, choisissez C++ (Windows)

☝🏼

Si on vous dit que .vscode/launch.json n’existe pas, voir Étape 6

Configurer le débogueur

6.1 — Créer la configuration de lancement

  1. Allez dans Run > Add a Configuration...
  2. Sélectionnez “C++ (Windows)”
  3. Un fichier .vscode/launch.json est créé. Remplacez son contenu par :
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug C++ (MSVC)",
            "type": "cppvsdbg",
            "request": "launch",
            "program": "${command:cmake.launchTargetPath}",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "console": "integratedTerminal"
        }
    ]
}

6.2 — Utiliser le débogueur

  1. Placez un point d’arrêt en cliquant dans la marge gauche d’une ligne de code (un point rouge apparaît)
  2. Appuyez sur F5 pour lancer le débogueur
  3. L’exécution s’arrête au point d’arrêt — vous pouvez inspecter les variables, avancer pas à pas (F10), entrer dans les fonctions (F11), etc.

6.3 Configurer les paramètres VS Code pour C++

Créez ou modifiez le fichier .vscode/settings.json dans votre projet :

{
    "C_Cpp.default.cppStandard": "c++20",
    "C_Cpp.default.compilerPath": "cl.exe",
    "cmake.configureOnOpen": true,
    "cmake.buildDirectory": "${workspaceFolder}/build",
    "editor.formatOnSave": true,
    "C_Cpp.clang_format_fallbackStyle": "Google",
    "files.associations": {
        "*.h": "cpp",
        "*.hpp": "cpp"
    }
}

6.4 Configurer l’Auto Save

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

ActionRaccourci
Palette de commandesCtrl+Shift+P
Compiler (Build)F7
Exécuter sans débogageCtrl+F5
DéboguerF5
Aller à la définitionF12
Voir les référencesShift+F12
Renommer un symboleF2
Formater le fichierShift+Alt+F
Terminal intégré`Ctrl+“
Chercher dans les fichiersCtrl+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

  1. Ouvrez Visual Studio

  2. Créez un nouveau projet “Console App”

  3. Remplacez le code généré par le code ci-dessus

  4. Appuyez sur Ctrl+F5 pour compiler et exécuter

  5. Vous devriez voir : The answer to life, the universe, and everything is 42.

Types fondamentaux et variables

Types entiers

TypeTaille typiquePlage de valeurs
short2 octets-32 768 à 32 767
int4 octets-2 147 483 648 à 2 147 483 647
long4 ou 8 octetsAu moins identique à int
long long8 octets±9,2 × 10^18
unsigned int4 octets0 à 4 294 967 295

Types à virgule flottante

TypeTaillePrécision
float4 octets~7 chiffres
double8 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

TypeTailleUtilisation
char1 octetCaractère unique (‘A’, ‘7’, ’!’)
bool1 octettrue ou false
autovariableLe 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 apples
int orange_count {5};                            // Number of oranges
int 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ée
unsigned a {};     // sûr : initialisé à zéro (C++11, accolades vides)
unsigned a = 0;    // sûr : initialisation explicite classique
unsigned a{0};     // sûr : initialisation directe par liste
unsigned 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-style
int 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 initialization
constexpr 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 types
import 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 screen
import 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 15
The value of orange_count is 5
The 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 :

  1. Demande à l’utilisateur son nom et son âge
  2. Calcule son année de naissance
  3. 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 :

  1. Demande à l’utilisateur son nom et son âge
  2. Calcule son année de naissance
  3. 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;
}
Lien vers l'original

Banque de cours - Cours 2 - Opérateurs, expressions et types

# Cours 2 - Opérateurs, expressions et types

Ze veux de la doc !

Mon site préféré pour apprendre: learncpp.com https://learncpp.com

Mon préféré, CPP Reference (cppreference.com) quick, easy, rapide, facile, to the point, vers le point CPP Reference

Par exemple, le prof il a fait rien qu’à m’embêter avec la function now() de systrem_clock de chrono de std: https://en.cppreference.com/w/cpp/chrono/system_clock/now

Opérateurs arithmétiques

OpérateurOpérationExempleRésultat
+Addition7 + 310
-Soustraction7 - 34
*Multiplication7 * 321
/Division7 / 32 (entier !)
%Modulo (reste)7 % 31

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 clair
int 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ÉquivalentExemple
+=x = x + nhealth += 10;
-=x = x - nhealth -= damage;
*=x = x * nscore *= multiplier;
/=x = x / nspeed /= 2;
%=x = x % nangle %= 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;
}

Conversions de types

Casts explicites : static_cast<> Quand vous voulez convertir intentionnellement, utilisez static_cast<> :

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 data
int 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 fish
import 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 :

  1. Demande à l’utilisateur une température en Celsius
  2. Convertit en Fahrenheit : F = C * 9/5 + 32
  3. Convertit en Kelvin : K = C + 273.15
  4. 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 :

  1. Lit deux nombres (autorisez les entrées décimales)
  2. Effectue +, -, *, / sur ces nombres
  3. Affiche les résultats pour toutes les opérations
  4. 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 celsius {};
  std::print("Entrez une température en Celsius : ");
  std::cin >> celsius;
 
  const double fahrenheit {celsius * 9.0 / 5.0 + 32.0};
  const double kelvin {celsius + 273.15};
 
  std::println("Celsius :    {:.1f} °C", celsius);
  std::println("Fahrenheit : {:.1f} °F", fahrenheit);
  std::println("Kelvin :     {:.1f} K", kelvin);
}

Solution de l'exercice 3 : Calculatrice mixte

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 hacking
i  = 0x5f3759df - ( i >> 1 );   // what the fuck?
y  = * ( float * ) &i;
y  = y * ( threehalfs - ( x2 * y * y ) );  // 1st iteration of Newton's method
 
return 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 up
volatile 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 :

  1. Demande à l’utilisateur une température en Celsius
  2. Convertit en Fahrenheit : F = C * 9/5 + 32
  3. Convertit en Kelvin : K = C + 273.15
  4. Affiche les trois valeurs formatées à 1 décimale

Indice : Utilisez std::cout << std::format("Temperature: {:.1f}", value) << std::endl; pour 1 décimale.

Exercice 3 : Calculatrice mixte
Construisez une calculatrice qui :

  1. Lit deux nombres (autorisez les entrées décimales)
  2. Effectue +, , , / sur ces nombres
  3. Affiche les résultats pour toutes les opérations
  4. 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

#include <iostream>
#include <format>
int main()
{
  double celsius {};
  std::cout << std::format("Entrez une température en Celsius : ");
  std::cin >> celsius;
 
  const double fahrenheit {celsius * 9.0 / 5.0 + 32.0};
  const double kelvin {celsius + 273.15};
 
  std::cout << std::format("Celsius :    {:.1f} °C", celsius) << std::endl;
  std::cout << std::format("Fahrenheit : {:.1f} °F", fahrenheit) << std::endl;
  std::cout << std::format("Kelvin :     {:.1f} K", kelvin) << std::endl;
}

Solution de l’exercice 3 : Calculatrice mixte

#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;
}
Lien vers l'original

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 :

  1. remaining devrait être double, ou utilisez static_cast<int>(health - damage) intentionnellement
  2. Il manque le std:: devant le cout et devant le endl
  3. Il manque << entre "Health: " et health
  4. Utilisez double x = 10 / 3.0; si vous voulez le résultat décimal
  5. Supprimez const ou ne réassignez pas — const signifie constant !

Prise de décisions

Opérateurs de comparaison

OpérateurSignificationExemple
==Égal àx == 5
!=Différent dex != 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érateurSignificationExemple
&&ANDx > 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 sort
if (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 vraie
if (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,

switch (choice) {
  case 1:
    std::cout << std::format("You chose option 1") << std::endl;
    break;        // Don't forget break!
  case 2:
    std::cout << std::format("You chose option 2") << std::endl;
    break;
  default:
    std::cout << std::format("Invalid choice") << std::endl;
    break;
}
int main()
{
    int choice = 1;
 
    switch (choice)
    {
    case 1:
        std::cout << std::format("You chose option 1") << std::endl;
        choice++;
        // j'ai oublié le break ici !
    case 2:
        std::cout << std::format("You chose option 2") << std::endl;
        break;
    default:
        std::cout << std::format("Invalid choice") << std::endl;
        break;
        
    choice = 1;
        
        // Ici, est-ce que les deux std::cout vont être affichées ?
    if (choice == 1)
    {
        std::cout << std::format("You chose option 1") << std::endl;
        choice++;
    }
    else if (choice == 2)
        std::cout << std::format("You chose option 2") << std::endl;
    else
        std::cout << std::format("Invalid choice") << std::endl;
    }

Opérateur ternaire

// condition ? value_if_true : value_if_false
int max = (a > b) ? a : b;
std::string status = (health > 0) ? "Alive" : "Dead";

Comparaison tripartite (opérateur Spaceship C++20)

// Three-way comparison of integers
#include <iostream>
#include <format>
#include <compare>
int main()
{
  std::cout << std::format("Please enter a number: ");
  int value;
  std::cin >> value;
 
  std::strong_ordering ordering{ value <=> 0 };
 
  std::cout << std::format("value < 0: {}", ordering == std::strong_ordering::less) << std::endl;
  std::cout << std::format("value > 0: {}", ordering == std::strong_ordering::greater) << std::endl;
  std::cout << std::format("value == 0: {}", ordering == std::strong_ordering::equal) << std::endl;
}

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 custom
struct 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.

int count {1};
while (count <= 10)
{
    std::cout << std::format("{}", count) << std::endl;
    ++count;
}

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 loop
for (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 premiers
for (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 |.

Avant les ranges (style STL classique) :

std::vector<int> nums = {1, 2, 3, 4, 5, 6};
std::vector<int> pairs;
std::vector<int> resultat;
 
// Étape 1 : filtrer → crée un vecteur intermédiaire
std::copy_if(nums.begin(), nums.end(), std::back_inserter(pairs),
    [](int n) { return n % 2 == 0; });
 
// Étape 2 : transformer → crée un autre vecteur intermédiaire
std::transform(pairs.begin(), pairs.end(), std::back_inserter(resultat),
    [](int n) { return n * 3; });

Chaque étape alloue de la mémoire et parcourt toute la collection. Avec un million d’éléments, c’est deux allocations et deux parcours complets.

Avec les ranges :

#include <ranges>
 
std::vector<int> nums = {1, 2, 3, 4, 5, 6};
 
auto vue = nums
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * 3; });

À 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éments
for (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 count1
double count2{3.14};       // Global count2
int count3;                // Global count3 - default initialization
 
int 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 à ceci
if (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;
}
Lien vers l'original

Banque de cours - Cours 4 - Fonctions

# Cours 4 - Fonctions

Où est Charlie le Bug ?

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 :

  1. Supprimer le ; après while (i < 10)( Sinon ça provoque une boucle infinie )
  2. Remplacer = par == : if (x == 5)( = is assignment, == is comparison )
  3. Ajouter des accolades : for (...) { cout << j; cout << " done"; }( only first line is in the loop (misleading indent )
  4. Ajouter break; après doSomething(); ( missing break — falls through to case 2 )
  5. 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 call
int 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() yet
int 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 body
 
int 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);    // OK
double compute(double x);    // error: cannot overload on return type alone
double compute(int x);          // overload 1
double compute(double x);       // overload 2
double compute(int x, int y);   // overload 3
 

Valeurs de paramètres par défaut

void greet(std::string name, std::string greeting = "Hello")
{
    std::cout << std::format("{}, {}!", greeting, name) << std::endl;
}
 
greet("Alice");           // "Hello, Alice!"
greet("Bob", "Bonjour"); // "Bonjour, Bob!"

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 prototype
 
int 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 it
double 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 = 15
After 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 valeurTypes simples (int, double, bool) que vous n’avez pas besoin de modifiervoid print(int x)
Passage par référence &Vous devez modifier l’originalvoid swap(int& a, int& b)
Passage par const ref const&Types volumineux que vous avez seulement besoin de lirevoid 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 :

FichierRôle
main.cppContient main() et la logique principale
colors.hHeader file — contient les déclarations (prototypes) des fonctions
colors.cppSource 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 :

int r1 = 220, g1 = 60, b1 = 10;      // Rouge Soleil
int r2 = 10,  g2 = 80, b2 = 210;     // Bleu Océan

Rien ne lie r1, g1 et b1 entre eux. Si on ajoute un nom, ça fait 4 variables par couleur.
Les fonctions deviennent lourdes :

void mixColors(int r1, int g1, int b1,
               int r2, int g2, int b2,
               int& outR, int& outG, int& outB);   // 9 paramètres !

La solution : struct Une struct regroupe des données qui vont ensemble dans un seul type :

struct Color {
    std::string name;
    int r, g, b;
};

On crée ensuite une variable de type Color comme n’importe quelle autre variable :

Color rougeSoleil{"Rouge Soleil", 220, 60, 10};
Color bleuOcean{"Bleu Océan", 10, 80, 210};

On accède aux membres avec le . :

std::cout << rougeSoleil.name;   // "Rouge Soleil"
std::cout << rougeSoleil.r;      // 220

Avant / Après

Sans structAvec struct
int r1, g1, b1;Color soleil;
4 variables par couleur1 variable qui contient tout
mixColors(r1, g1, b1, r2, g2, b2, outR, outG, outB)mixColors(soleil, ocean)
9 paramètres2 paramètres

En résumé

  • 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

  1. Créez un nouveau dossier appelé ColorLab.
  2. Ouvrez VSCode et “Open folder” de ColorLab
  3. À 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

Votre arborescence devrait ressembler à ceci :

ColorLab/
├── main.cpp
├── colors.h
└── colors.cpp
└── CMakeLists.txt

2 - Configurez le fichier CMakeLists.txt

cmake_minimum_required(VERSION 4.0)
project(colorlab LANGUAGES CXX)
 
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
add_executable(colorlab main.cpp colors.cpp colors.h)
 
if(MSVC)
    target_compile_options(colorlab PRIVATE /utf-8)
elseif(MINGW)
    target_compile_options(colorlab PRIVATE -pedantic -finput-charset=UTF-8 -fexec-charset=UTF-8)
    target_link_options(colorlab PRIVATE -static)
endif()

☝🏼

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 :

  1. La première fois que le compiler voit #include "colors.h", COLORS_H n’est pas encore défini.
  2. Donc il le définit (#define COLORS_H) et traite tout ce qui se trouve à l’intérieur.
  3. 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 _WIN32
void 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);
}
#endif
 
 
int 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

Lien vers l'original

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);     // Declaration
int 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-left
 
void 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 5
scores[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és std::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);
}
 

Tableaux dynamiques : std::vector & std::string

std::vector : Tableaux dynamiques

#include <iostream>
#include <format>
#include <vector>
 
int main()
{
    std::vector<int> scores;          // Empty vector
    scores.push_back(90);             // Add elements
    scores.push_back(85);
    scores.push_back(78);
 
    std::cout << std::format("Size: {}", scores.size()) << std::endl;       // 3
    std::cout << std::format("First: {}", scores[0]) << std::endl;          // 90
    std::cout << std::format("Last: {}", scores.back()) << std::endl;       // 78
 
    scores.pop_back();                              // Remove last element
    std::cout << std::format("After pop, size: {}", scores.size()) << std::endl; // 2
 
    // Iterate
    for (int s : scores)
        std::cout << std::format("Score: {}", s) << std::endl;
}

**Opérations clés de ****std::vector**

MéthodeDescription
push_back(x)Ajouter un élément à la fin
pop_back()Supprimer le dernier élément
size()Nombre d’éléments
empty()Vrai si aucun élément
clear()Supprimer tous les éléments
[i]Accéder à un élément (sans vérification)
at(i)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;
}

Opérations clés sur les chaînes

OpérationDescription
s.length() / s.size()Nombre de caractères
s[i]Accéder au caractère à la position i
s += "text"Ajouter à la fin
s.substr(pos, len)Extraire une sous-chaîne
s.find("text")Trouver la position d’une sous-chaîne
s.empty()Vrai si vide
std::getline(cin, s)Lire une ligne entière (espaces inclus)

std::cin >> textversus std::getline(std::cin, text)

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

Comportementstd::cin >> textestd::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 >> avant std::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);

Option 2 — std::cin.ignore() robuste (recommandée)

#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' inclus
std::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").

Exemple complet corrigé

#include <iostream>
#include <string>
#include <limits>
 
int main() {
    int age;
    std::string nom;
 
    std::cout << "Entrez votre âge : ";
    std::cin >> age;
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
 
    std::cout << "Entrez votre nom complet : ";
    std::getline(std::cin, nom);
 
    std::cout << "Âge : " << age << std::endl;
    std::cout << "Nom : " << nom << std::endl;
 
    return 0;
}

À retenir

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

Prérequis : ColorLab v1 (Semaine 4 - struct Color, fonctions, fichiers multiples, pass by value/ref)

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
// Semaine 4 : couleurs individuelles
Color soleil {"Rouge Soleil", 220, 60, 10};
Color ocean  {"Bleu Océan",   10, 80, 210};
 
// Semaine 5 : une palette entière !
std::vector<Color> palette = getWebColors();   // 12 couleurs d'un coup
int idx = findByName(palette, "Coral");         // recherche par nom
std::string hex = colorToHex(palette[idx]);     // "#FF7F50"

1 - Le Header File (colors.h) - v2

// 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

Aspectv1 (semaine 4)v2 (semaine 5)
struct ColorIntroduit (name, r, g, b)Réutilisé tel quel
CollectionsCouleurs individuellesstd::vector<Color> — palettes dynamiques
Couleurs prédéfiniesAucune12 couleurs web (getWebColors())
RechercheNon disponiblefindByName(), findByPartialName()
Opérations sur chaînesstd::string pour les nomsstring::find(), formatage hex
Format de sortiestd::cout << chaînéstd::format partout
ItérationPas de collection à itérerRange-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 _WIN32
void 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);
}
#endif
 
 
int 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.

std::cout << std::format("  [{:<20s}] ", color.name);

{:<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

NomCaractères visuelsOctets UTF-8Décalage
Rouge Soleil1212aucun
Bleu Océan1011 (é = 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)
  • visualLen10 (caractères)
  • padding20 + (11 - 10) = 21

std::format remplit jusqu’à 21 octets, ce qui donne visuellement 20 colonnes — alignement parfait.

Rappel : encodage UTF-8

PlageOctetsExemples
U+0000 – U+007F1A, z, 0, !
U+0080 – U+07FF2é, è, ñ, ü
U+0800 – U+FFFF3中, █, →
U+10000 – U+10FFFF4😀, 🎨

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 ?

ConceptOù 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éairefindByName() — parcourir un vector pour trouver un élément
Recherche par sous-chaînefindByPartialName() — 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 exacte
if (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 npos
if (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'abord
if (!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 !

Solution - Défi 1 Déclaration dans **colors.h** :

void sortByBrightness(std::vector<Color>& palette);

Définition dans **colors.cpp** :

// ------------------------------------------------------------
// 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;
}

Utilisation dans **main.cpp** :

int crimsonIdx = findByName(palette, "Crimson");
int limeIdx    = findByName(palette, "LimeGreen");
 
if (crimsonIdx >= 0 && limeIdx >= 0) {
    std::cout << "--- Gradient ---" << std::endl;
    displayGradient(palette[crimsonIdx], palette[limeIdx], 20);
}

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.

Lien vers l'original

Banque de cours - Cours 6 - On va au Gym

# Cours 6 - On va au Gym

Défis du ColorLab

Défi 1 — Fonctions utilitaires

  1. isPrimaryColor(const Color& c) — retourne true si la couleur est une couleur primaire pure (ex. : R:255 G:0 B:0).
  2. 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

Bonne chance, et amusez-vous bien au labo !

Lien vers l'original

Banque de cours - Cours 7 - On retourne au Gym

# Cours 7 - On retourne au Gym

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 aussi
Color 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;
}
Lien vers l'original

# Cours 8 - Xam

Travail personnel

Pas de devoir cette semaine !

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 classe
class 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 Outer
Ceci 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++ :

ClasseObjet
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 defaut
Chat 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 membreStack (Chat c;)Heap (new Chat)Possède son propre constructeur
int, float, etc.Valeur indéterminéeValeur indéterminéeNon
std::string, std::vector, etc.Initialise correctementInitialise correctementOui
Classe ou struct imbriquéeDepend de cette classeDepend de cette classeVariable
Chat c;
std::cout << c.age;  // Comportement indéfini : valeur garbage
std::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 defaut
Chien 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
}
  1. m_length, m_width, m_height sont d’abord construits par défaut (valeur garbage pour un double)
  2. Puis ecrases par l’affectation dans le corps

C’est un travail en double. Avec l’initializer list :

Box(double length, double width, double height)
    : m_length{length}, m_width{width}, m_height{height}
{
}

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 :

Box(double length, double width, double height)
    : m_length{length}, m_width{width}, m_height{height}
{}

Tu es obligé de passer les trois valeurs à la construction :

Box b{10.0, 5.0, 3.0}; // obligatoire
Box b;                 // erreur de compilation

Valeurs par défaut : deux approches

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 = 1
delete 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 directement
double 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 monde
 
protected:
    // 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 call
 
private:
    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)

#ifndef VECTOR2D_VECTOR2D_H
#define VECTOR2D_VECTOR2D_H
 
class Vector2D
{
public:
    Vector2D(double x=0.0, double y=0.0) : m_x(x), m_y(y) {}
 
    // Getters
    double getX() const;
    double getY() const;
 
    // Setters
    void setX(double x);
    void setY(double y);
 
    double magnitude() const;
 
    Vector2D operator+(const Vector2D& other) const;
 
private:
    double m_x, m_y;
};
 
#endif //VECTOR2D_VECTOR2D_H
 

Le fichier Vector2D.cpp (la définition des méthodes)

#include "Vector2D.h"
#include <cmath>
 
// Getters
double Vector2D::getX() const { return m_x; }
double Vector2D::getY() const { return m_y; }
 
// Setters
void Vector2D::setX(double x) { m_x = x; }
void Vector2D::setY(double y) { m_y = y; }
 
double Vector2D::magnitude() const
{
    return std::sqrt(m_x * m_x + m_y * m_y);
}
 
Vector2D Vector2D::operator+(const Vector2D& other) const
{
    return Vector2D(m_x + other.m_x, m_y + other.m_y);
}

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é à 255
c.setG(-50);       // clampé à 0
c.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:71
inv.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érentes
 
if (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:100
 
c.setR(999);        // clampé à 255
c.setG(-50);        // clampé à 0
c.setB(128);
c.display();        // R:255 G:0   B:128
 
std::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};
}

Utilisation dans **main.cpp** :

std::cout << "--- Couleurs et leurs inverses ---\\n";
std::vector<Color> palette = getWebColors();
 
for (const Color& c : palette) {
    Color inv = c.invert();
    std::cout << std::format("  {:<20s} R:{:<3d} G:{:<3d} B:{:<3d}  |  {:<25s} R:{:<3d} G:{:<3d} B:{:<3d}",
                 c.name(), c.r(), c.g(), c.b(),
                 inv.name(), inv.r(), inv.g(), inv.b()) << "\\n";
}

Sortie attendue :

--- Couleurs et leurs inverses ---
  Teal                 R:0   G:128 B:128  |  Inv:Teal                  R:255 G:127 B:127
  Salmon               R:250 G:128 B:114  |  Inv:Salmon                R:5   G:127 B:141
  Coral                R:255 G:127 B:80   |  Inv:Coral                 R:0   G:128 B:175
  Goldenrod            R:218 G:165 B:32   |  Inv:Goldenrod             R:37  G:90  B:223
  Orchid               R:218 G:112 B:214  |  Inv:Orchid                R:37  G:143 B:41
  SteelBlue            R:70  G:130 B:180  |  Inv:SteelBlue             R:185 G:125 B:75
  Tomato               R:255 G:99  B:71   |  Inv:Tomato                R:0   G:156 B:184
  MediumAquamarine     R:102 G:205 B:170  |  Inv:MediumAquamarine      R:153 G:50  B:85
  SlateGray            R:112 G:128 B:144  |  Inv:SlateGray             R:143 G:127 B:111
  Crimson              R:220 G:20  B:60   |  Inv:Crimson               R:35  G:235 B:195
  LimeGreen            R:50  G:205 B:50   |  Inv:LimeGreen             R:205 G:50  B:205
  DarkOrange           R:255 G:140 B:0    |  Inv:DarkOrange            R:0   G:115 B:255

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.

Lien vers 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 publics
struct Color {
    std::string name;
    int r {};
    int g {};
    int b {};
};
 
// Fonctions libres qui opèrent sur Color
void 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 :

  1. 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.
  2. Validation dans le constructeur - Le clamping (0–255) est intégré directement dans le constructeur. Impossible de créer une couleur invalide.
  3. 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

Aspectv2 (semaine 5)v3 (semaine 6)
Typestruct Color (tout public)class Color (private par défaut)
Accès aux donnéesDirect : color.rVia getters : color.r()
ValidationcreateColor() - fonction libreConstructeur avec clamping intégré
displayColor(c)Fonction libreMéthode : c.display()
brightness(c)Fonction libreMéthode : c.brightness()
mixColors(a, b, nom)Fonction libreMéthode : a.mix(b, nom)
clamp()Fonction libre publiqueMéthode statique privée
Compteur d’instancesNon disponibleColor::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>
#endif
 
int 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 ?

ConceptOù 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
ConstructeurColor("Rouge Soleil", 220, 60, 10) - validation automatique
Liste d’initialisation: m_name{name}, m_r{clamp(r)}, ... - initialisation directe
**Méthodes ****const**display() const, brightness() const, mix() const
Gettersname(), r(), g(), b() - accès contrôlé en lecture
**Membres ****static**s_count partagé entre toutes les instances
**Méthode ****static**Color::getCount() - appelée sans objet
EncapsulationLe 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é à 255
c.setG(-50);       // clampé à 0
c.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:71
inv.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érentes
 
if (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:100
 
c.setR(999);        // clampé à 255
c.setG(-50);        // clampé à 0
c.setB(128);
c.display();        // R:255 G:0   B:128
 
std::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};
}

Utilisation dans **main.cpp** :

std::cout << "--- Couleurs et leurs inverses ---\n";
std::vector<Color> palette = getWebColors();
 
for (const Color& c : palette) {
    Color inv = c.invert();
    std::cout << std::format("  {:<20s} R:{:<3d} G:{:<3d} B:{:<3d}  |  {:<25s} R:{:<3d} G:{:<3d} B:{:<3d}",
                 c.name(), c.r(), c.g(), c.b(),
                 inv.name(), inv.r(), inv.g(), inv.b()) << "\n";
}

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.

Lien vers 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érateurSignificationExemple
&x”adresse de x”&value
*ptr”valeur à l’adresse de ptr”*ptr = 100;
ptr->maccès membre via pointeurptr->name()

nullptr Un pointeur qui ne pointe vers rien doit valoir nullptr. Toujours vérifier avant de déréférencer.

int* ptr {nullptr};
if (ptr != nullptr) {
    *ptr = 42;        // sûr
}
// *ptr = 42;         // CRASH si ptr == nullptr !

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 value
int* ptr {&value};
std::cout << value;   // 100 - modifié à travers la référence

Pointeurs vs références (tableau de décision)

CaractéristiqueRéférence (T&)Pointeur (T*)
Peut être nul ?NonOui (nullptr)
Peut être réassigné ?NonOui
Syntaxe d’accèsDirect : refDéréférencement : *ptr, ptr->
Quand l’utiliserCible toujours valideCible 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 tas
std::cout << *ptr;         // 42
delete ptr;                // libère la mémoire
ptr = nullptr;             // bonne pratique

Trois pièges classiques :

  1. Fuite mémoire : oublier delete (la mémoire n’est jamais libérée).
  2. Pointeur pendant : utiliser ptr après delete ptr;.
  3. 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 tas
std::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.

RAII s’applique partout

{
    std::vector<int> v;
    std::lock_guard<std::mutex> lock(mtx);
    auto ptr = std::make_unique<Character>("Aria", 80, 100);
}

Dans ce bloc :

  • v libère sa mémoire automatiquement
  • lock déverrouille le mutex automatiquement
  • 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 :

UCLASS()
class MYGAME_API AMyCharacter : public APawn
{
    GENERATED_BODY()
 
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Health;
 
    UFUNCTION(BlueprintCallable)
    void TakeDamage(float Amount);
};

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 :

  1. Sérialiser des objets (sauvegardes, niveaux, réseau)
  2. Exposer des propriétés à l’éditeur (les champs visibles dans le panneau Details)
  3. Faire le pont avec Blueprint, un langage visuel
  4. 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éfixeSignificationExemple
AActeur dans le mondeAActor, AMyCharacter
UObjet géré par le GCUObject, UStaticMeshComponent
FStruct simple, sans GCFVector, FString
IInterfaceIInteractable
EEnumEInputType
TTemplateTArray, TMap, TSharedPtr

UE n’utilise pas la library standard C++ (STL, Standard Template Library)

UE a ses propres équivalents :

STLUE
std::vector<T>TArray<T>
std::map<K,V>TMap<K,V>
std::stringFString
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éation
AMyActor* 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 :

  1. 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
  1. Pour tout le reste (structs F, containers T, smart pointers T)
  • RAII s’applique normalement, comme en C++ pur
  1. 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
  1. 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 :

  1. Préférer les valeurs.
  2. Sinon, les références.
  3. Sinon, les pointeurs intelligents (unique_ptr, shared_ptr).
  4. 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.

std::vector<std::unique_ptr<Animal>> zoo;
zoo.push_back(std::make_unique<Lion>());
zoo.push_back(std::make_unique<Penguin>());
 
for (const auto& animal : zoo) {
    animal->speak();  // dispatch virtuel
}

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 null
public:
    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

// Header
class 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.

GLuint texture;
glGenTextures(1, &texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA,
             GL_UNSIGNED_BYTE, pixel_data);

Tableau comparatif des cinq langages

AspectC++RustJavaC#Python
Pointeurs brutsOui (T*)Oui (*const T, en unsafe)NonOptionnel (en unsafe)Non
Références syntaxiquesT&&T, &mut TTout est référenceTout est référence (sauf struct)Tout est référence
Propriété expliciteunique_ptr<T>Système d’ownership intégréGCGCGC + comptage
Partageshared_ptr<T>Rc<T>, Arc<T>ImpliciteImpliciteImplicite
Référence faibleweak_ptr<T>Weak<T>WeakReference<T>WeakReference<T>weakref.ref
VérificationRuntime + disciplineCompile-timeRuntimeRuntimeRuntime
Risque de fuiteÉlevé sans RAIITrès faibleCycles seulement (rare)Cycles seulement (rare)Cycles (gérés par GC cyclique)

Comparaison détaillée par langage

Rust : la propriété au compile-time

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).

// Polymorphisme : équivalent de vector<unique_ptr<Animal>>
let zoo: Vec<Box<dyn Animal>> = vec![
    Box::new(Lion::new()),
    Box::new(Penguin::new()),
];
 
// Observateur non-propriétaire : référence empruntée avec lifetime
struct Camera<'a> {
    target: Option<&'a Target>,
}
 
// Référence arrière : Weak pour casser les cycles
use std::rc::{Rc, Weak};
use std::cell::RefCell;
 
struct Node {
    children: RefCell<Vec<Rc<Node>>>,
    parent: RefCell<Weak<Node>>,
}

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 possible
List<Animal> zoo = new ArrayList<>();
zoo.add(new Lion());
zoo.add(new Penguin());
 
// Observateur non-propriétaire : juste une référence
class 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ée
import 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 Java
List<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 path
unsafe void ProcessPixels(byte* data, int length) {
    for (int i = 0; i < length; i++) data[i] = 255;
}
 
// WeakReference pour observation longue durée
WeakReference<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 typing
zoo = [Lion(), Penguin()]
for animal in zoo:
    animal.speak()
 
# Observateur non-propriétaire : assignation simple
class Camera:
    def __init__(self):
        self.target = None  # référence ou None
 
# Référence faible pour éviter les cycles
import weakref
 
class 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 :

  1. C++ expose tous les outils et laisse le développeur choisir. Puissance maximale, responsabilité maximale.
  2. Rust prend les meilleures pratiques du C++ moderne et les impose au compile-time. Garanties fortes, courbe d’apprentissage exigeante.
  3. C# propose un modèle managé par défaut avec des trappes d’évacuation pour la performance.
  4. Java pousse l’abstraction plus loin : pas de pointeurs, modèle uniforme par référence pour les objets.
  5. 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 :

IntentionOutil C++
Je possède cet objet, seul propriétaireunique_ptr<T>
Je partage la propriété entre plusieurs entitésshared_ptr<T>
Je casse un cycle de propriété partagéeweak_ptr<T>
L’objet existe toujours et ne change jamais de cibleT& (référence)
L’objet peut être null ou changer de cibleT* (pointeur brut)
Interop avec API C ou code bas niveauT* (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.

Bases de l'héritage

Classes de base et classes dérivées

class Box {
public:
    Box() = default;
    Box(double length, double width, double height)
        : m_length{length}, m_width{width}, m_height{height} {}
 
    double volume() const { return m_length * m_width * m_height; }
 
    double getLength() const { return m_length; }
    double getWidth()  const { return m_width; }
    double getHeight() const { return m_height; }
 
    virtual ~Box() = default;
 
protected:
    double m_length {1.0};
    double m_width  {1.0};
    double m_height {1.0};
};
 
class Carton : public Box {
public:
    explicit Carton(double l, double w, double h, std::string_view material = "Cardboard")
        : Box{l, w, h}, m_material{material} {}
 
    std::string material() const { return m_material; }
 
private:
    std::string m_material;
};

1. La relation EST-UN

class Carton : public Box {

Cette ligne signifie : **un **Carton** EST-UN ****Box**. C’est la relation d’héritage public.

Concrètement, ça veut dire :

  • Un Carton possède toutes les données et méthodes de Box
  • Partout où on attend un Box, on peut passer un Carton
  • L’inverse n’est pas vrai : un Box n’est pas forcément un Carton
Carton c{30, 20, 15, "Cardboard"};
Box& b = c;              // OK, un Carton EST-UN Box
double v = b.volume();   // OK, méthode héritée
 
Box base{10, 10, 10};
Carton& cr = base;       // ERREUR, un Box n'est pas forcément un Carton

Test mental pour valider l’héritage

Avant d’utiliser l’héritage, demande-toi : est-ce que cette phrase a du sens ?

  • “Un Carton est un Box” : oui, l’héritage est justifié
  • “Un Moteur est une Voiture” : non, c’est une relation A-UN (composition), pas EST-UN

L’héritage est souvent abusé. Quand tu hésites entre héritage et composition, choisis la composition par défaut.


2. Le mot-clé protected

protected:
    double m_length {1.0};
    double m_width  {1.0};
    double m_height {1.0};

protected est un troisième niveau de visibilité, entre public et private :

Mot-cléAccessible depuisAccessible depuis les classes fillesAccessible de l’extérieur
publicpartoutouioui
protectedla classe et les classes fillesouinon
privatela classe seulementnonnon

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.

Ici, on aurait pu écrire :

private:
    double m_length {1.0};
    double m_width  {1.0};
    double m_height {1.0};

Et Carton aurait quand même pu faire getLength() pour lire la valeur. C’est généralement préférable.


3. Le mot-clé virtual et le destructeur virtuel

virtual ~Box() = default;

Le mot-clé virtual active le polymorphisme dynamique : la résolution de la méthode appelée se fait à l’exécution, en fonction du type réel de l’objet.

Pourquoi un destructeur virtuel ?

Considère ce code :

Box* b = new Carton{30, 20, 15};
delete b;  // que se passe-t-il ?

Si le destructeur de Box n’est pas virtual :

  • Seul ~Box() est appelé
  • ~Carton() n’est jamais appelé
  • La string m_material n’est pas détruite proprement
  • Comportement indéfini, fuite mémoire potentielle

Si le destructeur est virtual :

  • ~Carton() est appelé d’abord (détruit m_material)
  • Puis ~Box() automatiquement
  • Tout est détruit correctement

La règle

Si une classe est destinée à être héritée et qu’on peut la manipuler via un pointeur de la classe de base, son destructeur doit être **virtual**.

Le coût : une indirection à l’exécution (table virtuelle). Acceptable dans 99% des cas.

= default indique simplement au compilateur de générer le destructeur par défaut, mais virtuel.


4. La liste d’initialisation et le chaînage du constructeur de base

explicit Carton(double l, double w, double h, std::string_view material = "Cardboard")
    : Box{l, w, h}, m_material{material} {}

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 :

  1. Le compilateur appelle le constructeur de Box avec l, w, h
  2. Puis m_material est initialisé avec material
  3. 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éfaut Box(), 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)

5. Le mot-clé explicit

explicit Carton(double l, double w, double h, std::string_view material = "Cardboard")

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 compilation
process(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).


6. Le mot-clé const sur les méthodes

double volume() const { return m_length * m_width * m_height; }
double getLength() const { return m_length; }

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

ConceptSignificationQuand l’utiliser
class B : public AB EST-UN AQuand la phrase “B est un A” est vraie
protectedaccessible aux classes fillesAvec parcimonie, préférer private + accesseurs
virtualdispatch dynamiqueSur les méthodes redéfinies dans les filles
virtual ~Class()destructeur virtuelDès qu’on hérite et qu’on peut manipuler par pointeur de base
explicitempêche conversion impliciteSur tous les constructeurs convertibles, par défaut
: Base{...}, m_x{...}liste d’initialisationToujours pour initialiser membres et base
const (méthode)promesse de non-modificationSur toute méthode qui ne modifie pas l’état
= defaultgénération par compilateurPour 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

ConceptSyntaxeIdée clé
Adresse&xAdresse mémoire de x
PointeurT* ptr = &x;Stocke une adresse
Déréférencement*ptr, ptr->mAccès à la valeur / au membre via pointeur
RéférenceT& ref = x;Alias permanent, jamais nul
nullptrT* ptr = nullptr;Pointeur qui ne pointe vers rien
new / deleteT* 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éritageclass D : public B { ... };”D est un B”
Chaînage de constructeurD(...) : B{...}, m_x{...} {}Initialise la base avant les membres
virtualvirtual void f();Fonction redéfinissable, résolue à l’exécution
overridevoid f() override;Indique et vérifie la redéfinition
Destructeur virtuelvirtual ~Base() = default;Obligatoire dans toute classe de base

Erreurs courantes

#ErreurConséquenceCorrection
1Déréférencer un nullptrCrashVérifier if (p != nullptr) avant *p
2Oublier delete après newFuite mémoirePréférer std::make_unique<T>()
3Utiliser un pointeur après deleteComportement indéfiniMettre ptr = nullptr; après delete
4Confondre . et ->Erreur de compilationobj.m pour un objet, ptr->m pour un pointeur
5Oublier virtual sur le destructeur de la baseDestructeur de la dérivée non appelévirtual ~Base() = default;
6Oublier override sur la redéfinitionBug silencieux si la signature diffèreToujours écrire override
7Ne pas chaîner le constructeur de baseMembres de base non initialisés correctementListe d’init : Derived(...) : Base{...}
8Object slicing (passage par valeur d’une dérivée)Perte de la partie dérivéePasser 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 public
class B : protected A { /* ... */ };   // heritage protege
class 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 Apublic Aprotected Aprivate A
publicreste publicdevient protecteddevient private
protectedreste protectedreste protecteddevient private
privateinaccessibleinaccessibleinaccessible

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.

void process(Box& b);
 
Carton c{30, 20, 15, "Cardboard"};
process(c);   // ERREUR : conversion inaccessible

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 :

// Approche heritage prive : Carton EST-implemente-avec un Box
class Carton : private Box {
public:
    using Box::volume;   // expose explicitement certaines methodes
};
 
// Approche composition : Carton A-UN Box (preferable)
class Carton {
public:
    double volume() const { return m_box.volume(); }
private:
    Box m_box;
};

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é :

  1. **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).
  2. 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.
  3. 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

LangageModèle
Java, C#Un seul type d’héritage, implicitement public
RustPas d’héritage du tout, uniquement composition et traits
PythonPas 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

TypePhrase mentaleFréquenceRecommandation
public“B EST-UN A”omniprésentutilisation normale
protected“B est-un A pour ses filles uniquement”quasi inexistantéviter
private“B est implémenté avec A”rarepré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

  1. É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.
  2. Réécrire un héritage privé en utilisant une composition équivalente, et comparer la lisibilité du code.
  3. 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;
};

Puis dans main :

std::unique_ptr<Shape> s = std::make_unique<Square>(3.0);
s->describe();
std::cout << "area = " << s->area() << "\n";

Vérifiez :

  1. Sans virtual sur describe(), qu’est-ce qui s’affiche ?
  2. Sans override sur Square::describe(), le code compile-t-il toujours ?
  3. Si vous remplacez unique_ptr<Shape> par Shape* s = new Square(3.0); et oubliez delete, que se passe-t-il en termes de mémoire ?

Solution shape.h

#ifndef SHAPE_SHAPE_H
#define SHAPE_SHAPE_H
 
 
class Shape {
public:
    Shape() = default;
    virtual double area() const = 0;
    virtual void describe() const;
    virtual ~Shape() = default;
};
 
 
#endif //SHAPE_SHAPE_H
 

shape.cpp

#include <iostream>
#include <ostream>
#include "shape.h"
 
double Shape::area() const
{
    return 0;
}
 
void Shape::describe() const
{
    std::cout << "I am a Shape" << std::endl;
}
 

square.cpp

 
#include "square.h"
 
#include <iostream>
#include <ostream>
 
Square::Square(double side)
{
    m_side = side;
}
 
double Square::area() const
{
    return m_side * m_side;
}
 
void Square::describe() const
{
    std::cout << "I am a square of size " << m_side << std::endl;
}
 

Square.h

#ifndef SHAPE_SQUARE_H
#define SHAPE_SQUARE_H
 
#include "shape.h"
 
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;
};
 
#endif //SHAPE_SQUARE_H

main.cpp

#include "shape.h"
#include  "square.h"
 
int main()
{
    Square square(5);
    square.describe();
    return 0;
}
 

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 code de référence

#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <numbers>
 
class Shape {
public:
    virtual double area() const = 0;
    virtual void describe() const = 0;
    virtual ~Shape() = default;
};
 
class Square : public Shape {
public:
    explicit Square(double side) : m_side{side} {}
 
    double area() const override { return m_side * m_side; }
 
    void describe() const override {
        std::cout << "Square de cote " << m_side
                  << ", aire = " << area() << '\\n';
    }
 
private:
    double m_side;
};
 
class Circle : public Shape {
public:
    explicit Circle(double radius) : m_radius{radius} {}
 
    double area() const override {
        return std::numbers::pi * m_radius * m_radius;
    }
 
    void describe() const override {
        std::cout << "Cercle de rayon " << m_radius
                  << ", aire = " << area() << '\\n';
    }
 
private:
    double m_radius;
};
 
int 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();
    }
}

1. La fonction virtuelle pure : = 0

virtual double area() const = 0;

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 :

  1. La méthode n’a pas d’implémentation dans cette classe
  2. 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.

Comparaison avec d’autres langages :

// C++
virtual double area() const = 0;
 
// Java
abstract double area();
 
// C#
public abstract double Area();
 
// Python (avec ABC)
@abstractmethod
def area(self): pass
 
// Rust (dans un trait)
fn area(&self) -> f64;

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 abstraite
auto s = std::make_unique<Shape>();         // ERREUR : Shape est abstraite
 
Shape* p = new Square{3.0};                 // OK : pointeur vers Shape
Shape& r = *p;                              // OK : reference vers Shape
std::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.


3. Le mot-clé override

double area() const override { return m_side * m_side; }

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() :

  1. Le compilateur ne sait pas quelle version appeler à la compilation
  2. À l’exécution, le programme regarde le type réel de l’objet pointé
  3. 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 = 9
Cercle de rayon 2, aire = 12.5664

6. Comparaison : virtuelle pure vs implémentation par défaut

On aurait pu écrire Shape autrement :

class Shape {
public:
    virtual double area() const { return 0.0; }   // implementation par defaut
    virtual void describe() const {
        std::cout << "Shape generique\\n";
    }
    virtual ~Shape() = default;
};

Avec = 0 (virtuelle pure)

  • Shape est abstraite, ne peut pas être instanciée
  • Les filles doivent redéfinir area() et describe()
  • Le compilateur garantit qu’on n’oubliera pas
  • 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

SyntaxeNomEffet
virtual f();virtuelle simplepeut être redéfinie, a une implémentation
virtual f() = 0;virtuelle puredoit être redéfinie, rend la classe abstraite
f() override;redéfinition explicitesécurité de compilation
f() final;méthode finaleempêche toute redéfinition ultérieure
class C finalclasse finaleempêche toute classe fille
virtual ~C() = default;destructeur virtuelindispensable pour héritage polymorphique

Exercices suggérés

  1. Ajouter une classe Triangle à la hiérarchie qui prend la base et la hauteur.
  2. Ajouter une méthode virtuelle pure perimeter() à Shape et l’implémenter dans toutes les filles.
  3. Essayer de retirer override et introduire une faute de frappe (area() const devient aera() const). Observer le message du compilateur.
  4. 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.
  5. 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 Baseclass AMyActor : public AActor
virtual void f() overridevirtual void BeginPlay() override
virtual ~Base() = defaultGéré par le moteur via UObject et le GC
std::unique_ptr<T>TObjectPtr<T>, UPROPERTY()
new / deleteNewObject<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)

  • Beginning C++23, Chapitre 12 (sections héritage public, virtual, override)

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 :

  1. Un paramètre de fonction qui doit lire une grosse std::string sans la copier.
  2. Un membre d’une classe Player qui peut avoir ou ne pas avoir d’arme équipée.
  3. Une fonction qui crée et retourne un nouvel Enemy alloué dynamiquement.
  4. 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 :

  • Données protégées : m_brand (string), m_speed (double, km/h).
  • Constructeur prenant marque et vitesse.
  • Méthode virtual void describe() const qui affiche "Vehicle: <brand>, <speed> km/h".
  • Destructeur virtuel.

Puis deux classes dérivées :

  • Car : public Vehicle qui ajoute m_numDoors (int) et redéfinit describe() pour afficher en plus le nombre de portes.
  • Bike : public Vehicle qui ajoute m_hasGears (bool) et redéfinit describe() pour afficher si le vélo a des vitesses.

Dans main, créez :

std::vector<std::unique_ptr<Vehicle>> garage;
garage.push_back(std::make_unique<Car>("Toyota", 180.0, 4));
garage.push_back(std::make_unique<Bike>("Specialized", 35.0, true));
garage.push_back(std::make_unique<Car>("Ferrari", 320.0, 2));
 
for (const auto& v : garage) {
    v->describe();   // appel polymorphique
}

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.

Lien vers l'original

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

Créer un projet C++

  1. Démarrez Epic Game Launcher (il devrait déjà être installé sur votre ordi. Sinon: https://store.epicgames.com/en-US/download )
  2. Installer UE 5.7.x si ce dernier n’est pas installé (5.7.4 au moment de ce document)
    1. Allez dans Options
    2. Ajoutez MetaHuman Creator Core Data, Editor symbols for debugging et au moins Windows ARM64 comme Target Platform)
  3. Cliquer sur ­”Apply”
  4. Vous avez 30 GB à installer
  5. Ouvrir Unreal Engine 5.7.x
  6. Créer un nouveau projet → Choisir un modèle (e.g. First-Person)
  7. Important** :** Sélectionner C++ (et non “Blueprint”) dans la section ProjectDefaults
  8. Target Platform → Desktop et Quality Preset → Maximum
  9. 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à:

  1. Sous Performance, clickez Settings
  2. Allez à l’onglet Advanced
  3. Sous Virtual memory, clickez Change
  4. Déselectionnez Automatically manage paging file size for all drives
  5. Si vous un disque rapide, choisissez celui-là,. sinon restez sur le drive C:.
  6. Choisir Custom size
    • Initial size: 65536 MB
    • Maximum size: 65536 MB
  7. Clickez sur Set, puis OK
  8. Rebootez votre ordi
  9. Vous avez rebooté ?
  10. Redémarrer Epic Game Launcher
  11. Dans la section Library Unreal Engine, double-cliquez sur votre projet MyCpp
  12. 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.

Structure du projet

MyProject/
├── Source/
│   ├── MyProject/
│   │   ├── Public/        ← Header files (.h)
│   │   ├── Private/       ← Implementation files (.cpp)
│   │   └── MyProject.Build.cs
│   └── MyProject.Target.cs
├── Content/               ← Assets, Blueprints
└── MyProject.uproject

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 !

1. Créer un compte JetBrains

  1. Aller sur https://account.jetbrains.com
  2. S’inscrire avec votre courriel étudiant Rubika (recommandé) ou personnel
  3. 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 :

  1. Choisir “Log in to JetBrains Account”
  2. Se connecter avec le compte créé à l’étape 1
  3. Dans la fenêtre de licence, sélectionner “Non-Commercial Use”
  4. Cocher la case confirmant l’usage non commercial (cours, projets personnels, etc.)
  5. 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

  1. Ouvrir l’Unreal Engine Editor 5.7.4
  2. Menu Edit → Editor Preferences
  3. Chercher “Source Code”
  4. Dans Source Code Editor, sélectionner Rider Uproject (Attention, pas “Rider”, mais bien “Rider Uproject”)
  5. Redémarrez UE Editor
  6. Le menu Tools affiche maintenant Refresh Rider Uproject Project et Open Rider Uproject
  7. Cliquer Tools → Refresh Rider Uproject Project pour générer/mettre à jour les fichiers de projet
  8. 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”:
    1. 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.

EmplacementOù 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 :

  1. 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.
  2. Garder l’éditeur Unreal ouvert et lancer Rider sur le même projet (Tools → Open Rider Uproject)
  3. 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 :

  1. Dans UE : Tools > New C++ Class > Actor, donne-lui un nom (ex. MyNode), crée-le.

  2. UE va lancer la compilation et Rider devrait s’ouvrir (ou prendre le focus) sur MyNode.h.

  3. Créer ensuite un Blueprint Class qui hérite de MyNode (clic droit dans Content Browser > Blueprint Class > chercher MyNode dans “All Classes”).

  4. Double-clique sur ce Blueprint.

  5. En haut à droite, tu devrais voir “Parent Class” et si tu cliques, le code va s’ouvrir dans Rider.

  6. 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 !

https://karimaktouf.ca/graphs/unreal-hierarchy/

Les macros Unreal

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 :

  • Quelles classes existent dans le programme.
  • Quels champs (variables membres) elles contiennent.
  • Quels noms portent ces champs.
  • Quelles signatures ont les méthodes.

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éfixeHérite de…ExempleGarbage Collected ?
AAActor ou un descendant (objets placés dans le monde)AMyActor, APawn, ACharacterOui
UUObject ou un descendant non-Actor (composants, libraries)UMyComponent, UStaticMeshComponent, UBlueprintFunctionLibraryOui
FAucun héritage UObject (struct simple, type valeur)FVector, FRotatorNon (type valeur, peut contenir des UPROPERTY)
TTemplateTArray, TMap<K,V>, TSubclassOfNon (mais voir note sur UPROPERTY)
IInterface C++ pure, implémentée par les classes UObjectIMonInterfaceNon (le jumeau UInterface l’est)
EEnumEColorModeNon
SSWidget (UI Slate)SButton, SCompoundWidgetNon (TSharedPtr, comptage de références)
bVariable booléenne (convention de nommage, pas une classe)bIsAlive, bCanJumps/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écificateurEffet
EditAnywhereModifiable dans le Details panel, sur l’instance comme sur le default
EditDefaultsOnlyModifiable uniquement sur les valeurs par défaut de la classe (pas sur l’instance placée dans le monde)
EditInstanceOnlyModifiable uniquement sur les instances placées dans le monde
VisibleAnywhereVisible mais NON modifiable
BlueprintReadWriteLisible ET assignable depuis Blueprint (Get + Set)
BlueprintReadOnlyLisible 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).

UFUNCTION() - Expose une fonction

UFUNCTION(BlueprintCallable, Category = "Combat")
void TakeDamage(int32 Amount);
 
UFUNCTION(BlueprintPure, Category = "Stats")
int32 GetHealth() const;

Ce que ça débloque :

  • 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écificateurEffet sur le noeud Blueprint
BlueprintCallableNoeud avec pin d’exécution (entrée et sortie blanches). Pour les fonctions qui ont un effet de bord (modifient l’état).
BlueprintPureNoeud 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).
BlueprintImplementableEventPas d’implémentation C++. La fonction sera écrite en Blueprint par les designers.
BlueprintNativeEventImplé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 pouce BlueprintCallable 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 ouvrante
 
public:
    // 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 dernier
 
UCLASS()
class VOTREPROJET_API UMaClasse : public UneClasseDeBase
{
    GENERATED_BODY()              // toujours en premier dans la classe
 
public:
    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

  1. Dans l’éditeur Unreal, d’abord ouvrir Rider (Tools → Open Rider Uproject)
  2. Toujours dans l’éditeur Unreal, Tools -> New C++ Class…
  3. Cocher Show All Classes en haut.
  4. Chercher BlueprintFunctionLibrary, sélectionner, cliquer Next.
  5. Cliquer sur le bouton Public pour s’assurer que la classe est dans le dossier publique.
  6. Nommer ColorLabLibrary (sans préfixe : Unreal ajoute U automatiquement).
  7. Cliquer Create Class

  1. 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()) :

public:
    UFUNCTION(BlueprintCallable, Category = "ColorLab|First Steps")
    static int32 ChannelTo255(float NormalizedValue);

É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 .cpp
 
int32 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.

  1. Dans le Content Browser,, clic droit sur le dossier “Content”, puis “New folder” → “MyCpp”

  2. À partir de maintenant, ce qu’on va créer, ce sera dans ce dossier donc cliquez dessus pour qu’il devienne le dossier activé.

  3. Dans le Content Browser, à l’intérieur du dossier vide MyCpp, clic droit -> Blueprint Class -> Actor. Le nommer BP_TestColorLab.

  4. Double-cliquer pour l’ouvrir.

  5. 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).

  6. Sur le noeud, mettre Normalized Value = 0.5.

  7. 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.

  8. 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.

  1. Compile + Save dans la barre d’outils du Blueprint (en haut à gauche).
  2. 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 :

  1. Créé une classe C++ héritant de UBlueprintFunctionLibrary.
  2. Marqué une fonction avec UFUNCTION(BlueprintCallable, ...) pour la rendre visible côté Blueprint.
  3. Compilé depuis Rider (avec hot-reload via Live Coding si UE Editor est ouvert).
  4. 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.

  1. Assurez-vous d’être dans le dossier Fibo dans le Content Browser
  2. Dans l’éditeur Unreal, menu Tools -> New C++ Class…
  3. La fenêtre “Choose Parent Class” s’ouvre. En haut, cocher** All Classes**.
  4. Dans le champ de recherche, taper BlueprintFunctionLibrary. Sélectionner BlueprintFunctionLibrary dans la liste, puis cliquer Next.
  5. Dans “Name”, saisir ProceduralLibrary (sans préfixe : Unreal ajoutera U tout seul, comme on l’a vu dans la convention de nommage).
  6. 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/).
  7. 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 :

#pragma once
 
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "ProceduralLibrary.generated.h"
 
UCLASS()
class MYCPP_API UProceduralLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
};

Quelques observations :

  • Les trois includes sont déjà présents, dans le bon ordre (.generated.h en dernier, comme expliqué dans la section macros).
  • MYCPP_API est une macro d’export DLL ajoutée automatiquement par Unreal. Ne pas y toucher : elle permet aux autres modules d’utiliser la classe.
  • GENERATED_BODY() est déjà placé en première ligne du corps de classe.

Modification à faire : ajouter la déclaration de la fonction. Modifier la classe ainsi :

UCLASS()
class MYCPP_API UProceduralLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
 
public:
    UFUNCTION(BlueprintCallable, Category = "Procedural|Patterns")
    static TArray<FVector> GenerateSunflowerSpiral(
        int32 NumPoints,
        float Spacing,
        float ZHeight = 0.0f
    );
};

Décortiquons la déclaration :

  • 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.

  1. 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.

  2. Nommer le Blueprint BP_SphereChip.

  3. Double-cliquer pour l’ouvrir.

  4. Dans le panneau Components (en haut à gauche), sélectionner le composant StaticMeshComponent (le seul présent par héritage).

  5. 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.

  1. 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

  1. Dans le Content Browser, dans le dossier Fibo, clic droit -> Blueprint Class -> Actor. Le nommer BP_SunflowerSpawner. Double-cliquer pour l’ouvrir.
  2. 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.
  3. 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.
  4. 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)
  5. 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.
  6. Connecter le pin d’exécution (blanc) du Generate Sunflower Spiral à l’entrée d’exécution du For Each Loop.
  7. 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.
  8. 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.
  9. Connecter le pin d’exécution Loop Body du For Each Loop à l’entrée d’exécution du Spawn Actor From Class.
  10. Cliquer Compile puis Save dans la barre d’outils du Blueprint (en haut à gauche).

  1. Fermer la fenêtre du Blueprint. Dans le Content Browser, glisser BP_SunflowerSpawner dans le viewport (n’importe où dans le niveau).
  2. 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.
Lien vers l'original

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

https://karimaktouf.ca/graphs/unreal-hierarchy/

Comprendre chaque classe de base

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/rotationAActor
Une entité contrôlable par le joueur ou l’IAAPawn
Un personnage humanoïde avec marche/saut intégrésACharacter
Une brique réutilisable à attacher à un ActorUActorComponent ou USceneComponent
Un conteneur de données ou gestionnaire sans présence dans le mondeUObject
Des fonctions utilitaires accessibles partout en BlueprintUBlueprintFunctionLibrary

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() :

// MAUVAIS
UStaticMeshComponent* Mesh;     // Le GC ne sait pas qu'on tient à ce pointeur.
                                 // Il peut le détruire sous nos pieds n'importe quand.
 
// CORRECT
UPROPERTY()
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âcheConstructorBeginPlayTick
Créer un composant (mesh, light, audio)OUInonnon
Charger un asset avec ConstructorHelpersOUInonnon
Initialiser une variable par défautOUInonnon
Spawner un autre ActornonOUIrare
Chercher une référence à un autre ActornonOUInon
Logique par frame (rotation, mouvement, animation custom)nonnonOUI
Démarrer un timer ou jouer un son d’intrononOUInon

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 PostInitializeComponents BeginPlay, 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 avant BeginPlay. 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é :

HookQuandCas d’usage typique
ConstructorCré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
PostInitializeComponentsAprès init des composants, avant BeginPlayBrancher composants entre eux
BeginPlayUne fois, juste avant la 1ère frameInit gameplay, spawner, chercher références
TickÀ chaque frame (si activé)Logique par frame (mouvement, animation custom)
EndPlayÀ la destructionLibé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

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float MaxHealth = 100.0f;
 
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stats")
float CurrentHealth;

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 UPROPERTY
 
UPROPERTY()
AAnotherActor* ReferenceActor;           // référence vers autre Actor : idem
 
UPROPERTY()
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.

3. Exposer une fonction comme noeud Blueprint

UFUNCTION(BlueprintCallable, Category = "Combat")
void TakeDamage(float Amount);
 
UFUNCTION(BlueprintPure, Category = "Stats")
float GetHealthPercent() const;

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.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
 
UCLASS()
class VOTREPROJET_API AMyActor : public AActor
{
    GENERATED_BODY()
 
public:
    AMyActor();
 
private:
    UPROPERTY(VisibleAnywhere)
    UStaticMeshComponent* CubeMesh;
};
// 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 :

PublicDependencyModuleNames.AddRange(new string[] {
    "Core", "CoreUObject", "Engine", "InputCore",
    "ProceduralMeshComponent"   // <- ajouté
});

Puis clic droit sur le .uprojectGenerate 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à :

ComposantRôleQuand l’utiliser
UStaticMeshComponentAffiche un mesh statique déjà fabriqué (un asset)La majorité des objets de décor et de props
UProceduralMeshComponentAffiche une géométrie générée en code (sommets + triangles)Nos meshs de S13-S14, zéro asset
USceneComponentComposant avec Transform mais sans rendu (point d’attache)Quand on veut un point pivot ou un parent invisible
UPointLightComponentLumière ponctuelleLampes, projectiles lumineux
UCameraComponentCaméraPawns avec POV personnalisé
USkeletalMeshComponentMesh animé avec squelettePersonnages, créatures
UAudioComponentSource sonore 3DSons attachés à des objets
UHierarchicalInstancedStaticMeshComponentSpawn massif d’instances du même meshScattering (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 monde
FRotator Rot = GetActorRotation();       // rotation dans le monde (degrés)
FVector Scale = GetActorScale3D();       // échelle dans le monde
FTransform 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=0
SetActorScale3D(FVector(2.0f));                   // tout en double
SetActorTransform(NouveauTransform);

Modification relative (adders, plus pratiques pour de l’animation) :

AddActorWorldOffset(FVector(10.0f, 0.0f, 0.0f));        // décale en world space
AddActorWorldRotation(FRotator(0.0f, 5.0f, 0.0f));      // rotation en world space
AddActorLocalOffset(FVector(0.0f, 0.0f, 50.0f));        // décale dans le repère local
AddActorLocalRotation(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 Actor
Params.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 avant BeginPlay, 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, filtrer
if (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.

FVector Origin, BoxExtent;
GetActorBounds(true, Origin, BoxExtent);  // true = ignorer les composants cachés

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++) :

UStaticMeshComponent* Mesh = FindComponentByClass<UStaticMeshComponent>();
 
TArray<UStaticMeshComponent*> AllMeshes;
GetComponents<UStaticMeshComponent>(AllMeshes);

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 Actors
SetActorHiddenInGame(true);                          // cacher visuellement (l'Actor existe encore)
SetActorEnableCollision(false);                      // désactiver collision globale
SetActorTickEnabled(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

  1. Dans l’éditeur Unreal, Tools → New C++ Class…
  2. Sélectionner Actor comme classe parente, cliquer Next.
  3. Nommer MyTestActor (sans le préfixe A : Unreal l’ajoute automatiquement).
  4. Vérifier que la classe est dans Public, cliquer Create Class.
  5. 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.

NameMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("NameMesh"));

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.

AppendBox : la signature

void AMyTestActor::AppendBox(const FVector& C, const FVector& H,
    TArray<FVector>& Verts, TArray<int32>& Tris,
    TArray<FVector>& Normals, TArray<FVector2D>& UVs)
  • 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.

Le lambda AddQuad

auto AddQuad = [&](const FVector& P0, const FVector& P1,
                   const FVector& P2, const FVector& P3, const FVector& N)
{ ... };

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.

Verts.Add(P0); Verts.Add(P1); Verts.Add(P2); Verts.Add(P3);

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).

UVs.Add(FVector2D(0, 0)); UVs.Add(FVector2D(1, 0));
UVs.Add(FVector2D(1, 1)); UVs.Add(FVector2D(0, 1));

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.

Tris.Add(Base);     Tris.Add(Base + 2); Tris.Add(Base + 1);
Tris.Add(Base);     Tris.Add(Base + 3); Tris.Add(Base + 2);

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.

BuildMesh : assembler et envoyer au GPU

TArray<FVector>  Verts;   TArray<int32>     Tris;
TArray<FVector>  Normals; TArray<FVector2D> UVs;

Quatre tableaux vides au départ. AppendBox les remplira.

AppendBox(FVector::ZeroVector, FVector(VoxelSize), Verts, Tris, Normals, UVs);

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.

NameMesh->CreateMeshSection_LinearColor(
    0, Verts, Tris, Normals, UVs,
    TArray<FLinearColor>(), TArray<FProcMeshTangent>(), false);

Envoie le mesh au GPU :

  • 0 : numéro de section (un mesh procédural peut avoir plusieurs sections ; ici une seule suffit),
  • les 4 tableaux qu’on vient de remplir,
  • deux tableaux vides pour les couleurs par sommet et les tangentes : on n’en a pas besoin (le matériau par défaut fera l’éclairage),
  • false : pas de collision (rendu pur, pas de physique).

OnConstruction : régénérer dès l'éditeur

void AMyTestActor::OnConstruction(const FTransform& Transform)
{
    Super::OnConstruction(Transform);
    BuildMesh();
}

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 : la rotation continue

void AMyTestActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    AddActorWorldRotation(FRotator(0.f, RotationSpeed * DeltaTime, 0.f));
}

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.

Puis remplacez le corps de BuildMesh :

void AMyTestActor::BuildMesh()
{
    TArray<FVector>  Verts;   TArray<int32>     Tris;
    TArray<FVector>  Normals; TArray<FVector2D> UVs;
 
    const float   Cell   = VoxelSize;
    const FVector Half(Cell * 0.45f);                    // voxel cubique + petit espace
    const int32   Stride = 6;                            // 5 colonnes de lettre + 1 d'espace
    const int32   Count  = DisplayName.Len();
    const float   CenterCol = (Count > 0) ? ((Count - 1) * Stride + 4) * 0.5f : 0.f;
 
    for (int32 i = 0; i < Count; ++i)
    {
        const FGlyph5x7* G = GetGlyph(FoldToAZ(DisplayName[i]));
        if (!G) { continue; }                            // espace / hors A-Z : on saute
        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,  // colonnes -> Y (gauche/droite)
		                (3 - row) * Cell);                         // lignes  -> Z (haut/bas)
		            AppendBox(Center, Half, Verts, Tris, Normals, UVs);
		        }
	      }
    }
 
    NameMesh->ClearAllMeshSections();
    NameMesh->CreateMeshSection_LinearColor(
        0, Verts, Tris, Normals, UVs,
        TArray<FLinearColor>(), TArray<FProcMeshTangent>(), false);
}

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.

Le namespace anonyme et la table de police

namespace
{
    struct FGlyph5x7 { const char* Rows[7]; };
 
    const FGlyph5x7* GetGlyph(TCHAR c)
    {
        static const FGlyph5x7 Font[26] = { /* A, B, C, ... */ };
        if (c < TEXT('A') || c > TEXT('Z')) { return nullptr; }
        return &Font[c - TEXT('A')];
    }
}

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 :

  1. 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.
  2. Le switch replie 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

const float   Cell   = VoxelSize;
const FVector Half(Cell * 0.45f);
const int32   Stride = 6;
const int32   Count  = DisplayName.Len();
const float   CenterCol = (Count > 0) ? ((Count - 1) * Stride + 4) * 0.5f : 0.f;
  • 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

NameMesh->ClearAllMeshSections();
NameMesh->CreateMeshSection_LinearColor(
    0, Verts, Tris, Normals, UVs,
    TArray<FLinearColor>(), TArray<FProcMeshTangent>(), false);

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

  1. Compilez (bouton Compile d’Unreal, ou Ctrl+F9 dans Rider).
  2. 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.

  1. L’éditeur Blueprint s’ouvre. (Variante : clic droit dans le Content Browser → Blueprint Class → dépliez All Classes, cherchez MyTestActor.)

  2. 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.

  3. 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.

  4. 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

  1. Compilez (bouton Compile d’Unreal, ou Ctrl+F9 dans Rider).

  2. Content Browser → C++ Classes → VotreProjet, glissez MyTestActor dans le viewport. Votre prénom apparaît en voxels sans lancer Play.

  3. 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.

  4. 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 :

  1. **Hérité de ****AActor** pour avoir un Transform, un cycle de vie et des composants.
  2. **Ajouté un ****UProceduralMeshComponent** : au lieu d’afficher un asset, vous fabriquez la géométrie en code.
  3. **É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).
  4. **Utilisé ****OnConstruction** (le hook vu en théorie) pour régénérer le mesh à chaque modification dans l’éditeur, sans Play.
  5. **Activé le ****Tick** avec DeltaTime pour une rotation indépendante du framerate.
  6. **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).

Lecture

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 FoldToAZ et 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 public
UPROPERTY(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
// MyTestActor.cpp
void AMyTestActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    if (!bRotate) return;
 
    const FVector Axis = RotationAxis.GetSafeNormal();
    const float AngleRad = FMath::DegreesToRadians(RotationSpeed * DeltaTime);
    AddActorWorldRotation(FQuat(Axis, AngleRad));
}

Solution Défi 2 - Oscillation sinusoïdale

// MyTestActor.h
UPROPERTY(EditAnywhere, Category = "Test") float FloatAmplitude = 50.f;
UPROPERTY(EditAnywhere, Category = "Test") float FloatSpeed = 2.f;
 
private:
    float ElapsedTime = 0.f;
    FVector StartLocation;
// MyTestActor.cpp
void AMyTestActor::BeginPlay()
{
    Super::BeginPlay();
    StartLocation = GetActorLocation();
}
 
void AMyTestActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
 
    ElapsedTime += DeltaTime;
    FVector NewLocation = StartLocation;
    NewLocation.Z += FMath::Sin(ElapsedTime * FloatSpeed) * FloatAmplitude;
    SetActorLocation(NewLocation);
 
    if (bRotate)
    {
        AddActorWorldRotation(FQuat(RotationAxis.GetSafeNormal(),
            FMath::DegreesToRadians(RotationSpeed * DeltaTime)));
    }
}

Solution Défi 3 - Dynamic Material Instance

// MyTestActor.h
UPROPERTY(EditAnywhere, Category = "Test")
FLinearColor CubeColor = FLinearColor::Red;
 
private:
    UPROPERTY()
    UMaterialInstanceDynamic* DynMaterial = nullptr;
// MyTestActor.cpp - dans BeginPlay
if (UMaterialInterface* BaseMat = NameMesh->GetMaterial(0))
{
    DynMaterial = UMaterialInstanceDynamic::Create(BaseMat, this);
    NameMesh->SetMaterial(0, DynMaterial);
    DynMaterial->SetVectorParameterValue(TEXT("BaseColor"), CubeColor);
}

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;
};
// RowSpawner.cpp
#include "RowSpawner.h"
 
ARowSpawner::ARowSpawner()
{
    PrimaryActorTick.bCanEverTick = false;
}
 
void ARowSpawner::BeginPlay()
{
    Super::BeginPlay();
    if (!ActorToSpawn) return;
 
    const FVector Origin = GetActorLocation();
    for (int32 i = 0; i < Count; ++i)
    {
        const FVector SpawnLoc = Origin + FVector(i * Spacing, 0.f, 0.f);
        GetWorld()->SpawnActor<AActor>(ActorToSpawn, SpawnLoc, FRotator::ZeroRotator);
    }
}

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 GetGlyph avant 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.

Lien vers l'original

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 :

  1. 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.

  2. 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

  1. Dans l’éditeur Unreal, Tools → New C++ Class…

  2. Classe parente Actor, Next.

  3. Nommer HeldCube (sans le préfixe A, Unreal l’ajoute), vérifier Public, Create Class.

  4. 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.

Le constructeur : Tick, mesh et collision

PrimaryActorTick.bCanEverTick = true;
 
CubeMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("CubeMesh"));
RootComponent = CubeMesh;
 
CubeMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);

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.

Tick, étape 1 : récupérer la caméra du joueur

APlayerCameraManager* Cam = UGameplayStatics::GetPlayerCameraManager(this, 0);
if (!Cam) { return; }

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)

const FVector  CamLoc = Cam->GetCameraLocation();
const FRotator CamRot = Cam->GetCameraRotation();
 
const FVector Forward  = CamRot.Vector();
const FVector TargetLoc = CamLoc
    + Forward * HoldDistance
    + FVector::UpVector * HoldHeightOffset;

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

const FVector NewLoc =
    FMath::VInterpTo(GetActorLocation(), TargetLoc, DeltaTime, FollowSpeed);
SetActorLocation(NewLoc);

VInterpTo(Current, Target, DeltaTime, Speed) est la brique d’interpolation amortie d’Unreal. À chaque frame, elle calcule une nouvelle position entre Current 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

const FRotator NewRot =
    FMath::RInterpTo(GetActorRotation(), CamRot, DeltaTime, RotationFollowSpeed);
SetActorRotation(NewRot);

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 passe DeltaTime à 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

  1. Compiler (bouton Compile d’Unreal, ou Ctrl+F9 dans Rider).
  2. Content Browser → C++ Classes → VotreProjet, glisser HeldCube dans le niveau, n’importe où.
  3. Lancer Play. Un cube apparaît à ~1 m devant vous, légèrement sous la ligne des yeux.
  4. Déplacez-vous, tournez la caméra : le cube vous suit, mais traîne un peu avant de se recaler. C’est l’inertie.
  5. 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.

Vrai wobble (ressort-amortisseur) + bob vertical

Le modèle : masse-ressort-amortisseur https://fr.wikipedia.org/wiki/Oscillateur_harmonique

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 :

Acceleration = Raideur * (Cible - Position) - Amortissement * Vitesse

Puis on intègre dans le temps (méthode d’Euler, la plus simple) :

Vitesse  += Acceleration * DeltaTime
Position += Vitesse      * DeltaTime

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 fort
UPROPERTY(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.

Le ressort : accélération depuis l’écart

const FVector Current = GetActorLocation();
const FVector Displacement = TargetLoc - Current;
const FVector Accel = Displacement * Stiffness
                    - CubeVelocity * Damping;

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.

L’intégration d’Euler : vitesse, puis position

CubeVelocity += Accel * DeltaTime;
SetActorLocation(Current + CubeVelocity * DeltaTime);

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.

La rotation : on garde RInterpTo

SetActorRotation(
    FMath::RInterpTo(GetActorRotation(), CamRot, DeltaTime, RotationFollowSpeed));

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

const float TargetSpeed =
    (TargetLoc - PrevTargetLoc).Size() / FMath::Max(DeltaTime, KINDA_SMALL_NUMBER);
PrevTargetLoc = TargetLoc;

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

SmoothedSpeed = FMath::FInterpTo(SmoothedSpeed, TargetSpeed, DeltaTime, 5.f);

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;
const float SpeedFactor = FMath::Clamp(SmoothedSpeed / BobSpeedRef, 0.f, 1.f);
const float Bob = FMath::Sin(BobTime) * BobAmplitude * SpeedFactor;
  • 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 SpeedFactor module 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 TargetLoc
const 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 cm
 
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Bob")
float BobFrequency = 9.f;      // vitesse de l'oscillation
 
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Bob")
float BobSpeedRef = 300.f;     // vitesse cible (cm/s) pour un bob à pleine amplitude
 
private:
    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 :

    const FVector Current = GetActorLocation();
    const FVector Displacement = BobbedTarget - Current; // Ici, remplacez TargetLoc par BobbedTarget
    const FVector Accel = Displacement * Stiffness - CubeVelocity * Damping;
    CubeVelocity += Accel * DeltaTime;
    SetActorLocation(Current + CubeVelocity * DeltaTime);

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.

Récapitulatif des concepts

ConceptRôle iciD’où ça vient
GetPlayerCameraManager, GetCameraLocation/RotationLire le joueur sans dépendre du personnageNouveau
Forward vector (FRotator::Vector()) + multiplicationPoint devant la caméraMaths S02-03
FMath::VInterpTo / RInterpToInertie simple (lag, ease-out)Nouveau
Ressort-amortisseur a = k·(cible-pos) - c·vWobble physique (dépassement, oscillation)Maths S02-03
Intégration d’Euler (v += a·dt, p += v·dt)Simuler le mouvement frame par frameDeltaTime S13
FMath::Sin piloté par la vitesseBob vertical (secondary motion)Sin du Défi 2, S13
UProceduralMeshComponent + AppendBox, Tick, UPROPERTY, * DeltaTimeSquelette de l’Actor + cube généré en C++S13
Variable d’état membre (CubeVelocity)Mémoriser la vitesse entre framesNouveau

Erreurs courantes

  • 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).

Travail personnel

Lecture

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".

Solutions

Essayez d’abord par vous-même avant de regarder.

Solution Défi 1 - Banking

// .h
UPROPERTY(EditAnywhere, Category = "Bank") float BankStrength = 0.4f;   // degrés par (deg/s) de yaw
UPROPERTY(EditAnywhere, Category = "Bank") float BankMax = 25.f;        // roll max
private:
    float PrevYaw = 0.f;
    float SmoothedRoll = 0.f;
// dans Tick, après avoir CamRot
const float YawRate = FMath::FindDeltaAngleDegrees(PrevYaw, CamRot.Yaw) / FMath::Max(DeltaTime, KINDA_SMALL_NUMBER);
PrevYaw = CamRot.Yaw;
 
const float TargetRoll = FMath::Clamp(-YawRate * BankStrength, -BankMax, BankMax);
SmoothedRoll = FMath::FInterpTo(SmoothedRoll, TargetRoll, DeltaTime, 6.f);
 
FRotator DesiredRot = CamRot;
DesiredRot.Roll += SmoothedRoll;
SetActorRotation(FMath::RInterpTo(GetActorRotation(), DesiredRot, DeltaTime, RotationFollowSpeed));

FindDeltaAngleDegrees gère proprement le passage de 359° à 0°. Le signe - fait pencher le cube dans le virage.

Solution Défi 2 - Couleur selon la vitesse

// .h
UPROPERTY(EditAnywhere, Category = "Color") FLinearColor SlowColor = FLinearColor(0.05f, 0.2f, 0.8f);
UPROPERTY(EditAnywhere, Category = "Color") FLinearColor FastColor = FLinearColor(0.9f, 0.3f, 0.05f);
private:
    UPROPERTY() UMaterialInstanceDynamic* DynMat = nullptr;
// 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 Tick
if (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).

Solution Défi 3 - VectorSpringInterp

#include "Kismet/KismetMathLibrary.h"
 
// .h
private:
    FVectorSpringState SpringState;
// 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);                 // TargetVelocityAmount
 
SetActorLocation(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.

Lien vers l'original