460 lines
16 KiB
C++
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
|