Files
Unreal_EmberWild/Source/EmberWild/Private/CharacterAppearanceComponent.cpp

579 lines
21 KiB
C++

// 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<int32>(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<int32>(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<ACharacter>(Owner))
{
TargetMesh = OwningCharacter->GetMesh();
}
if (!TargetMesh)
{
TargetMesh = Owner->FindComponentByClass<USkeletalMeshComponent>();
}
return TargetMesh;
}
USkeletalMeshComponent* UCharacterAppearanceComponent::CreateFollowerComponent(FName ComponentName)
{
AActor* Owner = GetOwner();
if (!Owner || !TargetMesh)
{
return nullptr;
}
USkeletalMeshComponent* Follower = NewObject<USkeletalMeshComponent>(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<int32>(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<FCharacterPartEntry>& 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<int32>(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<int32>(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<FName> 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<FLinearColor>& 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<int32>(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<int32>(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<ECharacterBodyType>(FMath::Clamp(Index, 0, static_cast<int32>(ECharacterBodyType::Count) - 1));
}
else if (Part.Equals(TEXT("eyes"), ESearchCase::IgnoreCase))
{
Next.EyesIndex = static_cast<uint8>(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<uint8>(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<uint8>(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<int32>(Body), static_cast<int32>(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)"));
}