(Feat) Add Animation Fps Charater
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
// 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)"));
|
||||
}
|
||||
Reference in New Issue
Block a user