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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user