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