// 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" #include "SurvivalUserSettings.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(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(TEXT("InteractionComponent")); InventoryComponent = CreateDefaultSubobject(TEXT("InventoryComponent")); SurvivalStats = CreateDefaultSubobject(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); // Les reglages d'abord : le pawn est recree a chaque respawn et repart des // valeurs de son CDO, il doit donc les relire lui-meme. L'abonnement couvre // ensuite les changements faits en cours de partie depuis le menu de pause. ApplyUserSettings(); if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) { Settings->OnSurvivalSettingsApplied.AddDynamic(this, &AFpsPlayer::ApplyUserSettings); } // Le FOV est interpole chaque frame vers BaseFOV : sans ce calage initial on // verrait la camera s'ouvrir lentement jusqu'a la valeur reglee a chaque // apparition. FirstPersonCamera->SetFieldOfView(BaseFOV); // 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(GetController())) { if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem(PlayerController->GetLocalPlayer())) { if (DefaultMappingContext) { // On ACTIVE le contexte ici, mais on ne le DECLARE pas comme // remappable : c'est USurvivalGameInstance qui s'en charge, parce // que le menu principal n'a pas de pawn et doit quand meme // afficher les touches. Subsystem->AddMappingContext(DefaultMappingContext, 0); } else { UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DefaultMappingContext n'est pas assigne, aucun input ne fonctionnera.")); } } } } void AFpsPlayer::EndPlay(const EEndPlayReason::Type EndPlayReason) { // Le singleton de reglages survit au monde : sans ce desabonnement, chaque // cadavre laisserait derriere lui un abonne mort de plus. if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) { Settings->OnSurvivalSettingsApplied.RemoveDynamic(this, &AFpsPlayer::ApplyUserSettings); } Super::EndPlay(EndPlayReason); } void AFpsPlayer::ApplyUserSettings() { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings) { // Pas de reglages : on garde les valeurs du Blueprint. Le jeu reste // jouable, il n'est simplement pas configurable. UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : USurvivalUserSettings introuvable, les valeurs du Blueprint sont conservees.")); return; } LookSensitivityX = Settings->GetLookMultiplierX(); LookSensitivityY = Settings->GetLookMultiplierY(); bInvertLookY = Settings->GetInvertLookY(); BaseFOV = Settings->GetFieldOfView(); CameraShakeMultiplier = Settings->GetCameraShakeMultiplier(); bSprintToggleMode = Settings->GetSprintToggle(); bCrouchToggleMode = Settings->GetCrouchToggle(); // Repasser en maintien alors que la touche n'est plus enfoncee laisserait le // personnage bloque en sprint : on repart d'un etat propre. if (!bSprintToggleMode && bIsSprinting) { StopSprint(); } } // 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) { // Le reglage s'applique ICI et pas sur chaque source d'impact : tout ce qui // secoue la camera passe par cette porte, y compris les explosions et les // reculs d'arme a venir. A 0, la camera ne bouge plus du tout. // Mis en attente plutot qu'applique directement : c'est UpdateCameraEffects // qui l'injecte progressivement dans le ressort. PendingPunch += Strength * CameraShakeMultiplier; } 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(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) { // Le mode maintien / bascule se decide dans les handlers et non ici : // rebrancher les bindings a chaud demanderait de reconstruire l'input // component a chaque changement de reglage. Input->BindAction(SprintAction, ETriggerEvent::Started, this, &AFpsPlayer::StartSprint); Input->BindAction(SprintAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleSprintReleased); } if (CrouchAction) { Input->BindAction(CrouchAction, ETriggerEvent::Started, this, &AFpsPlayer::StartCrouch); Input->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleCrouchReleased); } 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(); 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() 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(); // 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; } // En mode bascule, la meme touche coupe le sprint. if (bSprintToggleMode && bIsSprinting) { StopSprint(); return; } bIsSprinting = true; GetCharacterMovement()->MaxWalkSpeed = SprintSpeed; } void AFpsPlayer::StopSprint() { bIsSprinting = false; GetCharacterMovement()->MaxWalkSpeed = WalkSpeed; } void AFpsPlayer::HandleSprintReleased() { // En bascule, relacher la touche ne doit rien faire. if (bSprintToggleMode) { return; } StopSprint(); } 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( 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(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() { // En mode bascule, la meme touche releve le personnage. if (bCrouchToggleMode && bIsCrouched) { StopCrouch(); return; } StopSprint(); Crouch(); } void AFpsPlayer::HandleCrouchReleased() { if (bCrouchToggleMode) { return; } StopCrouch(); } 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; }