// Fill out your copyright notice in the Description page of Project Settings. #include "CharacterAppearanceComponent.h" #include "Components/SkeletalMeshComponent.h" #include "CharacterPartsDataAsset.h" #include "Engine/SkeletalMesh.h" #include "GameFramework/Character.h" #include "Materials/MaterialInstanceDynamic.h" namespace { /** * Vrai si les deux apparences designent exactement les memes PIECES. * * Les couleurs en sont volontairement exclues : elles se posent sur des * materiaux deja en place, sans toucher au montage. C'est cette distinction * qui evite qu'un curseur de couleur glisse reengendre six meshes par image. * * Prefixe du fichier oblige : le build unity concatene les .cpp du module, * un namespace anonyme n'isole rien. */ bool AppearanceHasSameParts(const FCharacterAppearance& A, const FCharacterAppearance& B) { return A.BodyType == B.BodyType && A.HeadIndex == B.HeadIndex && A.HairIndex == B.HairIndex && A.EyebrowsIndex == B.EyebrowsIndex && A.BeardIndex == B.BeardIndex && A.MustacheIndex == B.MustacheIndex && A.EyesIndex == B.EyesIndex; } /** Traduit l'argument d'une commande console en categorie. */ bool AppearanceParsePartCategory(const FString& Name, ECharacterPartCategory& OutCategory) { if (Name.Equals(TEXT("head"), ESearchCase::IgnoreCase)) { OutCategory = ECharacterPartCategory::Head; return true; } if (Name.Equals(TEXT("hair"), ESearchCase::IgnoreCase)) { OutCategory = ECharacterPartCategory::Hair; return true; } if (Name.Equals(TEXT("eyebrows"), ESearchCase::IgnoreCase)) { OutCategory = ECharacterPartCategory::Eyebrows; return true; } if (Name.Equals(TEXT("beard"), ESearchCase::IgnoreCase)) { OutCategory = ECharacterPartCategory::Beard; return true; } if (Name.Equals(TEXT("mustache"), ESearchCase::IgnoreCase)) { OutCategory = ECharacterPartCategory::Mustache; return true; } return false; } /** L'index de piece stocke dans l'apparence, par categorie. */ uint8* AppearancePartIndexPtr(FCharacterAppearance& Target, ECharacterPartCategory Category) { switch (Category) { case ECharacterPartCategory::Head: return &Target.HeadIndex; case ECharacterPartCategory::Hair: return &Target.HairIndex; case ECharacterPartCategory::Eyebrows: return &Target.EyebrowsIndex; case ECharacterPartCategory::Beard: return &Target.BeardIndex; case ECharacterPartCategory::Mustache: return &Target.MustacheIndex; default: return nullptr; } } } UCharacterAppearanceComponent::UCharacterAppearanceComponent() { // Aucun Tick : le montage est evenementiel, il ne se declenche qu'a // l'application d'une apparence. PrimaryComponentTick.bCanEverTick = false; PartComponents.SetNum(static_cast(ECharacterPartCategory::Count)); } void UCharacterAppearanceComponent::BeginPlay() { Super::BeginPlay(); // L'apparence par defaut, faute de mieux. Le lot reseau branchera ici la // lecture du PlayerState -- ce BeginPlay restera le repli du premier // lancement, quand aucun reglage n'a encore ete sauvegarde. ApplyAppearance(DefaultAppearance); } void UCharacterAppearanceComponent::OnUnregister() { // Les composants engendres appartiennent a l'acteur, pas a nous : ils // disparaissent avec lui. On lache seulement nos references, pour ne pas // retenir des objets que le GC doit pouvoir reprendre si le composant est // retire d'un acteur qui, lui, survit. PartComponents.Reset(); PartComponents.SetNum(static_cast(ECharacterPartCategory::Count)); ExtraBodyComponents.Reset(); bBuilt = false; Super::OnUnregister(); } void UCharacterAppearanceComponent::SetTargetMesh(USkeletalMeshComponent* InTargetMesh) { if (TargetMesh == InTargetMesh) { return; } TargetMesh = InTargetMesh; // Les pieces deja montees suivaient l'ancien porteur : les laisser en place // donnerait une tete qui garde la pose d'un corps dont on ne se sert plus. bBuilt = false; } USkeletalMeshComponent* UCharacterAppearanceComponent::ResolveTargetMesh() { if (TargetMesh) { return TargetMesh; } AActor* Owner = GetOwner(); if (!Owner) { return nullptr; } // L'ACharacter d'abord, et surtout pas « le premier SkeletalMesh trouve » : // AFpsPlayer porte aussi FirstPersonArms, et l'ordre des composants n'est // garanti par rien. if (const ACharacter* OwningCharacter = Cast(Owner)) { TargetMesh = OwningCharacter->GetMesh(); } if (!TargetMesh) { TargetMesh = Owner->FindComponentByClass(); } return TargetMesh; } USkeletalMeshComponent* UCharacterAppearanceComponent::CreateFollowerComponent(FName ComponentName) { AActor* Owner = GetOwner(); if (!Owner || !TargetMesh) { return nullptr; } USkeletalMeshComponent* Follower = NewObject(Owner, ComponentName); if (!Follower) { return nullptr; } Follower->SetupAttachment(TargetMesh); // Recopiees du porteur plutot que posees en dur : sur le pawn elles valent // « invisible pour moi, mais je garde mon ombre », sur le mannequin du menu // elles valent l'inverse. Le composant n'a pas a savoir dans lequel il vit. Follower->SetOwnerNoSee(TargetMesh->bOwnerNoSee); Follower->SetOnlyOwnerSee(TargetMesh->bOnlyOwnerSee); Follower->SetCastHiddenShadow(TargetMesh->bCastHiddenShadow); // Une piece d'apparence n'est pas une surface de jeu : la capsule porte la // collision, et une tete qui bloque un trace d'interaction se remarquerait // tout de suite. Follower->SetCollisionEnabled(ECollisionEnabled::NoCollision); Follower->RegisterComponent(); // Apres l'enregistrement : c'est la que le composant a son monde et sa // hierarchie, donc que la table de correspondance des os peut se batir. // La correspondance se fait PAR NOM D'OS, ce qui la rend indifferente au // fait que les tetes du pack de customisation soient sur SK_BodyA_empty // quand le corps est sur SK_BodyA. Follower->SetLeaderPoseComponent(TargetMesh); return Follower; } USkeletalMeshComponent* UCharacterAppearanceComponent::EnsurePartComponent(ECharacterPartCategory Category) { const int32 Index = static_cast(Category); if (!PartComponents.IsValidIndex(Index)) { return nullptr; } if (PartComponents[Index]) { return PartComponents[Index]; } const FName ComponentName(*FString::Printf(TEXT("AppearancePart_%d"), Index)); PartComponents[Index] = CreateFollowerComponent(ComponentName); return PartComponents[Index]; } void UCharacterAppearanceComponent::ApplyAppearance(const FCharacterAppearance& InAppearance) { if (!Catalog) { UE_LOG(LogTemp, Warning, TEXT("UCharacterAppearanceComponent : aucun catalogue assigne sur %s, rien n'est monte."), *GetNameSafe(GetOwner())); return; } if (!ResolveTargetMesh()) { UE_LOG(LogTemp, Warning, TEXT("UCharacterAppearanceComponent : aucun mesh porteur sur %s."), *GetNameSafe(GetOwner())); return; } const FCharacterAppearance Previous = Appearance; Appearance = InAppearance; // Systematique, et pas seulement pour ce qui vient du reseau : un catalogue // ampute entre deux versions du jeu laisserait des index qui ne designent // plus rien, y compris dans les reglages sauvegardes du joueur. Catalog->Sanitize(Appearance); const FCharacterBodySet* BodySet = Catalog->GetBodySet(Appearance.BodyType); if (!BodySet) { UE_LOG(LogTemp, Warning, TEXT("UCharacterAppearanceComponent : le catalogue %s n'a aucune silhouette."), *GetNameSafe(Catalog)); return; } if (!bBuilt || !AppearanceHasSameParts(Previous, Appearance)) { RebuildMeshes(*BodySet); bBuilt = true; } ApplyColors(*BodySet); OnAppearanceApplied.Broadcast(); } void UCharacterAppearanceComponent::RebuildMeshes(const FCharacterBodySet& BodySet) { // ------------------------------------------------------------------ // Le corps. La case 0 est le porteur, les suivantes des composants // enfants -- aujourd'hui aucune, demain les segments d'armure. // ------------------------------------------------------------------ if (BodySet.BodyMeshes.Num() > 0 && BodySet.BodyMeshes[0]) { SetPartMesh(TargetMesh, BodySet.BodyMeshes[0]); } if (BodySet.AnimClass) { // Reposee a chaque changement de silhouette : l'AnimBP est lie a un // squelette, et passer de A a B sans le changer figerait le corps. TargetMesh->SetAnimInstanceClass(BodySet.AnimClass); } const int32 ExtraCount = FMath::Max(0, BodySet.BodyMeshes.Num() - 1); for (int32 i = 0; i < ExtraCount; ++i) { if (!ExtraBodyComponents.IsValidIndex(i)) { const FName ComponentName(*FString::Printf(TEXT("AppearanceBody_%d"), i)); ExtraBodyComponents.Add(CreateFollowerComponent(ComponentName)); } if (USkeletalMeshComponent* Segment = ExtraBodyComponents[i]) { SetPartMesh(Segment, BodySet.BodyMeshes[i + 1]); } } // Les segments en trop apres un changement de silhouette : on les vide au // lieu de les detruire, pour ne pas payer une reconstruction a chaque // aller-retour entre deux corps dans l'ecran de customisation. for (int32 i = ExtraCount; i < ExtraBodyComponents.Num(); ++i) { if (USkeletalMeshComponent* Segment = ExtraBodyComponents[i]) { SetPartMesh(Segment, nullptr); } } // ------------------------------------------------------------------ // Les pieces. Un index a NoPart, ou une liste vide, laisse simplement le // composant sans mesh -- rien a detruire, rien a recreer au retour. // ------------------------------------------------------------------ auto ApplyPart = [this](ECharacterPartCategory Category, const TArray& Entries, uint8 PartIndex) { USkeletalMesh* Mesh = nullptr; if (PartIndex != FCharacterAppearance::NoPart && Entries.IsValidIndex(PartIndex)) { Mesh = Entries[PartIndex].Mesh; } // On n'engendre pas le composant pour rien : une silhouette sans barbe // n'a aucune raison de porter un SkeletalMeshComponent vide. const int32 Index = static_cast(Category); if (!Mesh && (!PartComponents.IsValidIndex(Index) || !PartComponents[Index])) { return; } if (USkeletalMeshComponent* PartComponent = EnsurePartComponent(Category)) { SetPartMesh(PartComponent, Mesh); } }; ApplyPart(ECharacterPartCategory::Head, BodySet.Heads, Appearance.HeadIndex); ApplyPart(ECharacterPartCategory::Hair, BodySet.Hairstyles, Appearance.HairIndex); ApplyPart(ECharacterPartCategory::Eyebrows, BodySet.Eyebrows, Appearance.EyebrowsIndex); ApplyPart(ECharacterPartCategory::Beard, BodySet.Beards, Appearance.BeardIndex); ApplyPart(ECharacterPartCategory::Mustache, BodySet.Mustaches, Appearance.MustacheIndex); // ------------------------------------------------------------------ // Les yeux : un MATERIAU sur un slot du mesh de tete, pas un mesh. Il se // pose ici et pas dans ApplyColors parce qu'un SetMaterial jette le // materiau dynamique en place -- le refaire a chaque image en engendrerait // un par frame, exactement ce que la separation des deux etapes evite. // ------------------------------------------------------------------ const int32 HeadSlot = static_cast(ECharacterPartCategory::Head); if (PartComponents.IsValidIndex(HeadSlot) && PartComponents[HeadSlot] && Catalog->EyeMaterials.IsValidIndex(Appearance.EyesIndex)) { if (UMaterialInterface* EyeMaterial = Catalog->EyeMaterials[Appearance.EyesIndex]) { USkeletalMeshComponent* HeadComponent = PartComponents[HeadSlot]; const int32 EyeSlot = FindEyeMaterialSlot(HeadComponent, BodySet.EyeMaterialSlotHint); if (EyeSlot != INDEX_NONE) { HeadComponent->SetMaterial(EyeSlot, EyeMaterial); } } } } int32 UCharacterAppearanceComponent::FindEyeMaterialSlot(const USkeletalMeshComponent* HeadComponent, FName SlotHint) { if (!HeadComponent || SlotHint.IsNone()) { return INDEX_NONE; } // Contains et pas une egalite : les six tetes du pack nomment leur slot // differemment (`M_Eye_A_01`, `M_Eye`, `M_Eyes_02`), mais toutes contiennent // le fragment -- et aucun slot de peau ne le contient. const FString Hint = SlotHint.ToString(); const TArray SlotNames = HeadComponent->GetMaterialSlotNames(); for (int32 i = 0; i < SlotNames.Num(); ++i) { if (SlotNames[i].ToString().Contains(Hint, ESearchCase::IgnoreCase)) { return i; } } // Rien pose plutot qu'un index au hasard : ecraser le slot de la peau // repeindrait le visage entier de la couleur des yeux, et on chercherait // longtemps d'ou ca vient. Le log, lui, se lit tout de suite. UE_LOG(LogTemp, Warning, TEXT("UCharacterAppearanceComponent : aucun slot de materiau ne contient '%s' sur le mesh de tete. ") TEXT("Les yeux ne seront pas poses. Slots disponibles : %s"), *Hint, *FString::JoinBy(SlotNames, TEXT(", "), [](const FName& Name) { return Name.ToString(); })); return INDEX_NONE; } void UCharacterAppearanceComponent::ApplyColors(const FCharacterBodySet& BodySet) { // Chaque parametre est pose sur TOUS les elements de chaque mesh, sans // chercher lequel le porte : un SetVectorParameterValue dont le parametre // n'existe pas dans le materiau est simplement ignore. Le slot des yeux // ignore donc « Skin Base color », et celui de la peau « Eye color ». Cela // evite de tenir a jour une table d'index de slots qui casserait au premier // mesh dont l'ordre des materiaux differe. // Le blanc en repli et pas le noir : un materiau teinte par une couleur // manquante doit rester lisible. Le blanc rend la texture telle qu'elle est, // le noir donnerait un personnage en silhouette qu'on prendrait pour un bug // de rendu plutot que pour une palette vide. auto PickColor = [](const TArray& Palette, uint8 Index) { return Palette.IsValidIndex(Index) ? Palette[Index] : FLinearColor::White; }; const FLinearColor SkinColor = PickColor(Catalog->SkinColors, Appearance.SkinColorIndex); const FLinearColor HairColor = PickColor(Catalog->HairColors, Appearance.HairColorIndex); const FLinearColor EyeColor = PickColor(Catalog->EyeColors, Appearance.EyeColorIndex); const FLinearColor UnderwearColor = PickColor(Catalog->UnderwearColors, Appearance.UnderwearColorIndex); // Le corps : peau et sous-vetement partagent le meme master. TintAllElements(TargetMesh, Catalog->SkinColorParameter, SkinColor); TintAllElements(TargetMesh, Catalog->UnderwearColorParameter, UnderwearColor); for (USkeletalMeshComponent* Segment : ExtraBodyComponents) { TintAllElements(Segment, Catalog->SkinColorParameter, SkinColor); TintAllElements(Segment, Catalog->UnderwearColorParameter, UnderwearColor); } // La tete porte la peau ET les yeux, sur deux masters differents. const int32 HeadSlot = static_cast(ECharacterPartCategory::Head); if (PartComponents.IsValidIndex(HeadSlot)) { TintAllElements(PartComponents[HeadSlot], Catalog->HeadSkinColorParameter, SkinColor); TintAllElements(PartComponents[HeadSlot], Catalog->EyeColorParameter, EyeColor); } // Cheveux, sourcils, barbe et moustache descendent tous de M_Hairstyle_Base : // un seul parametre les teint ensemble, ce qui est aussi le comportement // qu'on veut -- une barbe blonde sur des cheveux bruns se remarque. static const ECharacterPartCategory HairLikeCategories[] = { ECharacterPartCategory::Hair, ECharacterPartCategory::Eyebrows, ECharacterPartCategory::Beard, ECharacterPartCategory::Mustache }; for (const ECharacterPartCategory Category : HairLikeCategories) { const int32 Index = static_cast(Category); if (PartComponents.IsValidIndex(Index)) { TintAllElements(PartComponents[Index], Catalog->HairColorParameter, HairColor); } } } void UCharacterAppearanceComponent::SetPartMesh(USkeletalMeshComponent* MeshComponent, USkeletalMesh* NewMesh) { if (!MeshComponent || MeshComponent->GetSkeletalMeshAsset() == NewMesh) { // Rien a faire, et surtout : on ne jette pas les MID en place pour // rien. ApplyColors les reutilise, c'est ce qui rend le changement de // couleur seul aussi bon marche. return; } // LE point du montage a ne pas oublier : les materiaux surcharges d'un // composant vivent dans OverrideMaterials, un tableau qui lui appartient et // que changer de mesh ne touche PAS. Sans ce vidage, passer du corps A au // corps B garde le MID derive de MI_BodyA_a sur l'element 0 -- on obtient // une silhouette feminine peinte avec la peau et les abdominaux du modele // masculin. Le symptome trompe, parce que le mesh, lui, a bien change. MeshComponent->EmptyOverrideMaterials(); MeshComponent->SetSkeletalMeshAsset(NewMesh); } void UCharacterAppearanceComponent::TintAllElements(USkeletalMeshComponent* MeshComponent, FName Parameter, const FLinearColor& Color) { if (!MeshComponent || Parameter.IsNone() || !MeshComponent->GetSkeletalMeshAsset()) { return; } const int32 ElementCount = MeshComponent->GetNumMaterials(); for (int32 Element = 0; Element < ElementCount; ++Element) { // Rend le materiau dynamique DEJA en place s'il y en a un, et n'en cree // un que la premiere fois (verifie dans PrimitiveComponent.cpp). C'est // ce qui rend cette fonction sure a appeler par image. if (UMaterialInstanceDynamic* Dynamic = MeshComponent->CreateAndSetMaterialInstanceDynamic(Element)) { Dynamic->SetVectorParameterValue(Parameter, Color); } } } // ---------------------------------------------------------------------- // Reglage par nom, appele par les commandes console d'AFpsPlayer // ---------------------------------------------------------------------- void UCharacterAppearanceComponent::ApplyPartByName(const FString& Part, int32 Index) { if (!Catalog) { UE_LOG(LogTemp, Warning, TEXT("EmberPart : aucun catalogue assigne.")); return; } FCharacterAppearance Next = Appearance; if (Part.Equals(TEXT("body"), ESearchCase::IgnoreCase)) { Next.BodyType = static_cast(FMath::Clamp(Index, 0, static_cast(ECharacterBodyType::Count) - 1)); } else if (Part.Equals(TEXT("eyes"), ESearchCase::IgnoreCase)) { Next.EyesIndex = static_cast(FMath::Max(0, Index)); } else { ECharacterPartCategory Category; if (!AppearanceParsePartCategory(Part, Category)) { UE_LOG(LogTemp, Warning, TEXT("EmberPart : categorie inconnue '%s'. Attendu : body, head, hair, eyebrows, beard, mustache, eyes."), *Part); return; } if (uint8* Target = AppearancePartIndexPtr(Next, Category)) { // Un index negatif retire la piece : c'est la seule facon de se // raser la barbe depuis la console, NoPart valant 255. *Target = (Index < 0) ? FCharacterAppearance::NoPart : static_cast(Index); } } ApplyAppearance(Next); // Apres l'application, donc apres Sanitize : on affiche ce qui a REELLEMENT // ete pose, pas ce qui a ete demande. Un index hors bornes se lit alors tout // de suite, au lieu de laisser croire que la commande n'a rien fait. UE_LOG(LogTemp, Log, TEXT("EmberPart %s %d -> applique."), *Part, Index); LogAppearance(); } void UCharacterAppearanceComponent::ApplyColorByName(const FString& Part, int32 ColorIndex) { FCharacterAppearance Next = Appearance; const uint8 Index = static_cast(FMath::Max(0, ColorIndex)); if (Part.Equals(TEXT("skin"), ESearchCase::IgnoreCase)) { Next.SkinColorIndex = Index; } else if (Part.Equals(TEXT("hair"), ESearchCase::IgnoreCase)) { Next.HairColorIndex = Index; } else if (Part.Equals(TEXT("eye"), ESearchCase::IgnoreCase)) { Next.EyeColorIndex = Index; } else if (Part.Equals(TEXT("underwear"), ESearchCase::IgnoreCase)) { Next.UnderwearColorIndex = Index; } else { UE_LOG(LogTemp, Warning, TEXT("EmberColor : categorie inconnue '%s'. Attendu : skin, hair, eye, underwear."), *Part); return; } ApplyAppearance(Next); LogAppearance(); } void UCharacterAppearanceComponent::LogAppearance() const { if (!Catalog) { UE_LOG(LogTemp, Warning, TEXT("EmberAppearanceDump : aucun catalogue assigne.")); return; } const ECharacterBodyType Body = Appearance.BodyType; UE_LOG(LogTemp, Log, TEXT("--- Apparence de %s ---"), *GetNameSafe(GetOwner())); UE_LOG(LogTemp, Log, TEXT(" body = %d (0..%d)"), static_cast(Body), static_cast(ECharacterBodyType::Count) - 1); UE_LOG(LogTemp, Log, TEXT(" head = %d (%d options)"), Appearance.HeadIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Head)); UE_LOG(LogTemp, Log, TEXT(" hair = %d (%d options)"), Appearance.HairIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Hair)); UE_LOG(LogTemp, Log, TEXT(" eyebrows = %d (%d options)"), Appearance.EyebrowsIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Eyebrows)); UE_LOG(LogTemp, Log, TEXT(" beard = %d (%d options)"), Appearance.BeardIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Beard)); UE_LOG(LogTemp, Log, TEXT(" mustache = %d (%d options)"), Appearance.MustacheIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Mustache)); UE_LOG(LogTemp, Log, TEXT(" eyes = %d (%d options)"), Appearance.EyesIndex, Catalog->EyeMaterials.Num()); UE_LOG(LogTemp, Log, TEXT(" skin = couleur %d (%d couleurs)"), Appearance.SkinColorIndex, Catalog->SkinColors.Num()); UE_LOG(LogTemp, Log, TEXT(" hairColor = couleur %d (%d couleurs)"), Appearance.HairColorIndex, Catalog->HairColors.Num()); UE_LOG(LogTemp, Log, TEXT(" eyeColor = couleur %d (%d couleurs)"), Appearance.EyeColorIndex, Catalog->EyeColors.Num()); UE_LOG(LogTemp, Log, TEXT(" underwear = couleur %d (%d couleurs)"), Appearance.UnderwearColorIndex, Catalog->UnderwearColors.Num()); UE_LOG(LogTemp, Log, TEXT(" 255 = aucune piece (barbe rasee, crane nu)")); }