(Feat) Add footsteps

This commit is contained in:
2026-08-11 15:23:10 +02:00
parent e8e128457a
commit 176beab9fc
4776 changed files with 18806 additions and 121 deletions
@@ -0,0 +1,459 @@
// 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
@@ -0,0 +1,149 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "CharacterPreviewActor.h"
#include "Camera/CameraComponent.h"
#include "CharacterAppearanceComponent.h"
#include "Components/SkeletalMeshComponent.h"
ACharacterPreviewActor::ACharacterPreviewActor()
{
// Allume par SetFraming et eteint des que la camera est arrivee. Un
// mannequin immobile dans un menu n'a aucune raison de couter une frame.
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false;
USceneComponent* SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
SetRootComponent(SceneRoot);
TurntablePivot = CreateDefaultSubobject<USceneComponent>(TEXT("TurntablePivot"));
TurntablePivot->SetupAttachment(SceneRoot);
BodyMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("BodyMesh"));
BodyMesh->SetupAttachment(TurntablePivot);
// Aucune collision : c'est un decor de menu, rien ne doit le heurter ni le
// tracer.
BodyMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Les reperes de cadrage sont filles de la RACINE et pas du pivot : ils ne
// doivent surtout pas tourner avec le personnage, sinon la camera suivrait
// le clic-glisser au lieu de le montrer.
FullBodyViewSpot = CreateDefaultSubobject<USceneComponent>(TEXT("FullBodyViewSpot"));
FullBodyViewSpot->SetupAttachment(SceneRoot);
FullBodyViewSpot->SetRelativeLocation(FVector(260.f, 60.f, 95.f));
FullBodyViewSpot->SetRelativeRotation(FRotator(0.f, 180.f, 0.f));
FaceViewSpot = CreateDefaultSubobject<USceneComponent>(TEXT("FaceViewSpot"));
FaceViewSpot->SetupAttachment(SceneRoot);
FaceViewSpot->SetRelativeLocation(FVector(90.f, 25.f, 165.f));
FaceViewSpot->SetRelativeRotation(FRotator(0.f, 180.f, 0.f));
PreviewCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("PreviewCamera"));
PreviewCamera->SetupAttachment(SceneRoot);
AppearanceComponent = CreateDefaultSubobject<UCharacterAppearanceComponent>(TEXT("AppearanceComponent"));
}
void ACharacterPreviewActor::BeginPlay()
{
Super::BeginPlay();
// Le composant cherche le mesh d'un ACharacter par defaut ; ici il n'y en a
// pas, on lui designe le notre.
if (AppearanceComponent)
{
AppearanceComponent->SetTargetMesh(BodyMesh);
}
// Pose la camera sur le cadrage de depart sans interpolation : glisser
// depuis l'origine de l'acteur au premier affichage montrerait un travelling
// que personne n'a demande.
if (const USceneComponent* Spot = GetSpotForFraming(CurrentFraming))
{
PreviewCamera->SetRelativeLocationAndRotation(Spot->GetRelativeLocation(), Spot->GetRelativeRotation());
}
}
void ACharacterPreviewActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
UpdateFramingInterpolation(DeltaSeconds);
}
void ACharacterPreviewActor::ApplyAppearance(const FCharacterAppearance& InAppearance)
{
if (AppearanceComponent)
{
AppearanceComponent->ApplyAppearance(InAppearance);
}
}
void ACharacterPreviewActor::SetFraming(ECharacterPreviewFraming NewFraming)
{
if (CurrentFraming == NewFraming)
{
return;
}
CurrentFraming = NewFraming;
SetActorTickEnabled(true);
}
void ACharacterPreviewActor::AddPreviewYaw(float DeltaDegrees)
{
TurntableYaw = FRotator::NormalizeAxis(TurntableYaw + DeltaDegrees);
if (TurntablePivot)
{
TurntablePivot->SetRelativeRotation(FRotator(0.f, TurntableYaw, 0.f));
}
}
void ACharacterPreviewActor::ResetPreviewYaw()
{
TurntableYaw = 0.f;
if (TurntablePivot)
{
TurntablePivot->SetRelativeRotation(FRotator::ZeroRotator);
}
}
USceneComponent* ACharacterPreviewActor::GetSpotForFraming(ECharacterPreviewFraming InFraming) const
{
return (InFraming == ECharacterPreviewFraming::Face) ? FaceViewSpot : FullBodyViewSpot;
}
void ACharacterPreviewActor::UpdateFramingInterpolation(float DeltaSeconds)
{
const USceneComponent* Spot = GetSpotForFraming(CurrentFraming);
if (!Spot || !PreviewCamera)
{
SetActorTickEnabled(false);
return;
}
const FVector TargetLocation = Spot->GetRelativeLocation();
const FRotator TargetRotation = Spot->GetRelativeRotation();
const FVector NewLocation = FMath::VInterpTo(
PreviewCamera->GetRelativeLocation(), TargetLocation, DeltaSeconds, FramingInterpSpeed);
const FRotator NewRotation = FMath::RInterpTo(
PreviewCamera->GetRelativeRotation(), TargetRotation, DeltaSeconds, FramingInterpSpeed);
PreviewCamera->SetRelativeLocationAndRotation(NewLocation, NewRotation);
// Un interpolateur exponentiel n'atteint jamais sa cible exactement : sans
// ce seuil, le Tick ne s'eteindrait plus jamais. On termine le dernier
// millimetre d'un coup, il ne se voit pas.
const bool bLocationReached = NewLocation.Equals(TargetLocation, 0.5f);
const bool bRotationReached = NewRotation.Equals(TargetRotation, 0.5f);
if (bLocationReached && bRotationReached)
{
PreviewCamera->SetRelativeLocationAndRotation(TargetLocation, TargetRotation);
SetActorTickEnabled(false);
}
}
@@ -0,0 +1,48 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "CustomizationCategoryWidget.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
void UCustomizationCategoryWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
if (CategoryButton)
{
CategoryButton->OnClicked.AddDynamic(this, &UCustomizationCategoryWidget::HandleButtonClicked);
}
SetSelected(false);
}
void UCustomizationCategoryWidget::Setup(int32 InIndex, const FText& InLabel)
{
CategoryIndex = InIndex;
if (CategoryLabel)
{
CategoryLabel->SetText(InLabel);
}
}
void UCustomizationCategoryWidget::SetSelected(bool bInSelected)
{
if (CategoryLabel)
{
CategoryLabel->SetColorAndOpacity(FSlateColor(bInSelected ? SelectedLabelColor : UnselectedLabelColor));
}
if (SelectionBorder)
{
SelectionBorder->SetBrushColor(bInSelected ? SelectedBorderColor : UnselectedBorderColor);
}
}
void UCustomizationCategoryWidget::HandleButtonClicked()
{
OnCategoryClicked.Broadcast(CategoryIndex);
}
@@ -0,0 +1,88 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "CustomizationOptionWidget.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/Image.h"
#include "Components/TextBlock.h"
#include "Engine/Texture2D.h"
void UCustomizationOptionWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
if (OptionButton)
{
OptionButton->OnClicked.AddDynamic(this, &UCustomizationOptionWidget::HandleButtonClicked);
}
SetSelected(false);
}
void UCustomizationOptionWidget::SetupAsPart(int32 InIndex, UTexture2D* Icon)
{
OptionIndex = InIndex;
if (OptionImage)
{
// Le blanc et pas la couleur d'origine : SetupAsColor a pu teinter
// cette image lors d'un passage precedent, et la case est recyclee.
// Sans cette remise a plat, une vignette heriterait de la teinte de la
// pastille qui occupait la meme case avant elle.
OptionImage->SetColorAndOpacity(FLinearColor::White);
OptionImage->SetBrushFromTexture(Icon);
OptionImage->SetVisibility(Icon ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
}
if (IndexText)
{
// Le numero ne sert que de repli. Affiche a cote d'une vignette, il la
// salirait ; affiche seul, il rend la case utilisable.
IndexText->SetText(FText::AsNumber(InIndex + 1));
IndexText->SetVisibility(Icon ? ESlateVisibility::Collapsed : ESlateVisibility::HitTestInvisible);
}
}
void UCustomizationOptionWidget::SetupAsColor(int32 InIndex, const FLinearColor& Color)
{
OptionIndex = InIndex;
if (OptionImage)
{
// Aucune texture assignee : le pinceau par defaut d'une UImage est un
// aplat blanc, que la teinte suffit a colorer. C'est ce qui permet a
// une pastille de n'avoir besoin d'aucun asset.
OptionImage->SetBrushFromTexture(nullptr);
OptionImage->SetColorAndOpacity(Color);
OptionImage->SetVisibility(ESlateVisibility::Visible);
}
if (IndexText)
{
IndexText->SetVisibility(ESlateVisibility::Collapsed);
}
}
void UCustomizationOptionWidget::SetSelected(bool bInSelected)
{
if (SelectionBorder)
{
SelectionBorder->SetBrushColor(bInSelected ? SelectedBorderColor : UnselectedBorderColor);
}
// La case deja choisie n'est plus cliquable : re-cliquer une option active
// ne ferait rien, et un bouton qui ne fait rien laisse croire que le clic
// n'a pas ete pris. Le style Disabled du bouton la marque au passage --
// c'est un second signal de selection, gratuit, en plus du cadre.
if (OptionButton)
{
OptionButton->SetIsEnabled(!bInSelected);
}
}
void UCustomizationOptionWidget::HandleButtonClicked()
{
OnOptionClicked.Broadcast(OptionIndex);
}
@@ -0,0 +1,363 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "FootstepComponent.h"
#include "Components/CapsuleComponent.h"
#include "Engine/World.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/GameplayStatics.h"
UFootstepComponent::UFootstepComponent()
{
PrimaryComponentTick.bCanEverTick = true;
// 20 Hz, et pas la frequence d'affichage. Le composant n'accumule qu'une
// DISTANCE, mesuree entre deux positions exactes : allonger l'intervalle ne
// degrade donc rien, la somme des deplacements reste la meme. A 120 fps
// c'est six fois moins de reveils pour un resultat identique a l'oreille.
//
// Un TickComponent a nous et pas un appel depuis le Tick du pawn, contrairement
// a UHeadBobComponent : lui doit tourner avant que la camera soit composee, ici
// une frame de retard sur un son ne s'entend pas -- et surtout ce composant doit
// rester posable sur n'importe quel ACharacter, y compris ceux qu'on n'ecrit pas.
PrimaryComponentTick.TickInterval = 0.05f;
// Foulees calees sur les frequences de UHeadBobComponent, avec les vitesses
// du pawn : StrideLength = Vitesse / (2 x StrideFrequency).
// accroupi : 200 / (2 x 0.70) = 143
// marche : 400 / (2 x 1.10) = 182
// sprint : 650 / (2 x 1.55) = 210
// Elles sont ici et non en initialiseurs de membre pour que le constructeur
// reste le seul endroit a lire quand on cherche les valeurs par defaut.
CrouchGait = FFootstepGaitSettings(143.f, 0.35f);
WalkGait = FFootstepGaitSettings(182.f, 0.80f);
SprintGait = FFootstepGaitSettings(210.f, 1.00f);
}
void UFootstepComponent::BeginPlay()
{
Super::BeginPlay();
OwnerCharacter = Cast<ACharacter>(GetOwner());
if (!OwnerCharacter)
{
UE_LOG(LogTemp, Warning, TEXT("UFootstepComponent : le proprietaire n'est pas un ACharacter, les bruits de pas sont desactives."));
SetComponentTickEnabled(false);
return;
}
OwnerMovement = OwnerCharacter->GetCharacterMovement();
// Le seul signal de decollage et de reception qui existe sur les quatre
// machines. Voir le commentaire de HandleMovementModeChanged.
OwnerCharacter->MovementModeChangedDelegate.AddDynamic(this, &UFootstepComponent::HandleMovementModeChanged);
if (!SoundBank)
{
UE_LOG(LogTemp, Warning, TEXT("UFootstepComponent : aucune banque de sons assignee sur %s, aucun bruit de pas ne sera joue."),
*GetNameSafe(GetOwner()));
}
}
void UFootstepComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (OwnerCharacter)
{
OwnerCharacter->MovementModeChangedDelegate.RemoveDynamic(this, &UFootstepComponent::HandleMovementModeChanged);
}
Super::EndPlay(EndPlayReason);
}
void UFootstepComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!bEnableFootsteps || !OwnerCharacter || !OwnerMovement || !SoundBank || !ShouldPlaySounds())
{
return;
}
const FVector Location = OwnerCharacter->GetActorLocation();
// Premier tick valide : on se contente de memoriser la position. Sans ce
// garde-fou, le premier deplacement mesure partirait de l'origine du monde
// et declencherait une rafale de pas au spawn.
if (!bHasLastLocation)
{
LastLocation = Location;
bHasLastLocation = true;
return;
}
const float Travelled = (Location - LastLocation).Size2D();
LastLocation = Location;
// En l'air on ne compte pas de foulee : le vol est traite par les deux cues
// de HandleMovementModeChanged. On y profite du passage pour echantillonner
// la vitesse de chute, introuvable au moment de l'impact.
if (OwnerMovement->IsFalling())
{
MaxFallSpeed = FMath::Max(MaxFallSpeed, -OwnerMovement->Velocity.Z);
return;
}
const float Speed = OwnerMovement->Velocity.Size2D();
const bool bIsMoving = Speed > IdleSpeedThreshold;
if (bIsMoving && !bWasMoving)
{
// Entree en mouvement : on amorce le compteur pour que le premier pas
// tombe tout de suite plutot qu'apres deux metres de silence.
DistanceSinceLastStep = GetGaitSettings(SelectGait(Speed)).StrideLength * FirstStepFraction;
StepsSinceMoveStart = 0;
}
else if (!bIsMoving && bWasMoving)
{
// Immobilisation : le pas final. Conditionne a un vrai pas deja joue,
// sinon un simple a-coup de collision ferait claquer un arret alors que
// le personnage n'a pas avance d'un centimetre.
if (bPlayStopSounds && StepsSinceMoveStart > 0)
{
FVector StopLocation;
if (const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(StopLocation))
{
const FFootstepGaitSettings& StopSettings = GetGaitSettings(LastGait);
PlayCue(Sounds->GetStopSound(LastGait), StopSettings.Volume, StopSettings.PitchVariation, StopLocation);
}
}
DistanceSinceLastStep = 0.f;
StepsSinceMoveStart = 0;
}
bWasMoving = bIsMoving;
if (!bIsMoving)
{
return;
}
DistanceSinceLastStep += Travelled;
const EFootstepGait Gait = SelectGait(Speed);
const float StrideLength = GetGaitSettings(Gait).StrideLength;
if (DistanceSinceLastStep >= StrideLength)
{
// Soustraction et pas remise a zero : le reliquat de distance doit
// survivre au pas, sinon le rythme derive a chaque hoquet de framerate.
DistanceSinceLastStep -= StrideLength;
// Mais borne quand meme : apres une teleportation ou un long gel, le
// reliquat vaudrait plusieurs foulees et partirait en rafale.
DistanceSinceLastStep = FMath::Min(DistanceSinceLastStep, StrideLength);
++StepsSinceMoveStart;
PlayFootstepNow();
}
}
void UFootstepComponent::PlayFootstepNow()
{
if (!OwnerMovement || !SoundBank || !ShouldPlaySounds())
{
return;
}
const EFootstepGait Gait = SelectGait(OwnerMovement->Velocity.Size2D());
FVector StepLocation;
const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(StepLocation);
if (!Sounds)
{
return;
}
const FFootstepGaitSettings& Settings = GetGaitSettings(Gait);
PlayCue(Sounds->GetStepSound(Gait), Settings.Volume, Settings.PitchVariation, StepLocation);
// Memorise APRES coup : c'est cette allure-la que le son d'arret devra
// reprendre, pas celle qu'aura le personnage une fois immobile.
LastGait = Gait;
}
void UFootstepComponent::HandleMovementModeChanged(ACharacter* Character, EMovementMode PrevMovementMode, uint8 PreviousCustomMode)
{
if (!bEnableFootsteps || !OwnerMovement || !SoundBank || !ShouldPlaySounds())
{
return;
}
const EMovementMode NewMode = OwnerMovement->MovementMode;
// Decollage
if (NewMode == MOVE_Falling && PrevMovementMode == MOVE_Walking)
{
MaxFallSpeed = 0.f;
// Un saut, et pas une sortie de rebord : c'est le SIGNE de la vitesse
// verticale qui tranche, exactement comme dans l'AnimBP du corps. Sans
// ce test, marcher au bord d'une caisse ferait grogner le personnage.
if (OwnerMovement->Velocity.Z > 0.f)
{
FVector JumpLocation;
if (const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(JumpLocation))
{
const FFootstepGaitSettings& Settings = GetGaitSettings(SelectGait(OwnerMovement->Velocity.Size2D()));
PlayCue(Sounds->Jump, AirVolume, Settings.PitchVariation, JumpLocation);
}
}
// Le cycle de foulee repart de zero a la reception : bWasMoving remis a
// faux fait passer le premier tick au sol par la branche "entree en
// mouvement", donc par FirstStepFraction. Retomber en pleine course
// redonne ainsi un pas tout de suite.
DistanceSinceLastStep = 0.f;
StepsSinceMoveStart = 0;
bWasMoving = false;
return;
}
// Reception
if (PrevMovementMode == MOVE_Falling && NewMode == MOVE_Walking)
{
if (MaxFallSpeed > LandMinFallSpeed)
{
FVector LandLocation;
if (const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(LandLocation))
{
const FFootstepGaitSettings& Settings = GetGaitSettings(SelectGait(OwnerMovement->Velocity.Size2D()));
PlayCue(Sounds->Land, AirVolume, Settings.PitchVariation, LandLocation);
}
}
MaxFallSpeed = 0.f;
DistanceSinceLastStep = 0.f;
StepsSinceMoveStart = 0;
bWasMoving = false;
}
}
void UFootstepComponent::SetFootstepsEnabled(bool bEnabled)
{
bEnableFootsteps = bEnabled;
if (!bEnabled)
{
ResetStride();
}
}
void UFootstepComponent::ResetStride()
{
DistanceSinceLastStep = 0.f;
StepsSinceMoveStart = 0;
MaxFallSpeed = 0.f;
bWasMoving = false;
// La position memorisee aussi : un respawn deplace le pawn de plusieurs
// centaines de metres, et ce deplacement ne doit pas compter comme un pas.
bHasLastLocation = false;
}
EFootstepGait UFootstepComponent::SelectGait(float HorizontalSpeed) const
{
if (OwnerMovement && OwnerMovement->IsCrouching())
{
return EFootstepGait::Walk;
}
// Le seuil se lit sur la vitesse REELLE et pas sur AFpsPlayer::IsSprinting() :
// le composant reste ainsi posable sur un PNJ, qui n'a pas de touche Maj.
return (HorizontalSpeed > SprintSpeedThreshold) ? EFootstepGait::Run : EFootstepGait::Jog;
}
const FFootstepGaitSettings& UFootstepComponent::GetGaitSettings(EFootstepGait Gait) const
{
switch (Gait)
{
case EFootstepGait::Walk: return CrouchGait;
case EFootstepGait::Run: return SprintGait;
default: return WalkGait;
}
}
const FFootstepSurfaceSounds* UFootstepComponent::QueryGroundSounds(FVector& OutLocation) const
{
if (!SoundBank || !OwnerCharacter)
{
return nullptr;
}
const UCapsuleComponent* Capsule = OwnerCharacter->GetCapsuleComponent();
const float HalfHeight = Capsule ? Capsule->GetScaledCapsuleHalfHeight() : 0.f;
const FVector Center = OwnerCharacter->GetActorLocation();
const FVector Feet = Center - FVector(0.f, 0.f, HalfHeight);
// Repli de position : si le trace ne touche rien, le son part quand meme,
// depuis les pieds. Un silence serait plus difficile a diagnostiquer.
OutLocation = Feet;
const UWorld* World = GetWorld();
if (!World)
{
return &SoundBank->DefaultSurface;
}
// bTraceComplex : voir le commentaire de la propriete. Sans lui on recupere
// le PhysicalMaterial du BodySetup, presque toujours vide, et tout le decor
// sonne sur DefaultSurface.
FCollisionQueryParams Params(SCENE_QUERY_STAT(Footstep), bTraceComplex, OwnerCharacter);
Params.bReturnPhysicalMaterial = true;
FHitResult Hit;
if (!World->LineTraceSingleByChannel(Hit, Center, Feet - FVector(0.f, 0.f, TraceDepth), TraceChannel, Params))
{
return &SoundBank->DefaultSurface;
}
OutLocation = Hit.ImpactPoint;
return &SoundBank->GetSurfaceSounds(UGameplayStatics::GetSurfaceType(Hit));
}
void UFootstepComponent::PlayCue(USoundBase* Sound, float Volume, float PitchVariation, const FVector& Location) const
{
if (!Sound || Volume <= 0.f)
{
return;
}
const float Pitch = (PitchVariation > 0.f)
? FMath::FRandRange(1.f - PitchVariation, 1.f + PitchVariation)
: 1.f;
// Le pas du joueur local en 2D : joue en 3D depuis ses propres chevilles, il
// passe par l'attenuation et sonne creux, alors qu'il devrait etre le son le
// plus present du mixage. Les Cue n'ont pas de SoundClass, donc les deux
// chemins tombent sur SC_SFX, la classe par defaut du projet -- le curseur
// "Effets" les couvre sans qu'on ait rien a taguer.
if (bPlayLocalStepsIn2D && OwnerCharacter && OwnerCharacter->IsLocallyControlled())
{
UGameplayStatics::PlaySound2D(this, Sound, Volume * LocalVolumeScale, Pitch, 0.f, Concurrency, GetOwner());
return;
}
// PlaySoundAtLocation et pas SpawnSoundAttached : un pas est un one-shot de
// quelques dixiemes de seconde, l'attacher creerait puis detruirait un
// UAudioComponent trois fois par seconde et par joueur pour rien.
UGameplayStatics::PlaySoundAtLocation(this, Sound, Location, FRotator::ZeroRotator,
Volume, Pitch, 0.f, Attenuation, Concurrency, GetOwner());
}
bool UFootstepComponent::ShouldPlaySounds() const
{
// Un serveur dedie n'a pas de sortie audio : inutile d'y tracer le sol pour
// quatre personnages. Le projet tourne en serveur d'ecoute, mais la garde ne
// coute qu'une comparaison et evite d'y repenser le jour ou ca change.
const UWorld* World = GetWorld();
return World && World->GetNetMode() != NM_DedicatedServer;
}
@@ -0,0 +1,38 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "FootstepSoundsDataAsset.h"
USoundBase* FFootstepSurfaceSounds::GetStepSound(EFootstepGait Gait) const
{
switch (Gait)
{
case EFootstepGait::Walk: return WalkStep;
case EFootstepGait::Jog: return JogStep;
case EFootstepGait::Run: return RunStep;
default: return nullptr;
}
}
USoundBase* FFootstepSurfaceSounds::GetStopSound(EFootstepGait Gait) const
{
switch (Gait)
{
case EFootstepGait::Walk: return WalkStop;
case EFootstepGait::Jog: return JogStop;
case EFootstepGait::Run: return RunStop;
default: return nullptr;
}
}
const FFootstepSurfaceSounds& UFootstepSoundsDataAsset::GetSurfaceSounds(EPhysicalSurface Surface) const
{
// Find et pas FindRef : on rend une reference sur l'entree de la table, sans
// copier les huit pointeurs a chaque pas.
if (const FFootstepSurfaceSounds* Found = Surfaces.Find(Surface))
{
return *Found;
}
return DefaultSurface;
}
@@ -5,6 +5,7 @@
#include "FpsPlayer.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "HeldItemComponent.h"
void UFpsArmsAnimInstance::NativeInitializeAnimation()
{
@@ -47,9 +48,30 @@ void UFpsArmsAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
bIsMoving = GroundSpeed > MovingSpeedThreshold
&& !Movement->GetCurrentAcceleration().IsNearlyZero();
// Lu depuis le composant plutot que mis en cache dans un membre : apres un
// respawn le pawn est un NOUVEL acteur, avec un nouveau composant, et un
// pointeur garde ici pointerait sur celui du cadavre.
if (const UHeldItemComponent* HeldItems = Player->GetHeldItemComponent())
{
bHasItemInHand = HeldItems->HasItemInHand();
}
bIsSprinting = Player->IsSprinting();
bIsFalling = Movement->IsFalling();
bIsCrouching = Movement->IsCrouching();
VerticalSpeed = Movement->Velocity.Z;
// Les deux etats de deplacement au sol sont composes ICI et nulle part
// ailleurs. bIsSprinting ne dit que « la touche est enfoncee » : sans le ET
// avec bIsMoving, appuyer sur Maj a l'arret suffit a lancer l'animation de
// course, bras qui pompent devant un personnage qui ne bouge pas.
//
// Le sol et l'accroupi sont exclus des deux pour que ces booleens restent
// mutuellement exclusifs avec les etats Air et Crouch de la machine : deux
// conditions vraies en meme temps, c'est une transition prise au hasard.
const bool bGrounded = !bIsFalling && !bIsCrouching;
bIsRunning = bGrounded && bIsMoving && bIsSprinting;
bIsWalking = bGrounded && bIsMoving && !bIsSprinting;
// La cadence suit la vitesse REELLE et pas l'etat de sprint : c'est ce qui
// fait que les bras accelerent pendant la montee en vitesse, au lieu de
@@ -17,8 +17,10 @@
#include "InputActionValue.h"
#include "CharacterAppearanceComponent.h"
#include "CraftingComponent.h"
#include "FootstepComponent.h"
#include "FpsPlayerController.h"
#include "HeadBobComponent.h"
#include "HeldItemComponent.h"
#include "InteractionComponent.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
@@ -111,6 +113,8 @@ AFpsPlayer::AFpsPlayer()
CraftingComponent = CreateDefaultSubobject<UCraftingComponent>(TEXT("CraftingComponent"));
SurvivalStats = CreateDefaultSubobject<USurvivalStatsComponent>(TEXT("SurvivalStats"));
AppearanceComponent = CreateDefaultSubobject<UCharacterAppearanceComponent>(TEXT("AppearanceComponent"));
HeldItemComponent = CreateDefaultSubobject<UHeldItemComponent>(TEXT("HeldItemComponent"));
FootstepComponent = CreateDefaultSubobject<UFootstepComponent>(TEXT("FootstepComponent"));
// En vue FPS on ne veut pas voir son propre corps (mesh 3e personne),
// mais on garde son ombre.
@@ -377,6 +381,31 @@ void AFpsPlayer::OnRep_PlayerState()
RefreshAppearanceFromPlayerState();
}
void AFpsPlayer::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// Cote serveur uniquement : chez l'hote c'est ce chemin qui remplit sa
// propre main, IsLocallyControlled() ne devenant vrai qu'ici.
if (HeldItemComponent)
{
HeldItemComponent->RefreshFromInventory();
}
}
void AFpsPlayer::OnRep_Controller()
{
Super::OnRep_Controller();
// Le pendant client. Meme raison qu'OnRep_PlayerState juste au-dessus : rien
// ne dit lequel du pawn ou du controller arrive le premier, donc les deux
// bouts de la course appellent la meme fonction idempotente.
if (HeldItemComponent)
{
HeldItemComponent->RefreshFromInventory();
}
}
void AFpsPlayer::RefreshAppearanceFromPlayerState()
{
if (!AppearanceComponent)
@@ -470,6 +499,14 @@ void AFpsPlayer::Landed(const FHitResult& Hit)
FallSpeed, LandPunchMinFallSpeed);
}
// Le geste des bras suit le meme seuil bas que la camera : une marche
// d'escalier ne doit ni secouer la vue ni faire encaisser le personnage.
if (FallSpeed > LandPunchMinFallSpeed)
{
UAnimMontage* Montage = (FallSpeed >= HardLandingFallSpeed) ? HardLandMontage : LandMontage;
PlayArmsMontage(Montage);
}
LastFallSpeed = 0.f;
}
@@ -899,6 +936,11 @@ void AFpsPlayer::HandleDeath()
ClearTransientInputStates();
GetCharacterMovement()->DisableMovement();
// Un cadavre ne marche plus. Sans ca, la derniere foulee en cours produirait
// encore son pas final au moment ou la vitesse tombe a zero -- et HandleDeath
// tourne sur toutes les machines, donc les amis l'entendraient aussi.
FootstepComponent->SetFootstepsEnabled(false);
if (APlayerController* OwningController = Cast<APlayerController>(GetController()))
{
DisableInput(OwningController);
@@ -378,7 +378,7 @@ void AFpsPlayerController::SetupInputComponent()
if (PauseAction)
{
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandlePauseInput);
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandleEscapeInput);
}
else
{
@@ -956,7 +956,7 @@ void AFpsPlayerController::CheckStorageDistance()
}
}
void AFpsPlayerController::HandlePauseInput()
void AFpsPlayerController::HandleEscapeInput()
{
// La boite a le focus et consomme deja Echap elle-meme. Si elle l'a perdu,
// on l'ignore quand meme : repondre ici refermerait le menu en laissant la
@@ -1013,7 +1013,12 @@ void AFpsPlayerController::OpenPauseMenu()
}
bPauseMenuOpen = true;
PauseMenu->SetVisibility(ESlateVisibility::Visible);
// Open() et pas un simple SetVisibility : le menu prend AUSSI le focus
// clavier. Sans lui, Echap ne reviendrait plus des le premier clic sur un
// bouton -- un UButton prend le focus, et un widget focalise coupe la route
// des touches vers Enhanced Input.
PauseMenu->Open();
// AUCUN SetPause : en coop la pause est impossible par nature. Cote client
// l'appel est ignore -- la pause appartient a l'autorite -- et cote hote il
@@ -1052,9 +1057,11 @@ void AFpsPlayerController::ResumeGame()
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
PauseMenu->Close();
}
// FInputModeGameOnly repose le focus clavier sur le viewport : c'est ce qui
// rend leurs touches au pawn et au controller apres le passage du menu.
SetUiInputMode(false);
}
@@ -1197,7 +1204,7 @@ void AFpsPlayerController::OpenSettings()
// on pourrait cliquer Reprendre a travers l'ecran de reglages.
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
PauseMenu->SetInteractive(false);
}
}
@@ -1210,9 +1217,12 @@ void AFpsPlayerController::HandleSettingsCloseRequested()
SettingsMenu->Close();
}
// SetInteractive(true) reprend le focus clavier, que l'ecran de reglages
// avait pris pour lui et ne rend a personne en se fermant. L'oublier laisse
// le menu de pause affiche mais sourd a Echap.
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Visible);
PauseMenu->SetInteractive(true);
}
}
@@ -1260,7 +1270,7 @@ void AFpsPlayerController::RequestQuitToMainMenu()
// pourrait cliquer Reprendre alors que la question est encore posee.
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
PauseMenu->SetInteractive(false);
}
}
@@ -1356,7 +1366,7 @@ void AFpsPlayerController::BeginQuitToMainMenu()
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
PauseMenu->Close();
}
// L'ecran d'inventaire ou de coffre resterait affiche par-dessus le noir,
@@ -1378,9 +1388,11 @@ void AFpsPlayerController::HandleQuitToMenuCancelled()
QuitToMenuDialog->Close();
}
// Meme raison qu'a la fermeture des reglages : la boite avait le focus
// clavier, on le reprend en redevenant la couche active.
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Visible);
PauseMenu->SetInteractive(true);
}
}
@@ -0,0 +1,386 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "HeldItemComponent.h"
#include "Animation/AnimMontage.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/StaticMeshComponent.h"
#include "FpsPlayer.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#include "Net/UnrealNetwork.h"
UHeldItemComponent::UHeldItemComponent()
{
// Aucun Tick : le montage est evenementiel. Il ne se declenche qu'au
// changement de case active ou de contenu d'inventaire, et les deux sont
// deja des delegues.
PrimaryComponentTick.bCanEverTick = false;
// Obligatoire, et pas seulement pour repliquer HeldItem : sans lui le moteur
// JETTE SILENCIEUSEMENT Server_SetHeldItem. Aucun message, aucune erreur --
// l'objet reste simplement invisible chez les autres joueurs.
SetIsReplicatedByDefault(true);
}
void UHeldItemComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// COND_SkipOwner, exactement comme AFpsPlayer::bIsSprinting : le proprietaire
// est celui qui a DECIDE de ce changement et l'a deja applique en local. Le
// lui renvoyer poserait une valeur ayant un aller-retour de retard, ce qui
// fait clignoter l'objet quand on change de case rapidement.
DOREPLIFETIME_CONDITION(UHeldItemComponent, HeldItem, COND_SkipOwner);
}
void UHeldItemComponent::BeginPlay()
{
Super::BeginPlay();
AFpsPlayer* Player = GetOwningPlayer();
if (!Player)
{
UE_LOG(LogTemp, Warning, TEXT("UHeldItemComponent : porteur %s n'est pas un AFpsPlayer, aucun objet ne sera montre en main."),
*GetNameSafe(GetOwner()));
return;
}
if (UInventoryComponent* Inventory = Player->GetInventoryComponent())
{
Inventory->OnSelectedHotbarSlotChanged.AddDynamic(this, &UHeldItemComponent::HandleSelectedHotbarSlotChanged);
// Le second abonnement n'est pas une redondance : vider la case active
// au glisser-deposer, ou consommer la derniere charge de ce qu'on tient,
// ne change pas l'INDEX selectionne. Sans lui, la pomme resterait en main
// apres avoir ete mangee.
Inventory->OnInventoryChanged.AddDynamic(this, &UHeldItemComponent::HandleInventoryChanged);
}
// Chez l'hote et en solo, tout est deja en place a cet instant. Chez un
// client le controller peut manquer encore : AFpsPlayer rappellera alors
// depuis OnRep_Controller, et cette fonction est idempotente.
RefreshFromInventory();
}
void UHeldItemComponent::OnUnregister()
{
// Les composants engendres appartiennent a l'acteur et disparaissent avec
// lui. On ne lache que nos references, pour ne pas retenir des objets que le
// GC doit pouvoir reprendre.
FirstPersonHeldMesh = nullptr;
ThirdPersonHeldMesh = nullptr;
Super::OnUnregister();
}
AFpsPlayer* UHeldItemComponent::GetOwningPlayer() const
{
return Cast<AFpsPlayer>(GetOwner());
}
void UHeldItemComponent::HandleInventoryChanged()
{
RefreshFromInventory();
}
void UHeldItemComponent::HandleSelectedHotbarSlotChanged(int32 /*NewIndex*/)
{
RefreshFromInventory();
}
void UHeldItemComponent::RefreshFromInventory()
{
const AFpsPlayer* Player = GetOwningPlayer();
if (!Player)
{
return;
}
// Reposee AVANT la garde ci-dessous, et c'est tout l'interet : le passage de
// « pas encore controle » a « controle localement » est justement ce qui doit
// cacher l'objet en main, et AFpsPlayer rappelle cette fonction depuis
// OnRep_Controller. La poser apres la garde ne la ferait jamais tourner chez
// celui qui vient de prendre le controle.
UpdateLocalVisibility();
// LA garde de ce composant. Chez l'hote, le pawn de chaque ami porte aussi
// un inventaire et un UHeldItemComponent : lire la case active d'un pawn
// distant y trouverait toujours l'index 0, puisque SelectedHotbarIndex n'est
// pas replique. Sans ce test, le serveur remplacerait la hache d'un client
// par le contenu de sa premiere case, en boucle.
if (!Player->IsLocallyControlled())
{
return;
}
const UInventoryComponent* Inventory = Player->GetInventoryComponent();
if (!Inventory)
{
return;
}
const FInventorySlot Selected = Inventory->GetSelectedSlot();
SetHeldItem(Selected.IsEmpty() ? nullptr : Selected.Item.Get());
}
void UHeldItemComponent::SetHeldItem(UItemDataAsset* NewItem)
{
if (HeldItem == NewItem)
{
return;
}
HeldItem = NewItem;
ApplyHeldVisuals();
PlayEquipMontage();
// En local d'abord, pour que l'objet apparaisse sous les doigts sans attendre
// un aller-retour ; le serveur suit. Le sens inverse donnerait une main qui
// se remplit avec un temps de retard a chaque coup de molette.
if (const AActor* Owner = GetOwner(); Owner && !Owner->HasAuthority())
{
Server_SetHeldItem(NewItem);
}
}
void UHeldItemComponent::PlayEquipMontage()
{
// Ranger un objet ne joue rien : le pack ne livre que des gestes de sortie
// d'outil, et un desequipement sans animation ne se remarque pas -- l'objet
// suivant arrive dans la foulee avec le sien.
if (!HeldItem || !HeldItem->EquipMontage)
{
return;
}
AFpsPlayer* Player = GetOwningPlayer();
// Les bras sont en OnlyOwnerSee : chez l'hote, le pawn de chaque ami porte
// AUSSI un squelette de bras que personne ne rend. Y jouer un montage ferait
// tourner quatre machines a etats pour rien. La garde est deja tenue par
// RefreshFromInventory, on la repose ici parce qu'un futur appelant
// l'oublierait.
if (!Player || !Player->IsLocallyControlled())
{
return;
}
Player->PlayArmsMontage(HeldItem->EquipMontage);
}
void UHeldItemComponent::Server_SetHeldItem_Implementation(UItemDataAsset* NewItem)
{
if (HeldItem == NewItem)
{
return;
}
HeldItem = NewItem;
// Le serveur rend lui aussi les pawns distants -- l'hote voit ses amis. Sans
// cet appel, la hache n'existerait que chez les clients.
ApplyHeldVisuals();
}
void UHeldItemComponent::OnRep_HeldItem()
{
ApplyHeldVisuals();
}
void UHeldItemComponent::ApplyHeldVisuals()
{
EnsureHeldMeshComponents();
const UItemDataAsset* Item = HeldItem;
// Un objet peut tres bien n'avoir aucune representation en main : c'est le
// cas par defaut de toutes les ressources du jeu. On demonte alors, sans que
// ce soit une anomalie.
const bool bShow = Item != nullptr && Item->bShowInHand && Item->GetHeldMesh() != nullptr;
bItemVisibleInHand = bShow;
if (FirstPersonHeldMesh)
{
if (bShow)
{
Item->ApplyHeldVisualsTo(FirstPersonHeldMesh);
FirstPersonHeldMesh->SetRelativeTransform(Item->FirstPersonHeldOffset);
}
else
{
FirstPersonHeldMesh->SetStaticMesh(nullptr);
}
FirstPersonHeldMesh->SetVisibility(bShow);
}
if (ThirdPersonHeldMesh)
{
if (bShow)
{
Item->ApplyHeldVisualsTo(ThirdPersonHeldMesh);
ThirdPersonHeldMesh->SetRelativeTransform(Item->ThirdPersonHeldOffset);
}
else
{
ThirdPersonHeldMesh->SetStaticMesh(nullptr);
}
// Et surtout pas SetVisibility(bShow) : chez le joueur qui tient l'objet,
// ce mesh doit rester invisible tout en projetant son ombre.
UpdateLocalVisibility();
}
OnHeldItemChanged.Broadcast(HeldItem);
}
void UHeldItemComponent::UpdateLocalVisibility()
{
if (!ThirdPersonHeldMesh)
{
return;
}
const AFpsPlayer* Player = GetOwningPlayer();
// Le pawn qu'on controle est le seul dont on ne doit pas voir le corps -- donc
// le seul dont l'objet en main se cache. Chez les trois autres joueurs le meme
// pawn n'est pas controle localement, et l'outil s'y affiche normalement.
//
// Relu a CHAQUE passage plutot que pose une fois : chez un client le pawn
// arrive par replication AVANT son controller, et IsLocallyControlled() y est
// donc faux pendant les premieres images. Sans cette relecture, le joueur
// verrait sa propre pioche flotter devant lui pour le reste de la partie.
const bool bHideForLocalPlayer = Player != nullptr && Player->IsLocallyControlled();
const bool bHasMesh = ThirdPersonHeldMesh->GetStaticMesh() != nullptr;
ThirdPersonHeldMesh->SetVisibility(bHasMesh && !bHideForLocalPlayer);
}
void UHeldItemComponent::EnsureHeldMeshComponents()
{
AFpsPlayer* Player = GetOwningPlayer();
if (!Player)
{
return;
}
if (!FirstPersonHeldMesh)
{
FirstPersonHeldMesh = CreateHeldMeshComponent(
TEXT("FirstPersonHeldMesh"), Player->GetFirstPersonArms(), FirstPersonSocket,
/*bCastWorldShadow=*/false);
}
if (!ThirdPersonHeldMesh)
{
ThirdPersonHeldMesh = CreateHeldMeshComponent(
TEXT("ThirdPersonHeldMesh"), Player->GetMesh(), ThirdPersonSocket,
/*bCastWorldShadow=*/true);
}
}
UStaticMeshComponent* UHeldItemComponent::CreateHeldMeshComponent(FName ComponentName, USkeletalMeshComponent* Parent,
FName SocketName, bool bCastWorldShadow)
{
AActor* Owner = GetOwner();
if (!Owner || !Parent)
{
return nullptr;
}
// Un os introuvable n'est pas une erreur pour le moteur : il attache a la
// RACINE du composant, en silence, et l'objet apparait plante au milieu du
// torse. C'est ce log qui distingue « mal place » de « mal nomme », et la
// difference vaut une heure de recherche dans le mauvais fichier.
//
// Le test du mesh n'est pas superflu : UCharacterAppearanceComponent
// REMPLACE le mesh du corps a son BeginPlay, et l'ordre des BeginPlay entre
// composants n'est garanti par rien. Sans lui, un avertissement partirait un
// coup sur deux pour un os parfaitement valide -- soit exactement le faux
// indice que ce log est cense eviter. L'attachement, lui, se moque du
// moment : le nom est resolu a chaque lecture de la transform du socket.
if (!SocketName.IsNone() && Parent->GetSkeletalMeshAsset() && !Parent->DoesSocketExist(SocketName) && !bLoggedMissingSocket)
{
UE_LOG(LogTemp, Warning, TEXT("UHeldItemComponent : le socket ou l'os '%s' n'existe pas sur %s. L'objet en main sera attache a la racine du mesh."),
*SocketName.ToString(), *GetNameSafe(Parent->GetSkeletalMeshAsset()));
bLoggedMissingSocket = true;
}
UStaticMeshComponent* Held = NewObject<UStaticMeshComponent>(Owner, ComponentName);
if (!Held)
{
return nullptr;
}
Held->SetupAttachment(Parent, SocketName);
// AVANT l'enregistrement, et c'est le piege le plus couteux de ce fichier :
// un UStaticMeshComponent cree par NewObject nait en Mobility STATIC, alors
// qu'un sous-objet declare dans un Blueprint herite du Movable de son
// parent. Le mesh suit pourtant la main sans broncher, ce qui donne
// l'illusion que tout va bien -- mais sous Virtual Shadow Maps une primitive
// Static part dans le cache d'ombres statiques, qui n'est pas reinvalide
// quand elle bouge. Symptome : l'objet est la, son ombre non. Et il n'y a
// aucun message pour le dire.
Held->SetMobility(EComponentMobility::Movable);
// Un objet tenu en main n'est pas une surface de jeu. Sans ca, la hache
// bloquerait le trace d'interaction -- qui part exactement d'entre les bras.
Held->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Held->RegisterComponent();
// Apres l'enregistrement, et pas avant : les setters d'etat de rendu
// marquent le proxy « sale », ce qui ne veut rien dire sur un composant qui
// n'est pas encore dans le monde. Les poser des deux cotes de
// RegisterComponent est le genre de subtilite qui fait qu'un drapeau prend
// et l'autre non -- on les pose donc tous ici.
// « Visible pour moi seul » se recopie du porteur : c'est ce qui tient les
// bras, et un mesh d'arme n'a pas a savoir qu'il les habille.
Held->SetOnlyOwnerSee(Parent->bOnlyOwnerSee);
// bOwnerNoSee du porteur, en revanche, ne se recopie SURTOUT PAS, alors que
// c'est par la que le Mesh du Character se cache lui-meme. Mesure sur ce
// projet : cases « Cast Shadow » et « Hidden Shadow » cochees et actives sur
// le composant, aucune ombre tant que bOwnerNoSee valait vrai, ombre correcte
// des la seconde ou on cache le mesh par « Visible » a la place.
//
// Autrement dit bCastHiddenShadow n'honore, sur un UStaticMeshComponent, que
// la dissimulation par la VISIBILITE -- pas celle par le proprietaire. Le
// corps y echappe parce qu'il est un mesh SQUELETTIQUE, qui n'emprunte pas le
// meme chemin de relevance dans le renderer. D'ou le partage : le porteur se
// cache comme il veut, l'objet en main se cache par UpdateLocalVisibility().
//
// Ce n'est pas une perte de fonctionnalite : sur une machine donnee, le seul
// pawn dont on est proprietaire est celui qu'on controle, donc les deux
// mecanismes cachent exactement les memes objets aux memes joueurs.
// L'OMBRE ne se recopie pas non plus -- les deux meshes n'ont pas du tout le
// meme role. Celui du corps doit projeter son ombre MEME invisible pour son
// proprietaire (bCastHiddenShadow), sinon on voit son propre corps au sol
// sans l'outil qu'on tient. Celui des bras ne doit rien projeter du tout :
// colle a l'objectif, il barrerait le decor d'une ombre geante.
Held->SetCastShadow(bCastWorldShadow);
Held->SetCastHiddenShadow(bCastWorldShadow);
// Rien a montrer tant qu'aucun objet n'est monte.
Held->SetVisibility(false);
// Sans ca le composant existe, se rend et s'anime, mais reste INTROUVABLE
// dans le Details panel du pawn en PIE : le panneau ne liste que les
// sous-objets du template et les composants declares comme composants
// d'instance. C'est du confort de debogage, mais c'est ce confort qui
// manquait pour diagnostiquer cette ombre.
Owner->AddInstanceComponent(Held);
UE_LOG(LogTemp, Log, TEXT("UHeldItemComponent : '%s' monte sur '%s' (socket '%s' %s, ombre %s)."),
*ComponentName.ToString(), *Parent->GetName(), *SocketName.ToString(),
Parent->DoesSocketExist(SocketName) ? TEXT("trouve") : TEXT("INTROUVABLE"),
bCastWorldShadow ? TEXT("activee") : TEXT("desactivee"));
return Held;
}
@@ -11,6 +11,36 @@
#define LOCTEXT_NAMESPACE "ItemData"
namespace
{
/**
* Pose un mesh et son eventuel materiau de surcharge sur un composant.
*
* Prefixe du fichier oblige : le build unity concatene les .cpp du module,
* un namespace anonyme n'isole rien.
*/
void ItemDataApplyMeshAndMaterial(UStaticMeshComponent* Component, UStaticMesh* Mesh, UMaterialInterface* Material)
{
// OverrideMaterials appartient au COMPOSANT, pas au mesh : il survit donc
// a un changement d'asset. Sans ce vidage, un objet sans surcharge monte
// apres un objet qui en avait une garderait le materiau du precedent --
// le meme piege que les meshes de corps de la customisation. Il ne se
// voit que sur un composant REUTILISE, donc jamais sur un pickup fraichement
// engendre, et systematiquement sur l'objet en main ou sur un grill.
Component->EmptyOverrideMaterials();
Component->SetStaticMesh(Mesh);
// SetMaterial(0, nullptr) ne "retire" pas le materiau, il pose un slot vide
// et le mesh devient gris damier. Sans override, on ne touche a rien et le
// mesh garde le sien.
if (Material)
{
Component->SetMaterial(0, Material);
}
}
}
void UItemDataAsset::ApplyWorldVisualsTo(UStaticMeshComponent* Component) const
{
if (!Component)
@@ -18,15 +48,20 @@ void UItemDataAsset::ApplyWorldVisualsTo(UStaticMeshComponent* Component) const
return;
}
Component->SetStaticMesh(WorldMesh);
ItemDataApplyMeshAndMaterial(Component, WorldMesh, WorldMeshMaterial);
}
// SetMaterial(0, nullptr) ne "retire" pas le materiau, il pose un slot vide
// et le mesh devient gris damier. Sans override, on ne touche a rien et le
// mesh garde le sien.
if (WorldMeshMaterial)
void UItemDataAsset::ApplyHeldVisualsTo(UStaticMeshComponent* Component) const
{
if (!Component)
{
Component->SetMaterial(0, WorldMeshMaterial);
return;
}
// Le materiau de surcharge est celui du monde, sans equivalent "en main" :
// il sert a distinguer un cru d'un cuit, et cette distinction vaut aussi
// dans la main. Un second champ n'aurait fait que se desynchroniser.
ItemDataApplyMeshAndMaterial(Component, GetHeldMesh(), WorldMeshMaterial);
}
#if WITH_EDITOR
@@ -69,6 +104,22 @@ EDataValidationResult UItemDataAsset::IsDataValid(FDataValidationContext& Contex
Context.AddWarning(LOCTEXT("CookableWithCharges", "This item has multiple uses AND can be cooked. The remaining charges follow the item through cooking, which is rarely what you want for food."));
}
// Le symptome serait une main vide sans le moindre message : le composant
// aurait bien monte son mesh, avec un asset nul.
if (bShowInHand && !GetHeldMesh())
{
Context.AddError(LOCTEXT("HeldNoMesh", "'Show In Hand' is checked but neither 'Held Mesh' nor 'World Mesh' is assigned: nothing would appear in the player's hand."));
Result = EDataValidationResult::Invalid;
}
// Un outil invisible se remarque tout de suite en jeu, mais on cherche la
// cause du mauvais cote -- dans le composant, dans le socket, dans le rig --
// avant de penser a une case a cocher.
if (IsTool() && !bShowInHand)
{
Context.AddWarning(LOCTEXT("ToolNotShownInHand", "This item is a tool but 'Show In Hand' is unchecked: the player will harvest with an empty hand."));
}
return Result;
}
#endif
@@ -3,6 +3,9 @@
#include "MainMenuPlayerController.h"
#include "CharacterAppearanceComponent.h"
#include "CharacterCustomizationWidget.h"
#include "CharacterPreviewActor.h"
#include "ConfirmDialogWidget.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
@@ -13,6 +16,7 @@
#include "SessionSubsystem.h"
#include "SettingsMenuWidget.h"
#include "SurvivalGameInstance.h"
#include "SurvivalUserSettings.h"
#define LOCTEXT_NAMESPACE "MainMenu"
@@ -193,6 +197,25 @@ void AMainMenuPlayerController::StartGame()
return;
}
// L'ecran de customisation s'intercale ICI, entre le clic et le fondu :
// c'est le choix fait pour ce projet, on ne lance pas une partie sans etre
// passe par son personnage. Sans classe assignee, on retombe sur l'ancien
// comportement -- pratique pour tester le reste sans le traverser.
if (CustomizationMenuClass && !CustomizationMenu)
{
PendingAction = EMenuPendingAction::OpenCustomization;
if (MainMenu)
{
MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
}
// On fond au noir AVANT de bouger la camera : le mannequin est ailleurs
// dans la meme map, et un travelling jusqu'a lui traverserait le decor.
ScreenFade->FadeOut(CustomizationFadeDuration);
return;
}
if (GameLevel.IsNull())
{
UE_LOG(LogTemp, Error, TEXT("AMainMenuPlayerController : GameLevel n'est pas assignee, impossible de lancer la partie."));
@@ -203,7 +226,13 @@ void AMainMenuPlayerController::StartGame()
// Le menu reste visible pendant le fondu mais cesse de repondre : cliquer
// Quitter pendant le fondu vers la partie serait un piege.
if (MainMenu)
//
// Le test de visibilite est indispensable : quand on arrive ici depuis
// l'ecran de customisation, le menu a ete Collapsed a son ouverture, et
// HitTestInvisible le RESSUSCITERAIT -- on verrait reapparaitre Jouer,
// Reglages et Quitter par-dessus le personnage qu'on vient de valider.
// HitTestInvisible rend non cliquable, il ne cache pas.
if (MainMenu && MainMenu->GetVisibility() != ESlateVisibility::Collapsed)
{
MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
}
@@ -211,6 +240,139 @@ void AMainMenuPlayerController::StartGame()
ScreenFade->FadeOut(LeaveFadeDuration);
}
// ----------------------------------------------------------------------
// Ecran de customisation
// ----------------------------------------------------------------------
void AMainMenuPlayerController::OpenCustomization()
{
// Cherche au moment de l'ouverture et pas au BeginPlay : c'est le seul
// endroit ou l'on est sur que la map est entierement chargee, et un
// mannequin absent doit rester rattrapable.
if (!CharacterPreview)
{
for (TActorIterator<ACharacterPreviewActor> It(GetWorld()); It; ++It)
{
CharacterPreview = *It;
break;
}
}
if (!CharacterPreview)
{
UE_LOG(LogTemp, Warning,
TEXT("AMainMenuPlayerController : aucun ACharacterPreviewActor dans la map, l'ecran de customisation est saute."));
// On ne bloque pas le joueur pour un acteur manquant : meme logique
// qu'un echec de session, il a clique Jouer, il veut jouer.
CustomizationMenuClass = nullptr;
StartGame();
return;
}
CustomizationMenu = CreateWidget<UCharacterCustomizationWidget>(this, CustomizationMenuClass);
if (!CustomizationMenu)
{
return;
}
// TOUJOURS l'apparence par defaut, jamais celle persistee : cet ecran est
// une etape de CREATION, pas une reprise. Le jour ou un lobby verifiera que
// les quatre joueurs ont valide leur personnage, il lui faudra un point de
// depart identique pour tout le monde.
//
// La persistance ne devient pas inutile pour autant : elle sert au respawn,
// et surtout a qui arrive par invitation Steam -- celui-la ne passe jamais
// par ici et doit bien avoir un visage.
//
// « Par defaut » veut dire le DefaultAppearance du mannequin, pas les zeros
// du C++ : le personnage de depart se regle ainsi dans BP_CharacterPreview,
// a l'oeil et sans recompiler. C'est le meme reglage que celui pose au
// BeginPlay, donc l'ouverture de l'ecran ne change rien a ce qu'on voyait.
const UCharacterAppearanceComponent* PreviewAppearance = CharacterPreview->GetAppearanceComponent();
CustomizationMenu->SetupCustomization(
CharacterPreview,
PreviewAppearance ? PreviewAppearance->DefaultAppearance : FCharacterAppearance());
CustomizationMenu->OnConfirmed.AddDynamic(this, &AMainMenuPlayerController::HandleCustomizationConfirmed);
CustomizationMenu->OnCancelled.AddDynamic(this, &AMainMenuPlayerController::HandleCustomizationCancelled);
// Sans blend : l'ecran est noir a cet instant, personne ne voit la bascule.
// C'est justement ce que le fondu achete -- un travelling depuis l'angle de
// menu jusqu'au mannequin traverserait le decor de la map.
SetViewTarget(CharacterPreview);
if (MainMenu)
{
MainMenu->SetVisibility(ESlateVisibility::Collapsed);
}
CustomizationMenu->AddToViewport();
// L'ecran a des boutons ET un clic-glisser : il lui faut la souris, et le
// mode UI-only l'empeche de tomber dans le jeu.
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(CustomizationMenu->TakeWidget()));
SetShowMouseCursor(true);
ScreenFade->FadeIn(CustomizationFadeDuration);
}
void AMainMenuPlayerController::HandleCustomizationConfirmed()
{
// Persiste ET annonce -- mais nous sommes dans le menu, aucun serveur
// n'ecoute encore. Seule la persistance compte ici ; l'annonce partira du
// AnnounceCharacterAppearance() d'AFpsPlayerController, une fois la partie
// chargee et le PlayerState en place.
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->SetCharacterAppearance(CustomizationMenu->GetAppearance());
Settings->SaveSettings();
}
CloseCustomization();
// La classe est mise de cote le temps du lancement : sans ca, StartGame
// rouvrirait l'ecran qu'on vient de fermer.
CustomizationMenuClass = nullptr;
StartGame();
}
void AMainMenuPlayerController::HandleCustomizationCancelled()
{
// Le fondu de retour est deja lance : on ignore la demande plutot que de le
// relancer depuis le debut. La garde compte plus depuis qu'Echap declenche
// ce chemin -- une touche se repete bien plus facilement qu'un clic.
if (PendingAction != EMenuPendingAction::None)
{
return;
}
// Meme mecanique qu'a l'aller : on fond, PUIS on bascule la vue. Le retour
// se termine dans HandleFadeFinished.
PendingAction = EMenuPendingAction::CloseCustomization;
if (CustomizationMenu)
{
CustomizationMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
}
ScreenFade->FadeOut(CustomizationFadeDuration);
}
void AMainMenuPlayerController::CloseCustomization()
{
if (CustomizationMenu)
{
CustomizationMenu->RemoveFromParent();
CustomizationMenu = nullptr;
}
// Le menu principal reprend la main : son propre mode d'input est pose par
// le Blueprint, on remet simplement le curseur dans un etat coherent.
SetInputMode(FInputModeUIOnly());
SetShowMouseCursor(true);
}
void AMainMenuPlayerController::HandleHostFailed(FText Reason)
{
UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : session indisponible (%s), la partie se lance hors ligne."), *Reason.ToString());
@@ -386,8 +548,41 @@ void AMainMenuPlayerController::CloseGame()
void AMainMenuPlayerController::HandleFadeFinished()
{
// L'ecran est noir : c'est ICI qu'on bascule la vue sur le mannequin, et
// pas au clic. PendingAction est remise a None tout de suite, sinon le
// fondu d'eclaircissement qui suit repasserait par la meme branche.
if (PendingAction == EMenuPendingAction::OpenCustomization)
{
PendingAction = EMenuPendingAction::None;
OpenCustomization();
return;
}
if (PendingAction == EMenuPendingAction::CloseCustomization)
{
PendingAction = EMenuPendingAction::None;
CloseCustomization();
if (MainMenu)
{
MainMenu->SetVisibility(ESlateVisibility::Visible);
}
// Un nouvel angle au retour plutot que celui d'avant : PickCameraSpot
// evite deja le dernier utilise, donc le menu ne se rouvre jamais sur
// exactement la meme image.
if (AMenuCameraSpot* Spot = PickCameraSpot())
{
SetViewTarget(Spot);
}
ScreenFade->FadeIn(CustomizationFadeDuration);
return;
}
// Le delegue se declenche aussi a la fin du fondu d'ouverture, ou il n'y a
// rien a faire : c'est PendingAction qui distingue les deux cas.
// rien a faire : c'est PendingAction qui distingue les cas.
if (PendingAction != EMenuPendingAction::StartGame)
{
return;
@@ -6,6 +6,15 @@
#include "Components/Button.h"
#include "FpsPlayerController.h"
UPauseMenuWidget::UPauseMenuWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Sans ce drapeau le widget ne peut pas prendre le focus clavier, et
// NativeOnKeyDown ne serait jamais appele. Il ne se change qu'a la
// construction, d'ou sa presence ici et pas dans Open().
SetIsFocusable(true);
}
void UPauseMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
@@ -62,6 +71,56 @@ void UPauseMenuWidget::NativeDestruct()
Super::NativeDestruct();
}
void UPauseMenuWidget::Open()
{
SetInteractive(true);
}
void UPauseMenuWidget::Close()
{
SetVisibility(ESlateVisibility::Collapsed);
}
void UPauseMenuWidget::SetInteractive(bool bInteractive)
{
// HitTestInvisible et non Collapsed : le menu reste lisible derriere l'ecran
// qui passe devant, mais on ne peut plus cliquer Reprendre a travers.
SetVisibility(bInteractive ? ESlateVisibility::Visible : ESlateVisibility::HitTestInvisible);
if (bInteractive)
{
// Le focus doit etre repris a CHAQUE fois que le menu redevient la couche
// active, pas seulement a sa premiere ouverture : l'ecran de reglages et
// la boite de confirmation le prennent pour eux et ne le rendent pas.
SetKeyboardFocus();
}
}
FReply UPauseMenuWidget::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())
{
if (AFpsPlayerController* Controller = GetOwningPlayer<AFpsPlayerController>())
{
// HandleEscapeInput et non ResumeGame : c'est le controller qui tient
// la priorite entre boite, reglages, coffre, inventaire et pause. Un
// second chemin qui refermerait directement le menu laisserait
// l'ecran pose par-dessus affiche dans le vide.
Controller->HandleEscapeInput();
// 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);
}
void UPauseMenuWidget::HandleSettingsClicked()
{
if (AFpsPlayerController* Controller = GetOwningPlayer<AFpsPlayerController>())
+54 -8
View File
@@ -3,8 +3,9 @@
#include "PickupItem.h"
#include "Components/SphereComponent.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#include "Net/UnrealNetwork.h"
@@ -26,17 +27,20 @@ APickupItem::APickupItem()
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
SetRootComponent(Mesh);
// Purement decoratif : c'est la sphere qui porte la collision.
// Purement decoratif : c'est la boite qui porte la collision.
Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Le trace d'interaction passe par le canal Visibility. On ne bloque QUE
// ce canal : le pickup reste traversable par le joueur et les projectiles.
InteractionSphere = CreateDefaultSubobject<USphereComponent>(TEXT("InteractionSphere"));
InteractionSphere->SetupAttachment(Mesh);
InteractionSphere->InitSphereRadius(50.f);
InteractionSphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
InteractionSphere->SetCollisionResponseToAllChannels(ECR_Ignore);
InteractionSphere->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
InteractionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("InteractionBox"));
InteractionBox->SetupAttachment(Mesh);
// Valeur de repli seulement : FitInteractionBox l'ecrase des qu'un mesh est
// pose. Elle ne sert qu'aux pickups sans ItemData, et au cas ou
// bAutoFitInteractionBox serait decoche sans qu'on regle la boite.
InteractionBox->SetBoxExtent(FVector(25.f));
InteractionBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
InteractionBox->SetCollisionResponseToAllChannels(ECR_Ignore);
InteractionBox->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
}
void APickupItem::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
@@ -61,6 +65,48 @@ void APickupItem::ApplyItemVisuals()
{
ItemData->ApplyWorldVisualsTo(Mesh);
}
// Hors du if, et apres lui : la boite doit suivre le mesh REELLEMENT pose,
// que celui-ci vienne de l'ItemData ou qu'il ait ete impose a la main.
if (bAutoFitInteractionBox)
{
FitInteractionBox();
}
}
void APickupItem::FitInteractionBox()
{
const UStaticMesh* CurrentMesh = Mesh->GetStaticMesh();
if (!CurrentMesh)
{
// Pickup non configure, ou client dont l'ItemData n'est pas encore arrive :
// on garde la boite du Blueprint plutot que de l'ecraser par un volume nul.
// OnRep_ItemData repassera ici quand la donnee sera la.
return;
}
// La bounding box de l'ASSET, jamais Mesh->Bounds : ces dernieres sont en
// espace monde et deja multipliees par l'echelle de l'acteur. Or SetBoxExtent
// recoit une extension LOCALE que le moteur remultiplie a son tour -- une
// instance posee a l'echelle 2 aurait une boite quatre fois trop grande.
const FBox LocalBounds = CurrentMesh->GetBoundingBox();
// Le pivot d'un mesh d'asset pack tombe rarement au centre de sa geometrie :
// il est presque toujours au sol, parfois franchement decale. Une boite
// laissee a l'origine deborderait d'un cote et laisserait l'autre invisable.
InteractionBox->SetRelativeLocation(LocalBounds.GetCenter());
// ComponentMax et non un Max sur la norme : le plancher s'applique axe par
// axe, sinon relever l'epaisseur d'une branche rallongerait aussi la boite
// dans le sens ou elle etait deja bien assez grande.
const FVector Fitted = LocalBounds.GetExtent() + FVector(InteractionBoxPadding);
// bUpdateOverlaps a false : cette boite ne bloque que le canal Visibility et
// ne genere aucun evenement d'overlap. Le defaut (true) declencherait une
// requete physique par pickup a chaque spawn, pour rien.
InteractionBox->SetBoxExtent(
Fitted.ComponentMax(FVector(MinInteractionExtent)),
/*bUpdateOverlaps=*/ false);
}
void APickupItem::OnRep_ItemData()
@@ -52,11 +52,10 @@ AStorageContainer::AStorageContainer()
// FitInteractionBox() l'ecrase des qu'il y en a un.
InteractionBox->SetBoxExtent(FVector(40.f, 30.f, 30.f));
LidPivot = CreateDefaultSubobject<USceneComponent>(TEXT("LidPivot"));
LidPivot->SetupAttachment(BaseMesh);
// Enfant direct de la caisse : c'est le mesh lui-meme qu'on tourne, donc la
// charniere est le pivot que le pack a donne a SM_Chest_Top.
LidMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("LidMesh"));
LidMesh->SetupAttachment(LidPivot);
LidMesh->SetupAttachment(BaseMesh);
LidMesh->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
Storage = CreateDefaultSubobject<UInventoryComponent>(TEXT("Storage"));
@@ -235,7 +234,7 @@ void AStorageContainer::Tick(float DeltaTime)
void AStorageContainer::ApplyLidRotation()
{
if (!LidPivot)
if (!LidMesh)
{
return;
}
@@ -244,7 +243,7 @@ void AStorageContainer::ApplyLidRotation()
// s'arrete d'un coup, ce qui se lit comme une saccade sur un objet lourd.
const float Eased = FMath::SmoothStep(0.f, 1.f, LidAlpha);
LidPivot->SetRelativeRotation(OpenRotation * Eased);
LidMesh->SetRelativeRotation(OpenRotation * Eased);
}
#undef LOCTEXT_NAMESPACE