Import initial du projet Survival (UE 5.8)

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 11:29:07 +02:00
commit ed7c7fd991
2626 changed files with 14833 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
using System.Collections.Generic;
public class Survival_projetTarget : TargetRules
{
public Survival_projetTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V7;
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_8;
ExtraModuleNames.Add("Survival_projet");
}
}
@@ -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);
}
@@ -0,0 +1,85 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ConfirmDialogWidget.generated.h"
class UButton;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnConfirmDialogConfirmed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnConfirmDialogCancelled);
/**
* Boite de dialogue "es-tu sur ?", volontairement generique.
*
* Elle ne sait pas ce qu'elle confirme : elle pose la question et previent via
* ses delegues. C'est l'appelant qui decide de l'action. Ainsi la meme boite
* servira pour quitter, effacer une sauvegarde ou abandonner une partie.
*
* Classe Abstract : on cree un WBP_ConfirmDialog qui en herite.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UConfirmDialogWidget : public UUserWidget
{
GENERATED_BODY()
public:
UConfirmDialogWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
UPROPERTY(BlueprintAssignable, Category = "Dialogue")
FOnConfirmDialogConfirmed OnConfirmed;
UPROPERTY(BlueprintAssignable, Category = "Dialogue")
FOnConfirmDialogCancelled OnCancelled;
/**
* Affiche la boite et lui donne le focus clavier.
*
* Passe par ici et jamais par SetVisibility directement : sans le focus,
* la boite ne recevrait aucune touche et Echap resterait sans effet.
*/
UFUNCTION(BlueprintCallable, Category = "Dialogue")
void Open(const FText& InMessage);
/** Referme la boite. Ne previent personne : l'appelant sait ce qu'il fait. */
UFUNCTION(BlueprintCallable, Category = "Dialogue")
void Close();
protected:
/** Echap equivaut a cliquer Annuler. Decoche pour une question incontournable. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dialogue")
bool bCancelOnEscape = true;
/**
* Entree equivaut a cliquer Confirmer.
*
* Desactive par defaut, volontairement : sur une question destructrice, un
* joueur qui martele Entree validerait sans avoir lu.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dialogue")
bool bConfirmOnEnter = false;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ConfirmButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CancelButton;
/** Optionnel : une boite dont le texte est fige dans le Blueprint reste valide. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> MessageText;
private:
UFUNCTION()
void HandleConfirmClicked();
UFUNCTION()
void HandleCancelClicked();
};
+395
View File
@@ -0,0 +1,395 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "FpsPlayer.generated.h"
class APickupItem;
class UCameraComponent;
class UInputAction;
class UInputMappingContext;
class UInteractionComponent;
class UInventoryComponent;
class USurvivalStatsComponent;
struct FInputActionValue;
/**
* Personnage joueur en vue première personne.
* Les assets d'input (Input Mapping Context + Input Actions) se branchent
* dans le Blueprint enfant (BP_FpsPlayer), catégorie "Input".
*/
UCLASS()
class SURVIVAL_PROJET_API AFpsPlayer : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AFpsPlayer();
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
/** Vrai tant que le joueur sprinte (utile pour l'anim ou la stamina plus tard). */
UFUNCTION(BlueprintPure, Category = "Mouvement")
bool IsSprinting() const { return bIsSprinting; }
UFUNCTION(BlueprintPure, Category = "Camera")
UCameraComponent* GetFirstPersonCamera() const { return FirstPersonCamera; }
UFUNCTION(BlueprintPure, Category = "Interaction")
UInteractionComponent* GetInteractionComponent() const { return InteractionComponent; }
UFUNCTION(BlueprintPure, Category = "Inventaire")
UInventoryComponent* GetInventoryComponent() const { return InventoryComponent; }
UFUNCTION(BlueprintPure, Category = "Survie")
USurvivalStatsComponent* GetSurvivalStats() const { return SurvivalStats; }
/** Sert a l'UI pour afficher la vraie touche mappee, plutot qu'un "E" ecrit en dur. */
UFUNCTION(BlueprintPure, Category = "Input")
const UInputAction* GetInteractAction() const { return InteractAction; }
/**
* Coupe les etats maintenus avant de desactiver les inputs du pawn.
*
* Sprint et saut sont declenches par Started et arretes par Completed.
* Si on desactive l'input pendant que la touche est enfoncee, le Completed
* n'a plus personne pour l'ecouter et l'etat reste bloque : on rouvrirait
* le jeu en sprintant sans toucher a Maj.
*/
UFUNCTION(BlueprintCallable, Category = "Input")
void ClearTransientInputStates();
/**
* Retire des objets d'un slot et les fait apparaitre au sol devant le joueur.
* @param Quantity nombre d'exemplaires ; passe un grand nombre pour tout jeter.
* @return true si quelque chose est reellement sorti de l'inventaire.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool DropSlot(int32 SlotIndex, int32 Quantity);
/** Vide l'inventaire entier en un tas autour du personnage. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void DropAllItems();
UFUNCTION(BlueprintPure, Category = "Survie")
bool IsDead() const { return bIsDead; }
/**
* Secoue la caméra verticalement. Strength est la vitesse initiale en cm/s
* donnée à la caméra ; positif = vers le haut, négatif = vers le bas.
* Réutilisable pour une explosion, un recul d'arme, un coup reçu.
*/
UFUNCTION(BlueprintCallable, Category = "Camera")
void AddCameraPunch(float Strength);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
// Appelés par le CharacterMovement quand la capsule change de taille (accroupi)
virtual void OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust) override;
virtual void OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust) override;
// Réception au sol et décollage
virtual void Landed(const FHitResult& Hit) override;
virtual void OnJumped_Implementation() override;
// ------------------------------------------------------------------
// Composants
// ------------------------------------------------------------------
/** Caméra placée à hauteur des yeux, suit la rotation du controller. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
TObjectPtr<UCameraComponent> FirstPersonCamera;
/** Détecte et déclenche les interactions avec le monde. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction")
TObjectPtr<UInteractionComponent> InteractionComponent;
/**
* Inventaire du joueur. Sur le pawn et non sur le controller :
* un cadavre qui garde son butin fait partie du gameplay de survie.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
TObjectPtr<UInventoryComponent> InventoryComponent;
/** Vie, faim, soif. Sur le pawn : c'est ce corps qui a faim. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Survie")
TObjectPtr<USurvivalStatsComponent> SurvivalStats;
// ------------------------------------------------------------------
// Enhanced Input : assets à assigner dans le Blueprint
// ------------------------------------------------------------------
/** Contexte ajouté au joueur au BeginPlay (IMC_Default). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputMappingContext> DefaultMappingContext;
/** Déplacement, valeur Axis2D (X = droite/gauche, Y = avant/arrière). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> MoveAction;
/** Regard souris / stick droit, valeur Axis2D. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> LookAction;
/** Saut, valeur Digital (bool). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> JumpAction;
/** Sprint, valeur Digital (bool) — maintenu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> SprintAction;
/** Accroupi, valeur Digital (bool) — maintenu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> CrouchAction;
/** Interaction, valeur Digital (bool). */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> InteractAction;
/**
* Utilise l'objet actif de la barre rapide, valeur Digital (bool).
* A mapper sur le clic gauche.
*
* Bindee sur le PAWN volontairement : quand l'inventaire est ouvert, les
* inputs du pawn sont coupes, donc le clic gauche sert au glisser-deposer
* de l'UI sans qu'on ait le moindre test a ecrire.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> UseItemAction;
// ------------------------------------------------------------------
// Réglages : mouvement
// ------------------------------------------------------------------
/** Vitesse de marche normale (cm/s). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mouvement", meta = (ClampMin = "0"))
float WalkSpeed = 400.f;
/** Vitesse en sprint (cm/s). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mouvement", meta = (ClampMin = "0"))
float SprintSpeed = 650.f;
/** Vitesse accroupi (cm/s). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mouvement", meta = (ClampMin = "0"))
float CrouchSpeed = 200.f;
// ------------------------------------------------------------------
// Réglages : objets jetés au sol
// ------------------------------------------------------------------
/**
* Classe engendrée quand on jette un objet. Assigne ton BP_Pickup_Test :
* son ItemData est écrasé au spawn, un seul Blueprint suffit pour tout.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inventaire|Drop")
TSubclassOf<APickupItem> DroppedPickupClass;
/** Distance devant les yeux où l'objet est déposé (cm). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire|Drop", meta = (ClampMin = "10"))
float DropDistance = 120.f;
/** Hauteur au-dessus du sol trouvé, pour éviter que l'objet s'y encastre. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire|Drop")
float DropGroundOffset = 5.f;
// ------------------------------------------------------------------
// Réglages : regard
// ------------------------------------------------------------------
/**
* Sensibilité horizontale : degrés de rotation par unité de souris.
* 1.0 = brut (beaucoup trop rapide), ~0.4 est un bon point de départ.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (ClampMin = "0.01"))
float LookSensitivityX = 0.4f;
/** Sensibilité verticale. Souvent réglée un peu plus basse que l'horizontale. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (ClampMin = "0.01"))
float LookSensitivityY = 0.4f;
/** Inverse l'axe vertical de la souris. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
bool bInvertLookY = false;
/**
* Hauteur de l'oeil AU-DESSUS DES PIEDS, debout (cm).
* 160 cm correspond a un personnage d'environ 1m76.
* Mesure depuis le sol et non depuis la capsule : c'est ce qui permet
* d'annuler le saut instantane de la capsule quand elle retrecit.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
float EyeHeightStanding = 160.f;
/** Hauteur de l'oeil au-dessus des pieds, accroupi (cm). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
float EyeHeightCrouched = 104.f;
// ------------------------------------------------------------------
// Réglages : animation d'accroupissement
// ------------------------------------------------------------------
/** Durée en secondes de la transition debout <-> accroupi. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.01"))
float CrouchTransitionDuration = 0.4f;
/** Tangage (degrés) au pic de la transition. Positif = le regard plonge. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi")
float CrouchTiltPitch = 3.f;
/**
* Roulis (degrés) au pic de la transition : bascule sur le côté.
* A 0 par defaut, le mouvement reste purement vertical.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi")
float CrouchTiltRoll = 0.f;
// ------------------------------------------------------------------
// Réglages : impacts caméra (saut / atterrissage)
// ------------------------------------------------------------------
/**
* Fréquence du ressort de caméra en Hz : à quelle vitesse elle revient
* en place après un choc. Plus haut = plus sec.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0.1"))
float PunchFrequency = 3.f;
/**
* Amortissement du ressort. 1.0 = retour net sans rebond,
* 0.5 = un rebond visible, en dessous ça oscille longtemps.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0.05", ClampMax = "2.0"))
float PunchDamping = 0.85f;
/**
* Durée (s) sur laquelle une impulsion est injectée dans le ressort.
* A 0 elle est appliquée d'un coup : la caméra part à pleine vitesse en
* une frame, ce qui se lit comme un a-coup. Quelques centiemes suffisent
* a rendre l'entree en mouvement progressive.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0"))
float PunchImpulseRampTime = 0.06f;
/** Degrés de tangage ajoutés par cm de déplacement vertical de la caméra. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact")
float PunchPitchPerCm = 0.35f;
/** Amplitude maximale du déplacement vertical (cm), garde-fou. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0"))
float PunchMaxOffset = 50.f;
/**
* Impulsion (cm/s) à l'atterrissage, quand l'impact est au maximum.
* Ordre de grandeur : le deplacement en cm de la camera vaut environ
* cette valeur x 0.018 (avec les reglages de ressort par defaut).
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0"))
float LandPunchStrength = 2000.f;
/** En dessous de cette vitesse de chute (cm/s), aucun effet à l'atterrissage. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "0"))
float LandPunchMinFallSpeed = 150.f;
/**
* Vitesse de chute (cm/s) à partir de laquelle l'effet est au maximum.
* Un saut sur place retombe a peu pres a JumpZVelocity (450), donc cette
* valeur doit rester proche pour qu'un saut normal se sente deja bien.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact", meta = (ClampMin = "1"))
float LandPunchMaxFallSpeed = 900.f;
/**
* Impulsion (cm/s) au décollage du saut. Positif = la caméra s'enfonce
* un instant (compression des jambes) ; mets une valeur négative pour
* qu'elle parte vers le haut à la place.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Impact")
float JumpPunchStrength = 550.f;
// ------------------------------------------------------------------
// Réglages : champ de vision
// ------------------------------------------------------------------
/** FOV au repos, en degrés. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|FOV", meta = (ClampMin = "30", ClampMax = "150"))
float BaseFOV = 90.f;
/** Degrés de FOV ajoutés en sprint. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|FOV")
float SprintFOVOffset = 8.f;
/** Vitesse de transition du FOV. Plus haut = plus brusque. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|FOV", meta = (ClampMin = "0.1"))
float FOVInterpSpeed = 8.f;
// ------------------------------------------------------------------
// Handlers d'input
// ------------------------------------------------------------------
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartSprint();
void StopSprint();
void StartCrouch();
void StopCrouch();
void Interact();
/**
* Clic gauche : agit avec l'objet actif de la barre rapide.
* Aujourd'hui seuls les consommables font quelque chose ; c'est ici que
* viendront les outils et les armes.
*/
void UseSelectedItem();
private:
/** Fait avancer toutes les animations de caméra et les applique. */
void UpdateCameraEffects(float DeltaTime);
/** Branche sur OnDied du composant de survie. */
UFUNCTION()
void HandleDeath();
/** Fait apparaitre un pickup deja configure a un endroit donne. */
APickupItem* SpawnPickup(const struct FInventorySlot& Slot, const FVector& Location);
/** Descend une position jusqu'au sol, pour ne pas laisser d'objet en l'air. */
FVector PlaceOnGround(const FVector& Start) const;
bool bIsDead = false;
/** Progression de l'animation d'accroupissement : 0 = debout, 1 = accroupi. */
float CrouchAlpha = 0.f;
/** Valeur visée par CrouchAlpha. */
float CrouchTargetAlpha = 0.f;
/** +1 quand on s'accroupit, -1 quand on se relève : inverse le sens du tilt. */
float CrouchTiltDirection = 1.f;
/** Décalage vertical courant de la caméra (cm), état du ressort d'impact. */
float PunchOffset = 0.f;
/** Vitesse du ressort d'impact (cm/s). */
float PunchVelocity = 0.f;
/** Impulsion restant à injecter dans le ressort, étalée sur PunchImpulseRampTime. */
float PendingPunch = 0.f;
/** Vitesse verticale mémorisée pendant la chute, pour calibrer l'atterrissage. */
float LastFallSpeed = 0.f;
bool bIsSprinting = false;
/** Évite de spammer le log si IA_Look est mal configurée. */
bool bLoggedLookTypeWarning = false;
};
@@ -0,0 +1,169 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "FpsPlayerController.generated.h"
class UHotbarWidget;
class UInputAction;
class UInventoryComponent;
class UInteractionPromptWidget;
class UInventoryWidget;
class UScreenFadeComponent;
class USurvivalStatsWidget;
class UUserWidget;
struct FInputActionValue;
/**
* Controller du joueur. C'est ici que vit l'UI, et pas dans le personnage :
* le controller survit a la mort du pawn, alors que le personnage non.
* Si tu creais le HUD dans AFpsPlayer, il disparaitrait a chaque respawn.
*/
UCLASS()
class SURVIVAL_PROJET_API AFpsPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AFpsPlayerController();
/** Ouvre ou ferme l'inventaire. */
UFUNCTION(BlueprintCallable, Category = "UI")
void ToggleInventory();
UFUNCTION(BlueprintPure, Category = "UI")
bool IsInventoryOpen() const { return bInventoryOpen; }
protected:
virtual void BeginPlay() override;
virtual void OnPossess(APawn* InPawn) override;
virtual void SetupInputComponent() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
// ------------------------------------------------------------------
// Fondus
//
// La mécanique elle-même vit dans UScreenFadeComponent, partagé avec le
// menu principal. Ici on ne garde que les durées propres au jeu.
// La couleur et le WBP_Fade se règlent sur le composant.
// ------------------------------------------------------------------
/** Le voile. Assigne-lui ton WBP_Fade dans BP_FpsPlayerController. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UI|Fondu")
TObjectPtr<UScreenFadeComponent> ScreenFade;
/** Décoche pour démarrer sans fondu, pratique pendant les tests. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu")
bool bFadeInOnStart = true;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "0.1"))
float StartupFadeDuration = 1.5f;
/** Durée du fondu au noir quand le joueur meurt. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "0.1"))
float DeathFadeDuration = 1.f;
/**
* Temps total entre la mort et la réapparition.
* Doit rester supérieur à DeathFadeDuration, sinon le joueur réapparaît
* avant que l'écran soit entièrement noir.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "0.1"))
float RespawnDelay = 3.f;
/**
* Ouverture de l'inventaire, valeur Digital (bool).
* L'input d'interface vit sur le controller et non sur le pawn : ouvrir
* son sac doit rester possible meme sans corps a piloter.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> ToggleInventoryAction;
/**
* Jette l'objet survole, valeur Digital (bool). Ctrl enfonce = un seul
* exemplaire au lieu de la pile entiere.
* Sur le controller aussi : le pawn a ses inputs coupes quand l'inventaire
* est ouvert, donc un binding sur le pawn ne repondrait jamais.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> DropItemAction;
/**
* Selection directe d'un slot de la barre rapide, valeur Axis1D.
*
* Une seule action pour toutes les touches : dans IMC_Default, mappe les
* touches 1 a 6 sur cette action et donne a chacune un modificateur
* Scalar different (1, 2, 3...). Le handler lit la valeur et en deduit
* le slot. Six actions separees seraient six assets a maintenir.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> HotbarSelectAction;
/** Molette de souris pour defiler dans la barre, valeur Axis1D. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> HotbarScrollAction;
/**
* Viseur, affiche en permanence. Widget separe du prompt : ce dernier se
* met en Collapsed des qu'on ne vise plus rien, et emporterait le viseur
* avec lui s'ils partageaient le meme widget.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UUserWidget> CrosshairClass;
/** A assigner dans BP_FpsPlayerController : ton WBP_InteractionPrompt. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UInteractionPromptWidget> InteractionPromptClass;
/** Ton WBP_Inventory. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UInventoryWidget> InventoryClass;
/** Ton WBP_Hotbar, affiche en permanence. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UHotbarWidget> HotbarClass;
/** Ton WBP_SurvivalStats : les trois jauges. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<USurvivalStatsWidget> SurvivalStatsClass;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<UUserWidget> Crosshair;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<UInteractionPromptWidget> InteractionPrompt;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<UInventoryWidget> InventoryWidget;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<UHotbarWidget> Hotbar;
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
TObjectPtr<USurvivalStatsWidget> SurvivalStatsWidget;
private:
void CreateHudWidgets();
/** Jette la pile survolee dans la grille, si l'inventaire est ouvert. */
void DropHoveredItem();
void HandleHotbarSelect(const FInputActionValue& Value);
void HandleHotbarScroll(const FInputActionValue& Value);
/** Raccourci vers l'inventaire du pawn possede. */
UInventoryComponent* GetPawnInventory() const;
/** Branche sur OnDied du pawn possede. */
UFUNCTION()
void HandlePawnDied();
/** Detruit le cadavre et demande un nouveau pawn au GameMode. */
void RespawnPlayer();
FTimerHandle RespawnTimerHandle;
bool bInventoryOpen = false;
};
@@ -0,0 +1,66 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "HotbarWidget.generated.h"
class UHorizontalBox;
class UInventoryComponent;
class UInventorySlotWidget;
/**
* Barre rapide en bas de l'ecran, affichee en permanence.
*
* Elle ne possede aucune donnee : elle montre les N premiers slots de
* l'inventaire. Deplacer un objet de la grille vers la barre, c'est
* simplement le deplacer vers un slot d'index bas -- le glisser-deposer
* existant s'en occupe deja sans aucun code supplementaire.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UHotbarWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** Remet a jour contenu et selection. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void Refresh();
/** (Re)branche sur l'inventaire du pawn possede. Voir UInventoryWidget. */
void BindToOwningPawn();
protected:
/** A nommer exactement "SlotContainer" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UHorizontalBox> SlotContainer;
/** Le meme WBP_InventorySlot que la grille : une seule apparence a maintenir. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire")
TSubclassOf<UInventorySlotWidget> SlotWidgetClass;
/** Espacement entre les cases. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire", meta = (ClampMin = "0"))
float SlotPadding = 4.f;
private:
UFUNCTION()
void HandleInventoryChanged();
UFUNCTION()
void HandleSelectionChanged(int32 NewIndex);
void RebuildSlots(int32 DesiredCount);
UInventoryComponent* ResolveInventory() const;
TWeakObjectPtr<UInventoryComponent> BoundInventory;
/** UPROPERTY obligatoire : sans ca le GC collecterait les cases. */
UPROPERTY(Transient)
TArray<TObjectPtr<UInventorySlotWidget>> SlotWidgets;
};
@@ -0,0 +1,71 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Interactable.generated.h"
/**
* Classe technique requise par Unreal. On ne la manipule jamais directement,
* elle sert uniquement au systeme de reflection : c'est elle qu'on passe a
* Implements<UInteractable>() et a Execute_XXX().
*/
UINTERFACE(MinimalAPI, BlueprintType)
class UInteractable : public UInterface
{
GENERATED_BODY()
};
/**
* A implementer par tout ce avec quoi le joueur peut interagir : objets a
* ramasser, portes, coffres, etablis, plantes a recolter.
*
* Les fonctions sont des BlueprintNativeEvent : une classe C++ surcharge
* XXX_Implementation(), un Blueprint surcharge l'evenement du meme nom.
* Les deux fonctionnent, et l'appelant n'a pas a savoir lequel des deux
* il a en face -- c'est tout l'interet de passer par une interface.
*
* IMPORTANT : pour appeler ces fonctions depuis l'exterieur, utilise
* IInteractable::Execute_Interact(Actor, Interactor) et jamais
* Actor->Interact() directement. Seul le wrapper Execute_ sait router
* l'appel vers une implementation Blueprint.
*/
class SURVIVAL_PROJET_API IInteractable
{
GENERATED_BODY()
public:
/**
* Texte affiche au joueur quand il vise l'objet ("Ramasser", "Ouvrir"...).
* Renvoie du FText et non du FString : c'est le type localisable d'Unreal,
* indispensable si tu traduis le jeu un jour.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
FText GetInteractionPrompt() const;
virtual FText GetInteractionPrompt_Implementation() const { return FText::GetEmpty(); }
/**
* Permet de refuser l'interaction selon le contexte : porte verrouillee,
* inventaire plein, competence manquante. Renvoyer false empeche l'objet
* d'etre meme selectionne par le composant d'interaction.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
bool CanInteract(AActor* Interactor) const;
virtual bool CanInteract_Implementation(AActor* Interactor) const { return true; }
/** L'interaction elle-meme. Interactor est le pawn du joueur. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void Interact(AActor* Interactor);
virtual void Interact_Implementation(AActor* Interactor) {}
/** Le joueur commence a viser l'objet : surbrillance, son, animation. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void OnBeginFocus();
virtual void OnBeginFocus_Implementation() {}
/** Le joueur ne vise plus l'objet. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void OnEndFocus();
virtual void OnEndFocus_Implementation() {}
};
@@ -0,0 +1,103 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Engine/EngineTypes.h"
#include "InteractionComponent.generated.h"
/**
* Diffuse a chaque changement de cible visee. NewFocus vaut nullptr quand le
* joueur ne vise plus rien : c'est le signal pour cacher le prompt a l'ecran.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInteractionFocusChanged, AActor*, NewFocus, AActor*, OldFocus);
/**
* A poser sur le pawn du joueur. Lance regulierement un trace depuis le point
* de vue de la camera, garde en memoire l'objet IInteractable vise, et declenche
* l'interaction a la demande.
*
* Le composant ne connait aucun type d'objet concret : il ne parle qu'a
* l'interface. Tu peux ajouter des portes, des coffres ou des PNJ sans jamais
* revenir toucher a ce fichier.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class SURVIVAL_PROJET_API UInteractionComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInteractionComponent();
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/** Declenche l'interaction sur l'objet vise, s'il y en a un. */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void TryInteract();
/** L'acteur actuellement vise, ou nullptr. */
UFUNCTION(BlueprintPure, Category = "Interaction")
AActor* GetFocusedActor() const { return FocusedActor.Get(); }
/** Texte a afficher a l'ecran, vide si rien n'est vise. */
UFUNCTION(BlueprintPure, Category = "Interaction")
FText GetFocusedPrompt() const;
/** Branche ton widget la-dessus pour afficher / cacher le prompt. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnInteractionFocusChanged OnFocusChanged;
protected:
/** Portee du trace en cm. 250 = un peu plus d'un bras et demi. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction", meta = (ClampMin = "1"))
float InteractionRange = 250.f;
/**
* Rayon de la sphere balayee. Un trace en ligne pure oblige a viser au pixel
* pres et rend le jeu penible ; quelques cm de tolerance changent tout.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction", meta = (ClampMin = "0"))
float TraceRadius = 8.f;
/**
* Intervalle entre deux traces, en secondes. Inutile de tracer 120 fois par
* seconde pour un affichage que l'oeil lit a 20 Hz.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction", meta = (ClampMin = "0"))
float TraceInterval = 0.05f;
/** Canal de collision teste. Visibility convient dans la majorite des cas. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
/** Affiche le trace dans le monde, tres utile quand rien ne repond. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
bool bDrawDebug = false;
private:
/** Relance le trace et met a jour FocusedActor. */
void UpdateFocus();
/** Vrai si l'acteur implemente l'interface et accepte l'interaction. */
bool IsValidInteractable(AActor* Actor) const;
/** Change la cible en notifiant l'ancienne et la nouvelle. */
void SetFocusedActor(AActor* NewFocus);
/** Point de vue reel du joueur, camera comprise. */
bool GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const;
/** Cible courante. TWeakObjectPtr : si l'objet est detruit, on ne garde pas un pointeur mort. */
TWeakObjectPtr<AActor> FocusedActor;
/**
* Vrai si on avait une cible au dernier changement diffuse.
* Indispensable : quand la cible se detruit (pickup ramasse), FocusedActor
* se vide tout seul, et une simple comparaison de pointeurs ne verrait
* jamais la transition "j'avais une cible" -> "je n'en ai plus".
*/
bool bHasFocusedActor = false;
float TimeSinceLastTrace = 0.f;
};
@@ -0,0 +1,69 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InteractionPromptWidget.generated.h"
class UInteractionComponent;
class UTextBlock;
/**
* Affiche "Appuyez sur E pour Ramasser Objet" quand le joueur vise quelque
* chose d'interactif, et se cache sinon.
*
* La classe est Abstract : on ne l'instancie jamais directement, on cree un
* Widget Blueprint (WBP_InteractionPrompt) qui en herite. Le C++ porte la
* logique, le Blueprint porte la mise en page. C'est la separation standard
* en UMG, l'equivalent d'un prefab d'UI avec son script en Unity.
*
* Le widget ne fait AUCUN travail par frame : il s'abonne au delegue
* OnFocusChanged du composant d'interaction et ne se met a jour que lorsque
* la cible change reellement.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UInteractionPromptWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** (Re)branche sur le composant d'interaction du pawn possede. */
void BindToOwningPawn();
protected:
/**
* meta = (BindWidget) : Unreal relie automatiquement ce pointeur au widget
* nomme exactement "PromptText" dans le Widget Blueprint enfant. Si tu
* oublies de le creer ou que tu le renommes, le Blueprint refuse de
* compiler avec un message clair -- pas de crash silencieux au runtime.
*/
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> PromptText;
/**
* Texte affiche. {0} sera remplace par la touche, {1} par l'action.
* Editable dans le Widget Blueprint pour ajuster la formulation.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
FText PromptFormat;
UInteractionPromptWidget(const FObjectInitializer& ObjectInitializer);
private:
/** Appele par le composant d'interaction a chaque changement de cible. */
UFUNCTION()
void HandleFocusChanged(AActor* NewFocus, AActor* OldFocus);
/** Recalcule le texte et la visibilite. */
void Refresh();
/** Nom lisible de la touche actuellement mappee sur l'action Interagir. */
FText GetInteractKeyDisplayName() const;
/** Le composant auquel on est abonne, pour pouvoir se desabonner proprement. */
TWeakObjectPtr<UInteractionComponent> BoundComponent;
};
@@ -0,0 +1,230 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InventoryComponent.generated.h"
class UItemDataAsset;
/**
* Contenu d'un emplacement. Item nul = slot vide.
* Le tableau de slots a toujours exactement la taille de l'inventaire :
* l'index dans le tableau EST la position dans la grille, ce qui rend
* l'affichage et le futur drag & drop triviaux.
*/
USTRUCT(BlueprintType)
struct FInventorySlot
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
TObjectPtr<UItemDataAsset> Item = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
int32 Quantity = 0;
/**
* Charges restantes de l'exemplaire courant, pour les objets a usages
* multiples. Etat par instance, d'ou sa place ici et pas sur le DataAsset :
* une canette entamee ne doit pas entamer toutes les canettes du jeu.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
int32 RemainingUses = 0;
bool IsEmpty() const { return Item == nullptr || Quantity <= 0; }
void Clear()
{
Item = nullptr;
Quantity = 0;
RemainingUses = 0;
}
};
/** Diffuse a chaque modification du contenu ou de la taille. L'UI s'y abonne. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnInventoryChanged);
/** Diffuse quand le joueur change de slot actif dans la barre rapide. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSelectedHotbarSlotChanged, int32, NewIndex);
/**
* Inventaire en grille a slots, facon Raft / Valheim.
*
* Le nombre de slots est dynamique : BaseSlotCount fixe le minimum, et
* BonusSlotCount permet a un sac equipe de l'agrandir. Retirer le sac
* refuse si les objets ne rentrent plus, plutot que de les faire
* disparaitre en silence.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class SURVIVAL_PROJET_API UInventoryComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInventoryComponent();
virtual void InitializeComponent() override;
/**
* Ajoute des objets, en remplissant d'abord les piles entamees.
* @param RemainingUses charges de l'exemplaire ajoute ; -1 signifie "plein".
* Sert a rendre son etat a un objet ramasse au sol.
* @return le nombre d'exemplaires qui N'ONT PAS pu rentrer (0 = tout est passe).
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
int32 AddItem(UItemDataAsset* Item, int32 Quantity = 1, int32 RemainingUses = -1);
/**
* Consomme une utilisation du slot.
* Retire un exemplaire quand il ne reste plus de charge.
* @return true si quelque chose a ete consomme.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool ConsumeUse(int32 SlotIndex);
/**
* Retire des objets, en vidant d'abord les piles les plus petites.
* @return le nombre d'exemplaires reellement retires.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
int32 RemoveItem(UItemDataAsset* Item, int32 Quantity = 1);
/** Combien d'exemplaires de cet objet, toutes piles confondues. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetItemCount(const UItemDataAsset* Item) const;
/** Combien d'exemplaires pourraient encore rentrer, sans rien modifier. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetRoomFor(const UItemDataAsset* Item) const;
/**
* Un seul tableau, deux conteneurs distincts :
* index 0 .. HotbarSlotCount-1 -> barre rapide
* index HotbarSlotCount .. fin -> grille d'inventaire (+ bonus de sac)
*
* Un objet range dans la barre n'apparait PAS dans la grille, et
* inversement. Ils partagent le meme tableau uniquement pour que le
* glisser-deposer entre les deux ne demande aucune conversion d'index.
*/
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetTotalSlotCount() const { return FMath::Max(0, HotbarSlotCount + BackpackSlotCount + BonusSlotCount); }
/** Premier index affiche par la grille, juste apres la barre rapide. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetBackpackStartIndex() const { return GetHotbarSlotCount(); }
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetUsedSlotCount() const;
UFUNCTION(BlueprintPure, Category = "Inventaire")
const TArray<FInventorySlot>& GetSlots() const { return Slots; }
/**
* Deplace la pile entiere d'un slot vers un autre.
* - cible vide -> la pile entiere part
* - meme objet -> fusion jusqu'a MaxStackSize, le reste demeure a la source
* - objet different -> echange des deux slots
* @return true si quelque chose a change.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool MoveItem(int32 FromIndex, int32 ToIndex);
/**
* Deplace seulement une partie d'une pile.
*
* Un transfert partiel vers un slot occupe par un objet DIFFERENT est
* refuse : il n'existe aucune facon sensee d'echanger la moitie d'une pile
* contre autre chose. Seul un deplacement total peut provoquer un echange.
*
* @return true si quelque chose a change.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity);
/** Index du premier slot libre, ou INDEX_NONE si l'inventaire est plein. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 FindFirstEmptySlot() const;
/**
* Retire jusqu'a Quantity exemplaires d'un slot precis et renvoie ce qui a
* ete retire. Sert au drop au sol et, plus tard, au transfert vers un coffre.
*
* Different de RemoveItem() qui pioche par type sans se soucier du slot :
* ici le joueur a designe une case, on ne touche qu'a celle-la.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool TakeFromSlot(int32 SlotIndex, int32 Quantity, FInventorySlot& OutTaken);
/**
* Change le bonus de slots apporte par l'equipement (sac).
* @return false si reduire la taille laisserait des objets dehors ;
* dans ce cas rien n'est modifie et le sac ne doit pas etre retire.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool SetBonusSlotCount(int32 NewBonusSlots);
UPROPERTY(BlueprintAssignable, Category = "Inventaire")
FOnInventoryChanged OnInventoryChanged;
// ------------------------------------------------------------------
// Barre rapide
// ------------------------------------------------------------------
//
// La barre rapide n'est PAS un jeu de slots separe : ce sont les
// HotbarSlotCount premiers slots de l'inventaire, simplement affiches
// deux fois. Consequence directe : le glisser-deposer entre la barre et
// la grille fonctionne sans une ligne de code de plus, puisque ce sont
// les memes index dans le meme tableau.
/** Change le slot actif. Un index hors de la barre est ignore. */
UFUNCTION(BlueprintCallable, Category = "Inventaire|Barre rapide")
void SelectHotbarSlot(int32 Index);
/** Avance ou recule dans la barre, en boucle. */
UFUNCTION(BlueprintCallable, Category = "Inventaire|Barre rapide")
void CycleHotbarSelection(int32 Delta);
UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
int32 GetSelectedHotbarIndex() const { return SelectedHotbarIndex; }
UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
int32 GetHotbarSlotCount() const { return FMath::Min(HotbarSlotCount, Slots.Num()); }
/** Contenu du slot actif. C'est ce que le joueur "a en main". */
UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
FInventorySlot GetSelectedSlot() const;
UPROPERTY(BlueprintAssignable, Category = "Inventaire|Barre rapide")
FOnSelectedHotbarSlotChanged OnSelectedHotbarSlotChanged;
protected:
/** Cases affichees dans la grille d'inventaire. N'inclut PAS la barre rapide. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire", meta = (ClampMin = "0"))
int32 BackpackSlotCount = 20;
/** Slots supplementaires apportes par l'equipement (sac). Passe par SetBonusSlotCount(). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
int32 BonusSlotCount = 0;
/**
* Cases de la barre rapide, independantes de celles de la grille.
* Elles occupent les premiers index du tableau.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire|Barre rapide", meta = (ClampMin = "0"))
int32 HotbarSlotCount = 10;
/** Toujours dimensionne a GetTotalSlotCount(). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
TArray<FInventorySlot> Slots;
private:
/** Redimensionne le tableau si la taille cible a change. */
void EnsureSlotCount();
/** Regroupe les slots occupes au debut du tableau. */
void CompactSlots();
int32 SelectedHotbarIndex = 0;
};
@@ -0,0 +1,36 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/DragDropOperation.h"
#include "InventoryDragDropOperation.generated.h"
class UInventoryComponent;
/**
* Ce qui voyage entre la case qu'on saisit et celle ou on lache.
*
* On transporte un INDEX et non le contenu du slot : si l'inventaire change
* pendant le glisser (ramassage automatique, script), la cible relira l'etat
* reel au moment du drop au lieu d'appliquer une donnee perimee.
*/
UCLASS()
class SURVIVAL_PROJET_API UInventoryDragDropOperation : public UDragDropOperation
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, Category = "Inventaire")
int32 SourceSlotIndex = INDEX_NONE;
/**
* Combien d'exemplaires on deplace. Egal a la pile entiere pour un
* glisser normal, la moitie avec Maj, un seul avec Ctrl.
*/
UPROPERTY(BlueprintReadOnly, Category = "Inventaire")
int32 Quantity = 0;
UPROPERTY(BlueprintReadOnly, Category = "Inventaire")
TWeakObjectPtr<UInventoryComponent> SourceInventory;
};
@@ -0,0 +1,114 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InventoryComponent.h"
#include "InventorySlotWidget.generated.h"
class UImage;
class UInventoryWidget;
class UProgressBar;
class UTextBlock;
/**
* Une case de la grille d'inventaire : une icone et une quantite.
*
* Volontairement passive pour l'affichage : elle ne va jamais chercher
* l'information elle-meme, c'est la grille parente qui lui pousse son contenu
* via SetSlot(). Elle est en revanche autonome pour le glisser-deposer, parce
* que c'est elle qui recoit les evenements souris.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UInventorySlotWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
/** Appele une fois a la creation : dit a la case qui elle represente. */
void InitSlot(UInventoryComponent* InInventory, int32 InSlotIndex, UInventoryWidget* InOwningGrid);
/** Affiche le contenu d'un slot, ou se vide si le slot est vide. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void SetSlot(const FInventorySlot& InSlot);
/**
* Transforme cette instance en fantome de glisser : le fond de case
* disparait pour ne laisser flotter que l'objet lui-meme.
*/
void ConfigureAsDragVisual();
/** Affiche ou masque le cadre de selection (barre rapide). */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void SetSelected(bool bSelected);
protected:
// --- Glisser-deposer ---
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual void NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation) override;
virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override;
// --- Survol, pour que la touche "jeter" sache sur quoi agir ---
virtual void NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual void NativeOnMouseLeave(const FPointerEvent& InMouseEvent) override;
/** A nommer exactement "ItemIcon" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> ItemIcon;
/** A nommer exactement "QuantityText". */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> QuantityText;
/**
* Fond de case. BindWidgetOptional et non BindWidget : le Blueprint peut
* s'en passer sans casser la compilation, c'est purement decoratif.
* Nomme-le "SlotBackground" pour qu'il soit masque pendant le glisser.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> SlotBackground;
/**
* Cadre affiche quand la case est le slot actif de la barre rapide.
* Optionnel : la grille d'inventaire ne s'en sert jamais.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> SelectionBorder;
/**
* Petite barre d'usure en bas de la case, pour les objets a usages
* multiples. Masquee automatiquement sur tout le reste.
* A nommer "UsesBar" dans le Widget Blueprint.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UProgressBar> UsesBar;
/** Opacite du fantome qui suit le curseur pendant le glisser. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire", meta = (ClampMin = "0.05", ClampMax = "1.0"))
float DragVisualOpacity = 0.85f;
/** Opacite de la case d'origine tant que le glisser est en cours. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float DraggedSlotOpacity = 0.3f;
private:
/** Rebranche l'opacite normale, que le glisser ait abouti ou non. */
UFUNCTION()
void HandleDragFinished(UDragDropOperation* Operation);
TWeakObjectPtr<UInventoryComponent> OwningInventory;
/** La grille parente, pour lui signaler le survol. */
TWeakObjectPtr<UInventoryWidget> OwningGrid;
int32 SlotIndex = INDEX_NONE;
/** Dernier contenu recu, pour construire le fantome de glisser. */
FInventorySlot CachedSlot;
/** Vrai si cette instance est un fantome et non une vraie case de la grille. */
bool bIsDragVisual = false;
};
@@ -0,0 +1,85 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InventoryWidget.generated.h"
class UInventoryComponent;
class UInventorySlotWidget;
class UUniformGridPanel;
/**
* Grille d'inventaire. Cree ses cases dynamiquement a partir du nombre de
* slots du composant, et se remet a jour uniquement quand l'inventaire change.
*
* Le nombre de cases n'est PAS fige : quand un sac ajoutera des slots, la
* grille s'agrandira toute seule au prochain rafraichissement.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UInventoryWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** Reconstruit si necessaire, puis pousse le contenu dans chaque case. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void Refresh();
/** Index de la case sous le curseur, ou INDEX_NONE. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetHoveredSlotIndex() const { return HoveredSlotIndex; }
/** Appele par les cases quand le curseur entre ou sort. */
void NotifySlotHovered(int32 Index, bool bHovered);
/**
* (Re)branche le widget sur l'inventaire du pawn actuellement possede.
* Indispensable apres un respawn : le nouveau pawn a un nouveau composant,
* et l'ancien abonnement pointerait vers un objet detruit.
*/
void BindToOwningPawn();
protected:
/** A nommer exactement "SlotGrid" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UUniformGridPanel> SlotGrid;
/** Le widget de case a instancier : ton WBP_InventorySlot. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire")
TSubclassOf<UInventorySlotWidget> SlotWidgetClass;
/** Nombre de cases par ligne. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire", meta = (ClampMin = "1"))
int32 ColumnCount = 6;
protected:
/**
* Absorbe les depots qui ne visent aucune case. Sans ca, lacher un objet
* sur le fond du panneau declencherait OnDragCancelled -- et si un jour on
* y branche le drop au sol, le joueur jetterait son butin par accident.
*/
virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override;
private:
UFUNCTION()
void HandleInventoryChanged();
/** Cree ou supprime des cases pour coller au nombre de slots. */
void RebuildGrid(int32 DesiredCount);
/** Retrouve l'inventaire du pawn possede. */
UInventoryComponent* ResolveInventory() const;
TWeakObjectPtr<UInventoryComponent> BoundInventory;
int32 HoveredSlotIndex = INDEX_NONE;
/** UPROPERTY obligatoire : sans ca le GC collecterait les cases. */
UPROPERTY(Transient)
TArray<TObjectPtr<UInventorySlotWidget>> SlotWidgets;
};
@@ -0,0 +1,104 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ItemDataAsset.generated.h"
class UStaticMesh;
class UTexture2D;
/**
* Definition d'un TYPE d'objet : "Bois", "Pomme", "Corde".
* Un asset par type, cree dans le Content Browser, edite a la main.
*
* C'est l'equivalent exact d'un ScriptableObject Unity : une donnee partagee
* que tout le monde reference et que personne ne duplique. Cent buches dans
* le monde pointent toutes vers le meme UItemDataAsset "Bois".
*
* Regle a ne jamais enfreindre : ceci decrit ce qu'est un objet, jamais son
* etat. La quantite, la durabilite, l'usure appartiennent a l'inventaire ou
* a l'acteur, pas ici -- sinon ramasser une pomme modifierait toutes les
* pommes du jeu.
*/
UCLASS(BlueprintType)
class SURVIVAL_PROJET_API UItemDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/** Identifiant stable, utilise par la sauvegarde et les recettes de craft. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
FName ItemId;
/** Nom affiche au joueur. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
FText DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item", meta = (MultiLine = "true"))
FText Description;
/** Icone affichee dans la grille d'inventaire. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UTexture2D> Icon;
/** Mesh utilise quand l'objet est pose au sol. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UStaticMesh> WorldMesh;
/**
* Nombre maximum d'exemplaires par slot.
* 1 = ne s'empile pas (outils, armes), 20 = ressource classique.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item", meta = (ClampMin = "1"))
int32 MaxStackSize = 1;
/**
* Nombre d'utilisations avant que l'objet soit consomme.
* 1 = objet a usage unique. 3 = une canette qu'on boit en trois gorgees.
*
* Les valeurs de restauration sont appliquees A CHAQUE utilisation, pas
* reparties : une canette a 3 usages et 20 de soif rend 20 par gorgee.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item", meta = (ClampMin = "1"))
int32 MaxUses = 1;
/**
* Taille de pile reellement applicable.
*
* Un objet a charges ne s'empile jamais : deux canettes entamees a des
* niveaux differents ne peuvent pas partager un slot, il faudrait stocker
* deux compteurs dans une seule case. La regle est centralisee ici pour
* qu'aucun appelant ne puisse l'oublier.
*/
UFUNCTION(BlueprintPure, Category = "Item")
int32 GetEffectiveMaxStack() const { return MaxUses > 1 ? 1 : FMath::Max(1, MaxStackSize); }
/**
* Slots ajoutes a l'inventaire quand cet objet est equipe comme sac.
* Laisse a 0 pour tout ce qui n'est pas un contenant.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Sac", meta = (ClampMin = "0"))
int32 BonusInventorySlots = 0;
// ------------------------------------------------------------------
// Consommable
// ------------------------------------------------------------------
/** Coche pour qu'un clic gauche consomme l'objet et applique ses effets. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable")
bool bIsConsumable = false;
/** Faim rendue a la consommation. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable", meta = (EditCondition = "bIsConsumable"))
float HungerRestore = 0.f;
/** Soif rendue. Une valeur negative deshydrate -- utile pour l'eau salee. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable", meta = (EditCondition = "bIsConsumable"))
float ThirstRestore = 0.f;
/** Vie rendue. Negative pour un aliment avarie ou toxique. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable", meta = (EditCondition = "bIsConsumable"))
float HealthRestore = 0.f;
};
@@ -0,0 +1,27 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "MainMenuGameMode.generated.h"
/**
* GameMode de la map de menu.
*
* Son role principal est NEGATIF : empecher AFpsPlayer de spawner. La map de
* menu etant une copie de la map de jeu, sans ce GameMode Override dans ses
* World Settings, le joueur apparaitrait derriere le menu avec son HUD, ses
* jauges et son inventaire.
*
* Divergence Unity a garder en tete : ici le GameMode est une propriete DE LA
* MAP, pas un objet qu'on pose dedans. Il se choisit dans World Settings.
*/
UCLASS()
class SURVIVAL_PROJET_API AMainMenuGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AMainMenuGameMode();
};
@@ -0,0 +1,116 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MainMenuPlayerController.generated.h"
class AMenuCameraSpot;
class UConfirmDialogWidget;
class UMainMenuWidget;
class UScreenFadeComponent;
/**
* Ce qu'on fait une fois l'ecran devenu noir.
*
* Quitter n'y figure pas : la sortie est seche, sans fondu, une fois la
* confirmation obtenue.
*/
enum class EMenuPendingAction : uint8
{
None,
StartGame
};
/**
* Controller du menu principal. Il ne possede aucun pawn : il pose simplement
* sa vue sur une AMenuCameraSpot tiree au hasard et affiche le menu par-dessus.
*
* Comme dans le jeu, l'UI vit sur le controller et pas sur un acteur du monde.
*/
UCLASS()
class SURVIVAL_PROJET_API AMainMenuPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AMainMenuPlayerController();
/** Fondu au noir, puis chargement de la map de jeu. */
UFUNCTION(BlueprintCallable, Category = "Menu")
void StartSoloGame();
/** Ouvre la demande de confirmation. Ne quitte rien par elle-meme. */
UFUNCTION(BlueprintCallable, Category = "Menu")
void QuitGame();
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/**
* Ta map de jeu.
*
* TSoftObjectPtr et non une reference dure : une reference dure ferait
* charger la map de jeu EN MEME TEMPS que le menu, alors qu'on ne la veut
* qu'au clic sur Jouer. C'est l'equivalent d'une Addressable en Unity, par
* opposition a un champ public qui tire tout l'asset avec lui.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
TSoftObjectPtr<UWorld> GameLevel;
/** Ton WBP_MainMenu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
TSubclassOf<UMainMenuWidget> MainMenuClass;
/**
* Ton WBP_ConfirmDialog. Si tu le laisses vide, le bouton Quitter ferme le
* jeu directement -- pratique pour tester, mais laisse-le assigne.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
TSubclassOf<UConfirmDialogWidget> QuitDialogClass;
/** La question posee avant de fermer le jeu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu")
FText QuitDialogMessage;
/** Fondu d'arrivee sur le menu, au lancement du jeu. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu", meta = (ClampMin = "0.1"))
float OpeningFadeDuration = 1.5f;
/** Fondu de depart, quand on quitte le menu pour la partie. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu", meta = (ClampMin = "0.1"))
float LeaveFadeDuration = 1.f;
/** Le fondu, partage avec AFpsPlayerController. Assigne-lui ton WBP_Fade. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Menu")
TObjectPtr<UScreenFadeComponent> ScreenFade;
UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu")
TObjectPtr<UMainMenuWidget> MainMenu;
UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu")
TObjectPtr<UConfirmDialogWidget> QuitDialog;
private:
/** Tire un angle de vue au hasard, en evitant celui du dernier passage. */
AMenuCameraSpot* PickCameraSpot();
/** Le fondu vient de se terminer : on execute l'action mise de cote. */
UFUNCTION()
void HandleFadeFinished();
/** Oui : on ferme le jeu, immediatement. */
UFUNCTION()
void HandleQuitConfirmed();
/** Non : on referme la boite et on rend la main au menu. */
UFUNCTION()
void HandleQuitCancelled();
/** Ferme le jeu sans rien demander. */
void CloseGame();
EMenuPendingAction PendingAction = EMenuPendingAction::None;
};
@@ -0,0 +1,55 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MainMenuWidget.generated.h"
class UButton;
/**
* Le menu principal : quelques boutons par-dessus la vue du monde.
*
* Classe Abstract comme les autres widgets du projet : on cree un
* WBP_MainMenu qui en herite, le C++ porte la logique et le Blueprint la mise
* en page.
*
* Le widget ne decide de rien, il appelle AMainMenuPlayerController. Le jour ou
* un bouton devra faire une action reseau, elle sera deja du bon cote : un
* widget n'existe que sur la machine du joueur local, le serveur ne le voit pas.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UMainMenuWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
protected:
/** Lance la partie solo. Nomme exactement "PlayButton" dans le WBP. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> PlayButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> QuitButton;
/**
* Coop a venir. BindWidgetOptional et non BindWidget : tu peux dessiner ton
* WBP_MainMenu sans eux aujourd'hui, le Blueprint compilera quand meme.
* S'ils existent, ils sont grises automatiquement.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UButton> HostButton;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UButton> JoinButton;
private:
UFUNCTION()
void HandlePlayClicked();
UFUNCTION()
void HandleQuitClicked();
};
@@ -0,0 +1,33 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Camera/CameraActor.h"
#include "MenuCameraSpot.generated.h"
/**
* Un angle de vue possible pour le fond du menu principal.
*
* Pose-en autant que tu veux dans la map de menu : le controller en tire une au
* hasard a chaque lancement.
*
* Une classe dediee plutot qu'un tag "MenuCamera" sur une ACameraActor -- le
* reflexe Unity : la recherche est typee au lieu d'etre une chaine de
* caracteres, et la classe a un endroit ou porter ses propres reglages.
*/
UCLASS()
class SURVIVAL_PROJET_API AMenuCameraSpot : public ACameraActor
{
GENERATED_BODY()
public:
AMenuCameraSpot();
/**
* Decoche pour retirer cet angle du tirage sans supprimer l'acteur.
* Pratique pour mettre un cadrage de cote le temps de le retravailler.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Menu")
bool bEnabled = true;
};
@@ -0,0 +1,90 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interactable.h"
#include "PickupItem.generated.h"
class UItemDataAsset;
class USphereComponent;
class UStaticMeshComponent;
/**
* Un objet pose dans le monde. Il ne DECRIT pas l'objet, il ne fait que
* pointer vers son UItemDataAsset et porter une quantite.
*
* Consequence pratique : pour creer un nouveau type de ramassable, tu n'as
* plus besoin d'un Blueprint par objet. Un seul BP_Pickup suffit, tu changes
* juste l'ItemData sur chaque instance posee dans le niveau.
*/
UCLASS()
class SURVIVAL_PROJET_API APickupItem : public AActor, public IInteractable
{
GENERATED_BODY()
public:
APickupItem();
/**
* Configure un pickup engendre a l'execution (objet jete au sol).
* A appeler entre SpawnActorDeferred() et FinishSpawning() pour que
* OnConstruction ait deja les bonnes donnees et pose le bon mesh.
*/
UFUNCTION(BlueprintCallable, Category = "Item")
void InitPickup(UItemDataAsset* InItemData, int32 InQuantity, int32 InRemainingUses = -1);
// --- IInteractable ---
virtual FText GetInteractionPrompt_Implementation() const override;
virtual bool CanInteract_Implementation(AActor* Interactor) const override;
virtual void Interact_Implementation(AActor* Interactor) override;
virtual void OnBeginFocus_Implementation() override;
virtual void OnEndFocus_Implementation() override;
protected:
/**
* Sans garde WITH_EDITOR : on veut aussi que le mesh se pose correctement
* sur les pickups engendres a l'execution quand on jette un objet.
*/
virtual void OnConstruction(const FTransform& Transform) override;
/** Visuel uniquement, sans collision. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UStaticMeshComponent> Mesh;
/**
* Volume que le trace d'interaction vient toucher.
* Volontairement separe du mesh : beaucoup de meshes d'asset packs,
* en particulier la vegetation, n'ont aucune geometrie de collision.
* Avec une sphere dediee, le pickup fonctionne quel que soit le visuel.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<USphereComponent> InteractionSphere;
/** Quel objet ce pickup represente. A assigner sur chaque instance. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
TObjectPtr<UItemDataAsset> ItemData;
/** Combien d'exemplaires ce tas contient. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item", meta = (ClampMin = "1"))
int32 Quantity = 1;
/**
* Charges restantes pour un objet a usages multiples.
* -1 = plein, ce qui est le cas de tout objet pose a la main dans le niveau.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 RemainingUses = -1;
/**
* Reprend automatiquement le WorldMesh de l'ItemData dans l'editeur.
* Decoche pour imposer un mesh different sur une instance precise.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
bool bUseMeshFromItemData = true;
/** Active le Custom Depth quand l'objet est vise. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
bool bHighlightOnFocus = true;
};
@@ -0,0 +1,90 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ScreenFadeComponent.generated.h"
class UUserWidget;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnScreenFadeFinished);
/**
* Fondu plein ecran, pose sur un PlayerController.
*
* Un composant et non du code dans le controller : le fondu sert au jeu (mort,
* respawn) comme au menu principal, et deux implementations du meme voile
* finiraient par diverger.
*
* Le voile est un widget UMG et non le fondu camera du moteur : StartCameraFade
* ne teinte que le rendu de la SCENE et laisse tout le HUD visible par-dessus.
* On garde quand meme StartCameraFade en parallele, uniquement pour le fondu
* AUDIO, que le voile ne sait pas faire.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class SURVIVAL_PROJET_API UScreenFadeComponent : public UActorComponent
{
GENERATED_BODY()
public:
UScreenFadeComponent();
/**
* Se declenche a la fin de CHAQUE fondu, entrant comme sortant.
* A celui qui ecoute de savoir ce qu'il attendait.
*/
UPROPERTY(BlueprintAssignable, Category = "Fondu")
FOnScreenFadeFinished OnFadeFinished;
/** Du noir vers le jeu. */
UFUNCTION(BlueprintCallable, Category = "Fondu")
void FadeIn(float Duration);
/** Du jeu vers le noir, ou l'ecran reste jusqu'au prochain FadeIn. */
UFUNCTION(BlueprintCallable, Category = "Fondu")
void FadeOut(float Duration);
/** Couvre l'ecran immediatement, sans animation ni delegue. */
UFUNCTION(BlueprintCallable, Category = "Fondu")
void SnapToBlack();
UFUNCTION(BlueprintPure, Category = "Fondu")
bool IsFading() const { return bFading; }
protected:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/** Ton WBP_Fade : un simple rectangle plein ecran, sans logique. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Fondu")
TSubclassOf<UUserWidget> FadeWidgetClass;
/** Noir classique. Le blanc donne un reveil brutal. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Fondu")
FLinearColor FadeColor = FLinearColor::Black;
private:
/** Cree le voile au premier besoin, et pas avant : il depend du controller. */
void EnsureWidget();
/** 1 = ecran entierement couvert, 0 = voile invisible. */
void SetCoverage(float Coverage);
APlayerController* GetOwningController() const;
UPROPERTY(Transient)
TObjectPtr<UUserWidget> FadeWidget;
/** 0 = ecran couvert, 1 = jeu entierement visible. */
float Alpha = 1.f;
/** Valeur visee par Alpha : 0 pour partir au noir, 1 pour revenir. */
float Target = 1.f;
/** Duree du fondu en cours, distincte des reglages qui restent constants. */
float CurrentDuration = 1.f;
bool bFading = false;
};
@@ -0,0 +1,44 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "SurvivalGameInstance.generated.h"
/**
* Le seul objet qui survit aux changements de map.
*
* C'est l'equivalent le plus proche d'un singleton DontDestroyOnLoad d'Unity :
* il naît au lancement du jeu et meurt a sa fermeture. Tout le reste --
* GameMode, GameState, PlayerController, pawns, widgets -- est detruit et
* recree a chaque OpenLevel.
*
* Y mettre : les reglages, la sauvegarde en cours, et plus tard l'hebergement
* et le rejoindre-une-partie. Surtout pas d'etat de gameplay, qui appartient au
* monde et doit disparaitre avec lui.
*/
UCLASS()
class SURVIVAL_PROJET_API USurvivalGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
/** Nom de la derniere camera de menu utilisee. NAME_None au premier lancement. */
UFUNCTION(BlueprintPure, Category = "Menu")
FName GetLastMenuCameraName() const { return LastMenuCameraName; }
UFUNCTION(BlueprintCallable, Category = "Menu")
void SetLastMenuCameraName(FName SpotName) { LastMenuCameraName = SpotName; }
private:
/**
* Stocke un NOM et pas un index : les index changent des qu'on ajoute ou
* supprime une camera dans la map, et on retomberait sur autre chose.
*
* Ne sert qu'a eviter de retirer deux fois de suite le meme angle quand le
* joueur revient au menu -- d'ou le Transient, ca n'a pas a etre sauvegarde.
*/
UPROPERTY(Transient)
FName LastMenuCameraName = NAME_None;
};
@@ -0,0 +1,28 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "SurvivalGameState.generated.h"
/**
* L'etat du monde que TOUT LE MONDE doit connaitre.
*
* Vide pour l'instant, pose des maintenant parce que la rebrancher plus tard
* suppose de retoucher le GameMode et tous les Blueprints qui en derivent.
*
* Le piege a retenir pour la coop : AGameMode n'existe QUE sur le serveur.
* Sur un client, GetGameMode() renvoie nullptr, toujours -- ce n'est pas un bug
* mais le design du framework. Le GameMode arbitre, il ne se montre pas.
* Le GameState, lui, est replique a tout le monde : c'est le seul endroit ou
* un client peut lire l'etat de la partie.
*
* Y mettra : l'heure du jour, le numero de jour, la seed du monde, l'etat des
* ressources partagees. Pas les stats d'un joueur, qui vont sur son PlayerState.
*/
UCLASS()
class SURVIVAL_PROJET_API ASurvivalGameState : public AGameStateBase
{
GENERATED_BODY()
};
@@ -0,0 +1,23 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "SurvivalPlayerState.generated.h"
/**
* Ce que les AUTRES joueurs savent de toi.
*
* Vide pour l'instant. Y mettra : pseudo, couleur, temps de survie, nombre de
* morts -- tout ce qu'un coequipier doit pouvoir lire sur ton compte.
*
* La regle de partage avec le pawn est simple : le PlayerState survit a la mort
* du pawn, le pawn non. L'inventaire et les jauges restent donc sur le pawn,
* puisque la regle du jeu veut qu'on perde tout au sol en mourant.
*/
UCLASS()
class SURVIVAL_PROJET_API ASurvivalPlayerState : public APlayerState
{
GENERATED_BODY()
};
@@ -0,0 +1,116 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SurvivalStatsComponent.generated.h"
/** Diffuse a chaque changement de vie, faim ou soif. L'UI s'y abonne. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalStatsChanged);
/** Diffuse une seule fois, quand la vie atteint zero. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalDeath);
/**
* Vie, faim et soif du personnage.
*
* Les valeurs descendent sur un timer a basse frequence et non dans Tick :
* une jauge qui perd trois points par minute n'a aucune raison d'etre
* recalculee 120 fois par seconde.
*
* Les vitesses sont exprimees PAR MINUTE, parce que c'est l'unite dans
* laquelle on raisonne quand on equilibre un jeu de survie -- "cent points
* de faim en trente minutes" se lit, "0.055 par seconde" non.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class SURVIVAL_PROJET_API USurvivalStatsComponent : public UActorComponent
{
GENERATED_BODY()
public:
USurvivalStatsComponent();
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/** Rend de la vie, de la faim et de la soif. Les valeurs sont bornees au max. */
UFUNCTION(BlueprintCallable, Category = "Survie")
void ApplyRestore(float HealthAmount, float HungerAmount, float ThirstAmount);
UFUNCTION(BlueprintCallable, Category = "Survie")
void ApplyDamage(float Amount);
UFUNCTION(BlueprintPure, Category = "Survie")
float GetHealth() const { return Health; }
UFUNCTION(BlueprintPure, Category = "Survie")
float GetHunger() const { return Hunger; }
UFUNCTION(BlueprintPure, Category = "Survie")
float GetThirst() const { return Thirst; }
/** Ratios 0-1, directement utilisables par une ProgressBar. */
UFUNCTION(BlueprintPure, Category = "Survie")
float GetHealthPercent() const { return MaxHealth > 0.f ? Health / MaxHealth : 0.f; }
UFUNCTION(BlueprintPure, Category = "Survie")
float GetHungerPercent() const { return MaxHunger > 0.f ? Hunger / MaxHunger : 0.f; }
UFUNCTION(BlueprintPure, Category = "Survie")
float GetThirstPercent() const { return MaxThirst > 0.f ? Thirst / MaxThirst : 0.f; }
UFUNCTION(BlueprintPure, Category = "Survie")
bool IsDead() const { return bDead; }
UPROPERTY(BlueprintAssignable, Category = "Survie")
FOnSurvivalStatsChanged OnStatsChanged;
UPROPERTY(BlueprintAssignable, Category = "Survie")
FOnSurvivalDeath OnDied;
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Survie|Vie", meta = (ClampMin = "1"))
float MaxHealth = 100.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Survie|Faim", meta = (ClampMin = "1"))
float MaxHunger = 100.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Survie|Soif", meta = (ClampMin = "1"))
float MaxThirst = 100.f;
/** Points de faim perdus par minute. 3.3 vide une jauge pleine en ~30 min. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie|Faim", meta = (ClampMin = "0"))
float HungerDecayPerMinute = 3.3f;
/** La soif descend plus vite que la faim : c'est le besoin le plus pressant. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie|Soif", meta = (ClampMin = "0"))
float ThirstDecayPerMinute = 5.f;
/** Vie perdue par minute quand la faim est a zero. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie|Vie", meta = (ClampMin = "0"))
float StarvationDamagePerMinute = 6.f;
/** Vie perdue par minute quand la soif est a zero. Cumulable avec la faim. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie|Vie", meta = (ClampMin = "0"))
float DehydrationDamagePerMinute = 9.f;
/**
* Secondes entre deux mises a jour. Une seconde suffit largement et reste
* assez fin pour que les barres bougent de facon fluide a l'oeil.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Survie", meta = (ClampMin = "0.05"))
float UpdateInterval = 1.f;
private:
/** Appelee par le timer : fait descendre les jauges et applique les degats. */
void UpdateStats();
FTimerHandle UpdateTimerHandle;
float Health = 0.f;
float Hunger = 0.f;
float Thirst = 0.f;
bool bDead = false;
};
@@ -0,0 +1,73 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SurvivalStatsWidget.generated.h"
class UProgressBar;
class USurvivalStatsComponent;
/**
* Les trois jauges de survie : vie, faim, soif.
*
* Aucun Tick : le widget s'abonne a OnStatsChanged, qui ne se declenche
* qu'une fois par seconde ou lors d'une consommation.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API USurvivalStatsWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
UFUNCTION(BlueprintCallable, Category = "Survie")
void Refresh();
/** (Re)branche sur les stats du pawn possede. Voir UInventoryWidget. */
void BindToOwningPawn();
protected:
/** A nommer exactement "HealthBar", "HungerBar" et "ThirstBar". */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UProgressBar> HealthBar;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UProgressBar> HungerBar;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UProgressBar> ThirstBar;
/** En dessous de ce ratio, la barre passe en couleur d'alerte. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie", meta = (ClampMin = "0", ClampMax = "1"))
float CriticalThreshold = 0.2f;
/** Couleur d'alerte, commune aux trois jauges. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Survie")
FLinearColor CriticalColor = FLinearColor(0.9f, 0.1f, 0.1f, 1.f);
private:
UFUNCTION()
void HandleStatsChanged();
USurvivalStatsComponent* ResolveStats() const;
/** Applique le remplissage et la couleur d'une barre. */
void ApplyBar(UProgressBar* Bar, float Percent, const FLinearColor& BaseColor) const;
TWeakObjectPtr<USurvivalStatsComponent> BoundStats;
/**
* Couleurs d'origine relevees au NativeConstruct.
*
* On les capture depuis les widgets eux-memes plutot que de les redeclarer
* en C++ : la palette reste entierement dans le Designer, et le code ne
* fait que basculer vers l'alerte puis revenir.
*/
FLinearColor HealthBaseColor = FLinearColor::White;
FLinearColor HungerBaseColor = FLinearColor::White;
FLinearColor ThirstBaseColor = FLinearColor::White;
};
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class Survival_projet : ModuleRules
{
public Survival_projet(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG" });
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
@@ -0,0 +1,6 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Survival_projet.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, Survival_projet, "Survival_projet" );
+6
View File
@@ -0,0 +1,6 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
+15
View File
@@ -0,0 +1,15 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
using System.Collections.Generic;
public class Survival_projetEditorTarget : TargetRules
{
public Survival_projetEditorTarget( TargetInfo Target) : base(Target)
{
Type = TargetType.Editor;
DefaultBuildSettings = BuildSettingsVersion.V7;
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_8;
ExtraModuleNames.Add("Survival_projet");
}
}