// Fill out your copyright notice in the Description page of Project Settings. #include "FpsPlayer.h" #include "Animation/AnimInstance.h" #include "Animation/AnimMontage.h" #include "Camera/CameraComponent.h" #include "Camera/CameraTypes.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 "CraftingComponent.h" #include "HeadBobComponent.h" #include "InteractionComponent.h" #include "InventoryComponent.h" #include "ItemDataAsset.h" #include "Net/UnrealNetwork.h" #include "PickupItem.h" #include "SurvivalGameInstance.h" #include "SurvivalStatsComponent.h" #include "SurvivalUserSettings.h" float FCameraSpring::Update(float Step, float Frequency, float Damping, float RampTime, float MaxOffset) { // L'impulsion n'est pas versee d'un coup : la vitesse passerait de 0 a // plusieurs centaines d'unites par seconde en une frame, et cette // discontinuite se lit comme un a-coup. On l'etale sur RampTime. if (!FMath::IsNearlyZero(PendingImpulse)) { const float ReleaseRatio = (RampTime > KINDA_SMALL_NUMBER) ? FMath::Clamp(Step / RampTime, 0.f, 1.f) : 1.f; const float Released = PendingImpulse * ReleaseRatio; Velocity += Released; PendingImpulse -= 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 * Frequency; const float Acceleration = -(Omega * Omega * Offset) - (2.f * Damping * Omega * Velocity); Velocity += Acceleration * Step; Offset = FMath::Clamp(Offset + Velocity * Step, -MaxOffset, MaxOffset); return Offset; } // 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; // Bras vue premiere personne, accroches a la camera pour suivre le regard. FirstPersonArms = CreateDefaultSubobject(TEXT("FirstPersonArms")); FirstPersonArms->SetupAttachment(FirstPersonCamera); // Symetrique exact du mesh 3e personne ci-dessous : chacun ne voit que ses // propres bras. Sans ca, les bras d'un ami flotteraient devant notre objectif. FirstPersonArms->SetOnlyOwnerSee(true); // Un mesh colle a l'objectif projetterait une ombre enorme en travers du // decor. L'ombre du joueur, elle, vient du mesh 3e personne. FirstPersonArms->SetCastShadow(false); FirstPersonArms->bCastDynamicShadow = false; // Les bras ne sont pas dans le monde, ils sont devant l'objectif : une // collision les ferait pousser des objets et surtout bloquer le trace // d'interaction, qui part exactement d'entre eux. FirstPersonArms->SetCollisionEnabled(ECollisionEnabled::NoCollision); // Chez l'hote, le pawn de chaque ami porte AUSSI ce composant, invisible pour // tout le monde. Sans cette option le serveur evaluerait quatre AnimBlueprints // pour quatre meshes que personne ne rend. FirstPersonArms->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered; // Point de depart a affiner dans BP_FpsPlayer : la bonne valeur depend de // l'origine du FBX (racine aux pieds ? au bassin ?) et de l'orientation // d'export. Le yaw de -90 est le decalage habituel d'un FBX Blender/Maya. FirstPersonArms->SetRelativeLocationAndRotation(FVector(0.f, 0.f, -160.f), FRotator(0.f, -90.f, 0.f)); HeadBob = CreateDefaultSubobject(TEXT("HeadBob")); InteractionComponent = CreateDefaultSubobject(TEXT("InteractionComponent")); InventoryComponent = CreateDefaultSubobject(TEXT("InventoryComponent")); CraftingComponent = CreateDefaultSubobject(TEXT("CraftingComponent")); 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); // Sans cet abonnement, un objet fabrique alors que l'inventaire est plein // serait purement et simplement perdu. CraftingComponent->OnCraftOverflow.AddDynamic(this, &AFpsPlayer::HandleCraftOverflow); // 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) { // La DECLARATION des contextes remappables appartient au // GameInstance -- le menu principal n'a pas de pawn et doit // pourtant afficher les touches. On la demande quand meme ici, et // AVANT d'activer le contexte : c'est elle qui traduit les touches // vers la disposition clavier du joueur, et un AZERTY doit trouver // ses touches des la premiere image, pas au premier passage dans // l'ecran de reglages. L'appel est idempotent. if (USurvivalGameInstance* GameInstance = GetGameInstance()) { GameInstance->RegisterMappableContexts(); } 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(); // Le curseur "secousses de camera" vaut aussi pour le balancement : un joueur // qui le met a zero demande une camera fixe, pas une camera fixe qui respire. HeadBob->SetIntensityScale(CameraShakeMultiplier); // 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. Ressorts : impact (saut, atterrissage) et bascule de tete // ================================================================== PunchSpring.Update(Step, PunchFrequency, PunchDamping, PunchImpulseRampTime, PunchMaxOffset); // Le garde-fou est en unites normalisees, ou 1 = le pic d'une bascule // complete. A 1.5 on laisse passer un leger cumul quand deux impulsions se // suivent de tres pres, sans jamais laisser la tete partir. CrouchTiltSpring.Update(Step, CrouchTiltFrequency, CrouchTiltDamping, CrouchTiltRampTime, 1.5f); // ================================================================== // 3. Balancement de tete // ================================================================== // Integre ici et applique dans ApplyCameraTransform, exactement comme les // ressorts ci-dessus : c'est ce qui permet a CalcCamera de recomposer la // transform sans faire avancer le temps deux fois. HeadBob->UpdateBob(Step); // ================================================================== // 4. 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)); // L'application est laissee a ApplyCameraTransform : CalcCamera la rappelle // juste avant que la vue soit lue, avec l'etat le plus frais de la capsule. ApplyCameraTransform(); } void AFpsPlayer::ApplyCameraTransform() { // ================================================================== // 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 -- // a condition de lire les deux au meme instant, d'ou l'appel depuis CalcCamera. const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight(); // Le balancement s'AJOUTE au reste. Son Y est un decalage lateral dans le // repere de la capsule, dont le yaw suit deja le regard (bUseControllerRotationYaw) : // la tete se porte donc bien a droite de ce qu'on regarde, sans conversion. // Le tangage, lui, n'est pas dans la capsule -- c'est voulu, le balancement // reste horizontal meme quand on regarde ses pieds. const FVector BobOffset = HeadBob->GetLocationOffset(); FirstPersonCamera->SetRelativeLocation(FVector( BobOffset.X, BobOffset.Y, EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset + BobOffset.Z)); // ================================================================== // Rotation // ================================================================== // La bascule ne depend plus de l'avancement de la transition : c'est l'etat // d'un ressort, donc elle reste continue meme si le joueur change d'avis en // plein mouvement. L'ancienne forme sin(alpha*PI) multipliee par un signe // sautait de +CrouchTiltPitch a -CrouchTiltPitch en une frame des qu'on // s'accroupissait et se relevait coup sur coup. const float CrouchTilt = CrouchTiltSpring.Offset; FRotator ViewOffset(-CrouchTiltPitch * CrouchTilt, 0.f, CrouchTiltRoll * CrouchTilt); // 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 += PunchSpring.Offset * PunchPitchPerCm; // Roulis et tangage du balancement, dans le meme sac que le reste. ViewOffset += HeadBob->GetRotationOffset(); if (const AController* OwningController = GetController()) { FirstPersonCamera->SetWorldRotation(OwningController->GetControlRotation() + ViewOffset); } } void AFpsPlayer::CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult) { ApplyCameraTransform(); Super::CalcCamera(DeltaTime, OutResult); } void AFpsPlayer::AddCrouchTilt(float Direction) { // Impulsion normalisee pour qu'un ressort critique culmine exactement a 1 : // x(t) = v0 * t * e^(-w*t) atteint son maximum v0/(e*w) en t = 1/w, donc // v0 = e*w. CrouchTiltPitch et CrouchTiltRoll restent ainsi des degres au // pic, et non un reglage a l'aveugle. const float Omega = 2.f * PI * CrouchTiltFrequency; CrouchTiltSpring.AddImpulse(Direction * UE_EULERS_NUMBER * Omega); } 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. PunchSpring.AddImpulse(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; } SetSprinting(true); } void AFpsPlayer::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); // COND_SkipOwner : celui qui sprinte le sait deja, il vient de l'appliquer // en local. Le renvoyer ecraserait son etat courant par une valeur qui a un // aller-retour de retard -- exactement ce qui fait "sauter" un sprint qu'on // relance vite. DOREPLIFETIME_CONDITION(AFpsPlayer, bIsSprinting, COND_SkipOwner); } void AFpsPlayer::StopSprint() { SetSprinting(false); } void AFpsPlayer::SetSprinting(bool bNewSprinting) { if (bIsSprinting == bNewSprinting) { return; } bIsSprinting = bNewSprinting; ApplySprintSpeed(); // Applique en local d'abord pour que la vitesse soit instantanee sous les // doigts, puis previent le serveur. Le sens inverse -- attendre la reponse -- // donnerait un sprint qui demarre avec un temps de retard. if (!HasAuthority()) { Server_SetSprinting(bNewSprinting); } } void AFpsPlayer::Server_SetSprinting_Implementation(bool bNewSprinting) { bIsSprinting = bNewSprinting; ApplySprintSpeed(); } void AFpsPlayer::OnRep_IsSprinting() { ApplySprintSpeed(); } void AFpsPlayer::ApplySprintSpeed() { GetCharacterMovement()->MaxWalkSpeed = bIsSprinting ? SprintSpeed : WalkSpeed; } void AFpsPlayer::HandleSprintReleased() { // En bascule, relacher la touche ne doit rien faire. if (bSprintToggleMode) { return; } StopSprint(); } void AFpsPlayer::UseSelectedItem() { const int32 SelectedIndex = InventoryComponent->GetSelectedHotbarIndex(); // L'hote agit tout de suite, le client demande. On envoie meme si le slot // parait vide chez nous : c'est le serveur qui fait foi, et son inventaire // peut avoir une frame d'avance sur le notre. if (!HasAuthority()) { Server_UseItem(SelectedIndex); return; } PerformUseItem(SelectedIndex); } void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex) { PerformUseItem(SlotIndex); } void AFpsPlayer::PerformUseItem(int32 SlotIndex) { if (!HasAuthority() || !InventoryComponent->GetSlots().IsValidIndex(SlotIndex)) { return; } const FInventorySlot Selected = InventoryComponent->GetSlots()[SlotIndex]; 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(SlotIndex); } 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) { return DropFromInventory(InventoryComponent, SlotIndex, Quantity); } void AFpsPlayer::Server_DropFromInventory_Implementation(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity) { DropFromInventory(Inventory, SlotIndex, Quantity); } bool AFpsPlayer::DropFromInventory(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity) { if (!Inventory) { return false; } // Jeter engendre un acteur dans le monde : c'est une action d'autorite. // La position de depot est calculee cote SERVEUR a partir de la camera // repliquee du pawn -- a quelques centimetres pres du point vise par le // client, ce qui n'a aucune importance pour un objet pose au sol. if (!HasAuthority()) { Server_DropFromInventory(Inventory, SlotIndex, Quantity); return true; } if (!DroppedPickupClass) { UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, impossible de jeter un objet.")); return false; } FInventorySlot Taken; if (!Inventory->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 d'ou il vient, usure comprise. Inventory->AddItem(Taken.Item, Taken.Quantity, Taken.RemainingUses); return false; } return true; } void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses) { if (!Item || Quantity <= 0) { return; } if (!DroppedPickupClass) { UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sortis de la fabrication sont perdus."), Quantity, *GetNameSafe(Item)); return; } FInventorySlot Overflow; Overflow.Item = Item; Overflow.Quantity = Quantity; Overflow.RemainingUses = RemainingUses; // Aux pieds et non devant les yeux comme DropSlot : le joueur fabrique le // nez dans un menu, il ne vise rien. Poser a distance risquerait de traverser // une cloison qu'on ne regarde meme pas. const FVector DropLocation = GetActorLocation() + GetActorForwardVector() * 40.f; if (!SpawnPickup(Overflow, PlaceOnGround(DropLocation))) { UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s issus de la fabrication."), Quantity, *GetNameSafe(Item)); } } void AFpsPlayer::DropAllItems() { // Declenche par la mort, qui sera arbitree par le serveur. La garde est la // pour le jour ou un client l'appellerait : il engendrerait des pickups // visibles de lui seul, qui disparaitraient au premier nettoyage reseau. if (!HasAuthority()) { return; } 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. On teste l'INTENTION // (bWantsToCrouch) et non l'etat applique (bIsCrouched) : le CharacterMovement // ne redimensionne la capsule qu'a son propre tick, donc deux appuis dans la // meme frame verraient tous les deux bIsCrouched a false et le personnage // resterait accroupi. Symptome typique quand on martele la touche. if (bCrouchToggleMode && GetCharacterMovement()->bWantsToCrouch) { 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() { // Le geste part AVANT la RPC, pas apres sa reponse : un montage declenche a // l'aller-retour serveur donnerait des bras qui repondent 60 ms apres le clic, // et le joueur lirait ca comme un jeu qui rame. Le prix a payer est un geste // dans le vide si le serveur refuse l'interaction -- infiniment moins genant. // // Le test de type est ici et pas dans le composant d'interaction : celui-ci ne // connait que l'interface IInteractable et n'a pas a apprendre ce qu'est un // pickup. Un coffre ou un etabli n'ont pas a declencher un geste de ramassage. if (Cast(InteractionComponent->GetFocusedActor())) { PlayArmsMontage(GrabMontage); } InteractionComponent->TryInteract(); } float AFpsPlayer::PlayArmsMontage(UAnimMontage* Montage, float PlayRate) { // IsLocallyControlled : chez l'hote, le pawn de chaque ami passerait aussi par // ici a chaque ramassage et animerait un mesh que personne ne rend. if (!Montage || !IsLocallyControlled()) { return 0.f; } UAnimInstance* ArmsAnim = FirstPersonArms->GetAnimInstance(); return ArmsAnim ? ArmsAnim->Montage_Play(Montage, PlayRate) : 0.f; } void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust) { Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust); CrouchTargetAlpha = 1.f; AddCrouchTilt(1.f); } void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust) { Super::OnEndCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust); CrouchTargetAlpha = 0.f; AddCrouchTilt(-1.f); }