// 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(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(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(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(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()); Inventory->SelectHotbarSlot(Number - 1); } void AFpsPlayerController::HandleHotbarScroll(const FInputActionValue& Value) { const float Axis = Value.Get(); 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(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(this, CrosshairClass); if (Crosshair) { Crosshair->AddToViewport(ZOrderCrosshair); } } if (!InteractionPrompt) { if (InteractionPromptClass) { InteractionPrompt = CreateWidget(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(this, SurvivalStatsClass); if (SurvivalStatsWidget) { SurvivalStatsWidget->AddToViewport(ZOrderSurvivalStats); } } if (!Hotbar && HotbarClass) { Hotbar = CreateWidget(this, HotbarClass); if (Hotbar) { Hotbar->AddToViewport(ZOrderHotbar); } } if (!InventoryWidget) { if (InventoryClass) { InventoryWidget = CreateWidget(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(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); }