Import initial du projet Survival (UE 5.8)
Boucle de jeu complète : récolte, inventaire, consommation, stats de survie, mort et respawn. Gameplay en C++, Blueprints réservés au câblage d'assets. Assets binaires (.uasset, .umap, textures, audio) suivis via Git LFS. Binaries/, Intermediate/, Saved/ et DerivedDataCache/ sont ignorés : régénérés au build, ils pèsent 3 Go pour 1,3 Go de contenu utile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,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();
|
||||
};
|
||||
@@ -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" );
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user