5ba5fbfebf
Lot 2 du chantier : le corps du lot precedent devient assemblable. Tete, coiffure, sourcils, barbe et moustache sont montes en leader pose sur le mesh du Character, et les couleurs sont posees sur des MID. FCharacterAppearance tient en 11 octets : des INDEX et jamais des chemins d'assets. Un chemin couterait une chaine a chaque replication, alors qu'un index tient dans un octet et se VALIDE contre le catalogue -- un client ne peut donc pas demander un asset arbitraire. Le corps se monte depuis une LISTE de meshes meme si elle n'a qu'une case. Le pack livre exprès SKM_BodyA_torso, _arms, _legs_01/02/03 pour qu'une armure puisse eteindre les segments qu'elle recouvre : le jour venu la case 0 devient SKM_BodyA_empty et les huit segments suivent, sans que ce code change. Un pointeur unique imposerait de tout reprendre au premier plastron. Trois pieges rencontres, aucun visible depuis le code : - Les UFUNCTION(Exec) d'un UActorComponent ne sont JAMAIS appelees. La chaine de routage (UPlayer::Exec) interroge le monde, le PlayerInput, le controller, le pawn, le HUD, le GameMode, le CheatManager, le GameState et le camera manager -- jamais les composants, AActor ne surchargeant pas ProcessConsoleExec. Et rien ne proteste. D'ou EmberPart / EmberColor / EmberAppearanceDump posees sur AFpsPlayer, qui delegue. - L'ordre des slots de materiau varie d'un mesh a l'autre : sur SKM_HeadA_01 l'Element 0 est celui des YEUX et le 1 celui de la peau. Les six tetes venant de deux packs, on cherche le slot par son NOM et on ne pose rien si aucun ne correspond -- ecraser le slot de la peau repeint le visage entier de la couleur des yeux. - Un nom de parametre materiau se lit dans le Material INSTANCE, pas dans les chaines du master : M_Head_Base contient « Skin Base color » mais le parametre expose s'appelle « Skin ». D'ou des UPROPERTY de catalogue plutot que des constantes, pour corriger sans recompiler. Body B reste a remplir dans DA_CharacterParts, avec son ABP_PlayerBody_B et sans barbes ni moustaches -- le pack n'en livre pas, et une liste vide se comporte deja comme NoPart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
559 lines
20 KiB
C++
559 lines
20 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])
|
|
{
|
|
TargetMesh->SetSkeletalMeshAsset(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])
|
|
{
|
|
Segment->SetSkeletalMeshAsset(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])
|
|
{
|
|
Segment->SetSkeletalMeshAsset(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))
|
|
{
|
|
PartComponent->SetSkeletalMeshAsset(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::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)"));
|
|
}
|