(Feat) Add footsteps
This commit is contained in:
@@ -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>())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CharacterAppearanceTypes.h"
|
||||
#include "CharacterPreviewActor.h"
|
||||
#include "CharacterCustomizationWidget.generated.h"
|
||||
|
||||
class ACharacterPreviewActor;
|
||||
class UButton;
|
||||
class UCharacterPartsDataAsset;
|
||||
class UCustomizationCategoryWidget;
|
||||
class UCustomizationOptionWidget;
|
||||
class UPanelWidget;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCustomizationConfirmed);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCustomizationCancelled);
|
||||
|
||||
/**
|
||||
* Ce que le joueur peut regler, dans l'ordre d'affichage de la colonne.
|
||||
*
|
||||
* Pieces et couleurs sont dans le MEME enum : ce sont onze rangees de la meme
|
||||
* colonne, et les distinguer par deux listes obligerait a dessiner deux fois la
|
||||
* meme chose. La difference ne vit que dans BuildOptions, qui remplit la grille
|
||||
* de vignettes ou de pastilles selon le cas.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class ECustomizationCategory : uint8
|
||||
{
|
||||
Body,
|
||||
Head,
|
||||
Hair,
|
||||
Eyebrows,
|
||||
Beard,
|
||||
Mustache,
|
||||
Eyes,
|
||||
SkinColor,
|
||||
HairColor,
|
||||
EyeColor,
|
||||
UnderwearColor,
|
||||
|
||||
Count UMETA(Hidden)
|
||||
};
|
||||
|
||||
/**
|
||||
* L'ecran de customisation : la colonne de reglages a gauche, le personnage a
|
||||
* droite.
|
||||
*
|
||||
* Le widget ne monte rien lui-meme -- il pousse une FCharacterAppearance sur
|
||||
* ACharacterPreviewActor et lit le catalogue pour savoir quoi proposer. C'est
|
||||
* le meme partage des roles que partout ailleurs dans le projet : l'interface
|
||||
* affiche et demande, le systeme decide.
|
||||
*
|
||||
* Classe Abstract : on cree un WBP_CharacterCustomization qui en herite.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UCharacterCustomizationWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UCharacterCustomizationWidget(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
virtual void NativeOnInitialized() override;
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
/**
|
||||
* Echap = Retour, comme le bouton.
|
||||
*
|
||||
* Obligatoire ici et pas seulement confortable : dans la map du menu, le
|
||||
* controller ne binde AUCUNE action d'input. Tout Echap qui fonctionne passe
|
||||
* donc par le NativeOnKeyDown d'un widget -- il n'y a pas d'autre chemin.
|
||||
*/
|
||||
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
|
||||
|
||||
/** Le joueur valide : l'ecran a fini, l'apparence est dans GetAppearance(). */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Customization")
|
||||
FOnCustomizationConfirmed OnConfirmed;
|
||||
|
||||
/** Le joueur renonce : rien n'a ete valide, on rend la main au menu. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Customization")
|
||||
FOnCustomizationCancelled OnCancelled;
|
||||
|
||||
/**
|
||||
* Branche l'ecran sur son mannequin et son point de depart.
|
||||
*
|
||||
* A appeler AVANT d'ajouter le widget au viewport : NativeConstruct
|
||||
* reconstruit les listes, et il les batirait vides sans catalogue.
|
||||
*
|
||||
* Surtout PAS nommee Initialize : UUserWidget en a deja une, virtuelle, et
|
||||
* une surcharge de signature differente la masquerait au lieu de la
|
||||
* surcharger (C4263/C4264). Meme famille de piege que les membres nommes
|
||||
* Player ou Slot dans une classe derivee du moteur.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Customization")
|
||||
void SetupCustomization(ACharacterPreviewActor* InPreview, const FCharacterAppearance& InAppearance);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Customization")
|
||||
const FCharacterAppearance& GetAppearance() const { return Appearance; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Le catalogue. Assigne-lui le meme DA_CharacterParts que BP_FpsPlayer.
|
||||
*
|
||||
* Sur le widget et pas pris sur le mannequin : l'ecran doit pouvoir batir
|
||||
* ses listes meme si le mannequin manque encore, et un catalogue different
|
||||
* des deux cotes se verrait immediatement a l'ecran.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Customization")
|
||||
TObjectPtr<UCharacterPartsDataAsset> Catalog;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Customization")
|
||||
TSubclassOf<UCustomizationCategoryWidget> CategoryWidgetClass;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Customization")
|
||||
TSubclassOf<UCustomizationOptionWidget> OptionWidgetClass;
|
||||
|
||||
/** La colonne des categories. Un Vertical Box nomme exactement ainsi. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UPanelWidget> CategoryList;
|
||||
|
||||
/** La grille d'options. Un Wrap Box va tres bien. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UPanelWidget> OptionGrid;
|
||||
|
||||
/** Valide et lance la partie. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> PlayButton;
|
||||
|
||||
/** Retour au menu principal, sans rien valider. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> BackButton;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Rotation du personnage au clic-glisser
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
|
||||
virtual FReply NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
|
||||
virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
|
||||
|
||||
private:
|
||||
/** Reconstruit la colonne de gauche. Une seule fois, les categories sont fixes. */
|
||||
void BuildCategories();
|
||||
|
||||
/** Reconstruit la grille selon la categorie active. */
|
||||
void BuildOptions();
|
||||
|
||||
/** Repose l'apparence sur le mannequin et rafraichit la selection affichee. */
|
||||
void RefreshPreview();
|
||||
|
||||
/** Encadre la case qui porte le choix courant, efface les autres. */
|
||||
void RefreshSelection();
|
||||
|
||||
/** L'index courant dans la categorie active, pour l'encadrement. */
|
||||
int32 GetCurrentIndexForCategory(ECustomizationCategory Category) const;
|
||||
|
||||
/** Ecrit un choix dans l'apparence. Le seul endroit qui la modifie. */
|
||||
void SetIndexForCategory(ECustomizationCategory Category, int32 NewIndex);
|
||||
|
||||
/** Combien d'options a proposer, catalogue en main. */
|
||||
int32 GetOptionCountForCategory(ECustomizationCategory Category) const;
|
||||
|
||||
UFUNCTION()
|
||||
void HandleCategoryClicked(int32 CategoryIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleOptionClicked(int32 OptionIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandlePlayClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleBackClicked();
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<ACharacterPreviewActor> Preview;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UCustomizationCategoryWidget>> CategoryWidgets;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UCustomizationOptionWidget>> OptionWidgets;
|
||||
|
||||
FCharacterAppearance Appearance;
|
||||
|
||||
ECustomizationCategory ActiveCategory = ECustomizationCategory::Body;
|
||||
|
||||
/** Vrai entre l'appui et le relachement, pendant qu'on fait tourner. */
|
||||
bool bDragging = false;
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "CharacterAppearanceTypes.h"
|
||||
#include "CharacterPreviewActor.generated.h"
|
||||
|
||||
class UCameraComponent;
|
||||
class UCharacterAppearanceComponent;
|
||||
class USkeletalMeshComponent;
|
||||
|
||||
/**
|
||||
* Les deux cadrages de l'ecran de customisation.
|
||||
*
|
||||
* Deux et pas un par categorie : ce qu'on veut, c'est voir le visage quand on
|
||||
* choisit une barbe et la silhouette entiere quand on choisit un corps. Un
|
||||
* cadrage par categorie donnerait onze reglages a poser a la main pour onze
|
||||
* fois le meme resultat.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class ECharacterPreviewFraming : uint8
|
||||
{
|
||||
FullBody,
|
||||
Face,
|
||||
|
||||
Count UMETA(Hidden)
|
||||
};
|
||||
|
||||
/**
|
||||
* Le mannequin de l'ecran de customisation : un personnage pose dans la map de
|
||||
* menu, qu'on fait tourner a la souris et que la camera cadre selon ce que le
|
||||
* joueur est en train de regler.
|
||||
*
|
||||
* Il porte le MEME UCharacterAppearanceComponent que le pawn de jeu, et c'est
|
||||
* tout l'interet : le personnage qu'on regarde ici est monte par exactement le
|
||||
* code qui montera celui que les autres joueurs verront. Deux chemins de
|
||||
* montage finiraient par diverger, et le joueur s'en apercevrait au pire
|
||||
* moment -- une fois en partie.
|
||||
*/
|
||||
UCLASS()
|
||||
class SURVIVAL_PROJET_API ACharacterPreviewActor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ACharacterPreviewActor();
|
||||
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
/** Monte l'apparence sur le mannequin. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Preview")
|
||||
void ApplyAppearance(const FCharacterAppearance& InAppearance);
|
||||
|
||||
/** La camera glisse vers le cadrage demande. Sans effet s'il y est deja. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Preview")
|
||||
void SetFraming(ECharacterPreviewFraming NewFraming);
|
||||
|
||||
/** Fait pivoter le personnage. Appelee pendant un clic-glisser. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Preview")
|
||||
void AddPreviewYaw(float DeltaDegrees);
|
||||
|
||||
/** Remet le personnage face a la camera. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Preview")
|
||||
void ResetPreviewYaw();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Preview")
|
||||
UCameraComponent* GetPreviewCamera() const { return PreviewCamera; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Preview")
|
||||
UCharacterAppearanceComponent* GetAppearanceComponent() const { return AppearanceComponent; }
|
||||
|
||||
/** Le reglage vit ici et pas sur le widget : c'est une sensibilite de camera. */
|
||||
UFUNCTION(BlueprintPure, Category = "Preview")
|
||||
float GetYawPerPixel() const { return YawPerPixel; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/** Le porteur d'animation et de pose ; les pieces s'y accrochent. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview")
|
||||
TObjectPtr<USkeletalMeshComponent> BodyMesh;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview")
|
||||
TObjectPtr<UCharacterAppearanceComponent> AppearanceComponent;
|
||||
|
||||
/**
|
||||
* Pivot de rotation, entre la racine et le mesh.
|
||||
*
|
||||
* Le personnage tourne en faisant pivoter CE composant et jamais l'acteur :
|
||||
* la camera est fille de l'acteur, la faire tourner avec lui donnerait un
|
||||
* decor qui defile pendant qu'on regarde un personnage immobile.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview")
|
||||
TObjectPtr<USceneComponent> TurntablePivot;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview")
|
||||
TObjectPtr<UCameraComponent> PreviewCamera;
|
||||
|
||||
/**
|
||||
* Les deux positions de camera, a placer A LA SOURIS dans le viewport du
|
||||
* Blueprint plutot qu'a taper en coordonnees.
|
||||
*
|
||||
* Ce sont des reperes vides : la camera n'y est pas accrochee, elle glisse
|
||||
* vers eux. Sans quoi changer de categorie la ferait sauter d'un cadrage a
|
||||
* l'autre, ce qui se lit comme une coupure de montage.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview|Framing")
|
||||
TObjectPtr<USceneComponent> FullBodyViewSpot;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Preview|Framing")
|
||||
TObjectPtr<USceneComponent> FaceViewSpot;
|
||||
|
||||
/**
|
||||
* Vitesse de glissement de la camera entre deux cadrages.
|
||||
*
|
||||
* Une vitesse d'interpolation et pas une duree : le mouvement freine en
|
||||
* arrivant au lieu de s'arreter net, et deux changements de categorie
|
||||
* coup sur coup s'enchainent sans a-coup -- la camera repart d'ou elle est.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Preview|Framing", meta = (ClampMin = "0.5"))
|
||||
float FramingInterpSpeed = 6.f;
|
||||
|
||||
/** Degres de rotation par pixel de deplacement horizontal de la souris. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Preview|Turntable")
|
||||
float YawPerPixel = 0.35f;
|
||||
|
||||
private:
|
||||
/** Le repere vise par la camera, selon le cadrage courant. */
|
||||
USceneComponent* GetSpotForFraming(ECharacterPreviewFraming InFraming) const;
|
||||
|
||||
/**
|
||||
* Coupe le Tick quand la camera est arrivee.
|
||||
*
|
||||
* Meme principe que le couvercle d'AStorageContainer : cet acteur ne tick
|
||||
* que pendant qu'il bouge. Un mannequin immobile dans un menu n'a aucune
|
||||
* raison de couter une frame.
|
||||
*/
|
||||
void UpdateFramingInterpolation(float DeltaSeconds);
|
||||
|
||||
ECharacterPreviewFraming CurrentFraming = ECharacterPreviewFraming::FullBody;
|
||||
|
||||
/** Rotation accumulee par le clic-glisser, en degres. */
|
||||
float TurntableYaw = 0.f;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CustomizationCategoryWidget.generated.h"
|
||||
|
||||
class UBorder;
|
||||
class UButton;
|
||||
class UTextBlock;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCustomizationCategoryClicked, int32, CategoryIndex);
|
||||
|
||||
/**
|
||||
* Une ligne de la colonne de gauche : « Corps », « Cheveux », « Peau »...
|
||||
*
|
||||
* Elle ne sait pas ce qu'elle designe -- juste un libelle et un index, comme
|
||||
* USettingRowWidget ignore ce qu'il regle. C'est l'ecran qui traduit l'index en
|
||||
* categorie, et c'est ce qui permet de n'avoir qu'UN widget a dessiner pour les
|
||||
* onze entrees.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UCustomizationCategoryWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void NativeOnInitialized() override;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Customization")
|
||||
FOnCustomizationCategoryClicked OnCategoryClicked;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Customization")
|
||||
void Setup(int32 InIndex, const FText& InLabel);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Customization")
|
||||
void SetSelected(bool bInSelected);
|
||||
|
||||
/**
|
||||
* L'index de la categorie que porte cette rangee.
|
||||
*
|
||||
* Indispensable a l'ecran : les categories vides etant sautees, le rang de
|
||||
* la rangee dans la colonne ne vaut PAS son index de categorie.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Customization")
|
||||
int32 GetCategoryIndex() const { return CategoryIndex; }
|
||||
|
||||
protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> CategoryButton;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UTextBlock> CategoryLabel;
|
||||
|
||||
/** Optionnel : on peut aussi marquer la selection par la couleur du texte. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UBorder> SelectionBorder;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
|
||||
FLinearColor SelectedLabelColor = FLinearColor(1.f, 0.78f, 0.35f, 1.f);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
|
||||
FLinearColor UnselectedLabelColor = FLinearColor(0.8f, 0.8f, 0.8f, 1.f);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
|
||||
FLinearColor SelectedBorderColor = FLinearColor(1.f, 1.f, 1.f, 0.12f);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
|
||||
FLinearColor UnselectedBorderColor = FLinearColor(1.f, 1.f, 1.f, 0.f);
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleButtonClicked();
|
||||
|
||||
int32 CategoryIndex = 0;
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CustomizationOptionWidget.generated.h"
|
||||
|
||||
class UBorder;
|
||||
class UButton;
|
||||
class UImage;
|
||||
class UTextBlock;
|
||||
class UTexture2D;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCustomizationOptionClicked, int32, OptionIndex);
|
||||
|
||||
/**
|
||||
* Une case de la grille de customisation.
|
||||
*
|
||||
* La MEME classe sert les vignettes de pieces et les pastilles de couleur :
|
||||
* dans les deux cas c'est un bouton carre qui porte un visuel et sait s'il est
|
||||
* selectionne. Deux widgets auraient eu la meme logique de selection a tenir a
|
||||
* jour en double, pour la seule difference d'afficher une texture plutot qu'un
|
||||
* aplat.
|
||||
*
|
||||
* Classe Abstract : on cree un WBP_CustomizationOption qui en herite.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UCustomizationOptionWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void NativeOnInitialized() override;
|
||||
|
||||
/** Previent d'un clic du joueur, jamais d'un rafraichissement. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Customization")
|
||||
FOnCustomizationOptionClicked OnOptionClicked;
|
||||
|
||||
/**
|
||||
* Mode piece : affiche la vignette du catalogue.
|
||||
*
|
||||
* @param Icon peut etre nul -- la case retombe alors sur son numero. Une
|
||||
* option sans vignette doit rester choisissable, sinon le catalogue
|
||||
* est inutilisable tant que les trente-six textures ne sont pas
|
||||
* rendues, et on veut justement les capturer depuis cet ecran.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Customization")
|
||||
void SetupAsPart(int32 InIndex, UTexture2D* Icon);
|
||||
|
||||
/** Mode couleur : la pastille EST le visuel, aucune texture necessaire. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Customization")
|
||||
void SetupAsColor(int32 InIndex, const FLinearColor& Color);
|
||||
|
||||
/** Encadre la case si elle porte le choix courant. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Customization")
|
||||
void SetSelected(bool bInSelected);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Customization")
|
||||
int32 GetOptionIndex() const { return OptionIndex; }
|
||||
|
||||
protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> OptionButton;
|
||||
|
||||
/** Porte la vignette en mode piece, la couleur en mode couleur. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UImage> OptionImage;
|
||||
|
||||
/**
|
||||
* Le numero de repli, montre uniquement quand une piece n'a pas de
|
||||
* vignette. BindWidgetOptional : un WBP qui s'en passe reste valide, il
|
||||
* affichera simplement des cases vides.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UTextBlock> IndexText;
|
||||
|
||||
/** Le cadre de selection. Optionnel : on peut le rendre autrement. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UBorder> SelectionBorder;
|
||||
|
||||
/** Couleur du cadre quand la case porte le choix courant. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
|
||||
FLinearColor SelectedBorderColor = FLinearColor(1.f, 0.78f, 0.35f, 1.f);
|
||||
|
||||
/** Couleur du cadre au repos. Transparente par defaut : il disparait. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Customization")
|
||||
FLinearColor UnselectedBorderColor = FLinearColor(1.f, 1.f, 1.f, 0.f);
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleButtonClicked();
|
||||
|
||||
int32 OptionIndex = 0;
|
||||
};
|
||||
@@ -0,0 +1,362 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Engine/EngineTypes.h"
|
||||
#include "FootstepSoundsDataAsset.h"
|
||||
#include "FootstepComponent.generated.h"
|
||||
|
||||
class ACharacter;
|
||||
class UCharacterMovementComponent;
|
||||
class USoundAttenuation;
|
||||
class USoundBase;
|
||||
class USoundConcurrency;
|
||||
|
||||
/**
|
||||
* Les reglages d'une allure : sa foulee et son volume.
|
||||
*
|
||||
* Groupes dans une struct pour la meme raison que FHeadBobProfile : trois
|
||||
* allures a plat feraient neuf lignes melangees dans le Details panel, alors
|
||||
* que la struct donne un repli par allure.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FFootstepGaitSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
FFootstepGaitSettings() = default;
|
||||
|
||||
FFootstepGaitSettings(float InStrideLength, float InVolume)
|
||||
: StrideLength(InStrideLength)
|
||||
, Volume(InVolume)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Distance parcourue entre deux pas, en cm.
|
||||
*
|
||||
* C'est le reglage central du systeme, et il se calcule plutot qu'il ne se
|
||||
* devine : pour que les pas tombent en rythme avec le balancement de tete,
|
||||
* il faut StrideLength = Vitesse / (2 x StrideFrequency du profil de
|
||||
* UHeadBobComponent) -- le "2" parce qu'une foulee compte deux pas. Les
|
||||
* valeurs par defaut sortent de ce calcul avec les vitesses du pawn.
|
||||
*
|
||||
* Corollaire a retenir : changer WalkSpeed ou SprintSpeed sans retoucher ici
|
||||
* desynchronise le son de l'image, et ca s'entend avant de se voir.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "10", UIMax = "300"))
|
||||
float StrideLength = 180.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "0", UIMax = "2"))
|
||||
float Volume = 1.f;
|
||||
|
||||
/**
|
||||
* Variation aleatoire de hauteur, en plus ou en moins autour de 1.
|
||||
*
|
||||
* Le pack ne livre qu'un ou deux echantillons par surface et par allure :
|
||||
* sans ce grain, une course en ligne droite repete exactement le meme son
|
||||
* trois fois par seconde et l'oreille l'entend comme une boucle. Quelques
|
||||
* pourcents suffisent, au-dela le personnage change de poids a chaque pas.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "0", UIMax = "0.4"))
|
||||
float PitchVariation = 0.06f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Bruits de pas : mesure la distance parcourue, cherche la matiere sous les
|
||||
* pieds, joue le son qui va avec.
|
||||
*
|
||||
* ---------------------------------------------------------------------------
|
||||
* Pourquoi la distance et pas un AnimNotify
|
||||
* ---------------------------------------------------------------------------
|
||||
* C'est le choix structurant. Trois raisons de ne pas accrocher le son aux
|
||||
* animations :
|
||||
*
|
||||
* - Le corps 3e personne du joueur local est en bOwnerNoSee. Faire dependre
|
||||
* son propre son d'un mesh qu'il ne voit pas est fragile, et le devient
|
||||
* d'autant plus le jour ou l'on optimisera son tick d'animation.
|
||||
* - Les animations du pack n'ont aucun notify, et il faudrait en poser dans le
|
||||
* BlendSpace, ou ils se declenchent selon l'echantillon dominant : le
|
||||
* passage marche -> course y saute ou double un pas.
|
||||
* - Les bras FPS sont sur BasePose_Skeleton, qui n'a rien a voir avec
|
||||
* SK_BodyA. Aucun notify ne peut servir aux deux.
|
||||
*
|
||||
* La distance, elle, donne un espacement regulier, se regle en PIE sans
|
||||
* recompiler, et se comporte identiquement sur le pawn local et sur ceux des
|
||||
* amis.
|
||||
*
|
||||
* ---------------------------------------------------------------------------
|
||||
* Reseau : rien n'est replique, et rien n'a a l'etre
|
||||
* ---------------------------------------------------------------------------
|
||||
* Exactement le raisonnement de UPlayerBodyAnimInstance : tout ce que ce
|
||||
* composant lit DERIVE d'un etat deja synchronise -- la vitesse par le
|
||||
* CharacterMovement, l'accroupissement par ACharacter, le mode de deplacement
|
||||
* par la replication du mouvement. Chaque machine calcule donc les pas de tous
|
||||
* les pawns qu'elle connait. Un Multicast par pas, a trois pas par seconde et
|
||||
* par joueur, serait du trafic pur pour un resultat identique.
|
||||
*
|
||||
* Le son du joueur local part en 2D, celui des autres en 3D a leurs pieds :
|
||||
* un pas local joue en 3D depuis les chevilles est attenue et sonne creux.
|
||||
*/
|
||||
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
|
||||
class SURVIVAL_PROJET_API UFootstepComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFootstepComponent();
|
||||
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
/**
|
||||
* Coupe ou rallume les pas. Pour la mort, une cinematique, un mode fantome.
|
||||
* Coupe, le compteur de foulee est remis a zero : rallumer ne doit pas
|
||||
* declencher le pas reste en attente depuis l'autre bout de la carte.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Footsteps")
|
||||
void SetFootstepsEnabled(bool bEnabled);
|
||||
|
||||
/** Remet le cycle de foulee a zero. Pour une teleportation ou un respawn. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Footsteps")
|
||||
void ResetStride();
|
||||
|
||||
/**
|
||||
* Joue un pas immediatement, a l'allure courante. Sert aux montages et aux
|
||||
* AnimNotify si l'on veut un jour caler un geste precis sur un son.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Footsteps")
|
||||
void PlayFootstepNow();
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
/**
|
||||
* Saut et reception, sur TOUTES les machines.
|
||||
*
|
||||
* Et surtout pas ACharacter::Landed / OnJumped, qui semblent pourtant faits
|
||||
* pour ca : Landed vient de ProcessLanded, qui n'est appele que la ou le
|
||||
* mouvement est reellement simule, et OnJumped que la ou l'input est lu. Ni
|
||||
* l'un ni l'autre n'existe sur le pawn d'un ami vu depuis notre machine, ou
|
||||
* le mouvement arrive deja fait. Le mode de deplacement, lui, EST replique,
|
||||
* donc ce delegue est diffusé partout -- c'est le seul signal qui couvre les
|
||||
* quatre joueurs.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void HandleMovementModeChanged(ACharacter* Character, EMovementMode PrevMovementMode, uint8 PreviousCustomMode);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Assets
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** La table surface -> sons. Sans elle le composant ne fait rien. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
|
||||
TObjectPtr<UFootstepSoundsDataAsset> SoundBank;
|
||||
|
||||
/**
|
||||
* Attenuation appliquee aux pas des AUTRES joueurs (le pas local est en 2D).
|
||||
*
|
||||
* A remplir : le pack ne fournit aucune attenuation, donc sans cet asset les
|
||||
* cues sont jouees en 3D sans courbe de distance et s'entendent d'un bout a
|
||||
* l'autre de la carte au meme volume.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
|
||||
TObjectPtr<USoundAttenuation> Attenuation;
|
||||
|
||||
/**
|
||||
* Concurrence, facultative. A quatre joueurs qui courent, huit a douze pas
|
||||
* par seconde peuvent se superposer ; une regle a 4 ou 5 voix evite que les
|
||||
* pas mangent le reste du mixage.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
|
||||
TObjectPtr<USoundConcurrency> Concurrency;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Reglages generaux
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Coupe entierement l'effet sans avoir a vider la banque de sons. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
|
||||
bool bEnableFootsteps = true;
|
||||
|
||||
/**
|
||||
* Joue le pas du joueur local en 2D plutot qu'a la position de ses pieds.
|
||||
* Decoche pour l'entendre spatialise comme celui des autres -- utile pour
|
||||
* regler l'attenuation sans avoir besoin d'un second joueur.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
|
||||
bool bPlayLocalStepsIn2D = true;
|
||||
|
||||
/**
|
||||
* Multiplicateur applique au seul pas local. Sous 1 par defaut : un son 2D
|
||||
* arrive a pleine puissance dans les deux oreilles, la ou celui des autres
|
||||
* passe par l'attenuation.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "0", UIMax = "2"))
|
||||
float LocalVolumeScale = 0.7f;
|
||||
|
||||
/** Joue le pas final quand le personnage s'immobilise. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
|
||||
bool bPlayStopSounds = true;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Allures
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Accroupi. Valeurs par defaut dans le constructeur. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures")
|
||||
FFootstepGaitSettings CrouchGait;
|
||||
|
||||
/** Marche normale. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures")
|
||||
FFootstepGaitSettings WalkGait;
|
||||
|
||||
/** Sprint. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures")
|
||||
FFootstepGaitSettings SprintGait;
|
||||
|
||||
/**
|
||||
* En dessous de cette vitesse horizontale (cm/s), on considere le
|
||||
* personnage immobile. Meme seuil et meme raison que UHeadBobComponent :
|
||||
* marcher dans un mur laisse l'input actif alors que le corps n'avance plus.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures", meta = (ClampMin = "0"))
|
||||
float IdleSpeedThreshold = 10.f;
|
||||
|
||||
/**
|
||||
* Au-dessus de cette vitesse horizontale (cm/s), on passe sur l'allure de
|
||||
* sprint. A caler entre WalkSpeed et SprintSpeed du pawn, comme le seuil
|
||||
* homonyme du balancement de tete -- les deux doivent basculer ensemble,
|
||||
* sinon la tete accelere avant les pieds.
|
||||
*
|
||||
* La vitesse REELLE et pas AFpsPlayer::IsSprinting() : le composant reste
|
||||
* ainsi utilisable par un PNJ ou un animal, qui n'ont pas de touche Maj.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures", meta = (ClampMin = "0"))
|
||||
float SprintSpeedThreshold = 520.f;
|
||||
|
||||
/**
|
||||
* Fraction de foulee deja "parcourue" au demarrage du mouvement.
|
||||
*
|
||||
* A 0, le premier pas ne tombe qu'apres une foulee entiere : le personnage
|
||||
* part en silence sur presque deux metres, ce qui se lit comme un temps de
|
||||
* reaction du jeu. A 1 il claque avant que le corps ait bouge. La moitie
|
||||
* place le premier pas juste apres l'entree en mouvement.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures", meta = (ClampMin = "0", ClampMax = "1"))
|
||||
float FirstStepFraction = 0.5f;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Saut et reception
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Vitesse de chute (cm/s) en dessous de laquelle la reception est muette.
|
||||
* Descendre une marche d'escalier passe techniquement par un instant de
|
||||
* chute ; sans ce seuil, la moindre bosse du terrain claque.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Air", meta = (ClampMin = "0"))
|
||||
float LandMinFallSpeed = 150.f;
|
||||
|
||||
/** Volume du saut et de la reception. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Air", meta = (ClampMin = "0", UIMax = "2"))
|
||||
float AirVolume = 1.f;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Trace de sol
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Profondeur sondee sous les pieds, en cm. Quelques dizaines suffisent : le
|
||||
* trace part du centre de la capsule et on ne cherche le sol que lorsqu'on
|
||||
* marche dessus.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Trace", meta = (ClampMin = "1"))
|
||||
float TraceDepth = 60.f;
|
||||
|
||||
/**
|
||||
* Trace en collision COMPLEXE, et ce n'est pas negociable pour ce systeme.
|
||||
*
|
||||
* Un PhysicalMaterial pose sur un MATERIAU n'est rendu que par un trace
|
||||
* per-poly : le trace simple remonte celui du BodySetup du mesh, qui est
|
||||
* presque toujours vide. Le symptome d'un decochage est immediat et trompeur
|
||||
* -- tout le monde marche sur DefaultSurface, comme si la table n'etait pas
|
||||
* remplie.
|
||||
*
|
||||
* Le cout est nul a l'echelle du jeu : deux a trois traces par seconde et
|
||||
* par personnage, jamais un par frame.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Trace")
|
||||
bool bTraceComplex = true;
|
||||
|
||||
/** Canal du trace de sol. Visibility couvre tout le decor du projet. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Trace")
|
||||
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
|
||||
|
||||
private:
|
||||
/** L'allure correspondant a l'etat reel du CharacterMovement. */
|
||||
EFootstepGait SelectGait(float HorizontalSpeed) const;
|
||||
|
||||
/** Les reglages de l'allure courante. */
|
||||
const FFootstepGaitSettings& GetGaitSettings(EFootstepGait Gait) const;
|
||||
|
||||
/**
|
||||
* Cherche la matiere sous les pieds et rend les sons correspondants.
|
||||
* @param OutLocation ou jouer le son : le point d'impact, ou les pieds si
|
||||
* le trace n'a rien touche (le sol peut etre hors du canal trace).
|
||||
*/
|
||||
const FFootstepSurfaceSounds* QueryGroundSounds(FVector& OutLocation) const;
|
||||
|
||||
/** Joue une cue, en 2D chez le proprietaire et en 3D partout ailleurs. */
|
||||
void PlayCue(USoundBase* Sound, float Volume, float PitchVariation, const FVector& Location) const;
|
||||
|
||||
/** Vrai quand le son doit sortir des haut-parleurs de CETTE machine. */
|
||||
bool ShouldPlaySounds() const;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Etat
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Distance horizontale accumulee depuis le dernier pas, en cm. */
|
||||
float DistanceSinceLastStep = 0.f;
|
||||
|
||||
/** Position de l'acteur au tick precedent, pour mesurer le deplacement reel. */
|
||||
FVector LastLocation = FVector::ZeroVector;
|
||||
|
||||
/**
|
||||
* Vrai des le premier tick valide. Sans lui, le premier deplacement mesure
|
||||
* partirait de l'origine du monde et declencherait une salve de pas.
|
||||
*/
|
||||
bool bHasLastLocation = false;
|
||||
|
||||
/** Etat du tick precedent, pour detecter le demarrage et l'arret. */
|
||||
bool bWasMoving = false;
|
||||
|
||||
/** Allure du dernier pas joue : c'est elle qui choisit le son d'arret. */
|
||||
EFootstepGait LastGait = EFootstepGait::Jog;
|
||||
|
||||
/**
|
||||
* Nombre de pas depuis l'entree en mouvement. Le son d'arret n'est joue
|
||||
* qu'a partir de un : un a-coup de collision suffit sinon a faire claquer
|
||||
* un pas final alors que le personnage n'a pas bouge.
|
||||
*/
|
||||
int32 StepsSinceMoveStart = 0;
|
||||
|
||||
/**
|
||||
* Plus grande vitesse de chute observee, en cm/s et positive.
|
||||
*
|
||||
* Mesuree pendant la chute plutot que lue a l'impact : au moment ou le mode
|
||||
* repasse a Walking, le moteur a deja remis Velocity.Z a zero. Un tick a
|
||||
* 20 Hz l'echantillonne largement assez -- et le cas ou il n'a pas eu le
|
||||
* temps de tourner du tout est precisement celui d'une chute negligeable,
|
||||
* qu'on veut de toute facon silencieuse.
|
||||
*/
|
||||
float MaxFallSpeed = 0.f;
|
||||
|
||||
/** Caches : lus a chaque tick, pas question d'y caster. */
|
||||
TObjectPtr<ACharacter> OwnerCharacter;
|
||||
|
||||
TObjectPtr<UCharacterMovementComponent> OwnerMovement;
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Chaos/ChaosEngineInterface.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "FootstepSoundsDataAsset.generated.h"
|
||||
|
||||
class USoundBase;
|
||||
|
||||
/**
|
||||
* Les trois allures pour lesquelles le pack livre un son.
|
||||
*
|
||||
* Les noms sont ceux des fichiers du pack et PAS ceux des etats du jeu, alors
|
||||
* que la tentation serait grande de les appeler Crouch / Walk / Sprint. La
|
||||
* correspondance est etablie une seule fois, dans UFootstepComponent, et elle
|
||||
* n'est pas l'identite : le personnage marche a 400 cm/s, ce qui est deja un
|
||||
* trot -- d'ou accroupi = Walk, marche = Jog, sprint = Run. Nommer l'enum
|
||||
* d'apres les etats obligerait a ecrire "le son Walk est dans le champ Crouch"
|
||||
* dans le Details panel, ce qui est exactement le genre de traduction qu'on
|
||||
* finit par se tromper en relisant.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EFootstepGait : uint8
|
||||
{
|
||||
/** Le pas le plus discret du pack. Utilise quand le personnage est accroupi. */
|
||||
Walk,
|
||||
|
||||
/** Allure de marche normale du jeu (WalkSpeed). */
|
||||
Jog,
|
||||
|
||||
/** Sprint. */
|
||||
Run
|
||||
};
|
||||
|
||||
/**
|
||||
* La banque de sons d'UNE surface : les huit cues que le pack fournit pour
|
||||
* chacune de ses matieres.
|
||||
*
|
||||
* Tous les champs sont facultatifs. Une case vide ne produit aucun son et
|
||||
* n'est pas une erreur -- c'est ce qui permet de commencer avec trois surfaces
|
||||
* remplies et d'ajouter les autres au fil du temps, ou de renoncer aux sons
|
||||
* d'arret sans toucher au code.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FFootstepSurfaceSounds
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Les pas eux-memes, un par allure.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Steps")
|
||||
TObjectPtr<USoundBase> WalkStep;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Steps")
|
||||
TObjectPtr<USoundBase> JogStep;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Steps")
|
||||
TObjectPtr<USoundBase> RunStep;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Le pas final, joue quand le personnage s'immobilise.
|
||||
//
|
||||
// Ce n'est pas un ornement : sans lui, une course qui s'arrete coupe la
|
||||
// serie de pas net, comme si le personnage s'etait teleporte. Le pack les
|
||||
// livre exprès, avec un son plus traine que le pas courant.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stop")
|
||||
TObjectPtr<USoundBase> WalkStop;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stop")
|
||||
TObjectPtr<USoundBase> JogStop;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stop")
|
||||
TObjectPtr<USoundBase> RunStop;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Saut et reception. Une seule variante chacun, quelle que soit l'allure.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Air")
|
||||
TObjectPtr<USoundBase> Jump;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Air")
|
||||
TObjectPtr<USoundBase> Land;
|
||||
|
||||
/** Le pas courant d'une allure, ou nullptr si la case est vide. */
|
||||
USoundBase* GetStepSound(EFootstepGait Gait) const;
|
||||
|
||||
/** Le pas final d'une allure, ou nullptr si la case est vide. */
|
||||
USoundBase* GetStopSound(EFootstepGait Gait) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* La table de correspondance surface physique -> banque de sons.
|
||||
*
|
||||
* Un seul asset a remplir pour tout le jeu, et le SEUL endroit a toucher pour
|
||||
* ajouter une matiere. Le composant n'y range aucun etat : il le lit, c'est
|
||||
* tout, ce qui permet de le partager entre le joueur, les futurs PNJ et les
|
||||
* animaux sans se poser de question.
|
||||
*
|
||||
* La cle est le SurfaceType declare dans Project Settings > Physics, pas un
|
||||
* nom ni un chemin d'asset : c'est le seul identifiant que le moteur nous
|
||||
* rende depuis un trace, et il tient dans un octet.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SURVIVAL_PROJET_API UFootstepSoundsDataAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Une entree par matiere du jeu. Les surfaces absentes de cette table
|
||||
* tombent sur DefaultSurface.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Footsteps")
|
||||
TMap<TEnumAsByte<EPhysicalSurface>, FFootstepSurfaceSounds> Surfaces;
|
||||
|
||||
/**
|
||||
* Le repli, et il n'est pas facultatif en pratique.
|
||||
*
|
||||
* Un decor n'est jamais tague a 100 % : il restera toujours un rocher de
|
||||
* demo, un mesh importe a la va-vite ou un acteur de test sans materiau. Le
|
||||
* symptome d'un repli vide est un silence total sur ces surfaces, qu'on
|
||||
* confond immediatement avec "le systeme ne marche pas". Mettre ici les
|
||||
* cues de terre est le choix sur.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Footsteps")
|
||||
FFootstepSurfaceSounds DefaultSurface;
|
||||
|
||||
/**
|
||||
* Les sons d'une surface, ou DefaultSurface si elle n'est pas dans la table.
|
||||
* Ne rend jamais nullptr : l'appelant n'a qu'a tester les cues elles-memes.
|
||||
*/
|
||||
const FFootstepSurfaceSounds& GetSurfaceSounds(EPhysicalSurface Surface) const;
|
||||
};
|
||||
@@ -43,15 +43,65 @@ protected:
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||
bool bIsMoving = false;
|
||||
|
||||
/**
|
||||
* Intention brute de sprint : la touche est enfoncee, rien de plus.
|
||||
*
|
||||
* NE PAS la brancher sur une transition de la machine a etats -- c'est
|
||||
* exactement ce qui faisait courir les bras d'un joueur immobile qui appuyait
|
||||
* sur Maj. Elle reste publiee parce qu'elle est la seule a dire ce que le
|
||||
* joueur VEUT (un effet de FOV, un cout de stamina) ; pour l'animation, c'est
|
||||
* bIsRunning qu'on lit.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||
bool bIsSprinting = false;
|
||||
|
||||
/**
|
||||
* Sprint EFFECTIF : au sol, debout, et le joueur avance pour de bon.
|
||||
*
|
||||
* Toute la composition est faite ici, en un seul endroit, plutot que
|
||||
* reconstruite dans chaque transition du Blueprint. Une machine a etats a
|
||||
* autant d'entrees vers Run que d'etats voisins ; recomposer la condition a
|
||||
* la main dans chacune, c'est se garantir d'en oublier une.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||
bool bIsRunning = false;
|
||||
|
||||
/** Deplacement au sol, debout, sans sprint. Complementaire exact de bIsRunning. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||
bool bIsWalking = false;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||
bool bIsFalling = false;
|
||||
|
||||
/**
|
||||
* Vitesse verticale signee (cm/s), positive vers le haut.
|
||||
*
|
||||
* C'est elle, et pas bIsFalling, qui distingue un saut d'une chute : le
|
||||
* moteur appelle « falling » tout ce qui n'est pas au sol, donc sortir d'un
|
||||
* rebord jouerait l'animation de detente si on se fiait au seul booleen.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||
float VerticalSpeed = 0.f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||
bool bIsCrouching = false;
|
||||
|
||||
/**
|
||||
* Le joueur tient un objet VISIBLE dans la main.
|
||||
*
|
||||
* A brancher sur un « Blend Poses by bool » A L'INTERIEUR de l'etat de
|
||||
* deplacement, jamais sur une transition vers un etat jumeau. Deux etats
|
||||
* « marche avec » et « marche sans » doubleraient la machine et, surtout,
|
||||
* equiper un outil en pleine course claquerait d'un cycle a l'autre. Un blend
|
||||
* fond les deux poses, l'outil apparaissant pendant que la foulee continue.
|
||||
*
|
||||
* C'est le nombre de MESHES montes qu'elle reflete, pas le contenu de la case
|
||||
* active : la plupart des ressources n'ont pas bShowInHand, et les tenir ne
|
||||
* doit pas changer la posture des bras.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Held Item")
|
||||
bool bHasItemInHand = false;
|
||||
|
||||
/**
|
||||
* Vitesse de lecture du cycle de deplacement, a brancher sur la broche
|
||||
* Play Rate du noeud d'animation.
|
||||
|
||||
@@ -11,7 +11,9 @@ class UAnimMontage;
|
||||
class UCameraComponent;
|
||||
class UCharacterAppearanceComponent;
|
||||
class UCraftingComponent;
|
||||
class UFootstepComponent;
|
||||
class UHeadBobComponent;
|
||||
class UHeldItemComponent;
|
||||
class UInputAction;
|
||||
class UInputMappingContext;
|
||||
class UInteractionComponent;
|
||||
@@ -126,6 +128,12 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Apparence")
|
||||
UCharacterAppearanceComponent* GetAppearanceComponent() const { return AppearanceComponent; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Inventaire")
|
||||
UHeldItemComponent* GetHeldItemComponent() const { return HeldItemComponent; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Audio")
|
||||
UFootstepComponent* GetFootstepComponent() const { return FootstepComponent; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Commandes console d'apparence
|
||||
//
|
||||
@@ -252,6 +260,20 @@ public:
|
||||
*/
|
||||
virtual void OnRep_PlayerState() override;
|
||||
|
||||
/**
|
||||
* Les deux moities de « le controller est arrive ».
|
||||
*
|
||||
* UHeldItemComponent ne peut lire la case active QUE chez le joueur local,
|
||||
* et son BeginPlay peut tomber avant que le pawn soit possede -- chez un
|
||||
* client, le pawn et son controller arrivent par deux chemins independants,
|
||||
* exactement comme le PlayerState. Un joueur qui reapparait avec une hache
|
||||
* deja en case 1 aurait alors la main vide jusqu'a ce qu'il touche a son
|
||||
* inventaire. PossessedBy couvre le serveur, OnRep_Controller le client, et
|
||||
* RefreshFromInventory() est idempotente.
|
||||
*/
|
||||
virtual void PossessedBy(AController* NewController) override;
|
||||
virtual void OnRep_Controller() override;
|
||||
|
||||
protected:
|
||||
// Called when the game starts or when spawned
|
||||
virtual void BeginPlay() override;
|
||||
@@ -300,6 +322,35 @@ protected:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
|
||||
TObjectPtr<UAnimMontage> GrabMontage;
|
||||
|
||||
/**
|
||||
* Réception au sol, version légère (MTG_Land).
|
||||
*
|
||||
* Un montage et pas un état de la machine : l'atterrissage est un geste
|
||||
* PONCTUEL qui doit se superposer à ce que les bras faisaient déjà. Un état
|
||||
* imposerait de décider combien de temps y rester, et retomber en pleine
|
||||
* course y bloquerait le cycle de foulée le temps de l'animation.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
|
||||
TObjectPtr<UAnimMontage> LandMontage;
|
||||
|
||||
/** Réception lourde (MTG_HardLand), au-delà de HardLandingFallSpeed. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
|
||||
TObjectPtr<UAnimMontage> HardLandMontage;
|
||||
|
||||
/**
|
||||
* Vitesse de chute (cm/s) à partir de laquelle la réception passe en lourde.
|
||||
*
|
||||
* Doit rester nettement au-dessus de la retombée d'un saut sur place
|
||||
* (~JumpZVelocity, 450 par défaut), sinon chaque saut se termine par une
|
||||
* réception encaissée et le déplacement devient poussif.
|
||||
*
|
||||
* Le seuil BAS, lui, est LandPunchMinFallSpeed : sous cette vitesse aucun
|
||||
* montage n'est joué, exactement comme la caméra ne bouge pas. Une seule
|
||||
* valeur à comprendre pour les deux effets, qui restent donc cohérents.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation", meta = (ClampMin = "0"))
|
||||
float HardLandingFallSpeed = 800.f;
|
||||
|
||||
/**
|
||||
* Balancement de tête procédural : respiration à l'arrêt, pas à la marche,
|
||||
* foulée au sprint. Tous ses réglages sont dans le Details panel du composant,
|
||||
@@ -345,6 +396,26 @@ protected:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Apparence")
|
||||
TObjectPtr<UCharacterAppearanceComponent> AppearanceComponent;
|
||||
|
||||
/**
|
||||
* Montre dans la main l'objet de la case active de la barre rapide.
|
||||
*
|
||||
* Sur le pawn : ce sont ces bras-la et ce corps-la qui tiennent l'objet, et
|
||||
* un cadavre ne tient plus rien. Il lit l'inventaire du meme acteur.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
|
||||
TObjectPtr<UHeldItemComponent> HeldItemComponent;
|
||||
|
||||
/**
|
||||
* Bruits de pas.
|
||||
*
|
||||
* Sur le pawn et sur TOUS les pawns, pas seulement celui du joueur local :
|
||||
* le composant ne replique rien, chaque machine calcule les pas de tous les
|
||||
* corps qu'elle connait a partir de leur vitesse deja synchronisee. Le
|
||||
* mettre sur le controller n'aurait donc donne les pas que d'un joueur.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Audio")
|
||||
TObjectPtr<UFootstepComponent> FootstepComponent;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Enhanced Input : assets à assigner dans le Blueprint
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -89,6 +89,19 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "UI|Storage")
|
||||
bool IsStorageOpen() const { return bStorageOpen; }
|
||||
|
||||
/**
|
||||
* Echap, quel que soit le chemin par lequel il arrive.
|
||||
*
|
||||
* Une seule touche pour cinq usages, d'ou une priorite explicite : boite de
|
||||
* confirmation, reglages, coffre, inventaire, puis pause. Sans cet ordre on
|
||||
* se retrouve avec deux ecrans ouverts en meme temps.
|
||||
*
|
||||
* Publique parce qu'elle a DEUX appelants : l'action Enhanced Input quand le
|
||||
* viewport tient le focus clavier, et le NativeOnKeyDown d'un widget quand
|
||||
* c'est lui qui l'a -- les touches n'atteignent alors plus l'input du jeu.
|
||||
*/
|
||||
void HandleEscapeInput();
|
||||
|
||||
/** Ferme le menu de pause et relance le monde. */
|
||||
UFUNCTION(BlueprintCallable, Category = "UI|Pause")
|
||||
void ResumeGame();
|
||||
@@ -384,13 +397,6 @@ private:
|
||||
/** L'éclaircissement lui-même, après le temps de chauffe du monde. */
|
||||
void StartStartupFade();
|
||||
|
||||
/**
|
||||
* Echap. Une seule touche pour trois usages, d'ou une priorite explicite :
|
||||
* boite de confirmation, puis inventaire, puis pause. Sans cet ordre on se
|
||||
* retrouve avec l'inventaire et la pause ouverts en meme temps.
|
||||
*/
|
||||
void HandlePauseInput();
|
||||
|
||||
void OpenPauseMenu();
|
||||
|
||||
/** Oui : fondu au noir, puis retour au menu principal. */
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "HeldItemComponent.generated.h"
|
||||
|
||||
class AFpsPlayer;
|
||||
class UItemDataAsset;
|
||||
class UStaticMeshComponent;
|
||||
|
||||
/** Diffuse a chaque changement d'objet en main, nul compris. L'UI et l'anim s'y abonnent. */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHeldItemChanged, UItemDataAsset*, NewItem);
|
||||
|
||||
/**
|
||||
* Montre dans la main l'objet occupant la case active de la barre rapide.
|
||||
*
|
||||
* Un systeme = un composant, comme le reste du projet. Celui-ci ne decide de
|
||||
* rien et ne consomme rien : il regarde l'inventaire et pose deux meshes. Ce que
|
||||
* l'objet FAIT quand on clique reste dans AFpsPlayer::PerformUseItem.
|
||||
*
|
||||
* Deux meshes et pas un, parce qu'il y a deux corps a habiller :
|
||||
*
|
||||
* - les BRAS (FirstPersonArms), visibles du seul proprietaire, attaches a la
|
||||
* camera. C'est la hache qu'on voit soi-meme.
|
||||
* - le CORPS (le Mesh du Character), invisible pour son proprietaire mais vu
|
||||
* par les trois autres joueurs -- et dont on percoit sa propre ombre.
|
||||
*
|
||||
* Les bras recopient du porteur leur « visible pour moi seul ». Le mesh du corps,
|
||||
* lui, se cache par la VISIBILITE chez le seul joueur qui controle ce pawn --
|
||||
* bOwnerNoSee y couperait son ombre, voir UpdateLocalVisibility().
|
||||
*
|
||||
* ------------------------------------------------------------------
|
||||
* Reseau : c'est le CLIENT qui pilote, toujours
|
||||
* ------------------------------------------------------------------
|
||||
*
|
||||
* UInventoryComponent::SelectedHotbarIndex n'est PAS replique -- c'est une
|
||||
* intention purement locale, et il n'y a aucune raison de la mettre sur le
|
||||
* reseau. Le serveur ne peut donc pas deviner ce que le joueur a en main : lire
|
||||
* l'inventaire d'un pawn distant y trouverait toujours la case 0.
|
||||
*
|
||||
* Ce qui se replique est donc l'OBJET lui-meme, pas l'index. Le proprietaire
|
||||
* l'applique en local et l'annonce ; le serveur l'enregistre et le rediffuse aux
|
||||
* autres. Exactement le schema de bIsSprinting, COND_SkipOwner compris : renvoyer
|
||||
* a l'emetteur une valeur qui a un aller-retour de retard ne peut que faire
|
||||
* clignoter ce qu'il tient deja.
|
||||
*/
|
||||
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
|
||||
class SURVIVAL_PROJET_API UHeldItemComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UHeldItemComponent();
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
|
||||
/** L'objet actuellement montre dans la main. Nul = les mains sont vides. */
|
||||
UFUNCTION(BlueprintPure, Category = "Held Item")
|
||||
UItemDataAsset* GetHeldItem() const { return HeldItem; }
|
||||
|
||||
/**
|
||||
* Vrai quand un mesh est effectivement monte dans la main.
|
||||
*
|
||||
* Different de `GetHeldItem() != nullptr`, et la nuance compte pour
|
||||
* l'animation : la plupart des ressources du jeu n'ont pas bShowInHand, donc
|
||||
* les tenir ne met rien dans la paume. Se fier a l'objet ferait jouer la
|
||||
* posture « objet en main » sur des mains vides, ce qui se voit tout de suite.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Held Item")
|
||||
bool HasItemInHand() const { return bItemVisibleInHand; }
|
||||
|
||||
/**
|
||||
* Relit la case active et met la main a jour.
|
||||
*
|
||||
* Sans effet ailleurs que chez le joueur local : ce composant est le seul du
|
||||
* projet dont la SOURCE de verite vive uniquement sur la machine du
|
||||
* proprietaire. Idempotente, donc appelable a la volee.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Held Item")
|
||||
void RefreshFromInventory();
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Held Item")
|
||||
FOnHeldItemChanged OnHeldItemChanged;
|
||||
|
||||
/**
|
||||
* Os ou socket des BRAS auquel l'objet s'accroche.
|
||||
*
|
||||
* C'est un OS du rig et pas un socket taille sur mesure, d'ou les offsets
|
||||
* par objet dans UItemDataAsset : creer un socket par outil demanderait de
|
||||
* rouvrir le squelette a chaque nouvel objet, alors que l'offset se regle
|
||||
* dans le DataAsset qu'on est deja en train d'ecrire.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held Item")
|
||||
FName FirstPersonSocket = TEXT("RightHand_Holder_JTN");
|
||||
|
||||
/** Os de la main droite du mannequin UE5, porteur du mesh 3e personne. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held Item")
|
||||
FName ThirdPersonSocket = TEXT("hand_r");
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void OnUnregister() override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* L'objet en main. Un pointeur d'asset se replique comme n'importe quelle
|
||||
* reference d'objet -- c'est deja ce que fait FInventorySlot::Item, et c'est
|
||||
* pour ca qu'on peut se permettre d'envoyer l'objet plutot qu'un index.
|
||||
*
|
||||
* COND_SkipOwner : le proprietaire vient de l'appliquer en local, le lui
|
||||
* renvoyer ecraserait son etat courant par une valeur en retard.
|
||||
*/
|
||||
UPROPERTY(ReplicatedUsing = OnRep_HeldItem)
|
||||
TObjectPtr<UItemDataAsset> HeldItem;
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_HeldItem();
|
||||
|
||||
/** Point d'entree unique : applique en local, puis annonce au serveur. */
|
||||
void SetHeldItem(UItemDataAsset* NewItem);
|
||||
|
||||
UFUNCTION(Server, Reliable)
|
||||
void Server_SetHeldItem(UItemDataAsset* NewItem);
|
||||
|
||||
/** Monte ou demonte les deux meshes selon HeldItem. Tourne sur TOUTES les machines. */
|
||||
void ApplyHeldVisuals();
|
||||
|
||||
/**
|
||||
* Joue le geste de sortie d'outil sur les bras du joueur LOCAL.
|
||||
*
|
||||
* Appelee depuis SetHeldItem et pas depuis ApplyHeldVisuals, alors que les
|
||||
* deux disent « l'objet en main a change » : ApplyHeldVisuals tourne sur
|
||||
* toutes les machines (OnRep, serveur), et un geste de bras n'a de sens que
|
||||
* chez celui qui les voit.
|
||||
*/
|
||||
void PlayEquipMontage();
|
||||
|
||||
/**
|
||||
* Cache le mesh du CORPS chez le joueur qui controle ce pawn, et lui seul.
|
||||
*
|
||||
* Par la VISIBILITE et non par bOwnerNoSee : sur un UStaticMeshComponent,
|
||||
* bCastHiddenShadow n'honore que la premiere -- cache par le proprietaire,
|
||||
* l'objet perd son ombre en meme temps que son mesh. Detail dans le .cpp.
|
||||
*/
|
||||
void UpdateLocalVisibility();
|
||||
|
||||
/**
|
||||
* Cree les deux composants de mesh s'ils manquent.
|
||||
*
|
||||
* A la demande et pas dans le constructeur : le composant n'a alors aucune
|
||||
* hypothese sur l'ordre de creation des sous-objets d'AFpsPlayer, et un pawn
|
||||
* qui ne tient jamais rien ne paie pas deux composants pour rien.
|
||||
*/
|
||||
void EnsureHeldMeshComponents();
|
||||
|
||||
/**
|
||||
* Cree un mesh d'objet en main attache a Parent, ou nullptr si impossible.
|
||||
*
|
||||
* @param bCastWorldShadow l'objet projette-t-il une ombre dans le monde.
|
||||
* Vrai pour la main du corps, faux pour les bras -- un mesh colle a
|
||||
* l'objectif projetterait une ombre enorme en travers du decor.
|
||||
*/
|
||||
UStaticMeshComponent* CreateHeldMeshComponent(FName ComponentName, class USkeletalMeshComponent* Parent,
|
||||
FName SocketName, bool bCastWorldShadow);
|
||||
|
||||
/** Les deux delegues d'inventaire menent ici. */
|
||||
UFUNCTION()
|
||||
void HandleInventoryChanged();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleSelectedHotbarSlotChanged(int32 NewIndex);
|
||||
|
||||
/** Le pawn porteur, ou nullptr si le composant vit ailleurs. */
|
||||
AFpsPlayer* GetOwningPlayer() const;
|
||||
|
||||
/** Mesh vu par le seul proprietaire, accroche aux bras. */
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UStaticMeshComponent> FirstPersonHeldMesh;
|
||||
|
||||
/** Mesh vu par les autres joueurs, accroche a la main du corps. */
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UStaticMeshComponent> ThirdPersonHeldMesh;
|
||||
|
||||
/**
|
||||
* Un mesh est-il reellement monte en main, en cache.
|
||||
*
|
||||
* Recopie de la decision prise par ApplyHeldVisuals plutot que recalculee :
|
||||
* l'AnimInstance la lit a chaque frame, et surtout deux facons de repondre a
|
||||
* la meme question finiraient par diverger.
|
||||
*/
|
||||
bool bItemVisibleInHand = false;
|
||||
|
||||
/** Evite de repeter l'avertissement de socket introuvable a chaque montage. */
|
||||
bool bLoggedMissingSocket = false;
|
||||
};
|
||||
@@ -10,6 +10,25 @@ class UMaterialInterface;
|
||||
class UStaticMesh;
|
||||
class UTexture2D;
|
||||
|
||||
/**
|
||||
* Famille d'outil, pour l'appariement avec les ressources recoltables.
|
||||
*
|
||||
* C'est le TYPE qui decide de ce qu'on peut abattre, jamais le mesh ni le nom :
|
||||
* un arbre acceptera { Axe }, un filon { Pickaxe }. None n'est pas un outil du
|
||||
* tout -- c'est la valeur de tous les objets existants, qui ne changent donc pas
|
||||
* de comportement.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EToolType : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "Not a tool"),
|
||||
Axe,
|
||||
Pickaxe,
|
||||
|
||||
/** Sentinelle de comptage, jamais assignee a un objet. */
|
||||
Count UMETA(Hidden)
|
||||
};
|
||||
|
||||
/**
|
||||
* Definition d'un TYPE d'objet : "Bois", "Pomme", "Corde".
|
||||
* Un asset par type, cree dans le Content Browser, edite a la main.
|
||||
@@ -106,6 +125,100 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Backpack", meta = (ClampMin = "0"))
|
||||
int32 BonusInventorySlots = 0;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// En main
|
||||
//
|
||||
// Ce qu'on voit quand l'objet occupe la case active de la barre rapide.
|
||||
// C'est UHeldItemComponent qui lit ce bloc ; il ne concerne donc que le
|
||||
// visuel, jamais ce que l'objet fait.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Cet objet apparait-il dans la main quand il est selectionne.
|
||||
*
|
||||
* Faux par defaut, et c'est delibere : les deux offsets ci-dessous se reglent
|
||||
* a l'oeil, objet par objet, sur un socket donne. Un vrai par defaut ferait
|
||||
* surgir toutes les ressources deja creees dans le poing du joueur, chacune
|
||||
* de travers, sans que personne l'ait demande.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held")
|
||||
bool bShowInHand = false;
|
||||
|
||||
/**
|
||||
* Mesh tenu en main. Vide = on reprend WorldMesh.
|
||||
*
|
||||
* A ne remplir que si l'objet au sol et l'objet en main different vraiment --
|
||||
* une buche posee en tas contre une buche unique, typiquement. Une hache,
|
||||
* elle, est la meme des deux cotes.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held", meta = (EditCondition = "bShowInHand"))
|
||||
TObjectPtr<UStaticMesh> HeldMesh;
|
||||
|
||||
/**
|
||||
* Ajustement du mesh sur le socket des BRAS (vue premiere personne).
|
||||
*
|
||||
* Necessaire parce qu'on s'attache a un OS du rig et pas a un socket taille
|
||||
* pour l'objet : le pivot d'un mesh d'outil tombe presque toujours sur son
|
||||
* centre, jamais sur le manche. Se regle a l'oeil en PIE.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held", meta = (EditCondition = "bShowInHand"))
|
||||
FTransform FirstPersonHeldOffset;
|
||||
|
||||
/**
|
||||
* Ajustement sur la main du CORPS (ce que les autres joueurs voient).
|
||||
*
|
||||
* Separe du precedent, et il le restera : les bras FPS et le mannequin UE5
|
||||
* sont deux rigs distincts, avec deux echelles et deux orientations d'os. Une
|
||||
* transform commune donnerait forcement une hache de travers d'un cote.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held", meta = (EditCondition = "bShowInHand"))
|
||||
FTransform ThirdPersonHeldOffset;
|
||||
|
||||
/**
|
||||
* Geste joue sur les BRAS quand cet objet arrive en main (MTG_Axe_Equip...).
|
||||
*
|
||||
* Un montage et pas un etat de la machine a etats : sortir un outil est un
|
||||
* geste PONCTUEL qui doit se superposer a ce que les bras faisaient deja --
|
||||
* meme raison que LandMontage. Un etat imposerait de decider combien de temps
|
||||
* y rester, et changer d'outil en pleine course y bloquerait la foulee.
|
||||
*
|
||||
* Vide = l'objet apparait sans geste, ce qui reste parfaitement jouable.
|
||||
* Rien n'est replique : les bras ne sont vus que par leur proprietaire.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Held")
|
||||
TObjectPtr<class UAnimMontage> EquipMontage;
|
||||
|
||||
/** Le mesh a montrer en main, en appliquant le repli sur WorldMesh. */
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Held")
|
||||
UStaticMesh* GetHeldMesh() const { return HeldMesh ? HeldMesh.Get() : WorldMesh.Get(); }
|
||||
|
||||
/**
|
||||
* Pose le mesh tenu en main et son materiau sur un composant.
|
||||
* Pendant exact d'ApplyWorldVisualsTo, dont c'est le second appelant annonce.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Held")
|
||||
void ApplyHeldVisualsTo(class UStaticMeshComponent* Component) const;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Outil
|
||||
//
|
||||
// Volontairement reduit au TYPE pour l'instant : la puissance de recolte et
|
||||
// le cout en durabilite d'un coup n'ont pas encore de lecteur, et inventer
|
||||
// leur forme avant d'ecrire les ressources recoltables serait deviner.
|
||||
//
|
||||
// L'usure, elle, est deja tranchee : ce sera MaxUses / RemainingUses, comme
|
||||
// une canette. Un outil neuf part donc a MaxUses charges, l'inventaire les
|
||||
// suit deja au sol comme dans un coffre, et WBP_ItemDetails affiche deja la
|
||||
// barre correspondante -- rien a ajouter le jour venu.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Ce que cet objet sait recolter. None = ce n'est pas un outil. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Tool")
|
||||
EToolType ToolType = EToolType::None;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Tool")
|
||||
bool IsTool() const { return ToolType != EToolType::None; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Consommable
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "MainMenuPlayerController.generated.h"
|
||||
|
||||
class ACharacterPreviewActor;
|
||||
class AMenuCameraSpot;
|
||||
class UCharacterCustomizationWidget;
|
||||
class UConfirmDialogWidget;
|
||||
class UMainMenuWidget;
|
||||
class UScreenFadeComponent;
|
||||
@@ -27,7 +29,17 @@ enum class EMenuPendingAction : uint8
|
||||
* n'est donc chargee qu'a la reponse du service en ligne -- ou tout de suite
|
||||
* si celui-ci refuse, puisqu'un echec reseau ne doit pas empecher de jouer.
|
||||
*/
|
||||
StartGame
|
||||
StartGame,
|
||||
|
||||
/**
|
||||
* On ne quitte PAS la map : le mannequin est pose dans MenuScene, on ne fait
|
||||
* que porter la vue sur lui. Le fondu sert a masquer la bascule -- un
|
||||
* travelling depuis l'angle de menu traverserait le decor.
|
||||
*/
|
||||
OpenCustomization,
|
||||
|
||||
/** Retour du mannequin vers un angle de menu, meme mecanique en sens inverse. */
|
||||
CloseCustomization
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -102,6 +114,35 @@ protected:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
|
||||
TSubclassOf<USettingsMenuWidget> SettingsMenuClass;
|
||||
|
||||
/**
|
||||
* Ton WBP_CharacterCustomization. Laisse-le vide pour retrouver l'ancien
|
||||
* comportement -- Jouer lance directement la partie. Pratique pour tester
|
||||
* autre chose sans traverser l'ecran a chaque fois.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu|Customization")
|
||||
TSubclassOf<UCharacterCustomizationWidget> CustomizationMenuClass;
|
||||
|
||||
/**
|
||||
* Le mannequin pose dans MenuScene. Cherche par classe au BeginPlay plutot
|
||||
* qu'assigne ici : une reference d'acteur dans un CDO de controller ne
|
||||
* survit pas au chargement de la map.
|
||||
*/
|
||||
UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu|Customization")
|
||||
TObjectPtr<ACharacterPreviewActor> CharacterPreview;
|
||||
|
||||
UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu|Customization")
|
||||
TObjectPtr<UCharacterCustomizationWidget> CustomizationMenu;
|
||||
|
||||
/**
|
||||
* Duree de CHAQUE moitie du fondu entre le menu et l'ecran de customisation.
|
||||
* Le passage complet dure donc le double.
|
||||
*
|
||||
* Plus court que le fondu de depart vers la partie : on ne change pas de
|
||||
* map, il n'y a rien a charger, et une attente ici ne serait qu'une attente.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu|Customization", meta = (ClampMin = "0.05"))
|
||||
float CustomizationFadeDuration = 0.35f;
|
||||
|
||||
/** Fondu d'arrivee sur le menu, au lancement du jeu. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu", meta = (ClampMin = "0.1"))
|
||||
float OpeningFadeDuration = 1.5f;
|
||||
@@ -146,6 +187,26 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleSettingsCloseRequested();
|
||||
|
||||
/**
|
||||
* Ouvre l'ecran de customisation et pose la vue sur le mannequin.
|
||||
*
|
||||
* L'ordre compte : SetViewTarget d'abord, widget ensuite. L'inverse
|
||||
* afficherait la colonne par-dessus l'ancien cadrage pendant toute la duree
|
||||
* du blend.
|
||||
*/
|
||||
void OpenCustomization();
|
||||
|
||||
/** Le joueur a valide : on soumet l'apparence, puis on lance la partie. */
|
||||
UFUNCTION()
|
||||
void HandleCustomizationConfirmed();
|
||||
|
||||
/** Le joueur renonce : retour au menu, rien n'est ecrit. */
|
||||
UFUNCTION()
|
||||
void HandleCustomizationCancelled();
|
||||
|
||||
/** Referme l'ecran et rend la vue a l'angle de menu. */
|
||||
void CloseCustomization();
|
||||
|
||||
/**
|
||||
* La session n'a pas pu etre creee -- Steam eteint, hors ligne, service en
|
||||
* panne. On charge la map QUAND MEME.
|
||||
|
||||
@@ -24,9 +24,39 @@ class SURVIVAL_PROJET_API UPauseMenuWidget : public UUserWidget
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPauseMenuWidget(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct() override;
|
||||
|
||||
/**
|
||||
* Echap, quand c'est CE widget qui tient le focus clavier.
|
||||
*
|
||||
* Indispensable et pas seulement confortable : les widgets UMG sont des
|
||||
* FRERES du SViewport dans l'arbre Slate, jamais ses enfants. Une touche
|
||||
* remonte donc le long des parents du widget focalise et n'atteint jamais le
|
||||
* viewport -- Enhanced Input ne la voit pas. Or le menu de pause est le seul
|
||||
* ecran in-game plein de UButton, et un bouton prend le focus au clic : sans
|
||||
* ce relais, Echap cesse de refermer le menu des le premier clic dedans.
|
||||
*/
|
||||
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
|
||||
|
||||
/** Affiche le menu et lui rend le focus clavier. */
|
||||
void Open();
|
||||
|
||||
/** Masque le menu. Le focus revient au viewport avec le mode d'input. */
|
||||
void Close();
|
||||
|
||||
/**
|
||||
* Actif (cliquable et focalise) ou simplement lisible derriere un ecran pose
|
||||
* par-dessus -- reglages, boite de confirmation.
|
||||
*
|
||||
* Redonner le focus en redevenant actif est la moitie qui manque le plus
|
||||
* souvent : l'ecran qui passait devant l'avait pris pour lui, et personne ne
|
||||
* le rend en se fermant.
|
||||
*/
|
||||
void SetInteractive(bool bInteractive);
|
||||
|
||||
protected:
|
||||
/** Reprend la partie. Nomme exactement "ResumeButton" dans le WBP. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
#include "Interactable.h"
|
||||
#include "PickupItem.generated.h"
|
||||
|
||||
class UBoxComponent;
|
||||
class UItemDataAsset;
|
||||
class USphereComponent;
|
||||
class UStaticMeshComponent;
|
||||
|
||||
/**
|
||||
@@ -60,12 +60,28 @@ protected:
|
||||
|
||||
/**
|
||||
* Volume que le trace d'interaction vient toucher.
|
||||
* Volontairement separe du mesh : beaucoup de meshes d'asset packs,
|
||||
* en particulier la vegetation, n'ont aucune geometrie de collision.
|
||||
* Avec une sphere dediee, le pickup fonctionne quel que soit le visuel.
|
||||
*
|
||||
* Volontairement separe du mesh : beaucoup de meshes d'asset packs, en
|
||||
* particulier la vegetation, n'ont aucune geometrie de collision. Avec un
|
||||
* volume dedie, le pickup fonctionne quel que soit le visuel.
|
||||
*
|
||||
* Une BOITE et non une sphere, contrairement a la premiere version : la
|
||||
* sphere englobante d'un objet allonge a pour rayon sa DEMI-DIAGONALE, ce qui
|
||||
* est le pire cas geometrique possible. Une branche de 120x20x15 donnait une
|
||||
* sphere de 62 cm de rayon, soit vingt-sept fois le volume de l'objet. Le
|
||||
* symptome n'etait pas cosmetique : deux pickups poses cote a cote voyaient
|
||||
* leurs spheres se chevaucher, et le trace retenait le premier volume touche
|
||||
* plutot que l'objet reellement vise.
|
||||
*
|
||||
* Elle epouse toute seule le mesh pose (voir FitInteractionBox) : un seul
|
||||
* BP_Pickup sert des centaines d'objets, une taille fixe serait forcement
|
||||
* fausse pour la plupart.
|
||||
*
|
||||
* Contrairement a celle du coffre, elle ne bloque QUE le canal Visibility :
|
||||
* un objet au sol se traverse, il ne barre pas le passage.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item")
|
||||
TObjectPtr<USphereComponent> InteractionSphere;
|
||||
TObjectPtr<UBoxComponent> InteractionBox;
|
||||
|
||||
/**
|
||||
* Quel objet ce pickup represente. A assigner sur chaque instance.
|
||||
@@ -105,6 +121,15 @@ protected:
|
||||
*/
|
||||
void ApplyItemVisuals();
|
||||
|
||||
/**
|
||||
* Cale la boite d'interaction sur le mesh actuellement pose.
|
||||
*
|
||||
* Elle lit le mesh du COMPOSANT et pas celui de l'ItemData : le cas
|
||||
* bUseMeshFromItemData decoche -- un mesh impose a la main sur une instance --
|
||||
* a autant besoin d'une boite a sa taille que les autres.
|
||||
*/
|
||||
void FitInteractionBox();
|
||||
|
||||
/**
|
||||
* Reprend automatiquement le WorldMesh de l'ItemData dans l'editeur.
|
||||
* Decoche pour imposer un mesh different sur une instance precise.
|
||||
@@ -112,6 +137,44 @@ protected:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
|
||||
bool bUseMeshFromItemData = true;
|
||||
|
||||
/**
|
||||
* La boite d'interaction epouse toute seule le mesh pose.
|
||||
*
|
||||
* Coche par defaut : changer l'ItemData d'une instance doit suffire, sans
|
||||
* avoir a retailler un volume a la main derriere. Decoche-le pour un objet
|
||||
* qu'on veut volontairement ramassable de plus loin, ou de plus pres.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Interaction")
|
||||
bool bAutoFitInteractionBox = true;
|
||||
|
||||
/**
|
||||
* Marge ajoutee sur chaque axe, en cm.
|
||||
*
|
||||
* Additive et surtout pas multiplicative : c'est tout l'interet du passage a
|
||||
* une boite. Multiplier la demi-epaisseur d'une branche -- 7 cm -- par 1.2
|
||||
* ajoute un centimetre et demi, autant dire rien, alors que le meme facteur
|
||||
* rallonge son grand axe de 12 cm. La tolerance de visee ne depend pas de la
|
||||
* taille de l'objet, elle se compte en centimetres.
|
||||
*
|
||||
* Garde-la petite : le trace d'interaction est deja un balayage spherique
|
||||
* tolerant, gonfler la boite ferait surtout voler la cible aux voisins.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Interaction",
|
||||
meta = (EditCondition = "bAutoFitInteractionBox", ClampMin = "0", Units = "cm"))
|
||||
float InteractionBoxPadding = 4.f;
|
||||
|
||||
/**
|
||||
* Demi-dimension plancher, appliquee axe par axe, en cm.
|
||||
*
|
||||
* Une baie ou un clou donneraient une boite de deux centimetres, impossible
|
||||
* a viser meme avec la tolerance du balayage. Axe par axe et non sur le
|
||||
* volume entier : une branche a besoin qu'on epaississe son epaisseur, pas
|
||||
* qu'on rallonge sa longueur.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Interaction",
|
||||
meta = (EditCondition = "bAutoFitInteractionBox", ClampMin = "0", Units = "cm"))
|
||||
float MinInteractionExtent = 10.f;
|
||||
|
||||
/** Active le Custom Depth quand l'objet est vise. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
|
||||
bool bHighlightOnFocus = true;
|
||||
|
||||
@@ -42,7 +42,7 @@ struct FStorageStartingItem
|
||||
*
|
||||
* Toute la collision tient dans UNE boite, et les deux meshes n'en portent
|
||||
* aucune. C'est la difference avec ACraftingStation, qui se contente de son
|
||||
* mesh : un coffre est en deux morceaux dont l'un PIVOTE. Se reposer sur leurs
|
||||
* mesh : un coffre est en deux morceaux dont l'un TOURNE. Se reposer sur leurs
|
||||
* collisions imposerait que le pack en fournisse deux correctes, et laisserait
|
||||
* surtout un trou dans la surface visable des que le couvercle se leve --
|
||||
* viser le haut du coffre ouvert ne toucherait plus rien.
|
||||
@@ -137,26 +137,24 @@ protected:
|
||||
* L'unique collision du coffre : ce que le trace d'interaction vient
|
||||
* toucher, et ce qui empeche le joueur de traverser.
|
||||
*
|
||||
* Fille de la caisse et non du pivot : elle ne suit surtout pas le
|
||||
* couvercle. Un coffre ouvert doit se viser au meme endroit qu'un coffre
|
||||
* Fille de la caisse et non du couvercle : elle ne suit surtout pas ce
|
||||
* dernier. Un coffre ouvert doit se viser au meme endroit qu'un coffre
|
||||
* ferme, sinon le prompt disparait au moment precis ou l'on s'en sert.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
|
||||
TObjectPtr<UBoxComponent> InteractionBox;
|
||||
|
||||
/**
|
||||
* La charniere. Place-la a la souris sur l'arete arriere du coffre.
|
||||
* Le couvercle : SM_Chest_Top, enfant direct de la caisse.
|
||||
*
|
||||
* Ce composant vide n'est pas un luxe : le pivot de SM_Chest_Top est celui
|
||||
* que le pack lui a donne, presque jamais la charniere. Faire tourner le
|
||||
* mesh directement le ferait pivoter autour de son centre et traverser la
|
||||
* caisse. Ici on tourne le pivot, et le mesh suit avec son decalage.
|
||||
* C'est LUI qu'on tourne, donc la charniere est le pivot que l'artiste a
|
||||
* donne au mesh -- un StaticMeshComponent tourne toujours autour de
|
||||
* l'origine de son mesh. Rien a placer a la main, mais rien a rattraper non
|
||||
* plus : si un pack posait ce pivot au centre du couvercle, l'ouverture se
|
||||
* verrait de travers et il faudrait corriger le pivot dans l'editeur de
|
||||
* mesh, pas ici.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
|
||||
TObjectPtr<USceneComponent> LidPivot;
|
||||
|
||||
/** Le couvercle : SM_Chest_Top, enfant du pivot. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
|
||||
TObjectPtr<UStaticMeshComponent> LidMesh;
|
||||
|
||||
/** Le contenu. Un inventaire ordinaire, sans barre rapide. */
|
||||
@@ -219,12 +217,12 @@ protected:
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Rotation RELATIVE du pivot couvercle ouvert. Le repos vaut toujours zero.
|
||||
* Rotation RELATIVE du couvercle ouvert. Le repos vaut toujours zero.
|
||||
*
|
||||
* Un FRotator complet et pas un simple angle : selon l'orientation que tu
|
||||
* donnes au pivot, la charniere peut tomber sur le pitch, le yaw ou le roll.
|
||||
* Regle les trois a l'oeil dans le viewport plutot que d'aller comprendre
|
||||
* quel axe local du mesh pointe ou.
|
||||
* Un FRotator complet et pas un simple angle : selon l'orientation dans
|
||||
* laquelle le mesh a ete exporte, la charniere peut tomber sur le pitch, le
|
||||
* yaw ou le roll. Regle les trois a l'oeil dans le viewport plutot que
|
||||
* d'aller comprendre quel axe local du mesh pointe ou.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Lid")
|
||||
FRotator OpenRotation = FRotator(-100.f, 0.f, 0.f);
|
||||
@@ -244,7 +242,7 @@ protected:
|
||||
TObjectPtr<USoundBase> CloseSound;
|
||||
|
||||
private:
|
||||
/** Recopie LidAlpha dans la rotation du pivot. */
|
||||
/** Recopie LidAlpha dans la rotation du couvercle. */
|
||||
void ApplyLidRotation();
|
||||
|
||||
/** Cale la boite sur le volume combine des deux meshes, couvercle ferme. */
|
||||
|
||||
@@ -26,6 +26,14 @@ public class Survival_projet : ModuleRules
|
||||
// AnimGraphRuntime : UKismetAnimationLibrary::CalculateDirection, qui remplace
|
||||
// la methode homonyme d'UAnimInstance depreciee en 5.8. Prive et pas public --
|
||||
// seul le .cpp de UPlayerBodyAnimInstance l'appelle, son header n'en montre rien.
|
||||
// PhysicsCore : EPhysicalSurface, la cle de la table de UFootstepSoundsDataAsset.
|
||||
// Le header passe sans, Engine l'exposant deja, mais le LIEN echoue --
|
||||
// l'enum est REFLECHIE, donc UHT genere un appel a son constructeur
|
||||
// Z_Construct_UEnum_PhysicsCore_EPhysicalSurface, qui vit dans ce module.
|
||||
// Symptome : ca compile et ca casse a l'edition de liens (LNK2019).
|
||||
// Public parce que FootstepSoundsDataAsset.h montre le type dans sa TMap.
|
||||
PublicDependencyModuleNames.Add("PhysicsCore");
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "EngineSettings", "AnimGraphRuntime" });
|
||||
|
||||
// Coop Steam : on ne depend QUE de l'API generique (OnlineSubsystem) et de
|
||||
|
||||
Reference in New Issue
Block a user