# Customisation de personnage Le joueur choisit sa silhouette, son visage, sa coiffure et ses couleurs avant de lancer une partie. Les trois autres joueurs le voient tel qu'il s'est fait. Ce document a **deux moitiés**, à lire séparément : - **Partie A — pour l'artiste** : ajouter une coiffure, une couleur, un corps. Aucune ligne de code, aucune recompilation. Commence au §1. - **Partie B — pour le développeur** : architecture, réseau, pièges du moteur. Commence au §6. --- # PARTIE A — Ajouter du contenu ## 1. Le principe en une phrase **Tout passe par un seul asset : `DA_CharacterParts`.** C'est la table qui dit « l'option n° 3 de la catégorie Cheveux, c'est ce mesh-là ». Le code ne connaît que des numéros ; c'est le catalogue qui leur donne un sens. Conséquence directe : **ajouter du contenu ne demande jamais de toucher au code**, et jamais de fermer l'éditeur. ``` DA_CharacterParts ├── Body Sets une entrée par silhouette (A, B, …) │ ├── Body Meshes les morceaux du corps │ ├── Anim Class l'AnimBP qui l'anime │ ├── Heads ┐ │ ├── Hairstyles │ les listes d'options, │ ├── Eyebrows │ propres à CETTE silhouette │ ├── Beards │ │ └── Mustaches ┘ └── Palettes partagées par toutes les silhouettes ├── Skin Colors ├── Hair Colors ├── Eye Colors ├── Underwear Colors └── Eye Materials ``` ## 2. Ajouter une coiffure 1. Ouvre `Content/Game/Data/Character/DA_CharacterParts` 2. Déplie **Body Sets** → l'entrée du corps concerné (`[0]` = A, `[1]` = B) 3. Déplie **Hairstyles** → clique le **+** 4. Dans la nouvelle entrée : - **Mesh** = ton `SKM_…` - **Icon** = la vignette (facultative, voir §5) 5. Sauvegarde C'est tout. La coiffure apparaît dans l'écran au prochain lancement. > **L'ORDRE DE LA LISTE EST LE CONTRAT.** Un index est ce qui part sur le réseau > et ce qui se sauvegarde. **Insérer une entrée au milieu décale tout ce qui > suit** : les personnages déjà créés changeront de coiffure sans prévenir. > **Ajoute toujours à la fin.** Même procédé pour **Heads**, **Eyebrows**, **Beards**, **Mustaches**. ## 3. Ajouter une couleur 1. Déplie **Palettes** → la palette voulue (`Skin Colors`, `Hair Colors`…) 2. Clique le **+** 3. Double-clique la case de couleur → dans le picker, colle une valeur dans le champ **Hex sRGB** (le plus fiable) ou règle R/G/B à la main 4. Sauvegarde Les palettes sont **partagées par toutes les silhouettes** : une teinte de peau ajoutée profite à A comme à B. Deux nuanciers à tenir à jour divergeraient au premier ajout. > Même règle d'ordre qu'au §2 : **on ajoute à la fin**. ### Ce que chaque palette pilote | Palette | Ce qu'elle teint | |---|---| | **Skin Colors** | le corps **et** le visage, ensemble | | **Hair Colors** | cheveux, **barbe, moustache et sourcils** en même temps | | **Eye Colors** | l'iris | | **Underwear Colors** | le sous-vêtement | Une barre de couleur de cheveux qui ne teindrait pas la barbe donnerait un personnage incohérent — c'est voulu qu'elles suivent. ## 4. Ajouter une silhouette Plus lourd, mais toujours sans code. 1. **Body Sets** → **+** → une nouvelle entrée 2. **Body Meshes** → ajoute ton `SKM_` de corps (une seule case aujourd'hui) 3. **Anim Class** → l'AnimBP à utiliser > **Tu peux réutiliser `ABP_PlayerBody_A`** si ton nouveau corps partage les > mêmes **noms d'os**. Le moteur valide la compatibilité par les noms, pas par > l'identité du squelette. C'est ce qui permet à Body A et Body B de partager un > seul AnimBP : une correction profite aux deux. 4. Remplis **Heads**, **Hairstyles**, **Eyebrows** — ces listes appartiennent à la silhouette 5. Laisse **Beards** et **Mustaches** vides si elle n'en porte pas : les catégories **disparaissent d'elles-mêmes** de la colonne de l'écran ## 5. Les vignettes Le champ **Icon** est un `UTexture2D`, exactement comme l'icône d'un objet d'inventaire. **Il est facultatif.** Une entrée sans vignette affiche son **numéro** dans la case — l'option reste choisissable. C'est délibéré : le catalogue doit rester utilisable avant que les trente-six vignettes existent, et le moyen le plus simple de les fabriquer est justement de capturer l'écran de customisation une fois qu'il tourne. ## 6. Le décor de l'écran Le personnage montré pendant la customisation est un acteur posé dans `MenuScene` : **`BP_CharacterPreview`**. Trois choses comptent quand tu le places : - **Hors du champ des `MenuCameraSpot`**, sinon il apparaît en arrière-plan du menu principal - **Devant un fond qui tient debout** — c'est ce qu'on voit derrière lui pendant toute la customisation - **Éclairé de face.** On passe l'essentiel du temps en gros plan sur le visage ; à contre-jour, les teints de peau deviennent illisibles et c'est précisément ce que le joueur règle ### Les deux cadrages `FullBodyViewSpot` et `FaceViewSpot` sont deux repères vides que tu **déplaces à la souris** dans le viewport du Blueprint. La caméra glisse de l'un à l'autre selon la catégorie ouverte : | Catégorie ouverte | Cadrage | |---|---| | Silhouette, Teint, Sous-vêtement | `FullBodyViewSpot` | | tout le reste | `FaceViewSpot` | Pense à **décaler le personnage vers la droite** de l'image : la colonne d'UI occupe la gauche. --- # PARTIE B — Architecture ## 7. Vue d'ensemble | Fichier | Rôle | |---|---| | `CharacterAppearanceTypes.h` | `FCharacterAppearance` (11 octets), les enums, `FCharacterPartEntry` | | `CharacterPartsDataAsset` | le catalogue : index → assets, palettes, noms de paramètres matériau | | `CharacterAppearanceComponent` | monte les pièces, pose les couleurs. Sur le pawn **et** sur le mannequin | | `CharacterPreviewActor` | le mannequin du menu : mesh + caméra + cadrages | | `CharacterCustomizationWidget` | l'écran, la colonne, la grille | | `CustomizationCategoryWidget` | une rangée de la colonne | | `CustomizationOptionWidget` | une case : vignette **ou** pastille | | `SurvivalPlayerState` | porte l'apparence répliquée | | `SurvivalUserSettings` | la persiste dans le `.ini` | ## 8. `FCharacterAppearance` — pourquoi des index 11 octets, **que des index**, jamais de `TSoftObjectPtr` ni de chemin d'asset. Deux raisons qui vont dans le même sens : - un chemin coûte une chaîne à chaque réplication, un index tient dans un octet ; - un index se **valide** contre le catalogue, alors qu'un chemin envoyé par un client pourrait désigner n'importe quel asset du jeu. Bénéfice collatéral : la struct se sérialise telle quelle dans le `.ini` (`CharacterAppearance=(BodyType=BodyA,HeadIndex=0,…)`). Une apparence faite de références d'assets aurait donné une douzaine de chemins à écrire, qu'une seule renommée aurait cassés silencieusement. `NoPart` vaut **255** : c'est « aucune pièce », pour une barbe rasée. ## 9. Le montage — `UCharacterAppearanceComponent` Un `USkeletalMeshComponent` par catégorie, enfant du mesh porteur, en **Leader Pose**. Un seul mesh porte l'animation, les autres recopient sa pose **par nom d'os** — ce qui marche même entre squelettes distincts. Le composant recopie sur chaque pièce le `bOwnerNoSee` et le `bCastHiddenShadow` du porteur. C'est ce qui fait que le **mannequin du menu est visible** sans une ligne de code de plus, alors que le corps du joueur reste invisible à son propriétaire. ### Le corps se monte depuis une LISTE `BodyMeshes` est un `TArray` même s'il ne contient qu'une case. Le pack livre exprès `SKM_BodyA_torso`, `_arms`, `_legs_01/02/03`, `_hands`, `_feet` : le jour où une armure devra cacher les segments qu'elle recouvre, la case 0 deviendra `SKM_BodyA_empty` — porteur d'animation invisible — et les huit segments suivront derrière, **sans que ce code change**. > **Contrainte à vérifier avant tout achat de pack d'armure : il doit être > skinné sur `SK_BodyA` / `SK_BodyB`.** ### Les couleurs Chaque paramètre est posé sur **tous les éléments** de chaque mesh, sans chercher lequel le porte : un `SetVectorParameterValue` dont le paramètre n'existe pas est simplement ignoré. Le slot des yeux ignore donc `Skin`, celui de la peau ignore `Eye color`. Cela évite une table d'index de slots qui casserait au premier mesh mal rangé. ## 10. Réseau et persistance ``` USurvivalUserSettings ──lit──► AnnounceCharacterAppearance() (.ini du joueur) │ ▼ SubmitCharacterAppearance() ← POINT D'ENTRÉE UNIQUE │ │ persiste Server_SetCharacterAppearance │ ASurvivalPlayerState (autorité) │ ┌──────────┴──────────┐ OnRep (clients) appel direct (hôte) └──────────┬──────────┘ ▼ ApplyAppearanceToPawn() ``` **`SubmitCharacterAppearance()` persiste ET annonce, jamais l'un sans l'autre.** Persister sans annoncer laisserait le joueur seul à se voir changé ; annoncer sans persister le ferait repartir au visage par défaut au lancement suivant. L'apparence vit sur le **PlayerState** et pas sur le pawn : elle décrit le JOUEUR, pas le corps. Le pawn meurt et réapparaît, le PlayerState non — réapparaître avec le visage d'un autre n'aurait aucun sens, alors que perdre son inventaire au sol est la règle du jeu. **Rien n'est validé côté PlayerState, délibérément** : borner un index demande le catalogue, que seul le composant connaît — or il passe déjà tout ce qu'il monte par `Catalog->Sanitize()`, sur chaque machine. ## 11. Le parcours de l'écran ``` clic Jouer → fondu au noir → SetViewTarget(BP_CharacterPreview) → colonne affichée → fondu depuis le noir ├─ « Jouer » → apparence persistée → fondu de départ → GameScene └─ « Retour » → fondu → nouvel angle de menu ``` On ne charge **aucune map** : le mannequin est posé dans `MenuScene`. La bascule de caméra se fait pendant que l'écran est noir — un travelling depuis l'angle du menu traverserait le décor. **L'écran part toujours de l'apparence par défaut**, jamais de la persistée. C'est un provisoire assumé : à terme l'apparence appartiendra à la **sauvegarde** et non au joueur, et l'on regardera au clic sur *Jouer* si cette partie a déjà un personnage. `SetupCustomization(Preview, Appearance)` prend déjà l'apparence en paramètre — il n'y aura qu'à changer son origine. Laisser **`Customization Menu Class` vide** sur `BP_MainMenuPlayerController` restaure l'ancien comportement (Jouer lance directement). Pratique pour tester autre chose sans traverser l'écran. ## 12. Les pièges du moteur rencontrés Aucun n'était visible depuis le code. Ils sont ici pour ne pas être redécouverts. ### Les `UFUNCTION(Exec)` d'un `UActorComponent` ne sont JAMAIS appelées La chaîne de routage (`UPlayer::Exec`, dans `Player.cpp`) interroge le monde, le `PlayerInput`, le contrôleur, le **pawn**, le HUD, le GameMode, le `CheatManager`, le GameState et le camera manager — **jamais les composants**, `AActor` ne surchargeant pas `ProcessConsoleExec` pour les parcourir. Et rien ne proteste : la commande est introuvable, sans message. D'où `EmberPart` / `EmberColor` / `EmberAppearanceDump` posées sur `AFpsPlayer`, qui délègue. *(`EmberJoin` marche parce que `UGameInstance`, lui, route vers ses subsystems.)* ### `OverrideMaterials` survit à un changement de mesh `SetSkeletalMeshAsset` remplace l'asset mais ne touche pas au tableau `OverrideMaterials`, qui appartient au **composant**. Sans vidage préalable, passer du corps A au corps B garde le MID dérivé de `MI_BodyA_a` : on obtient une silhouette féminine peinte avec la peau et les abdominaux du modèle masculin. Le symptôme trompe, parce que le mesh, lui, a bien changé. D'où `SetPartMesh()`, qui appelle `EmptyOverrideMaterials()` avant d'assigner — et ne fait rien si le mesh est déjà le bon, pour que les MID survivent à un simple changement de couleur. ### L'ordre des slots de matériau varie d'un mesh à l'autre Sur `SKM_HeadA_01`, l'**Element 0 est celui des YEUX** et l'Element 1 celui de la peau — l'inverse de l'intuition. Les six têtes viennent de deux packs différents, rien ne garantit qu'elles s'accordent. On cherche donc le slot **par son nom** (`EyeMaterialSlotHint`, valeur `Eye`), et si aucun ne correspond on ne pose rien et on l'écrit dans le log : écraser le slot de la peau repeint le visage entier de la couleur des yeux, symptôme bien plus long à diagnostiquer qu'une ligne de log. ### Un nom de paramètre matériau se lit dans le Material INSTANCE Le binaire de `M_Head_Base` contient la chaîne « Skin Base color », mais le paramètre réellement exposé s'appelle **`Skin`**. Le chercher sous l'autre nom dans `MI_HeadA_01_a` ne rend aucun résultat. Corollaire : les noms de paramètres sont des `UPROPERTY` du catalogue et pas des constantes C++, pour qu'une erreur se corrige sans recompiler. ### `LeaderPoseComponent` est `BlueprintReadOnly` Elle n'apparaît **jamais** dans le Details panel — la section « Leader Pose Component » qu'on y voit ne contient que des cases de bornes et de LOD. Elle ne s'assigne que par `SetLeaderPoseComponent()`, en C++ ou en Construction Script. ### `Approx Size` d'un Skeletal Mesh n'est pas sa hauteur visible Les bounds incluent une marge et celles du Physics Asset. `SKM_BodyA` affiche 184 alors que le personnage entier mesure à peu près autant. Pour juger une taille : viewport en vue **Front**, et la ligne verte (`Z = 0`) comme repère — le bas de la capsule est à `-HalfHeight`, le haut à `+HalfHeight`. ### `Initialize` masque une virtuelle d'`UUserWidget` `UUserWidget::Initialize()` existe et est virtuelle. Une surcharge de signature différente la **masque** au lieu de la surcharger (C4263/C4264). D'où `SetupCustomization`. Même famille de piège que les membres nommés `Player` ou `Slot` dans une classe dérivée du moteur. ## 13. Les commandes console Sur le pawn, en partie. Elles appliquent en local **et** soumettent (persistance + annonce au serveur), donc elles testent toute la chaîne. ``` EmberPart body 0..1 silhouette EmberPart head 0..5 visage EmberPart hair 0..12 coiffure EmberPart eyebrows 0..5 sourcils EmberPart beard 0..5 barbe EmberPart mustache 0..5 moustache EmberPart eyes 0..4 forme d'iris EmberPart beard -1 retire la pièce EmberColor skin 0..11 teint EmberColor hair 0..5 cheveux + barbe + sourcils + moustache EmberColor eye 0..4 iris EmberColor underwear 0..3 sous-vêtement EmberAppearanceDump état courant + nombre d'options par catégorie ``` `EmberAppearanceDump` est le premier réflexe de diagnostic : un compte à **0** signale une liste vide dans le catalogue. ## 14. Dépannage | Symptôme | Cause | |---|---| | La colonne de l'écran est vide | `Catalog`, `Category Widget Class` ou `Option Widget Class` non assignés dans les **Class Defaults** de `WBP_CharacterCustomization` (mode **Graph** → *Class Defaults*) | | *Jouer* lance la partie sans montrer l'écran | `Customization Menu Class` vide sur `BP_MainMenuPlayerController` | | Écran noir qui ne s'éclaircit pas | aucun `BP_CharacterPreview` dans la map — un avertissement l'écrit dans l'Output Log | | Le visage ne suit pas la couleur de peau | `Head Skin Color Parameter` faux dans le catalogue (`Skin`, pas `Skin Base color`) | | Le visage entier prend la couleur des yeux | `Eye Material Slot Hint` ne correspond à aucun slot — le log liste les slots disponibles | | Le corps B garde la peau du corps A | régression de `SetPartMesh()` : les overrides ne sont plus vidés | | Une pièce flotte détachée du corps | leader pose non posé sur ce composant | | Corps figé en pose de référence | `Anim Class` absente du Body Set | | Les pastilles de couleur sont invisibles | le pinceau d'`OptionImage` est en `Draw As = Image` sans texture — un pinceau en mode Image sans texture ne peint rien | | En PIE à 2, les deux joueurs sont identiques | normal : les deux fenêtres lisent le même `.ini`. Change l'un des deux à la console | ## 15. Limites connues - **Les bras vus en 1re personne ne sont pas customisés.** Leur squelette (`BasePose_Skeleton`) n'a rien à voir avec `SK_BodyA` ; les rendre customisables imposerait de retargeter toutes les animations FPS. Raft assume le même compromis. - **La physique de cheveux est perdue en leader pose.** Coiffures, barbes et moustaches ont leur physics asset, mais l'AnimBP post-process d'un *follower* ne tourne pas. La démo du pack accepte déjà ce compromis. - **Tatouages et maquillage ne sont pas exposés.** `M_Head_Base` et `M_Body_Base` en ont pourtant les paramètres : ce ne sont que des couleurs et des masques de plus à ajouter au catalogue, aucune refonte. - **Pas d'animation accroupie.** Le pack n'en livre pas. Mais le squelette est celui du **Mannequin UE5** (`ik_foot_root`, `index_metacarpal_l`…), donc toute animation vendue « UE5 Mannequin compatible » se branche **sans retarget**. ## 16. Étendre **Ajouter une catégorie** (tatouages, maquillage) : 1. un champ dans `FCharacterAppearance` 2. une valeur dans `ECharacterPartCategory` (mesh) ou une palette (couleur) 3. une valeur dans `ECustomizationCategory` + son libellé dans `CharacterCustomizationLabels::Get` 4. les branches correspondantes dans `GetOptionCountForCategory`, `GetCurrentIndexForCategory` et `SetIndexForCategory` L'écran, lui, n'a rien à changer : la colonne se construit depuis l'enum. **Brancher les sauvegardes** : une seule ligne, dans `AMainMenuPlayerController::OpenCustomization()` — passer l'apparence de la save au lieu de `FCharacterAppearance()`.