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

460 lines
16 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "CharacterCustomizationWidget.h"
#include "CharacterPartsDataAsset.h"
#include "Components/Button.h"
#include "Components/PanelWidget.h"
#include "CustomizationCategoryWidget.h"
#include "CustomizationOptionWidget.h"
#define LOCTEXT_NAMESPACE "CharacterCustomization"
namespace CharacterCustomizationLabels
{
/**
* Le libelle de chaque categorie, dans l'ordre de l'enum.
*
* Ici et pas en UPROPERTY : ce sont des textes d'interface, pas des
* reglages. Les exposer inviterait a les vider par erreur, et une colonne
* aux rangees anonymes est inutilisable.
*
* Nom de namespace unique dans TOUT le module : le build unity concatene
* les .cpp, et un namespace anonyme n'isolerait rien.
*/
static FText Get(ECustomizationCategory Category)
{
switch (Category)
{
case ECustomizationCategory::Body: return LOCTEXT("CatBody", "Silhouette");
case ECustomizationCategory::Head: return LOCTEXT("CatHead", "Visage");
case ECustomizationCategory::Hair: return LOCTEXT("CatHair", "Cheveux");
case ECustomizationCategory::Eyebrows: return LOCTEXT("CatEyebrows", "Sourcils");
case ECustomizationCategory::Beard: return LOCTEXT("CatBeard", "Barbe");
case ECustomizationCategory::Mustache: return LOCTEXT("CatMustache", "Moustache");
case ECustomizationCategory::Eyes: return LOCTEXT("CatEyes", "Yeux");
case ECustomizationCategory::SkinColor: return LOCTEXT("CatSkinColor", "Teint");
case ECustomizationCategory::HairColor: return LOCTEXT("CatHairColor", "Couleur des cheveux");
case ECustomizationCategory::EyeColor: return LOCTEXT("CatEyeColor", "Couleur des yeux");
case ECustomizationCategory::UnderwearColor: return LOCTEXT("CatUnderwear", "Sous-vetement");
default: return FText::GetEmpty();
}
}
/** Une categorie de couleur remplit la grille de pastilles, pas de vignettes. */
static bool IsColor(ECustomizationCategory Category)
{
return Category >= ECustomizationCategory::SkinColor;
}
/**
* Le cadrage a montrer. Seules la silhouette et le sous-vetement demandent
* de reculer : tout le reste se juge sur le visage.
*/
static ECharacterPreviewFraming GetFraming(ECustomizationCategory Category)
{
const bool bWholeBody = (Category == ECustomizationCategory::Body)
|| (Category == ECustomizationCategory::SkinColor)
|| (Category == ECustomizationCategory::UnderwearColor);
return bWholeBody ? ECharacterPreviewFraming::FullBody : ECharacterPreviewFraming::Face;
}
/** La categorie de pieces correspondante, pour interroger le catalogue. */
static ECharacterPartCategory ToPartCategory(ECustomizationCategory Category)
{
switch (Category)
{
case ECustomizationCategory::Head: return ECharacterPartCategory::Head;
case ECustomizationCategory::Hair: return ECharacterPartCategory::Hair;
case ECustomizationCategory::Eyebrows: return ECharacterPartCategory::Eyebrows;
case ECustomizationCategory::Beard: return ECharacterPartCategory::Beard;
default: return ECharacterPartCategory::Mustache;
}
}
}
UCharacterCustomizationWidget::UCharacterCustomizationWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Sans ce drapeau, NativeOnKeyDown ne serait jamais appele -- et le
// SetWidgetToFocus() pose par AMainMenuPlayerController::OpenCustomization
// serait lui aussi sans effet : Slate REFUSE de focaliser un widget qui ne
// declare pas supporter le focus clavier.
SetIsFocusable(true);
}
void UCharacterCustomizationWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
// Sans ca, le clic-glisser qui fait tourner le personnage ne repond jamais :
// un UUserWidget ne recoit NativeOnMouseButtonDown que si sa Visibility vaut
// Visible, et le defaut d'un widget cree est SelfHitTestInvisible -- il
// laisse passer les clics au lieu de les capter. Impose ici plutot que
// laisse a la Designer, ou l'oubli serait silencieux.
//
// Les boutons ne sont pas prives de leurs clics pour autant : Slate teste
// les enfants AVANT le parent, donc seul ce qui est lache dans le vide
// arrive jusqu'ici -- c'est-a-dire sur le personnage.
SetVisibility(ESlateVisibility::Visible);
if (PlayButton)
{
PlayButton->OnClicked.AddDynamic(this, &UCharacterCustomizationWidget::HandlePlayClicked);
}
if (BackButton)
{
BackButton->OnClicked.AddDynamic(this, &UCharacterCustomizationWidget::HandleBackClicked);
}
}
void UCharacterCustomizationWidget::NativeConstruct()
{
Super::NativeConstruct();
BuildCategories();
BuildOptions();
RefreshPreview();
// Le focus est repris ici et pas seulement par le SetWidgetToFocus du
// controller : les deux chemins existent, mais celui-ci est le seul qui vaut
// encore apres une reconstruction du widget.
SetKeyboardFocus();
}
void UCharacterCustomizationWidget::SetupCustomization(ACharacterPreviewActor* InPreview, const FCharacterAppearance& InAppearance)
{
Preview = InPreview;
Appearance = InAppearance;
// Systematique : un catalogue ampute entre deux versions du jeu laisserait
// dans les reglages sauvegardes des index qui ne designent plus rien.
if (Catalog)
{
Catalog->Sanitize(Appearance);
}
}
void UCharacterCustomizationWidget::BuildCategories()
{
if (!CategoryList || !CategoryWidgetClass)
{
return;
}
CategoryList->ClearChildren();
CategoryWidgets.Reset();
CategoryWidgets.Reserve(static_cast<int32>(ECustomizationCategory::Count));
for (int32 i = 0; i < static_cast<int32>(ECustomizationCategory::Count); ++i)
{
const ECustomizationCategory Category = static_cast<ECustomizationCategory>(i);
// Une categorie sans option n'a pas de rangee : c'est ce qui fait
// disparaitre « Barbe » et « Moustache » quand on passe sur le corps B,
// pour lequel le pack n'en livre aucune. Une rangee qui ouvre une grille
// vide se lit comme un bug.
if (GetOptionCountForCategory(Category) <= 0)
{
continue;
}
UCustomizationCategoryWidget* Row = CreateWidget<UCustomizationCategoryWidget>(this, CategoryWidgetClass);
if (!Row)
{
continue;
}
Row->Setup(i, CharacterCustomizationLabels::Get(Category));
Row->OnCategoryClicked.AddDynamic(this, &UCharacterCustomizationWidget::HandleCategoryClicked);
CategoryList->AddChild(Row);
CategoryWidgets.Add(Row);
}
// La categorie active a pu disparaitre en changeant de silhouette : on
// retombe sur la premiere rangee encore presente plutot que sur une grille
// vide.
if (GetOptionCountForCategory(ActiveCategory) <= 0 && CategoryWidgets.Num() > 0)
{
ActiveCategory = ECustomizationCategory::Body;
}
}
void UCharacterCustomizationWidget::BuildOptions()
{
if (!OptionGrid || !OptionWidgetClass || !Catalog)
{
return;
}
OptionGrid->ClearChildren();
OptionWidgets.Reset();
const int32 Count = GetOptionCountForCategory(ActiveCategory);
OptionWidgets.Reserve(Count);
for (int32 i = 0; i < Count; ++i)
{
UCustomizationOptionWidget* Option = CreateWidget<UCustomizationOptionWidget>(this, OptionWidgetClass);
if (!Option)
{
continue;
}
if (CharacterCustomizationLabels::IsColor(ActiveCategory))
{
const TArray<FLinearColor>* Palette = nullptr;
switch (ActiveCategory)
{
case ECustomizationCategory::SkinColor: Palette = &Catalog->SkinColors; break;
case ECustomizationCategory::HairColor: Palette = &Catalog->HairColors; break;
case ECustomizationCategory::EyeColor: Palette = &Catalog->EyeColors; break;
default: Palette = &Catalog->UnderwearColors; break;
}
Option->SetupAsColor(i, (*Palette)[i]);
}
else if (ActiveCategory == ECustomizationCategory::Body)
{
Option->SetupAsPart(i, Catalog->BodySets.IsValidIndex(i) ? Catalog->BodySets[i].Icon : nullptr);
}
else if (ActiveCategory == ECustomizationCategory::Eyes)
{
// Les yeux n'ont pas de vignette : ce sont des materiaux, pas des
// entrees de catalogue. La case retombe sur son numero.
Option->SetupAsPart(i, nullptr);
}
else
{
const ECharacterPartCategory PartCategory = CharacterCustomizationLabels::ToPartCategory(ActiveCategory);
const TArray<FCharacterPartEntry>& Entries = Catalog->GetParts(Appearance.BodyType, PartCategory);
Option->SetupAsPart(i, Entries.IsValidIndex(i) ? Entries[i].Icon : nullptr);
}
Option->OnOptionClicked.AddDynamic(this, &UCharacterCustomizationWidget::HandleOptionClicked);
OptionGrid->AddChild(Option);
OptionWidgets.Add(Option);
}
RefreshSelection();
}
void UCharacterCustomizationWidget::RefreshPreview()
{
if (Preview)
{
Preview->ApplyAppearance(Appearance);
Preview->SetFraming(CharacterCustomizationLabels::GetFraming(ActiveCategory));
}
}
void UCharacterCustomizationWidget::RefreshSelection()
{
const int32 Current = GetCurrentIndexForCategory(ActiveCategory);
for (int32 i = 0; i < OptionWidgets.Num(); ++i)
{
if (OptionWidgets[i])
{
OptionWidgets[i]->SetSelected(OptionWidgets[i]->GetOptionIndex() == Current);
}
}
for (UCustomizationCategoryWidget* Row : CategoryWidgets)
{
if (Row)
{
Row->SetSelected(false);
}
}
// La rangee active n'est pas au meme rang que la categorie dans l'enum :
// les categories vides ont ete sautees. On la retrouve par son libelle
// d'index, pose dans Setup.
const int32 ActiveIndex = static_cast<int32>(ActiveCategory);
for (UCustomizationCategoryWidget* Row : CategoryWidgets)
{
if (Row && Row->GetCategoryIndex() == ActiveIndex)
{
Row->SetSelected(true);
break;
}
}
}
int32 UCharacterCustomizationWidget::GetCurrentIndexForCategory(ECustomizationCategory Category) const
{
switch (Category)
{
case ECustomizationCategory::Body: return static_cast<int32>(Appearance.BodyType);
case ECustomizationCategory::Head: return Appearance.HeadIndex;
case ECustomizationCategory::Hair: return Appearance.HairIndex;
case ECustomizationCategory::Eyebrows: return Appearance.EyebrowsIndex;
case ECustomizationCategory::Beard: return Appearance.BeardIndex;
case ECustomizationCategory::Mustache: return Appearance.MustacheIndex;
case ECustomizationCategory::Eyes: return Appearance.EyesIndex;
case ECustomizationCategory::SkinColor: return Appearance.SkinColorIndex;
case ECustomizationCategory::HairColor: return Appearance.HairColorIndex;
case ECustomizationCategory::EyeColor: return Appearance.EyeColorIndex;
case ECustomizationCategory::UnderwearColor: return Appearance.UnderwearColorIndex;
default: return 0;
}
}
void UCharacterCustomizationWidget::SetIndexForCategory(ECustomizationCategory Category, int32 NewIndex)
{
const uint8 Value = static_cast<uint8>(FMath::Clamp(NewIndex, 0, 254));
switch (Category)
{
case ECustomizationCategory::Body: Appearance.BodyType = static_cast<ECharacterBodyType>(Value); break;
case ECustomizationCategory::Head: Appearance.HeadIndex = Value; break;
case ECustomizationCategory::Hair: Appearance.HairIndex = Value; break;
case ECustomizationCategory::Eyebrows: Appearance.EyebrowsIndex = Value; break;
case ECustomizationCategory::Beard: Appearance.BeardIndex = Value; break;
case ECustomizationCategory::Mustache: Appearance.MustacheIndex = Value; break;
case ECustomizationCategory::Eyes: Appearance.EyesIndex = Value; break;
case ECustomizationCategory::SkinColor: Appearance.SkinColorIndex = Value; break;
case ECustomizationCategory::HairColor: Appearance.HairColorIndex = Value; break;
case ECustomizationCategory::EyeColor: Appearance.EyeColorIndex = Value; break;
case ECustomizationCategory::UnderwearColor: Appearance.UnderwearColorIndex = Value; break;
default: break;
}
}
int32 UCharacterCustomizationWidget::GetOptionCountForCategory(ECustomizationCategory Category) const
{
if (!Catalog)
{
return 0;
}
switch (Category)
{
case ECustomizationCategory::Body: return Catalog->BodySets.Num();
case ECustomizationCategory::Eyes: return Catalog->EyeMaterials.Num();
case ECustomizationCategory::SkinColor: return Catalog->SkinColors.Num();
case ECustomizationCategory::HairColor: return Catalog->HairColors.Num();
case ECustomizationCategory::EyeColor: return Catalog->EyeColors.Num();
case ECustomizationCategory::UnderwearColor: return Catalog->UnderwearColors.Num();
default:
return Catalog->GetPartCount(Appearance.BodyType, CharacterCustomizationLabels::ToPartCategory(Category));
}
}
void UCharacterCustomizationWidget::HandleCategoryClicked(int32 CategoryIndex)
{
ActiveCategory = static_cast<ECustomizationCategory>(CategoryIndex);
BuildOptions();
RefreshPreview();
}
void UCharacterCustomizationWidget::HandleOptionClicked(int32 OptionIndex)
{
SetIndexForCategory(ActiveCategory, OptionIndex);
// Changer de silhouette change TOUT le reste : les listes de tetes, de
// coiffures et de sourcils appartiennent au corps, et le corps B n'a ni
// barbe ni moustache. On rebatit donc la colonne entiere, apres avoir
// laisse Sanitize ramener les index devenus hors bornes.
if (ActiveCategory == ECustomizationCategory::Body)
{
if (Catalog)
{
Catalog->Sanitize(Appearance);
}
BuildCategories();
BuildOptions();
}
else
{
RefreshSelection();
}
RefreshPreview();
}
void UCharacterCustomizationWidget::HandlePlayClicked()
{
OnConfirmed.Broadcast();
}
void UCharacterCustomizationWidget::HandleBackClicked()
{
OnCancelled.Broadcast();
}
FReply UCharacterCustomizationWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
const FKey Key = InKeyEvent.GetKey();
// GetVirtualKey() et non la constante, depreciee en 5.8 : la resolution se
// fait a l'appel, donc selon la manette reellement branchee.
if (Key == EKeys::Escape || Key == EKeys::Virtual_Gamepad_Back.GetVirtualKey())
{
// Exactement le bouton Retour, jamais un second chemin vers le menu : le
// controller a un fondu et une bascule de camera a jouer au retour, et
// c'est OnCancelled qui les declenche.
HandleBackClicked();
// Handled : sans ca la touche continue de remonter et serait consommee
// une seconde fois par ce qui se trouve derriere.
return FReply::Handled();
}
return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
}
// ----------------------------------------------------------------------
// Rotation du personnage
//
// Sur le widget plein ecran et pas sur une zone dediee : les boutons de la
// colonne consomment deja leurs propres clics, donc tout ce qui arrive ici a
// forcement ete lache dans le vide -- c'est-a-dire sur le personnage.
// ----------------------------------------------------------------------
FReply UCharacterCustomizationWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
{
bDragging = true;
// CaptureMouse : sans elle, sortir du widget en glissant relacherait le
// suivi et laisserait bDragging a vrai pour toujours.
return FReply::Handled().CaptureMouse(TakeWidget());
}
return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);
}
FReply UCharacterCustomizationWidget::NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && bDragging)
{
bDragging = false;
return FReply::Handled().ReleaseMouseCapture();
}
return Super::NativeOnMouseButtonUp(InGeometry, InMouseEvent);
}
FReply UCharacterCustomizationWidget::NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
if (bDragging && Preview)
{
// Signe inverse : glisser vers la droite doit tourner le personnage
// vers la droite, donc le faire pivoter dans l'autre sens.
Preview->AddPreviewYaw(-InMouseEvent.GetCursorDelta().X * Preview->GetYawPerPixel());
return FReply::Handled();
}
return Super::NativeOnMouseMove(InGeometry, InMouseEvent);
}
#undef LOCTEXT_NAMESPACE