Import initial du projet Survival (UE 5.8)

Boucle de jeu complète : récolte, inventaire, consommation, stats de survie,
mort et respawn. Gameplay en C++, Blueprints réservés au câblage d'assets.

Assets binaires (.uasset, .umap, textures, audio) suivis via Git LFS.
Binaries/, Intermediate/, Saved/ et DerivedDataCache/ sont ignorés :
régénérés au build, ils pèsent 3 Go pour 1,3 Go de contenu utile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 11:29:07 +02:00
commit ed7c7fd991
2626 changed files with 14833 additions and 0 deletions
@@ -0,0 +1,102 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "ConfirmDialogWidget.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "InputCoreTypes.h"
UConfirmDialogWidget::UConfirmDialogWidget(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 UConfirmDialogWidget::NativeConstruct()
{
Super::NativeConstruct();
if (ConfirmButton)
{
ConfirmButton->OnClicked.AddDynamic(this, &UConfirmDialogWidget::HandleConfirmClicked);
}
if (CancelButton)
{
CancelButton->OnClicked.AddDynamic(this, &UConfirmDialogWidget::HandleCancelClicked);
}
}
void UConfirmDialogWidget::Open(const FText& InMessage)
{
if (MessageText)
{
MessageText->SetText(InMessage);
}
SetVisibility(ESlateVisibility::Visible);
// Le focus doit etre repris a CHAQUE ouverture : le refermer le rend au
// menu, et une boite visible mais sans focus ignorerait le clavier.
SetKeyboardFocus();
}
void UConfirmDialogWidget::Close()
{
SetVisibility(ESlateVisibility::Collapsed);
}
FReply UConfirmDialogWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
const FKey Key = InKeyEvent.GetKey();
// Virtual_Back en plus d'Echap : Slate y fait pointer le bouton "retour" de
// la manette selon la plateforme (B sur Xbox, Rond sur PlayStation). Une
// seule ligne et le clavier comme la manette sont couverts, sans avoir a
// enumerer les touches de chaque constructeur.
if (bCancelOnEscape && (Key == EKeys::Escape || Key == EKeys::Virtual_Back))
{
OnCancelled.Broadcast();
// Handled et non Unhandled : sans ca la touche continue de remonter et
// serait consommee une seconde fois par ce qui se trouve derriere.
return FReply::Handled();
}
if (bConfirmOnEnter && (Key == EKeys::Enter || Key == EKeys::Virtual_Accept))
{
OnConfirmed.Broadcast();
return FReply::Handled();
}
return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
}
void UConfirmDialogWidget::HandleConfirmClicked()
{
OnConfirmed.Broadcast();
}
void UConfirmDialogWidget::HandleCancelClicked()
{
OnCancelled.Broadcast();
}
void UConfirmDialogWidget::NativeDestruct()
{
if (ConfirmButton)
{
ConfirmButton->OnClicked.RemoveDynamic(this, &UConfirmDialogWidget::HandleConfirmClicked);
}
if (CancelButton)
{
CancelButton->OnClicked.RemoveDynamic(this, &UConfirmDialogWidget::HandleCancelClicked);
}
Super::NativeDestruct();
}
@@ -0,0 +1,547 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "FpsPlayer.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerController.h"
#include "InputActionValue.h"
#include "InteractionComponent.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#include "PickupItem.h"
#include "SurvivalStatsComponent.h"
// Sets default values
AFpsPlayer::AFpsPlayer()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
// Gabarit du joueur : ~176 cm de haut, 34 cm de rayon
UCapsuleComponent* Capsule = GetCapsuleComponent();
Capsule->InitCapsuleSize(34.f, 88.f);
// La souris fait tourner le personnage horizontalement (yaw) ;
// le pitch reste sur le controller et n'est applique qu'a la camera.
bUseControllerRotationYaw = true;
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
// Camera a hauteur des yeux. La position est exprimee par rapport au centre
// de la capsule, d'ou la soustraction de la demi-hauteur.
FirstPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
FirstPersonCamera->SetupAttachment(Capsule);
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightStanding - Capsule->GetUnscaledCapsuleHalfHeight()));
FirstPersonCamera->SetFieldOfView(BaseFOV);
// On pilote la rotation nous-memes dans UpdateCameraEffects() : bUsePawnControlRotation
// ecraserait la rotation chaque frame et tous les effets de tilt seraient invisibles.
FirstPersonCamera->bUsePawnControlRotation = false;
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
SurvivalStats = CreateDefaultSubobject<USurvivalStatsComponent>(TEXT("SurvivalStats"));
// En vue FPS on ne veut pas voir son propre corps (mesh 3e personne),
// mais on garde son ombre.
GetMesh()->SetOwnerNoSee(true);
GetMesh()->SetCastHiddenShadow(true);
UCharacterMovementComponent* Movement = GetCharacterMovement();
Movement->MaxWalkSpeed = WalkSpeed;
Movement->MaxWalkSpeedCrouched = CrouchSpeed;
Movement->JumpZVelocity = 450.f;
Movement->AirControl = 0.3f;
Movement->BrakingDecelerationWalking = 2000.f;
// 120 cm de haut accroupi (une vraie position accroupie, pas un rampant)
Movement->SetCrouchedHalfHeight(60.f);
// Indispensable, sinon Crouch() est ignore
Movement->GetNavAgentPropertiesRef().bCanCrouch = true;
}
// Called when the game starts or when spawned
void AFpsPlayer::BeginPlay()
{
Super::BeginPlay();
SurvivalStats->OnDied.AddDynamic(this, &AFpsPlayer::HandleDeath);
// On enregistre le mapping context aupres du sous-systeme Enhanced Input
// du joueur local. Sans ca, aucune touche n'est lue.
if (const APlayerController* PlayerController = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
if (DefaultMappingContext)
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DefaultMappingContext n'est pas assigne, aucun input ne fonctionnera."));
}
}
}
}
// Called every frame
void AFpsPlayer::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// On memorise la vitesse de chute a chaque frame : au moment ou Landed()
// est appele, la vitesse a deja pu etre remise a zero par le moteur.
if (GetCharacterMovement()->IsFalling())
{
LastFallSpeed = GetCharacterMovement()->Velocity.Z;
}
UpdateCameraEffects(DeltaTime);
}
void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
{
// Un gros hoquet de framerate (chargement, breakpoint) ferait diverger
// l'integration du ressort ci-dessous. On plafonne le pas de temps.
const float Step = FMath::Min(DeltaTime, 1.f / 30.f);
// ==================================================================
// 1. Animation d'accroupissement
// ==================================================================
// CrouchAlpha va de 0 (debout) a 1 (accroupi) en CrouchTransitionDuration
// secondes. Duree fixe plutot qu'un FInterpTo : le mouvement est identique
// a chaque fois et se termine vraiment, au lieu de tendre asymptotiquement.
if (!FMath::IsNearlyEqual(CrouchAlpha, CrouchTargetAlpha))
{
const float CrouchStep = Step / FMath::Max(CrouchTransitionDuration, KINDA_SMALL_NUMBER);
CrouchAlpha = FMath::Clamp(CrouchAlpha + CrouchStep * FMath::Sign(CrouchTargetAlpha - CrouchAlpha), 0.f, 1.f);
}
// ==================================================================
// 2. Ressort d'impact (saut, atterrissage, et tout AddCameraPunch)
// ==================================================================
// L'impulsion n'est pas versee d'un coup : la vitesse de la camera passerait
// de 0 a plusieurs centaines de cm/s en une frame, et cette discontinuite se
// lit comme un a-coup. On l'etale sur PunchImpulseRampTime.
if (!FMath::IsNearlyZero(PendingPunch))
{
const float ReleaseRatio = (PunchImpulseRampTime > KINDA_SMALL_NUMBER)
? FMath::Clamp(Step / PunchImpulseRampTime, 0.f, 1.f)
: 1.f;
const float Released = PendingPunch * ReleaseRatio;
PunchVelocity += Released;
PendingPunch -= Released;
}
// Ressort amorti classique : acceleration = -k*x - c*v.
// On exprime k et c a partir d'une frequence et d'un taux d'amortissement,
// beaucoup plus parlants a regler qu'une raideur brute.
const float Omega = 2.f * PI * PunchFrequency;
const float Acceleration = -(Omega * Omega * PunchOffset) - (2.f * PunchDamping * Omega * PunchVelocity);
PunchVelocity += Acceleration * Step;
PunchOffset = FMath::Clamp(PunchOffset + PunchVelocity * Step, -PunchMaxOffset, PunchMaxOffset);
// ==================================================================
// 3. FOV de sprint
// ==================================================================
// On exige un vrai deplacement : sinon le FOV s'ouvrirait juste en
// maintenant Shift a l'arret, ce qui donne un effet de pompage bizarre.
const bool bMovingFastEnough = GetCharacterMovement()->Velocity.SizeSquared2D() > FMath::Square(WalkSpeed * 0.9f);
const float TargetFOV = BaseFOV + ((bIsSprinting && bMovingFastEnough) ? SprintFOVOffset : 0.f);
FirstPersonCamera->SetFieldOfView(FMath::FInterpTo(FirstPersonCamera->FieldOfView, TargetFOV, Step, FOVInterpSpeed));
// ==================================================================
// 4. Application : position
// ==================================================================
// SmoothStep donne une courbe en S : demarrage doux, acceleration, arrivee douce.
const float Smoothed = FMath::SmoothStep(0.f, 1.f, CrouchAlpha);
const float EyeHeightAboveFeet = FMath::Lerp(EyeHeightStanding, EyeHeightCrouched, Smoothed);
// Point cle : quand la capsule retrecit, le CharacterMovement redescend l'acteur
// d'un coup pour garder les pieds au sol. La camera, attachee a la capsule,
// suivait ce saut instantane -- c'est ce qui rendait le crouch "snap".
// En raisonnant en hauteur d'oeil AU-DESSUS DES PIEDS et en soustrayant la
// demi-hauteur courante de la capsule, ce saut est annule exactement.
const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightAboveFeet - CapsuleHalfHeight + PunchOffset));
// ==================================================================
// 5. Application : rotation
// ==================================================================
// sin(alpha * PI) vaut 0 aux deux extremites et 1 au milieu : le tilt monte
// puis redescend tout seul pendant la transition, et est nul au repos.
// CrouchTiltDirection inverse le sens selon qu'on descend ou qu'on remonte.
const float CrouchTiltShape = FMath::Sin(CrouchAlpha * PI) * CrouchTiltDirection;
FRotator ViewOffset(-CrouchTiltPitch * CrouchTiltShape, 0.f, CrouchTiltRoll * CrouchTiltShape);
// Le regard suit le deplacement vertical de la camera : elle descend, on
// regarde vers le bas. C'est ce qui vend l'impact a l'atterrissage.
ViewOffset.Pitch += PunchOffset * PunchPitchPerCm;
if (const AController* OwningController = GetController())
{
FirstPersonCamera->SetWorldRotation(OwningController->GetControlRotation() + ViewOffset);
}
}
void AFpsPlayer::AddCameraPunch(float Strength)
{
// Mis en attente plutot qu'applique directement : c'est UpdateCameraEffects
// qui l'injecte progressivement dans le ressort.
PendingPunch += Strength;
}
void AFpsPlayer::Landed(const FHitResult& Hit)
{
Super::Landed(Hit);
// L'amplitude suit la vitesse de chute : une marche d'escalier ne doit pas
// secouer autant qu'une chute de dix metres.
const float FallSpeed = FMath::Abs(LastFallSpeed);
if (FallSpeed > LandPunchMinFallSpeed)
{
const float Range = FMath::Max(LandPunchMaxFallSpeed - LandPunchMinFallSpeed, 1.f);
const float Ratio = FMath::Clamp((FallSpeed - LandPunchMinFallSpeed) / Range, 0.f, 1.f);
AddCameraPunch(-LandPunchStrength * Ratio);
// Aide au reglage : montre la vitesse d'impact reelle et l'amplitude
// obtenue. Supprime ce log quand le ressenti te convient.
UE_LOG(LogTemp, Log, TEXT("Atterrissage : chute %.0f cm/s, ratio %.2f, impulsion %.0f cm/s (~%.1f cm de camera)"),
FallSpeed, Ratio, LandPunchStrength * Ratio, LandPunchStrength * Ratio * 0.018f);
}
else
{
UE_LOG(LogTemp, Log, TEXT("Atterrissage ignore : chute %.0f cm/s, sous le seuil de %.0f cm/s"),
FallSpeed, LandPunchMinFallSpeed);
}
LastFallSpeed = 0.f;
}
void AFpsPlayer::OnJumped_Implementation()
{
Super::OnJumped_Implementation();
AddCameraPunch(-JumpPunchStrength);
}
// Called to bind functionality to input
void AFpsPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PlayerInputComponent);
if (!Input)
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : l'input component n'est pas un UEnhancedInputComponent."));
return;
}
if (MoveAction)
{
Input->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AFpsPlayer::Move);
}
if (LookAction)
{
Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &AFpsPlayer::Look);
}
if (JumpAction)
{
// Jump() et StopJumping() viennent directement de ACharacter
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
}
if (SprintAction)
{
Input->BindAction(SprintAction, ETriggerEvent::Started, this, &AFpsPlayer::StartSprint);
Input->BindAction(SprintAction, ETriggerEvent::Completed, this, &AFpsPlayer::StopSprint);
}
if (CrouchAction)
{
Input->BindAction(CrouchAction, ETriggerEvent::Started, this, &AFpsPlayer::StartCrouch);
Input->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AFpsPlayer::StopCrouch);
}
if (InteractAction)
{
Input->BindAction(InteractAction, ETriggerEvent::Started, this, &AFpsPlayer::Interact);
}
if (UseItemAction)
{
Input->BindAction(UseItemAction, ETriggerEvent::Started, this, &AFpsPlayer::UseSelectedItem);
}
}
void AFpsPlayer::Move(const FInputActionValue& Value)
{
const FVector2D Axis = Value.Get<FVector2D>();
if (Axis.IsNearlyZero() || !Controller)
{
return;
}
// On se deplace par rapport a la direction du regard, mais a plat :
// on ne garde que le yaw pour ne pas "voler" en regardant vers le haut.
const FRotator YawRotation(0.f, Controller->GetControlRotation().Yaw, 0.f);
const FVector Forward = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector Right = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Forward, Axis.Y);
AddMovementInput(Right, Axis.X);
}
void AFpsPlayer::Look(const FInputActionValue& Value)
{
// Si IA_Look n'est pas en Axis2D, Get<FVector2D>() renvoie toujours Y = 0
// et le regard reste bloque sur l'axe horizontal. On le signale une fois.
if (Value.GetValueType() != EInputActionValueType::Axis2D && !bLoggedLookTypeWarning)
{
bLoggedLookTypeWarning = true;
UE_LOG(LogTemp, Error,
TEXT("AFpsPlayer : IA_Look doit avoir Value Type = Axis2D (Vector2D). ")
TEXT("Avec un autre type, l'axe vertical est ignore."));
}
const FVector2D Axis = Value.Get<FVector2D>();
// Pas de DeltaTime ici : la souris fournit un delta, pas une vitesse.
// Le multiplier par DeltaTime rendrait la visee dependante du framerate.
AddControllerYawInput(Axis.X * LookSensitivityX);
AddControllerPitchInput(Axis.Y * LookSensitivityY * (bInvertLookY ? -1.f : 1.f));
}
void AFpsPlayer::StartSprint()
{
// Pas de sprint accroupi : on se releve d'abord
if (bIsCrouched)
{
return;
}
bIsSprinting = true;
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
}
void AFpsPlayer::StopSprint()
{
bIsSprinting = false;
GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
}
void AFpsPlayer::UseSelectedItem()
{
const FInventorySlot Selected = InventoryComponent->GetSelectedSlot();
if (Selected.IsEmpty())
{
return;
}
const UItemDataAsset* Item = Selected.Item;
// Objet non consommable : on ne fait rien pour l'instant. C'est ici que
// viendront les outils (frapper), les armes (tirer), les placeables.
if (!Item->bIsConsumable)
{
return;
}
// Les valeurs de restauration sont appliquees a CHAQUE utilisation.
SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore);
// ConsumeUse gere les deux cas : usage unique (l'exemplaire disparait) et
// usages multiples (une charge en moins, l'objet reste tant qu'il en a).
// On vise le slot actif precisement, et pas RemoveItem qui piocherait
// n'importe quelle pile du meme objet, y compris dans le sac.
InventoryComponent->ConsumeUse(InventoryComponent->GetSelectedHotbarIndex());
}
FVector AFpsPlayer::PlaceOnGround(const FVector& Start) const
{
// APickupItem ne simule pas la physique : sans ce trace vers le bas,
// l'objet resterait suspendu en l'air la ou on l'a lache.
FCollisionQueryParams Params(SCENE_QUERY_STAT(DropGroundTrace), /*bTraceComplex=*/false, this);
FHitResult GroundHit;
if (GetWorld()->LineTraceSingleByChannel(GroundHit, Start, Start - FVector(0.f, 0.f, 500.f), ECC_Visibility, Params))
{
return GroundHit.ImpactPoint + FVector(0.f, 0.f, DropGroundOffset);
}
return Start;
}
APickupItem* AFpsPlayer::SpawnPickup(const FInventorySlot& Slot, const FVector& Location)
{
if (!DroppedPickupClass || Slot.IsEmpty())
{
return nullptr;
}
// SpawnActorDeferred et non SpawnActor : il faut poser ItemData AVANT que
// OnConstruction et BeginPlay tournent, sinon le pickup apparait sans mesh
// et son CanInteract le rendrait inerte.
const FTransform SpawnTransform(FRotator(0.f, GetActorRotation().Yaw, 0.f), Location);
APickupItem* Dropped = GetWorld()->SpawnActorDeferred<APickupItem>(
DroppedPickupClass, SpawnTransform, this, nullptr,
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
if (!Dropped)
{
return nullptr;
}
Dropped->InitPickup(Slot.Item, Slot.Quantity, Slot.RemainingUses);
Dropped->FinishSpawning(SpawnTransform);
return Dropped;
}
bool AFpsPlayer::DropSlot(int32 SlotIndex, int32 Quantity)
{
if (!DroppedPickupClass)
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, impossible de jeter un objet."));
return false;
}
FInventorySlot Taken;
if (!InventoryComponent->TakeFromSlot(SlotIndex, Quantity, Taken) || Taken.IsEmpty())
{
return false;
}
const FVector EyeLocation = FirstPersonCamera->GetComponentLocation();
const FVector Forward = FirstPersonCamera->GetForwardVector();
FVector DropLocation = EyeLocation + Forward * DropDistance;
// Un mur devant nous : on depose juste avant, sinon l'objet finirait
// de l'autre cote de la cloison et deviendrait inatteignable.
FCollisionQueryParams Params(SCENE_QUERY_STAT(DropItemTrace), /*bTraceComplex=*/false, this);
FHitResult WallHit;
if (GetWorld()->LineTraceSingleByChannel(WallHit, EyeLocation, DropLocation, ECC_Visibility, Params))
{
DropLocation = WallHit.ImpactPoint - Forward * 20.f;
}
if (!SpawnPickup(Taken, PlaceOnGround(DropLocation)))
{
// On ne perd pas l'objet : il retourne dans l'inventaire, usure comprise.
InventoryComponent->AddItem(Taken.Item, Taken.Quantity, Taken.RemainingUses);
return false;
}
return true;
}
void AFpsPlayer::DropAllItems()
{
if (!DroppedPickupClass)
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, le butin est perdu."));
return;
}
const int32 SlotCount = InventoryComponent->GetSlots().Num();
const FVector Center = GetActorLocation();
int32 Scattered = 0;
for (int32 Index = 0; Index < SlotCount; ++Index)
{
FInventorySlot Taken;
// TakeFromSlot renvoie false sur un slot vide : pas besoin de tester.
if (!InventoryComponent->TakeFromSlot(Index, MAX_int32, Taken) || Taken.IsEmpty())
{
continue;
}
// Angle d'or : repartit les objets en spirale reguliere autour du
// corps, sans qu'ils se superposent ni forment de motif visible.
const float Angle = FMath::DegreesToRadians(Scattered * 137.5f);
const float Radius = 40.f + Scattered * 4.f;
const FVector Offset(FMath::Cos(Angle) * Radius, FMath::Sin(Angle) * Radius, 0.f);
SpawnPickup(Taken, PlaceOnGround(Center + Offset));
++Scattered;
}
UE_LOG(LogTemp, Log, TEXT("AFpsPlayer : %d piles laissees au sol a la mort."), Scattered);
}
void AFpsPlayer::HandleDeath()
{
if (bIsDead)
{
return;
}
bIsDead = true;
// Tout tombe sur place. On le fait AVANT de couper les inputs : l'ordre
// n'a pas d'importance fonctionnelle, mais perdre le butin en silence
// serait le pire bug possible dans un jeu de survie.
DropAllItems();
ClearTransientInputStates();
GetCharacterMovement()->DisableMovement();
if (APlayerController* OwningController = Cast<APlayerController>(GetController()))
{
DisableInput(OwningController);
}
}
void AFpsPlayer::ClearTransientInputStates()
{
StopSprint();
StopJumping();
// L'accroupissement n'est volontairement pas annule : c'est un etat
// visible que le joueur maitrise, et il se retablit tout seul au
// prochain appui sur la touche.
}
void AFpsPlayer::StartCrouch()
{
StopSprint();
Crouch();
}
void AFpsPlayer::StopCrouch()
{
// UnCrouch() echoue silencieusement s'il y a un plafond : le perso
// reste accroupi jusqu'a ce que la place se libere.
UnCrouch();
}
void AFpsPlayer::Interact()
{
InteractionComponent->TryInteract();
}
void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
{
Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
CrouchTargetAlpha = 1.f;
CrouchTiltDirection = 1.f;
}
void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
{
Super::OnEndCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
CrouchTargetAlpha = 0.f;
CrouchTiltDirection = -1.f;
}
@@ -0,0 +1,394 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "FpsPlayerController.h"
#include "Blueprint/UserWidget.h"
#include "EnhancedInputComponent.h"
#include "FpsPlayer.h"
#include "GameFramework/GameModeBase.h"
#include "SurvivalStatsComponent.h"
#include "HotbarWidget.h"
#include "InputActionValue.h"
#include "InputCoreTypes.h"
#include "InventoryComponent.h"
#include "InteractionPromptWidget.h"
#include "InventoryWidget.h"
#include "ScreenFadeComponent.h"
#include "SurvivalStatsWidget.h"
namespace
{
// Ordre d'empilement des widgets plein ecran, du fond vers l'avant.
//
// La barre rapide passe AU-DESSUS de l'inventaire, et pas l'inverse : le
// Background Blur de l'inventaire floute tout ce qui est rendu en dessous
// de lui. Avec un ZOrder inferieur, la barre se retrouverait floutee et
// inutilisable des l'ouverture de l'inventaire.
//
// Bonus : etant devant, elle reste une cible de depot valide -- on peut
// glisser un objet de la grille directement dans la barre.
//
// Pas de 10 pour laisser de la place aux widgets a venir.
constexpr int32 ZOrderCrosshair = 0;
constexpr int32 ZOrderSurvivalStats = 5;
constexpr int32 ZOrderInteractionPrompt = 10;
constexpr int32 ZOrderInventory = 20;
constexpr int32 ZOrderHotbar = 30;
}
AFpsPlayerController::AFpsPlayerController()
{
// Le fondu est un systeme a part entiere, donc un composant -- et le menu
// principal reutilise exactement le meme.
//
// Rappel Unreal : ce constructeur tourne aussi dans l'editeur, sur le CDO.
// Il ne doit contenir que de la construction, jamais de logique de jeu.
ScreenFade = CreateDefaultSubobject<UScreenFadeComponent>(TEXT("ScreenFade"));
}
void AFpsPlayerController::BeginPlay()
{
Super::BeginPlay();
if (bFadeInOnStart)
{
ScreenFade->FadeIn(StartupFadeDuration);
}
}
void AFpsPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// On cree les widgets ici et pas dans BeginPlay : a ce stade le pawn est
// garanti possede, et le prompt comme l'inventaire ont besoin de lui pour
// trouver leurs composants des leur NativeConstruct.
CreateHudWidgets();
// Apres un respawn les widgets existent deja, mais pointent vers les
// composants du cadavre. On les rebranche sur le nouveau pawn.
if (Crosshair) { /* pas d'abonnement */ }
if (InteractionPrompt) { InteractionPrompt->BindToOwningPawn(); }
if (Hotbar) { Hotbar->BindToOwningPawn(); }
if (InventoryWidget) { InventoryWidget->BindToOwningPawn(); }
if (SurvivalStatsWidget) { SurvivalStatsWidget->BindToOwningPawn(); }
if (AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(InPawn))
{
if (USurvivalStatsComponent* Stats = PlayerPawn->GetSurvivalStats())
{
Stats->OnDied.AddDynamic(this, &AFpsPlayerController::HandlePawnDied);
}
}
}
void AFpsPlayerController::HandlePawnDied()
{
// L'inventaire ouvert au moment de la mort resterait affiche par-dessus
// l'ecran noir, curseur compris.
if (bInventoryOpen)
{
ToggleInventory();
}
ScreenFade->FadeOut(DeathFadeDuration);
GetWorldTimerManager().SetTimer(
RespawnTimerHandle, this, &AFpsPlayerController::RespawnPlayer,
FMath::Max(RespawnDelay, DeathFadeDuration), /*bLoop=*/false);
}
void AFpsPlayerController::RespawnPlayer()
{
// On depossede AVANT de detruire : detruire un pawn encore possede laisse
// le controller dans un etat incoherent le temps d'une frame.
APawn* PreviousPawn = GetPawn();
UnPossess();
if (IsValid(PreviousPawn))
{
PreviousPawn->Destroy();
}
// RestartPlayer fait apparaitre un nouveau pawn sur un PlayerStart et le
// possede. Le nouveau USurvivalStatsComponent repart plein tout seul dans
// son BeginPlay : rien a reinitialiser a la main.
if (AGameModeBase* GameMode = GetWorld()->GetAuthGameMode())
{
GameMode->RestartPlayer(this);
}
else
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayerController : aucun GameMode, impossible de reapparaitre."));
}
ScreenFade->FadeIn(StartupFadeDuration);
}
void AFpsPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(InputComponent);
if (!Input)
{
return;
}
if (ToggleInventoryAction)
{
Input->BindAction(ToggleInventoryAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleInventory);
}
if (DropItemAction)
{
Input->BindAction(DropItemAction, ETriggerEvent::Started, this, &AFpsPlayerController::DropHoveredItem);
}
// La barre rapide est bindee sur le controller et non sur le pawn : ainsi
// changer d'objet actif reste possible meme quand l'inventaire est ouvert
// et que les inputs du pawn sont coupes.
if (HotbarSelectAction)
{
Input->BindAction(HotbarSelectAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandleHotbarSelect);
}
if (HotbarScrollAction)
{
Input->BindAction(HotbarScrollAction, ETriggerEvent::Triggered, this, &AFpsPlayerController::HandleHotbarScroll);
}
}
UInventoryComponent* AFpsPlayerController::GetPawnInventory() const
{
AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
return PlayerPawn ? PlayerPawn->GetInventoryComponent() : nullptr;
}
void AFpsPlayerController::HandleHotbarSelect(const FInputActionValue& Value)
{
UInventoryComponent* Inventory = GetPawnInventory();
if (!Inventory)
{
return;
}
// La valeur vient du modificateur Scalar de la touche : 1 pour la touche 1,
// 2 pour la touche 2, etc. On repasse en index a partir de zero.
const int32 Number = FMath::RoundToInt(Value.Get<float>());
Inventory->SelectHotbarSlot(Number - 1);
}
void AFpsPlayerController::HandleHotbarScroll(const FInputActionValue& Value)
{
const float Axis = Value.Get<float>();
if (FMath::IsNearlyZero(Axis))
{
return;
}
if (UInventoryComponent* Inventory = GetPawnInventory())
{
// Molette vers le haut = slot precedent, comme dans la plupart des FPS.
Inventory->CycleHotbarSelection(Axis > 0.f ? -1 : 1);
}
}
void AFpsPlayerController::DropHoveredItem()
{
if (!bInventoryOpen || !InventoryWidget)
{
return;
}
const int32 SlotIndex = InventoryWidget->GetHoveredSlotIndex();
if (SlotIndex == INDEX_NONE)
{
return;
}
AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
if (!PlayerPawn)
{
return;
}
// Ctrl enfonce : un seul exemplaire, comme pour le glisser. On lit l'etat
// de la touche plutot que d'en faire une action separee : le geste est
// modal, pas une commande distincte.
const bool bSingle = IsInputKeyDown(EKeys::LeftControl) || IsInputKeyDown(EKeys::RightControl);
// MAX_int32 pour "tout" : DropSlot borne de lui-meme a ce que contient le slot.
PlayerPawn->DropSlot(SlotIndex, bSingle ? 1 : MAX_int32);
}
void AFpsPlayerController::CreateHudWidgets()
{
// Un widget n'existe que sur la machine du joueur local : inutile et
// incorrect d'en creer un sur un serveur dedie.
if (!IsLocalController())
{
return;
}
if (!Crosshair && CrosshairClass)
{
Crosshair = CreateWidget<UUserWidget>(this, CrosshairClass);
if (Crosshair)
{
Crosshair->AddToViewport(ZOrderCrosshair);
}
}
if (!InteractionPrompt)
{
if (InteractionPromptClass)
{
InteractionPrompt = CreateWidget<UInteractionPromptWidget>(this, InteractionPromptClass);
if (InteractionPrompt)
{
InteractionPrompt->AddToViewport(ZOrderInteractionPrompt);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InteractionPromptClass n'est pas assignee, aucun prompt ne s'affichera."));
}
}
// Le voile de fondu n'est plus cree ici : UScreenFadeComponent s'en charge
// dans son propre BeginPlay, et son ZOrder de 1000 le garde devant tout le
// HUD quel que soit l'ordre d'ajout.
if (!SurvivalStatsWidget && SurvivalStatsClass)
{
SurvivalStatsWidget = CreateWidget<USurvivalStatsWidget>(this, SurvivalStatsClass);
if (SurvivalStatsWidget)
{
SurvivalStatsWidget->AddToViewport(ZOrderSurvivalStats);
}
}
if (!Hotbar && HotbarClass)
{
Hotbar = CreateWidget<UHotbarWidget>(this, HotbarClass);
if (Hotbar)
{
Hotbar->AddToViewport(ZOrderHotbar);
}
}
if (!InventoryWidget)
{
if (InventoryClass)
{
InventoryWidget = CreateWidget<UInventoryWidget>(this, InventoryClass);
if (InventoryWidget)
{
InventoryWidget->AddToViewport(ZOrderInventory);
// Cree des le depart pour que son abonnement au delegue soit
// actif, mais cache : ainsi il reste a jour meme ferme, et
// l'ouverture est instantanee.
InventoryWidget->SetVisibility(ESlateVisibility::Collapsed);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InventoryClass n'est pas assignee, l'inventaire ne s'ouvrira pas."));
}
}
}
void AFpsPlayerController::ToggleInventory()
{
if (!InventoryWidget)
{
return;
}
bInventoryOpen = !bInventoryOpen;
InventoryWidget->SetVisibility(bInventoryOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
if (bInventoryOpen)
{
// GameAndUI et non UIOnly : en UIOnly, plus aucune action Enhanced
// Input ne passerait et on ne pourrait plus refermer avec Tab.
FInputModeGameAndUI Mode;
Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
Mode.SetHideCursorDuringCapture(false);
SetInputMode(Mode);
SetShowMouseCursor(true);
// Le mode GameAndUI laisse passer le jeu : sans ces deux appels, la
// souris ferait tourner la camera pendant qu'on navigue dans la grille.
SetIgnoreLookInput(true);
SetIgnoreMoveInput(true);
// Les deux appels ci-dessus ne couvrent que le deplacement et le regard.
// Accroupi, sprint, saut et interaction resteraient actifs -- appuyer
// sur Ctrl pour glisser un objet ferait s'accroupir le personnage.
// DisableInput retire l'input component du PAWN de la pile ; celui du
// controller reste, donc Tab continue de repondre.
if (PlayerPawn)
{
PlayerPawn->ClearTransientInputStates();
PlayerPawn->DisableInput(this);
}
}
else
{
SetInputMode(FInputModeGameOnly());
SetShowMouseCursor(false);
// Reset et non SetIgnoreXxx(false) : ces fonctions gerent un compteur,
// et un desequilibre finirait par bloquer definitivement les inputs.
ResetIgnoreLookInput();
ResetIgnoreMoveInput();
if (PlayerPawn)
{
PlayerPawn->EnableInput(this);
}
}
}
void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (InventoryWidget)
{
InventoryWidget->RemoveFromParent();
InventoryWidget = nullptr;
}
if (InteractionPrompt)
{
InteractionPrompt->RemoveFromParent();
InteractionPrompt = nullptr;
}
if (Hotbar)
{
Hotbar->RemoveFromParent();
Hotbar = nullptr;
}
if (SurvivalStatsWidget)
{
SurvivalStatsWidget->RemoveFromParent();
SurvivalStatsWidget = nullptr;
}
if (Crosshair)
{
Crosshair->RemoveFromParent();
Crosshair = nullptr;
}
// Le voile de fondu est nettoye par UScreenFadeComponent::EndPlay.
Super::EndPlay(EndPlayReason);
}
@@ -0,0 +1,173 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "HotbarWidget.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "FpsPlayer.h"
#include "GameFramework/PlayerController.h"
#include "InventoryComponent.h"
#include "InventorySlotWidget.h"
void UHotbarWidget::NativeConstruct()
{
Super::NativeConstruct();
BindToOwningPawn();
}
void UHotbarWidget::BindToOwningPawn()
{
UInventoryComponent* NewInventory = ResolveInventory();
if (BoundInventory.Get() == NewInventory)
{
return;
}
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.RemoveDynamic(this, &UHotbarWidget::HandleInventoryChanged);
BoundInventory->OnSelectedHotbarSlotChanged.RemoveDynamic(this, &UHotbarWidget::HandleSelectionChanged);
}
BoundInventory = NewInventory;
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.AddDynamic(this, &UHotbarWidget::HandleInventoryChanged);
BoundInventory->OnSelectedHotbarSlotChanged.AddDynamic(this, &UHotbarWidget::HandleSelectionChanged);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UHotbarWidget : aucun UInventoryComponent trouve sur le pawn possede."));
}
// Les cases memorisent l'ancien composant : on les recree.
for (UInventorySlotWidget* SlotWidget : SlotWidgets)
{
if (SlotWidget)
{
SlotWidget->RemoveFromParent();
}
}
SlotWidgets.Reset();
Refresh();
}
void UHotbarWidget::NativeDestruct()
{
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.RemoveDynamic(this, &UHotbarWidget::HandleInventoryChanged);
BoundInventory->OnSelectedHotbarSlotChanged.RemoveDynamic(this, &UHotbarWidget::HandleSelectionChanged);
}
BoundInventory.Reset();
Super::NativeDestruct();
}
UInventoryComponent* UHotbarWidget::ResolveInventory() const
{
const APlayerController* OwningController = GetOwningPlayer();
AFpsPlayer* Player = OwningController ? Cast<AFpsPlayer>(OwningController->GetPawn()) : nullptr;
return Player ? Player->GetInventoryComponent() : nullptr;
}
void UHotbarWidget::HandleInventoryChanged()
{
Refresh();
}
void UHotbarWidget::HandleSelectionChanged(int32 NewIndex)
{
// Seule la surbrillance bouge : inutile de repousser tout le contenu.
for (int32 Index = 0; Index < SlotWidgets.Num(); ++Index)
{
if (SlotWidgets[Index])
{
SlotWidgets[Index]->SetSelected(Index == NewIndex);
}
}
}
void UHotbarWidget::Refresh()
{
if (!SlotContainer)
{
UE_LOG(LogTemp, Warning, TEXT("UHotbarWidget::Refresh : SlotContainer est nul. Le widget nomme 'SlotContainer' manque dans le Blueprint."));
return;
}
if (!BoundInventory.IsValid())
{
return;
}
const TArray<FInventorySlot>& Slots = BoundInventory->GetSlots();
const int32 Count = BoundInventory->GetHotbarSlotCount();
RebuildSlots(Count);
const int32 Selected = BoundInventory->GetSelectedHotbarIndex();
for (int32 Index = 0; Index < SlotWidgets.Num(); ++Index)
{
if (!SlotWidgets[Index] || !Slots.IsValidIndex(Index))
{
continue;
}
SlotWidgets[Index]->SetSlot(Slots[Index]);
SlotWidgets[Index]->SetSelected(Index == Selected);
}
}
void UHotbarWidget::RebuildSlots(int32 DesiredCount)
{
if (SlotWidgets.Num() == DesiredCount)
{
return;
}
if (!SlotWidgetClass)
{
UE_LOG(LogTemp, Warning, TEXT("UHotbarWidget : SlotWidgetClass n'est pas assignee, la barre restera vide."));
return;
}
while (SlotWidgets.Num() > DesiredCount)
{
const int32 Last = SlotWidgets.Num() - 1;
if (SlotWidgets[Last])
{
SlotWidgets[Last]->RemoveFromParent();
}
SlotWidgets.RemoveAt(Last);
}
while (SlotWidgets.Num() < DesiredCount)
{
UInventorySlotWidget* NewSlot = CreateWidget<UInventorySlotWidget>(this, SlotWidgetClass);
if (!NewSlot)
{
UE_LOG(LogTemp, Error, TEXT("UHotbarWidget : impossible de creer une case a partir de %s. Recompile ce Widget Blueprint."),
*GetNameSafe(SlotWidgetClass));
break;
}
const int32 Index = SlotWidgets.Num();
// Meme indexation que la grille : la case 0 de la barre EST le slot 0
// de l'inventaire. C'est ce qui rend le glisser-deposer entre les deux
// gratuit -- ils manipulent le meme tableau.
NewSlot->InitSlot(BoundInventory.Get(), Index, nullptr);
if (UHorizontalBoxSlot* BoxSlot = SlotContainer->AddChildToHorizontalBox(NewSlot))
{
BoxSlot->SetPadding(FMargin(SlotPadding));
}
SlotWidgets.Add(NewSlot);
}
}
@@ -0,0 +1,173 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "InteractionComponent.h"
#include "DrawDebugHelpers.h"
#include "Engine/World.h"
#include "GameFramework/Controller.h"
#include "GameFramework/Pawn.h"
#include "Interactable.h"
UInteractionComponent::UInteractionComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// On espace les traces : c'est une requete physique, pas gratuite, et
// l'affichage du prompt n'a pas besoin d'etre rafraichi chaque frame.
TimeSinceLastTrace += DeltaTime;
if (TimeSinceLastTrace < TraceInterval)
{
return;
}
TimeSinceLastTrace = 0.f;
UpdateFocus();
}
bool UInteractionComponent::GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const
{
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
if (!OwnerPawn)
{
return false;
}
// On passe par le controller plutot que d'aller chercher la camera nous-memes :
// GetPlayerViewPoint renvoie exactement ce que le joueur voit a l'ecran, y
// compris les effets de camera. Le trace part donc pile du centre du viseur.
if (const AController* OwnerController = OwnerPawn->GetController())
{
OwnerController->GetPlayerViewPoint(OutLocation, OutRotation);
return true;
}
return false;
}
void UInteractionComponent::UpdateFocus()
{
FVector ViewLocation;
FRotator ViewRotation;
if (!GetViewPoint(ViewLocation, ViewRotation))
{
SetFocusedActor(nullptr);
return;
}
const FVector TraceEnd = ViewLocation + ViewRotation.Vector() * InteractionRange;
FCollisionQueryParams Params(SCENE_QUERY_STAT(InteractionTrace), /*bTraceComplex=*/false, GetOwner());
// Sphere balayee plutot que rayon pur : quelques cm de tolerance suffisent
// pour que viser un petit objet cesse d'etre un exercice de precision.
FHitResult Hit;
const bool bHit = GetWorld()->SweepSingleByChannel(
Hit,
ViewLocation,
TraceEnd,
FQuat::Identity,
TraceChannel,
FCollisionShape::MakeSphere(TraceRadius),
Params);
AActor* Candidate = bHit ? Hit.GetActor() : nullptr;
if (!IsValidInteractable(Candidate))
{
Candidate = nullptr;
}
SetFocusedActor(Candidate);
#if ENABLE_DRAW_DEBUG
if (bDrawDebug)
{
const FColor Color = Candidate ? FColor::Green : FColor::Red;
DrawDebugLine(GetWorld(), ViewLocation, TraceEnd, Color, false, TraceInterval, 0, 1.f);
if (bHit)
{
DrawDebugSphere(GetWorld(), Hit.ImpactPoint, TraceRadius, 12, Color, false, TraceInterval);
}
}
#endif
}
bool UInteractionComponent::IsValidInteractable(AActor* Actor) const
{
if (!IsValid(Actor) || !Actor->Implements<UInteractable>())
{
return false;
}
// L'objet a le dernier mot : une porte verrouillee peut refuser d'etre visee.
return IInteractable::Execute_CanInteract(Actor, GetOwner());
}
void UInteractionComponent::SetFocusedActor(AActor* NewFocus)
{
AActor* OldFocus = FocusedActor.Get();
const bool bWantsFocus = (NewFocus != nullptr);
// On compare les pointeurs ET l'etat booleen. Sans le booleen, le cas
// "la cible s'est detruite toute seule" passerait inapercu : FocusedActor
// renvoie deja nullptr, donc nullptr == nullptr, et on sortirait sans
// prevenir l'UI -- le prompt resterait affiche indefiniment.
if (OldFocus == NewFocus && bHasFocusedActor == bWantsFocus)
{
return;
}
// IsValid() et pas seulement != nullptr : l'ancienne cible a pu etre detruite
// entre deux traces, typiquement un objet qu'on vient de ramasser.
if (IsValid(OldFocus) && OldFocus->Implements<UInteractable>())
{
IInteractable::Execute_OnEndFocus(OldFocus);
}
FocusedActor = NewFocus;
bHasFocusedActor = bWantsFocus;
if (NewFocus)
{
IInteractable::Execute_OnBeginFocus(NewFocus);
}
OnFocusChanged.Broadcast(NewFocus, OldFocus);
}
void UInteractionComponent::TryInteract()
{
// On retrace avant d'agir : jusqu'a TraceInterval secondes ont pu s'ecouler
// depuis le dernier trace, et interagir avec un objet qu'on ne vise plus
// serait un bug tres visible.
UpdateFocus();
AActor* Target = FocusedActor.Get();
if (!Target)
{
return;
}
IInteractable::Execute_Interact(Target, GetOwner());
// L'interaction vient peut-etre de detruire la cible, ou de la rendre
// non-interactive (porte maintenant ouverte). On reevalue tout de suite
// plutot que d'attendre le prochain trace periodique.
UpdateFocus();
}
FText UInteractionComponent::GetFocusedPrompt() const
{
AActor* Target = FocusedActor.Get();
if (!IsValid(Target))
{
return FText::GetEmpty();
}
return IInteractable::Execute_GetInteractionPrompt(Target);
}
@@ -0,0 +1,133 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "InteractionPromptWidget.h"
#include "Components/TextBlock.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h"
#include "FpsPlayer.h"
#include "GameFramework/PlayerController.h"
#include "InteractionComponent.h"
#define LOCTEXT_NAMESPACE "InteractionPrompt"
UInteractionPromptWidget::UInteractionPromptWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PromptFormat = LOCTEXT("PromptFormat", "[{0}] {1}");
}
void UInteractionPromptWidget::NativeConstruct()
{
Super::NativeConstruct();
// NativeConstruct est l'equivalent de BeginPlay pour un widget : il est
// appele une fois que le widget est ajoute au viewport.
BindToOwningPawn();
}
void UInteractionPromptWidget::BindToOwningPawn()
{
const APlayerController* OwningController = GetOwningPlayer();
const AFpsPlayer* Player = OwningController ? Cast<AFpsPlayer>(OwningController->GetPawn()) : nullptr;
UInteractionComponent* NewComponent = Player ? Player->GetInteractionComponent() : nullptr;
if (BoundComponent.Get() == NewComponent)
{
return;
}
if (BoundComponent.IsValid())
{
BoundComponent->OnFocusChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandleFocusChanged);
}
BoundComponent = NewComponent;
if (BoundComponent.IsValid())
{
// Abonnement au delegue : le widget ne se reveille que quand la cible
// change vraiment. Pas de Tick, pas de polling.
BoundComponent->OnFocusChanged.AddDynamic(this, &UInteractionPromptWidget::HandleFocusChanged);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("InteractionPromptWidget : le pawn possede n'a pas de UInteractionComponent, le prompt restera cache."));
}
Refresh();
}
void UInteractionPromptWidget::NativeDestruct()
{
// Desabonnement systematique. Sans ca, le delegue garderait une reference
// vers un widget detruit -- typiquement au changement de niveau.
if (BoundComponent.IsValid())
{
BoundComponent->OnFocusChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandleFocusChanged);
}
BoundComponent.Reset();
Super::NativeDestruct();
}
void UInteractionPromptWidget::HandleFocusChanged(AActor* NewFocus, AActor* OldFocus)
{
Refresh();
}
void UInteractionPromptWidget::Refresh()
{
const FText Prompt = BoundComponent.IsValid() ? BoundComponent->GetFocusedPrompt() : FText::GetEmpty();
if (Prompt.IsEmpty())
{
// Collapsed et non Hidden : Hidden garde la place reservee dans la
// mise en page, Collapsed la libere completement.
SetVisibility(ESlateVisibility::Collapsed);
return;
}
if (PromptText)
{
PromptText->SetText(FText::Format(PromptFormat, GetInteractKeyDisplayName(), Prompt));
}
// HitTestInvisible : le prompt s'affiche mais ne capture pas la souris,
// sinon il volerait les clics destines au jeu.
SetVisibility(ESlateVisibility::HitTestInvisible);
}
FText UInteractionPromptWidget::GetInteractKeyDisplayName() const
{
const APlayerController* OwningController = GetOwningPlayer();
const AFpsPlayer* Player = OwningController ? Cast<AFpsPlayer>(OwningController->GetPawn()) : nullptr;
if (!Player)
{
return FText::GetEmpty();
}
const UInputAction* Action = Player->GetInteractAction();
if (!Action)
{
return FText::GetEmpty();
}
// On demande au sous-systeme Enhanced Input quelle touche est REELLEMENT
// mappee sur l'action. Si tu changes E en F dans IMC_Default, ou si le
// joueur remappe ses touches un jour, le texte suit tout seul.
if (const UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(OwningController->GetLocalPlayer()))
{
const TArray<FKey> Keys = Subsystem->QueryKeysMappedToAction(Action);
if (Keys.Num() > 0)
{
return Keys[0].GetDisplayName(/*bLongDisplayName=*/false);
}
}
return FText::GetEmpty();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,494 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
UInventoryComponent::UInventoryComponent()
{
PrimaryComponentTick.bCanEverTick = false;
// Necessaire pour que InitializeComponent() soit appele.
bWantsInitializeComponent = true;
}
void UInventoryComponent::InitializeComponent()
{
Super::InitializeComponent();
EnsureSlotCount();
SelectedHotbarIndex = FMath::Clamp(SelectedHotbarIndex, 0, FMath::Max(0, GetHotbarSlotCount() - 1));
}
void UInventoryComponent::SelectHotbarSlot(int32 Index)
{
const int32 Count = GetHotbarSlotCount();
if (Index < 0 || Index >= Count || Index == SelectedHotbarIndex)
{
return;
}
SelectedHotbarIndex = Index;
OnSelectedHotbarSlotChanged.Broadcast(SelectedHotbarIndex);
}
void UInventoryComponent::CycleHotbarSelection(int32 Delta)
{
const int32 Count = GetHotbarSlotCount();
if (Count <= 0 || Delta == 0)
{
return;
}
// Modulo positif : en C++, -1 % 6 vaut -1 et non 5. Sans le double
// modulo, reculer depuis le premier slot donnerait un index negatif.
const int32 NewIndex = ((SelectedHotbarIndex + Delta) % Count + Count) % Count;
SelectHotbarSlot(NewIndex);
}
FInventorySlot UInventoryComponent::GetSelectedSlot() const
{
return Slots.IsValidIndex(SelectedHotbarIndex) ? Slots[SelectedHotbarIndex] : FInventorySlot();
}
void UInventoryComponent::EnsureSlotCount()
{
const int32 Target = GetTotalSlotCount();
if (Slots.Num() != Target)
{
Slots.SetNum(Target);
}
}
int32 UInventoryComponent::AddItem(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
{
if (!Item || Quantity <= 0)
{
return Quantity;
}
EnsureSlotCount();
const int32 MaxStack = Item->GetEffectiveMaxStack();
const int32 MaxUses = FMath::Max(1, Item->MaxUses);
// -1 = "plein" : un objet neuf, ou un ajout qui ne precise pas d'etat.
const int32 StartingUses = (RemainingUses < 0) ? MaxUses : FMath::Clamp(RemainingUses, 0, MaxUses);
int32 Remaining = Quantity;
// Premiere passe : completer les piles deja entamees. On le fait avant
// d'ouvrir un nouveau slot, sinon on gaspillerait des emplacements en
// laissant des piles a moitie vides un peu partout.
for (FInventorySlot& Slot : Slots)
{
if (Remaining <= 0)
{
break;
}
if (Slot.Item == Item && Slot.Quantity < MaxStack)
{
const int32 Added = FMath::Min(Remaining, MaxStack - Slot.Quantity);
Slot.Quantity += Added;
Remaining -= Added;
}
}
// Deuxieme passe : ouvrir de nouveaux slots. La BARRE RAPIDE est servie en
// premier, pour qu'un objet ramasse soit immediatement utilisable sans
// passer par l'inventaire.
auto FillEmptyRange = [&](int32 First, int32 Last)
{
for (int32 Index = First; Index < Last && Remaining > 0; ++Index)
{
FInventorySlot& Slot = Slots[Index];
if (Slot.IsEmpty())
{
const int32 Added = FMath::Min(Remaining, MaxStack);
Slot.Item = Item;
Slot.Quantity = Added;
Slot.RemainingUses = StartingUses;
Remaining -= Added;
}
}
};
// Inverse ces deux lignes si tu veux un jour servir l'inventaire d'abord.
const int32 BackpackStart = GetBackpackStartIndex();
FillEmptyRange(0, BackpackStart);
// Barre pleine : on deborde dans l'inventaire.
FillEmptyRange(BackpackStart, Slots.Num());
if (Remaining < Quantity)
{
OnInventoryChanged.Broadcast();
}
return Remaining;
}
int32 UInventoryComponent::RemoveItem(UItemDataAsset* Item, int32 Quantity)
{
if (!Item || Quantity <= 0)
{
return 0;
}
int32 Remaining = Quantity;
// On vide les plus petites piles d'abord : ca libere des slots au plus
// vite au lieu de laisser trainer plusieurs piles a moitie pleines.
while (Remaining > 0)
{
FInventorySlot* Best = nullptr;
for (FInventorySlot& Slot : Slots)
{
if (Slot.Item == Item && Slot.Quantity > 0)
{
if (!Best || Slot.Quantity < Best->Quantity)
{
Best = &Slot;
}
}
}
if (!Best)
{
break;
}
const int32 Taken = FMath::Min(Remaining, Best->Quantity);
Best->Quantity -= Taken;
Remaining -= Taken;
if (Best->Quantity <= 0)
{
Best->Clear();
}
}
const int32 Removed = Quantity - Remaining;
if (Removed > 0)
{
OnInventoryChanged.Broadcast();
}
return Removed;
}
bool UInventoryComponent::ConsumeUse(int32 SlotIndex)
{
if (!Slots.IsValidIndex(SlotIndex))
{
return false;
}
FInventorySlot& Slot = Slots[SlotIndex];
if (Slot.IsEmpty())
{
return false;
}
const int32 MaxUses = FMath::Max(1, Slot.Item->MaxUses);
// Objet a usage unique : un exemplaire disparait, point.
if (MaxUses <= 1)
{
Slot.Quantity -= 1;
if (Slot.Quantity <= 0)
{
Slot.Clear();
}
OnInventoryChanged.Broadcast();
return true;
}
Slot.RemainingUses -= 1;
if (Slot.RemainingUses <= 0)
{
Slot.Quantity -= 1;
if (Slot.Quantity <= 0)
{
Slot.Clear();
}
else
{
// Cas theorique : un objet a charges ne s'empile pas. On recharge
// quand meme l'exemplaire suivant, au cas ou une pile serait
// construite autrement un jour.
Slot.RemainingUses = MaxUses;
}
}
OnInventoryChanged.Broadcast();
return true;
}
int32 UInventoryComponent::GetItemCount(const UItemDataAsset* Item) const
{
if (!Item)
{
return 0;
}
int32 Total = 0;
for (const FInventorySlot& Slot : Slots)
{
if (Slot.Item == Item)
{
Total += Slot.Quantity;
}
}
return Total;
}
int32 UInventoryComponent::GetRoomFor(const UItemDataAsset* Item) const
{
if (!Item)
{
return 0;
}
const int32 MaxStack = Item->GetEffectiveMaxStack();
int32 Room = 0;
for (const FInventorySlot& Slot : Slots)
{
if (Slot.IsEmpty())
{
Room += MaxStack;
}
else if (Slot.Item == Item)
{
Room += FMath::Max(0, MaxStack - Slot.Quantity);
}
}
return Room;
}
int32 UInventoryComponent::GetUsedSlotCount() const
{
int32 Used = 0;
for (const FInventorySlot& Slot : Slots)
{
if (!Slot.IsEmpty())
{
++Used;
}
}
return Used;
}
int32 UInventoryComponent::FindFirstEmptySlot() const
{
for (int32 Index = 0; Index < Slots.Num(); ++Index)
{
if (Slots[Index].IsEmpty())
{
return Index;
}
}
return INDEX_NONE;
}
bool UInventoryComponent::TakeFromSlot(int32 SlotIndex, int32 Quantity, FInventorySlot& OutTaken)
{
OutTaken.Clear();
if (Quantity <= 0 || !Slots.IsValidIndex(SlotIndex))
{
return false;
}
FInventorySlot& Slot = Slots[SlotIndex];
if (Slot.IsEmpty())
{
return false;
}
const int32 Taken = FMath::Min(Quantity, Slot.Quantity);
OutTaken.Item = Slot.Item;
OutTaken.Quantity = Taken;
// L'etat d'usure part avec l'objet : jeter une canette entamee puis la
// reprendre ne doit pas la remplir.
OutTaken.RemainingUses = Slot.RemainingUses;
Slot.Quantity -= Taken;
if (Slot.Quantity <= 0)
{
Slot.Clear();
}
OnInventoryChanged.Broadcast();
return true;
}
bool UInventoryComponent::MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity)
{
if (Quantity <= 0 || FromIndex == ToIndex || !Slots.IsValidIndex(FromIndex) || !Slots.IsValidIndex(ToIndex))
{
return false;
}
FInventorySlot& From = Slots[FromIndex];
if (From.IsEmpty())
{
return false;
}
// Deplacement total : on retombe sur le comportement complet, echange compris.
if (Quantity >= From.Quantity)
{
return MoveItem(FromIndex, ToIndex);
}
FInventorySlot& To = Slots[ToIndex];
const int32 MaxStack = From.Item->GetEffectiveMaxStack();
if (To.IsEmpty())
{
const int32 Transferred = FMath::Min(Quantity, MaxStack);
To.Item = From.Item;
To.Quantity = Transferred;
To.RemainingUses = From.RemainingUses;
From.Quantity -= Transferred;
OnInventoryChanged.Broadcast();
return true;
}
if (To.Item == From.Item)
{
const int32 Transferable = FMath::Min(Quantity, MaxStack - To.Quantity);
if (Transferable <= 0)
{
return false;
}
To.Quantity += Transferable;
From.Quantity -= Transferable;
OnInventoryChanged.Broadcast();
return true;
}
// Objet different et transfert partiel : aucune interpretation raisonnable.
// On refuse plutot que d'inventer un comportement que le joueur ne
// comprendrait pas.
return false;
}
bool UInventoryComponent::MoveItem(int32 FromIndex, int32 ToIndex)
{
if (FromIndex == ToIndex || !Slots.IsValidIndex(FromIndex) || !Slots.IsValidIndex(ToIndex))
{
return false;
}
FInventorySlot& From = Slots[FromIndex];
if (From.IsEmpty())
{
return false;
}
FInventorySlot& To = Slots[ToIndex];
// Cible vide : la pile entiere part, quelle que soit sa taille.
if (To.IsEmpty())
{
To = From;
From.Clear();
OnInventoryChanged.Broadcast();
return true;
}
// Meme objet : on fusionne dans la limite de la pile. Pour un objet a
// charges, GetEffectiveMaxStack vaut 1, donc on tombe toujours sur
// l'echange -- deux canettes entamees ne fusionnent jamais.
if (To.Item == From.Item)
{
const int32 MaxStack = From.Item->GetEffectiveMaxStack();
const int32 Transferable = FMath::Min(From.Quantity, MaxStack - To.Quantity);
// Cible deja pleine : on echange plutot que de ne rien faire, sinon le
// geste du joueur resterait sans effet et passerait pour un bug.
if (Transferable <= 0)
{
Swap(From, To);
OnInventoryChanged.Broadcast();
return true;
}
To.Quantity += Transferable;
From.Quantity -= Transferable;
if (From.Quantity <= 0)
{
From.Clear();
}
OnInventoryChanged.Broadcast();
return true;
}
// Objets differents : simple echange.
Swap(From, To);
OnInventoryChanged.Broadcast();
return true;
}
bool UInventoryComponent::SetBonusSlotCount(int32 NewBonusSlots)
{
const int32 NewTotal = FMath::Max(0, HotbarSlotCount + BackpackSlotCount + FMath::Max(0, NewBonusSlots));
// On refuse plutot que de detruire du butin en silence. C'est a l'appelant
// (le code qui deséquipe le sac) de decider quoi faire : bloquer le retrait,
// ou vider le surplus au sol avant de reessayer.
if (GetUsedSlotCount() > NewTotal)
{
return false;
}
BonusSlotCount = FMath::Max(0, NewBonusSlots);
// Compacter avant de tronquer : sans ca, un objet range dans un slot de
// fin serait supprime alors qu'il reste de la place au debut.
CompactSlots();
Slots.SetNum(NewTotal);
OnInventoryChanged.Broadcast();
return true;
}
void UInventoryComponent::CompactSlots()
{
const int32 Start = GetBackpackStartIndex();
const int32 Count = Slots.Num() - Start;
if (Count <= 0)
{
return;
}
// On ne compacte QUE la portion sac. Compacter tout le tableau ferait
// remonter des objets du sac dans la barre rapide sans que le joueur
// l'ait demande -- et la troncature ne retire de toute facon que la fin.
TArray<FInventorySlot> Backpack;
Backpack.Append(Slots.GetData() + Start, Count);
Backpack.StableSort([](const FInventorySlot& A, const FInventorySlot& B)
{
return !A.IsEmpty() && B.IsEmpty();
});
for (int32 Index = 0; Index < Count; ++Index)
{
Slots[Start + Index] = Backpack[Index];
}
}
@@ -0,0 +1,239 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "InventorySlotWidget.h"
#include "Components/Image.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
#include "Components/Widget.h"
#include "InventoryDragDropOperation.h"
#include "InventoryWidget.h"
#include "ItemDataAsset.h"
void UInventorySlotWidget::NativeConstruct()
{
Super::NativeConstruct();
// Force en C++ plutot que de dependre d'un reglage du Blueprint : une case
// non testable a la souris ne recevrait jamais NativeOnMouseButtonDown, et
// le glisser-deposer serait mysterieusement inerte.
// NativeConstruct s'execute APRES ConfigureAsDragVisual() pour un fantome
// (le widget n'est construit qu'une fois ajoute a la couche de drag),
// d'ou le test : sans lui, on remettrait le fantome en Visible et il
// intercepterait le depot destine a la case du dessous.
SetVisibility(bIsDragVisual ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Visible);
}
void UInventorySlotWidget::SetSelected(bool bSelected)
{
if (SelectionBorder)
{
SelectionBorder->SetVisibility(bSelected ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed);
}
}
void UInventorySlotWidget::ConfigureAsDragVisual()
{
bIsDragVisual = true;
// Le cadre de selection appartient a la barre, pas a l'objet.
if (SelectionBorder)
{
SelectionBorder->SetVisibility(ESlateVisibility::Collapsed);
}
// Le fond de case appartient a la grille, pas a l'objet. Le trainer sous
// le curseur donne l'impression de deplacer la case elle-meme.
if (SlotBackground)
{
SlotBackground->SetVisibility(ESlateVisibility::Collapsed);
}
SetVisibility(ESlateVisibility::HitTestInvisible);
}
void UInventorySlotWidget::InitSlot(UInventoryComponent* InInventory, int32 InSlotIndex, UInventoryWidget* InOwningGrid)
{
OwningInventory = InInventory;
SlotIndex = InSlotIndex;
OwningGrid = InOwningGrid;
}
void UInventorySlotWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
Super::NativeOnMouseEnter(InGeometry, InMouseEvent);
if (OwningGrid.IsValid())
{
OwningGrid->NotifySlotHovered(SlotIndex, true);
}
}
void UInventorySlotWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent)
{
Super::NativeOnMouseLeave(InMouseEvent);
if (OwningGrid.IsValid())
{
OwningGrid->NotifySlotHovered(SlotIndex, false);
}
}
void UInventorySlotWidget::SetSlot(const FInventorySlot& InSlot)
{
CachedSlot = InSlot;
// Filet de securite : toute mise a jour de contenu remet la case en pleine
// opacite, meme si un glisser s'est termine par un chemin imprevu.
if (!bIsDragVisual)
{
SetRenderOpacity(1.f);
}
const bool bEmpty = InSlot.IsEmpty();
if (ItemIcon)
{
// Hidden et non Collapsed : on veut que la case garde sa taille dans
// la grille meme vide, sinon la mise en page sauterait a chaque
// ramassage.
if (bEmpty || !InSlot.Item->Icon)
{
ItemIcon->SetVisibility(ESlateVisibility::Hidden);
}
else
{
ItemIcon->SetBrushFromTexture(InSlot.Item->Icon, /*bMatchSize=*/false);
ItemIcon->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
if (QuantityText)
{
// On n'affiche pas "x1" : ca alourdit la grille pour rien.
if (bEmpty || InSlot.Quantity <= 1)
{
QuantityText->SetVisibility(ESlateVisibility::Hidden);
}
else
{
QuantityText->SetText(FText::AsNumber(InSlot.Quantity));
QuantityText->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
if (UsesBar)
{
// Collapsed et non Hidden : une barre d'usure sur un objet qui n'en a
// pas ne doit meme pas reserver sa place au bas de la case.
const int32 MaxUses = bEmpty ? 1 : FMath::Max(1, InSlot.Item->MaxUses);
if (MaxUses <= 1)
{
UsesBar->SetVisibility(ESlateVisibility::Collapsed);
}
else
{
UsesBar->SetPercent(FMath::Clamp(static_cast<float>(InSlot.RemainingUses) / static_cast<float>(MaxUses), 0.f, 1.f));
UsesBar->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
}
FReply UInventorySlotWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
if (CachedSlot.IsEmpty() || !InMouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton))
{
return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);
}
// On ne demarre pas le glisser tout de suite : DetectDrag attend que la
// souris bouge reellement. Sans ca, un simple clic serait interprete comme
// un debut de glisser et on ne pourrait jamais faire autre chose.
return FReply::Handled().DetectDrag(TakeWidget(), EKeys::LeftMouseButton);
}
void UInventorySlotWidget::NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation)
{
Super::NativeOnDragDetected(InGeometry, InMouseEvent, OutOperation);
if (CachedSlot.IsEmpty() || !OwningInventory.IsValid() || SlotIndex == INDEX_NONE)
{
return;
}
// Modificateurs au moment de la saisie : Maj pour la moitie, Ctrl pour un
// seul. Conventions etablies du genre, le joueur les connait deja.
int32 DraggedQuantity = CachedSlot.Quantity;
if (InMouseEvent.IsControlDown())
{
DraggedQuantity = 1;
}
else if (InMouseEvent.IsShiftDown())
{
// Moitie arrondie au superieur : sur une pile de 7 on emporte 4.
// C'est la convention de Minecraft et consorts.
DraggedQuantity = (CachedSlot.Quantity + 1) / 2;
}
UInventoryDragDropOperation* Operation = NewObject<UInventoryDragDropOperation>();
Operation->SourceSlotIndex = SlotIndex;
Operation->Quantity = DraggedQuantity;
Operation->SourceInventory = OwningInventory;
Operation->Pivot = EDragPivot::CenterCenter;
// Le fantome sous le curseur est une copie de cette meme case : aucun
// widget supplementaire a maintenir, et l'apparence reste coherente.
// Il affiche la quantite EMPORTEE, pas la pile complete.
if (UInventorySlotWidget* Visual = CreateWidget<UInventorySlotWidget>(GetOwningPlayer(), GetClass()))
{
FInventorySlot VisualSlot = CachedSlot;
VisualSlot.Quantity = DraggedQuantity;
Visual->SetSlot(VisualSlot);
Visual->ConfigureAsDragVisual();
Visual->SetRenderOpacity(DragVisualOpacity);
Operation->DefaultDragVisual = Visual;
}
// On n'estompe la case source que si la pile part en entier. Lors d'une
// division une partie reste sur place, et la faire palir serait mentir
// sur ce qu'elle contient encore.
if (DraggedQuantity >= CachedSlot.Quantity)
{
SetRenderOpacity(DraggedSlotOpacity);
}
// Les deux issues possibles remettent l'opacite : depot reussi, ou
// abandon hors de la grille.
Operation->OnDrop.AddDynamic(this, &UInventorySlotWidget::HandleDragFinished);
Operation->OnDragCancelled.AddDynamic(this, &UInventorySlotWidget::HandleDragFinished);
OutOperation = Operation;
}
void UInventorySlotWidget::HandleDragFinished(UDragDropOperation* Operation)
{
SetRenderOpacity(1.f);
}
bool UInventorySlotWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation)
{
const UInventoryDragDropOperation* Payload = Cast<UInventoryDragDropOperation>(InOperation);
if (!Payload || SlotIndex == INDEX_NONE)
{
return Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
}
// On refuse les transferts entre deux inventaires differents : ce sera le
// jour ou tu ajouteras les coffres, avec ses propres regles.
if (!Payload->SourceInventory.IsValid() || Payload->SourceInventory != OwningInventory)
{
return false;
}
// MoveItemQuantity diffuse OnInventoryChanged, donc la grille se rafraichit
// seule. Une quantite egale a la pile entiere retombe sur le comportement
// complet, echange compris.
return OwningInventory->MoveItemQuantity(Payload->SourceSlotIndex, SlotIndex, Payload->Quantity);
}
@@ -0,0 +1,212 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "InventoryWidget.h"
#include "Components/UniformGridPanel.h"
#include "Components/UniformGridSlot.h"
#include "FpsPlayer.h"
#include "GameFramework/PlayerController.h"
#include "InventoryComponent.h"
#include "InventoryDragDropOperation.h"
#include "InventorySlotWidget.h"
void UInventoryWidget::NativeConstruct()
{
Super::NativeConstruct();
BindToOwningPawn();
}
void UInventoryWidget::BindToOwningPawn()
{
UInventoryComponent* NewInventory = ResolveInventory();
if (BoundInventory.Get() == NewInventory)
{
return;
}
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.RemoveDynamic(this, &UInventoryWidget::HandleInventoryChanged);
}
BoundInventory = NewInventory;
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.AddDynamic(this, &UInventoryWidget::HandleInventoryChanged);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UInventoryWidget : aucun UInventoryComponent trouve sur le pawn possede."));
}
// Les cases gardent l'ancien composant en memoire : on force leur
// recreation en vidant la grille.
for (UInventorySlotWidget* SlotWidget : SlotWidgets)
{
if (SlotWidget)
{
SlotWidget->RemoveFromParent();
}
}
SlotWidgets.Reset();
Refresh();
}
void UInventoryWidget::NativeDestruct()
{
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.RemoveDynamic(this, &UInventoryWidget::HandleInventoryChanged);
}
BoundInventory.Reset();
Super::NativeDestruct();
}
UInventoryComponent* UInventoryWidget::ResolveInventory() const
{
const APlayerController* OwningController = GetOwningPlayer();
AFpsPlayer* Player = OwningController ? Cast<AFpsPlayer>(OwningController->GetPawn()) : nullptr;
return Player ? Player->GetInventoryComponent() : nullptr;
}
void UInventoryWidget::HandleInventoryChanged()
{
Refresh();
}
void UInventoryWidget::NotifySlotHovered(int32 Index, bool bHovered)
{
if (bHovered)
{
HoveredSlotIndex = Index;
}
else if (HoveredSlotIndex == Index)
{
// Le test d'egalite evite d'effacer le survol quand les evenements
// arrivent dans l'ordre "entre dans B" puis "sort de A".
HoveredSlotIndex = INDEX_NONE;
}
}
bool UInventoryWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation)
{
Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
// On n'arrive ici que si AUCUNE case n'a traite le depot : les widgets
// enfants sont consultes en premier et arretent la propagation.
const UInventoryDragDropOperation* Payload = Cast<UInventoryDragDropOperation>(InOperation);
if (!Payload || Payload->SourceSlotIndex == INDEX_NONE)
{
return true;
}
// Lache DANS la grille mais entre deux cases (l'espacement) : on absorbe.
// Jeter un objet au sol parce que le joueur a rate une case de trois
// pixels serait impardonnable dans un jeu de survie.
if (SlotGrid && SlotGrid->GetCachedGeometry().IsUnderLocation(InDragDropEvent.GetScreenSpacePosition()))
{
return true;
}
// Lache hors de la grille : on jette au sol, comme dans Valheim ou Raft.
if (const APlayerController* OwningController = GetOwningPlayer())
{
if (AFpsPlayer* Player = Cast<AFpsPlayer>(OwningController->GetPawn()))
{
Player->DropSlot(Payload->SourceSlotIndex, Payload->Quantity);
}
}
return true;
}
void UInventoryWidget::Refresh()
{
if (!SlotGrid)
{
UE_LOG(LogTemp, Warning, TEXT("UInventoryWidget::Refresh : SlotGrid est nul. Le widget nomme 'SlotGrid' manque dans le Blueprint."));
return;
}
if (!BoundInventory.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("UInventoryWidget::Refresh : aucun inventaire lie."));
return;
}
const TArray<FInventorySlot>& Slots = BoundInventory->GetSlots();
// La grille commence APRES la barre rapide : les deux conteneurs sont
// independants, un objet dans la barre n'apparait pas ici.
const int32 Offset = BoundInventory->GetBackpackStartIndex();
const int32 GridCount = FMath::Max(0, Slots.Num() - Offset);
// Le nombre de slots peut changer en cours de partie (sac equipe).
// On ne reconstruit que si la taille a bouge : recreer toutes les cases
// a chaque ramassage serait du gaspillage pur.
RebuildGrid(GridCount);
for (int32 Index = 0; Index < SlotWidgets.Num(); ++Index)
{
const int32 Absolute = Offset + Index;
if (SlotWidgets[Index] && Slots.IsValidIndex(Absolute))
{
SlotWidgets[Index]->SetSlot(Slots[Absolute]);
}
}
}
void UInventoryWidget::RebuildGrid(int32 DesiredCount)
{
if (SlotWidgets.Num() == DesiredCount)
{
return;
}
if (!SlotWidgetClass)
{
UE_LOG(LogTemp, Warning, TEXT("UInventoryWidget : SlotWidgetClass n'est pas assignee, la grille restera vide."));
return;
}
// Trop de cases : on retire le surplus par la fin.
while (SlotWidgets.Num() > DesiredCount)
{
const int32 Last = SlotWidgets.Num() - 1;
if (SlotWidgets[Last])
{
SlotWidgets[Last]->RemoveFromParent();
}
SlotWidgets.RemoveAt(Last);
}
// Pas assez : on en cree, en les placant en ligne / colonne.
const int32 Columns = FMath::Max(1, ColumnCount);
while (SlotWidgets.Num() < DesiredCount)
{
UInventorySlotWidget* NewSlot = CreateWidget<UInventorySlotWidget>(this, SlotWidgetClass);
if (!NewSlot)
{
// Arrive typiquement quand le Widget Blueprint de case ne compile
// plus, apres un changement de BindWidget cote C++ par exemple.
UE_LOG(LogTemp, Error, TEXT("UInventoryWidget : impossible de creer une case a partir de %s. Recompile ce Widget Blueprint."),
*GetNameSafe(SlotWidgetClass));
break;
}
const int32 Index = SlotWidgets.Num();
// On donne l'index ABSOLU dans le tableau, pas la position dans la
// grille. C'est ce qui rend le glisser entre la barre et la grille
// gratuit : les deux manipulent les memes index.
const int32 Absolute = BoundInventory->GetBackpackStartIndex() + Index;
NewSlot->InitSlot(BoundInventory.Get(), Absolute, this);
SlotGrid->AddChildToUniformGrid(NewSlot, Index / Columns, Index % Columns);
SlotWidgets.Add(NewSlot);
}
}
@@ -0,0 +1,23 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "MainMenuGameMode.h"
#include "MainMenuPlayerController.h"
AMainMenuGameMode::AMainMenuGameMode()
{
PlayerControllerClass = AMainMenuPlayerController::StaticClass();
// Aucun corps a piloter dans le menu : le controller pose sa vue sur une
// camera de la map.
DefaultPawnClass = nullptr;
// Va de pair avec DefaultPawnClass nul. Sans ce booleen, le GameMode tente
// quand meme de faire apparaitre un pawn a chaque connexion et remplit le
// log d'avertissements "Couldn't spawn Pawn of type NULL".
bStartPlayersAsSpectators = true;
// Pas de AHUD : tout l'affichage passe par UMG.
HUDClass = nullptr;
}
@@ -0,0 +1,256 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "MainMenuPlayerController.h"
#include "ConfirmDialogWidget.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
#include "MainMenuWidget.h"
#include "MenuCameraSpot.h"
#include "ScreenFadeComponent.h"
#include "SurvivalGameInstance.h"
#define LOCTEXT_NAMESPACE "MainMenu"
namespace
{
// Le menu reste sous le voile de fondu, qui est en 1000.
constexpr int32 ZOrderMainMenu = 0;
// La boite de confirmation passe devant le menu, mais sous le fondu.
constexpr int32 ZOrderQuitDialog = 100;
}
AMainMenuPlayerController::AMainMenuPlayerController()
{
// Meme composant que dans le jeu : un seul fondu a maintenir.
ScreenFade = CreateDefaultSubobject<UScreenFadeComponent>(TEXT("ScreenFade"));
bShowMouseCursor = true;
QuitDialogMessage = LOCTEXT("QuitConfirm", "Quitter le jeu ?");
}
void AMainMenuPlayerController::BeginPlay()
{
Super::BeginPlay();
// L'ecran part du noir : sans ca on verrait une frame de la map, cadree
// n'importe comment, avant que le fondu ne commence.
ScreenFade->SnapToBlack();
if (AMenuCameraSpot* Spot = PickCameraSpot())
{
// Sans blend : on arrive directement sur l'angle tire au sort.
SetViewTarget(Spot);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : aucune AMenuCameraSpot active dans la map, le menu s'affichera sur la vue par defaut."));
}
if (MainMenuClass)
{
MainMenu = CreateWidget<UMainMenuWidget>(this, MainMenuClass);
if (MainMenu)
{
MainMenu->AddToViewport(ZOrderMainMenu);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : MainMenuClass n'est pas assignee, aucun menu ne s'affichera."));
}
// GameAndUI et non UIOnly : UIOnly avale aussi la console et les raccourcis
// du moteur, ce qui rend le debug penible. Il n'y a de toute facon aucun
// input de jeu dans cette map, rien ne peut passer a travers.
FInputModeGameAndUI Mode;
Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
Mode.SetHideCursorDuringCapture(false);
SetInputMode(Mode);
SetShowMouseCursor(true);
ScreenFade->OnFadeFinished.AddDynamic(this, &AMainMenuPlayerController::HandleFadeFinished);
ScreenFade->FadeIn(OpeningFadeDuration);
}
AMenuCameraSpot* AMainMenuPlayerController::PickCameraSpot()
{
UWorld* World = GetWorld();
if (!World)
{
return nullptr;
}
USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>();
const FName LastUsed = SurvivalGameInstance ? SurvivalGameInstance->GetLastMenuCameraName() : NAME_None;
// TActorIterator plutot que GetAllActorsOfClass : pas de tableau d'AActor*
// intermediaire, pas de cast par element.
TArray<AMenuCameraSpot*> AllSpots;
TArray<AMenuCameraSpot*> Candidates;
for (TActorIterator<AMenuCameraSpot> It(World); It; ++It)
{
AMenuCameraSpot* Spot = *It;
if (!Spot->bEnabled)
{
continue;
}
AllSpots.Add(Spot);
// On ecarte l'angle du dernier passage pour ne pas revoir deux fois de
// suite le meme decor en revenant au menu.
if (Spot->GetFName() != LastUsed)
{
Candidates.Add(Spot);
}
}
// Une seule camera dans la map : on la reprend plutot que de n'afficher
// aucune vue.
if (Candidates.Num() == 0)
{
Candidates = MoveTemp(AllSpots);
}
if (Candidates.Num() == 0)
{
return nullptr;
}
AMenuCameraSpot* Chosen = Candidates[FMath::RandRange(0, Candidates.Num() - 1)];
if (SurvivalGameInstance)
{
SurvivalGameInstance->SetLastMenuCameraName(Chosen->GetFName());
}
return Chosen;
}
void AMainMenuPlayerController::StartSoloGame()
{
// Le fondu est deja lance : on ignore le second clic plutot que d'empiler
// deux chargements.
if (PendingAction != EMenuPendingAction::None)
{
return;
}
if (GameLevel.IsNull())
{
UE_LOG(LogTemp, Error, TEXT("AMainMenuPlayerController : GameLevel n'est pas assignee, impossible de lancer la partie."));
return;
}
PendingAction = EMenuPendingAction::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)
{
MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
}
ScreenFade->FadeOut(LeaveFadeDuration);
}
void AMainMenuPlayerController::QuitGame()
{
// Une partie est deja en cours de chargement : la question n'a plus de sens.
if (PendingAction != EMenuPendingAction::None)
{
return;
}
// Sans boite assignee on ne bloque pas le joueur dans son menu : on ferme.
if (!QuitDialogClass)
{
UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : QuitDialogClass n'est pas assignee, fermeture sans confirmation."));
CloseGame();
return;
}
// Creee au premier clic seulement, puis reutilisee : une boite qu'on ouvre
// et ferme plusieurs fois n'a aucune raison d'etre reconstruite.
if (!QuitDialog)
{
QuitDialog = CreateWidget<UConfirmDialogWidget>(this, QuitDialogClass);
if (!QuitDialog)
{
CloseGame();
return;
}
QuitDialog->OnConfirmed.AddDynamic(this, &AMainMenuPlayerController::HandleQuitConfirmed);
QuitDialog->OnCancelled.AddDynamic(this, &AMainMenuPlayerController::HandleQuitCancelled);
QuitDialog->AddToViewport(ZOrderQuitDialog);
}
QuitDialog->Open(QuitDialogMessage);
// Le menu reste lisible derriere mais cesse de repondre : sans ca, on
// pourrait relancer une partie alors que la question est encore posee.
if (MainMenu)
{
MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
void AMainMenuPlayerController::HandleQuitConfirmed()
{
// Pas de fondu : le joueur vient de confirmer, le faire patienter une
// seconde de plus ne serait qu'agacant.
CloseGame();
}
void AMainMenuPlayerController::HandleQuitCancelled()
{
if (QuitDialog)
{
QuitDialog->Close();
}
if (MainMenu)
{
MainMenu->SetVisibility(ESlateVisibility::Visible);
}
}
void AMainMenuPlayerController::CloseGame()
{
UKismetSystemLibrary::QuitGame(this, this, EQuitPreference::Quit, /*bIgnorePlatformRestrictions=*/false);
}
void AMainMenuPlayerController::HandleFadeFinished()
{
// 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.
if (PendingAction == EMenuPendingAction::StartGame)
{
UGameplayStatics::OpenLevelBySoftObjectPtr(this, GameLevel);
}
}
void AMainMenuPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (QuitDialog)
{
QuitDialog->RemoveFromParent();
QuitDialog = nullptr;
}
if (MainMenu)
{
MainMenu->RemoveFromParent();
MainMenu = nullptr;
}
Super::EndPlay(EndPlayReason);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,50 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "MainMenuWidget.h"
#include "Components/Button.h"
#include "MainMenuPlayerController.h"
void UMainMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
if (PlayButton)
{
PlayButton->OnClicked.AddDynamic(this, &UMainMenuWidget::HandlePlayClicked);
}
if (QuitButton)
{
QuitButton->OnClicked.AddDynamic(this, &UMainMenuWidget::HandleQuitClicked);
}
// Les boutons de coop sont visibles mais inertes : ils annoncent la couleur
// sans promettre une partie qui echouerait.
if (HostButton)
{
HostButton->SetIsEnabled(false);
}
if (JoinButton)
{
JoinButton->SetIsEnabled(false);
}
}
void UMainMenuWidget::HandlePlayClicked()
{
if (AMainMenuPlayerController* Controller = GetOwningPlayer<AMainMenuPlayerController>())
{
Controller->StartSoloGame();
}
}
void UMainMenuWidget::HandleQuitClicked()
{
if (AMainMenuPlayerController* Controller = GetOwningPlayer<AMainMenuPlayerController>())
{
Controller->QuitGame();
}
}
@@ -0,0 +1,27 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "MenuCameraSpot.h"
#include "Camera/CameraComponent.h"
AMenuCameraSpot::AMenuCameraSpot()
{
// La camera est fixe et c'est le controller qui lui donne la vue : rien a
// calculer par frame.
PrimaryActorTick.bCanEverTick = false;
// A NE PAS TOUCHER dans le detail panel : "Auto Activate for Player" doit
// rester sur Disabled, sa valeur par defaut. C'est AMainMenuPlayerController
// qui decide quelle camera prend la vue, apres tirage au sort ; une camera
// auto-activee courtcircuiterait le tirage et gagnerait toujours.
// Le champ est prive dans ACameraActor, on ne peut que le laisser tel quel.
if (UCameraComponent* Camera = GetCameraComponent())
{
// ACameraActor contraint le ratio d'image a 16/9 par defaut. Sans ca, le
// menu s'affiche avec des bandes noires des que la fenetre a un autre
// format -- un ultra-large, ou simplement l'apercu PIE redimensionne.
Camera->bConstrainAspectRatio = false;
}
}
@@ -0,0 +1,135 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "PickupItem.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#define LOCTEXT_NAMESPACE "PickupItem"
APickupItem::APickupItem()
{
PrimaryActorTick.bCanEverTick = false;
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
SetRootComponent(Mesh);
// Purement decoratif : c'est la sphere 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);
}
void APickupItem::InitPickup(UItemDataAsset* InItemData, int32 InQuantity, int32 InRemainingUses)
{
ItemData = InItemData;
Quantity = FMath::Max(1, InQuantity);
RemainingUses = InRemainingUses;
}
void APickupItem::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
// Confort d'edition : le mesh suit l'objet choisi, tu vois immediatement
// dans le viewport ce que tu es en train de poser. Vaut aussi a l'execution
// pour les objets jetes, puisque OnConstruction s'execute a FinishSpawning.
if (bUseMeshFromItemData && ItemData && ItemData->WorldMesh)
{
Mesh->SetStaticMesh(ItemData->WorldMesh);
}
}
FText APickupItem::GetInteractionPrompt_Implementation() const
{
if (!ItemData)
{
return LOCTEXT("PickupUnconfigured", "Objet non configure");
}
if (Quantity > 1)
{
return FText::Format(LOCTEXT("PickupPromptStack", "Ramasser {0} x{1}"),
ItemData->DisplayName, FText::AsNumber(Quantity));
}
return FText::Format(LOCTEXT("PickupPrompt", "Ramasser {0}"), ItemData->DisplayName);
}
bool APickupItem::CanInteract_Implementation(AActor* Interactor) const
{
// Un pickup sans donnee est une erreur de configuration : on le rend
// inerte plutot que de laisser le joueur ramasser du vide.
return ItemData != nullptr;
}
void APickupItem::Interact_Implementation(AActor* Interactor)
{
UInventoryComponent* Inventory = Interactor ? Interactor->FindComponentByClass<UInventoryComponent>() : nullptr;
if (!Inventory)
{
UE_LOG(LogTemp, Warning, TEXT("APickupItem : %s n'a pas d'UInventoryComponent."), *GetNameSafe(Interactor));
return;
}
const int32 Leftover = Inventory->AddItem(ItemData, Quantity, RemainingUses);
const int32 Added = Quantity - Leftover;
// Inventaire plein : on ne detruit surtout pas le tas, on retire seulement
// ce qui est rentre. Le joueur peut revenir chercher le reste.
if (Added <= 0)
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 2.f, FColor::Red, TEXT("Inventaire plein"));
}
return;
}
// Retour temporaire, le temps que l'UI d'inventaire existe.
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 2.f, FColor::Green,
FString::Printf(TEXT("+%d %s (total : %d, slots : %d/%d)"),
Added,
*ItemData->DisplayName.ToString(),
Inventory->GetItemCount(ItemData),
Inventory->GetUsedSlotCount(),
Inventory->GetTotalSlotCount()));
}
if (Leftover > 0)
{
Quantity = Leftover;
return;
}
Destroy();
}
void APickupItem::OnBeginFocus_Implementation()
{
if (bHighlightOnFocus)
{
Mesh->SetRenderCustomDepth(true);
}
}
void APickupItem::OnEndFocus_Implementation()
{
if (bHighlightOnFocus)
{
Mesh->SetRenderCustomDepth(false);
}
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,192 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "ScreenFadeComponent.h"
#include "Blueprint/UserWidget.h"
#include "Camera/PlayerCameraManager.h"
#include "GameFramework/PlayerController.h"
namespace
{
// Le voile doit passer devant absolument tout, y compris les widgets qu'on
// ajouterait plus tard sans y penser.
constexpr int32 ZOrderFade = 1000;
}
UScreenFadeComponent::UScreenFadeComponent()
{
// Le composant ne tourne QUE pendant un fondu : on allume le tick dans
// FadeIn/FadeOut et on l'eteint des la cible atteinte. Ticker en permanence
// pour une valeur qui ne bouge qu'une seconde par mort serait du gaspillage.
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = false;
}
void UScreenFadeComponent::BeginPlay()
{
Super::BeginPlay();
// Cree des maintenant pour que le voile puisse couvrir des la premiere
// frame, avant meme que le reste du HUD existe.
EnsureWidget();
}
APlayerController* UScreenFadeComponent::GetOwningController() const
{
return Cast<APlayerController>(GetOwner());
}
void UScreenFadeComponent::EnsureWidget()
{
if (FadeWidget)
{
return;
}
APlayerController* Controller = GetOwningController();
if (!Controller)
{
UE_LOG(LogTemp, Error, TEXT("UScreenFadeComponent : doit etre pose sur un PlayerController, pas sur %s."),
*GetNameSafe(GetOwner()));
return;
}
// Un widget n'existe que sur la machine du joueur local : inutile et
// incorrect d'en creer un sur un serveur dedie.
if (!Controller->IsLocalController())
{
return;
}
if (!FadeWidgetClass)
{
UE_LOG(LogTemp, Warning, TEXT("UScreenFadeComponent : FadeWidgetClass n'est pas assignee, aucun fondu ne s'affichera."));
return;
}
FadeWidget = CreateWidget<UUserWidget>(Controller, FadeWidgetClass);
if (FadeWidget)
{
FadeWidget->AddToViewport(ZOrderFade);
SetCoverage(1.f - Alpha);
}
}
void UScreenFadeComponent::SetCoverage(float Coverage)
{
if (!FadeWidget)
{
return;
}
const float Clamped = FMath::Clamp(Coverage, 0.f, 1.f);
FadeWidget->SetRenderOpacity(Clamped);
// HitTestInvisible et jamais Visible : un voile qui capture la souris
// bloquerait les boutons du menu meme completement transparent.
FadeWidget->SetVisibility(FMath::IsNearlyZero(Clamped)
? ESlateVisibility::Collapsed
: ESlateVisibility::HitTestInvisible);
}
void UScreenFadeComponent::FadeIn(float Duration)
{
EnsureWidget();
if (APlayerController* Controller = GetOwningController())
{
if (Controller->PlayerCameraManager)
{
Controller->PlayerCameraManager->StartCameraFade(
/*FromAlpha=*/1.f, /*ToAlpha=*/0.f,
Duration, FadeColor,
/*bShouldFadeAudio=*/true);
}
}
CurrentDuration = FMath::Max(Duration, 0.01f);
Alpha = 0.f;
Target = 1.f;
bFading = true;
SetCoverage(1.f);
SetComponentTickEnabled(true);
}
void UScreenFadeComponent::FadeOut(float Duration)
{
EnsureWidget();
if (APlayerController* Controller = GetOwningController())
{
if (Controller->PlayerCameraManager)
{
// bHoldWhenFinished : l'ecran reste noir au lieu de revenir tout seul.
Controller->PlayerCameraManager->StartCameraFade(
/*FromAlpha=*/0.f, /*ToAlpha=*/1.f,
Duration, FadeColor,
/*bShouldFadeAudio=*/true, /*bHoldWhenFinished=*/true);
}
}
CurrentDuration = FMath::Max(Duration, 0.01f);
Target = 0.f;
bFading = true;
SetComponentTickEnabled(true);
}
void UScreenFadeComponent::SnapToBlack()
{
EnsureWidget();
if (APlayerController* Controller = GetOwningController())
{
if (Controller->PlayerCameraManager)
{
Controller->PlayerCameraManager->SetManualCameraFade(1.f, FadeColor, /*bFadeAudio=*/true);
}
}
Alpha = 0.f;
Target = 0.f;
bFading = false;
SetCoverage(1.f);
SetComponentTickEnabled(false);
}
void UScreenFadeComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!bFading)
{
return;
}
const float Step = DeltaTime / CurrentDuration;
Alpha = FMath::Clamp(Alpha + Step * FMath::Sign(Target - Alpha), 0.f, 1.f);
// Alpha va de 0 (ecran couvert) a 1 (jeu visible) : le voile suit l'inverse.
SetCoverage(1.f - Alpha);
if (FMath::IsNearlyEqual(Alpha, Target))
{
bFading = false;
// On eteint le tick AVANT de prevenir : un abonne qui relance un fondu
// dans son handler doit pouvoir le rallumer.
SetComponentTickEnabled(false);
OnFadeFinished.Broadcast();
}
}
void UScreenFadeComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (FadeWidget)
{
FadeWidget->RemoveFromParent();
FadeWidget = nullptr;
}
Super::EndPlay(EndPlayReason);
}
@@ -0,0 +1,127 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "SurvivalStatsComponent.h"
#include "Engine/World.h"
#include "TimerManager.h"
USurvivalStatsComponent::USurvivalStatsComponent()
{
// Pas de Tick : tout passe par un timer a basse frequence.
PrimaryComponentTick.bCanEverTick = false;
}
void USurvivalStatsComponent::BeginPlay()
{
Super::BeginPlay();
Health = MaxHealth;
Hunger = MaxHunger;
Thirst = MaxThirst;
bDead = false;
if (UWorld* World = GetWorld())
{
World->GetTimerManager().SetTimer(
UpdateTimerHandle, this, &USurvivalStatsComponent::UpdateStats,
FMath::Max(UpdateInterval, 0.05f), /*bLoop=*/true);
}
OnStatsChanged.Broadcast();
}
void USurvivalStatsComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// Un timer laisse en vie apres la destruction du composant rappellerait
// une methode sur un objet mort.
if (UWorld* World = GetWorld())
{
World->GetTimerManager().ClearTimer(UpdateTimerHandle);
}
Super::EndPlay(EndPlayReason);
}
void USurvivalStatsComponent::UpdateStats()
{
if (bDead)
{
return;
}
// Les vitesses sont par minute, l'intervalle en secondes.
const float Minutes = FMath::Max(UpdateInterval, 0.05f) / 60.f;
Hunger = FMath::Max(0.f, Hunger - HungerDecayPerMinute * Minutes);
Thirst = FMath::Max(0.f, Thirst - ThirstDecayPerMinute * Minutes);
// Faim ET soif a zero cumulent leurs degats : mourir de soif pendant
// qu'on meurt de faim doit aller plus vite.
float Damage = 0.f;
if (Hunger <= 0.f)
{
Damage += StarvationDamagePerMinute * Minutes;
}
if (Thirst <= 0.f)
{
Damage += DehydrationDamagePerMinute * Minutes;
}
if (Damage > 0.f)
{
Health = FMath::Max(0.f, Health - Damage);
}
OnStatsChanged.Broadcast();
if (Health <= 0.f && !bDead)
{
bDead = true;
if (UWorld* World = GetWorld())
{
World->GetTimerManager().ClearTimer(UpdateTimerHandle);
}
UE_LOG(LogTemp, Log, TEXT("USurvivalStatsComponent : %s est mort."), *GetNameSafe(GetOwner()));
OnDied.Broadcast();
}
}
void USurvivalStatsComponent::ApplyRestore(float HealthAmount, float HungerAmount, float ThirstAmount)
{
if (bDead)
{
return;
}
Health = FMath::Clamp(Health + HealthAmount, 0.f, MaxHealth);
Hunger = FMath::Clamp(Hunger + HungerAmount, 0.f, MaxHunger);
Thirst = FMath::Clamp(Thirst + ThirstAmount, 0.f, MaxThirst);
OnStatsChanged.Broadcast();
}
void USurvivalStatsComponent::ApplyDamage(float Amount)
{
if (bDead || Amount <= 0.f)
{
return;
}
Health = FMath::Max(0.f, Health - Amount);
OnStatsChanged.Broadcast();
if (Health <= 0.f)
{
bDead = true;
if (UWorld* World = GetWorld())
{
World->GetTimerManager().ClearTimer(UpdateTimerHandle);
}
OnDied.Broadcast();
}
}
@@ -0,0 +1,104 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "SurvivalStatsWidget.h"
#include "Components/ProgressBar.h"
#include "FpsPlayer.h"
#include "GameFramework/PlayerController.h"
#include "SurvivalStatsComponent.h"
void USurvivalStatsWidget::NativeConstruct()
{
Super::NativeConstruct();
// Releve des couleurs telles que reglees dans le Designer, avant que le
// code ne commence a les remplacer.
if (HealthBar)
{
HealthBaseColor = HealthBar->GetFillColorAndOpacity();
}
if (HungerBar)
{
HungerBaseColor = HungerBar->GetFillColorAndOpacity();
}
if (ThirstBar)
{
ThirstBaseColor = ThirstBar->GetFillColorAndOpacity();
}
BindToOwningPawn();
}
void USurvivalStatsWidget::BindToOwningPawn()
{
USurvivalStatsComponent* NewStats = ResolveStats();
if (BoundStats.Get() == NewStats)
{
return;
}
if (BoundStats.IsValid())
{
BoundStats->OnStatsChanged.RemoveDynamic(this, &USurvivalStatsWidget::HandleStatsChanged);
}
BoundStats = NewStats;
if (BoundStats.IsValid())
{
BoundStats->OnStatsChanged.AddDynamic(this, &USurvivalStatsWidget::HandleStatsChanged);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("USurvivalStatsWidget : aucun USurvivalStatsComponent trouve sur le pawn possede."));
}
Refresh();
}
void USurvivalStatsWidget::NativeDestruct()
{
if (BoundStats.IsValid())
{
BoundStats->OnStatsChanged.RemoveDynamic(this, &USurvivalStatsWidget::HandleStatsChanged);
}
BoundStats.Reset();
Super::NativeDestruct();
}
USurvivalStatsComponent* USurvivalStatsWidget::ResolveStats() const
{
const APlayerController* OwningController = GetOwningPlayer();
AFpsPlayer* Player = OwningController ? Cast<AFpsPlayer>(OwningController->GetPawn()) : nullptr;
return Player ? Player->GetSurvivalStats() : nullptr;
}
void USurvivalStatsWidget::HandleStatsChanged()
{
Refresh();
}
void USurvivalStatsWidget::Refresh()
{
if (!BoundStats.IsValid())
{
return;
}
ApplyBar(HealthBar, BoundStats->GetHealthPercent(), HealthBaseColor);
ApplyBar(HungerBar, BoundStats->GetHungerPercent(), HungerBaseColor);
ApplyBar(ThirstBar, BoundStats->GetThirstPercent(), ThirstBaseColor);
}
void USurvivalStatsWidget::ApplyBar(UProgressBar* Bar, float Percent, const FLinearColor& BaseColor) const
{
if (!Bar)
{
return;
}
Bar->SetPercent(FMath::Clamp(Percent, 0.f, 1.f));
Bar->SetFillColorAndOpacity(Percent <= CriticalThreshold ? CriticalColor : BaseColor);
}