Files
Unreal_EmberWild/Docs/CharacterCustomization.md
2026-08-11 15:23:10 +02:00

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

  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.

  1. Remplis Heads, Hairstyles, Eyebrows — ces listes appartiennent à la silhouette
  2. 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 GraphClass 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().