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