18 KiB
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
- Ouvre
Content/Game/Data/Character/DA_CharacterParts - Déplie Body Sets → l'entrée du corps concerné (
[0]= A,[1]= B) - Déplie Hairstyles → clique le +
- Dans la nouvelle entrée :
- Mesh = ton
SKM_… - Icon = la vignette (facultative, voir §5)
- Mesh = ton
- 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
- Déplie Palettes → la palette voulue (
Skin Colors,Hair Colors…) - Clique le +
- 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
- 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.
- Body Sets → + → une nouvelle entrée
- Body Meshes → ajoute ton
SKM_de corps (une seule case aujourd'hui) - Anim Class → l'AnimBP à utiliser
Tu peux réutiliser
ABP_PlayerBody_Asi 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.
- Remplis Heads, Hairstyles, Eyebrows — ces listes appartiennent à la silhouette
- 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 avecSK_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_BaseetM_Body_Baseen 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) :
- un champ dans
FCharacterAppearance - une valeur dans
ECharacterPartCategory(mesh) ou une palette (couleur) - une valeur dans
ECustomizationCategory+ son libellé dansCharacterCustomizationLabels::Get - les branches correspondantes dans
GetOptionCountForCategory,GetCurrentIndexForCategoryetSetIndexForCategory
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().