// Fill out your copyright notice in the Description page of Project Settings. #include "HarvestableResource.h" #include "Components/StaticMeshComponent.h" #include "Engine/World.h" #include "FpsPlayer.h" #include "HarvestableDataAsset.h" #include "ItemDataAsset.h" #include "Kismet/GameplayStatics.h" #include "NiagaraFunctionLibrary.h" #include "Net/UnrealNetwork.h" #include "TimerManager.h" AHarvestableResource::AHarvestableResource() { // Le Tick ne sert QU'AU balancement, et il s'allume tout seul le temps de // l'amortissement. Une foret de quatre cents arbres qui ticke en permanence // pour ne rien faire est exactement ce qu'on evite ici. PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.bStartWithTickEnabled = false; // Acteur du monde : le compte des coups appartient au serveur. Quatre clients // qui compteraient chacun leurs points de vie divergeraient, et l'arbre // tomberait chez l'un alors qu'il tient encore chez l'autre. bReplicates = true; SetReplicateMovement(false); Mesh = CreateDefaultSubobject(TEXT("Mesh")); SetRootComponent(Mesh); // QueryAndPhysics : l'arbre barre le passage en plus d'etre visable. Le trace // du coup passe par le canal Visibility, que la collision par defaut bloque. Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); // Le balancement bouge le mesh a l'execution : Static le cuirait sur place et // la rotation n'aurait aucun effet a l'ecran, sans le moindre avertissement. Mesh->SetMobility(EComponentMobility::Movable); } void AHarvestableResource::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AHarvestableResource, bDepleted); } void AHarvestableResource::BeginPlay() { Super::BeginPlay(); // Retenue ici et pas au constructeur : la rotation relative du mesh peut avoir // ete reglee dans le Blueprint ou sur l'instance posee dans le niveau, et c'est // a CETTE valeur que le balancement doit revenir. BaseMeshRotation = Mesh->GetRelativeRotation(); // AVANT le premier ApplyDepletionVisuals, qui peut deja poser la souche chez un // client arrivant sur un arbre abattu : on capturerait alors la souche comme // etat intact, et l'arbre ne repousserait jamais. IntactMesh = Mesh->GetStaticMesh(); if (HasAuthority() && HarvestData) { CurrentHealth = HarvestData->Health; } // Sur toutes les machines : chez l'hote parce qu'aucun OnRep ne se declenche // pour lui, chez un client parce que l'ordre d'arrivee entre la construction de // l'acteur et ses proprietes repliquees n'est garanti par rien. ApplyDepletionVisuals(); if (!HarvestData) { UE_LOG(LogTemp, Warning, TEXT("AHarvestableResource : %s n'a pas de HarvestData, il ne se recoltera jamais."), *GetName()); } } bool AHarvestableResource::AcceptsTool(const UItemDataAsset* Tool) const { if (!HarvestData) { return false; } // Mains vides = EToolType::None, exactement comme un objet qui n'est pas un // outil : tenir une pomme ne doit pas abattre plus qu'un poing. const EToolType HeldType = (Tool && Tool->IsTool()) ? Tool->ToolType : EToolType::None; return HeldType == HarvestData->RequiredTool; } EHarvestResult AHarvestableResource::TryHarvest(AFpsPlayer* Harvester, const UItemDataAsset* Tool, float Power, const FVector& ImpactPoint, const FVector& HitDirection) { // La garde est ici et pas seulement chez l'appelant : c'est le point d'entree // unique du systeme, et rien ne garantit qu'un futur appelant y pense. if (!HasAuthority() || !HarvestData || !Harvester || Power <= 0.f) { return EHarvestResult::Invalid; } if (bDepleted) { // Un « toc » et rien d'autre : le joueur doit entendre que sa souche ne // donne plus rien, sans y perdre de durabilite. Multicast_PlayHarvestFeedback(EHarvestFeedback::Ineffective, ImpactPoint, HitDirection); return EHarvestResult::AlreadyDepleted; } if (!AcceptsTool(Tool)) { Multicast_PlayHarvestFeedback(EHarvestFeedback::Ineffective, ImpactPoint, HitDirection); return EHarvestResult::WrongTool; } CurrentHealth -= Power; // Le butin par coup part AVANT le test d'epuisement : le coup qui abat rend // donc les deux tables. C'est ce que le joueur attend -- son dernier coup a // bien entaille le tronc en plus de le faire tomber. GrantDrops(HarvestData->PerHitDrops, Harvester); if (CurrentHealth > 0.f) { Multicast_PlayHarvestFeedback(EHarvestFeedback::Hit, ImpactPoint, HitDirection); return EHarvestResult::Harvested; } GrantDrops(HarvestData->DepletionDrops, Harvester); Multicast_PlayHarvestFeedback(EHarvestFeedback::FinalBlow, ImpactPoint, HitDirection); Deplete(); return EHarvestResult::Depleted; } int32 AHarvestableResource::GrantDrops(const TArray& Table, AFpsPlayer* Harvester) { int32 Granted = 0; for (const FHarvestDrop& Drop : Table) { const int32 Quantity = Drop.Roll(); if (Quantity <= 0) { continue; } // Passe par le pawn et pas par son inventaire : ce qui ne rentre pas doit // tomber au sol, et un UInventoryComponent ne sait pas engendrer un acteur. Harvester->GiveItemOrDrop(Drop.Item, Quantity); ++Granted; } return Granted; } void AHarvestableResource::Deplete() { bDepleted = true; // Chez l'hote aucun OnRep ne se declenche : on applique nous-memes juste apres // avoir ecrit la propriete. Meme schema que SetCharacterAppearance. ApplyDepletionVisuals(); const float Delay = HarvestData ? HarvestData->RegrowDelay : 0.f; if (Delay <= 0.f) { return; } GetWorldTimerManager().SetTimer(RegrowTimer, this, &AHarvestableResource::Regrow, Delay, /*bLoop=*/false); } void AHarvestableResource::Regrow() { if (!HasAuthority()) { return; } CurrentHealth = HarvestData ? HarvestData->Health : 0.f; bDepleted = false; ApplyDepletionVisuals(); } void AHarvestableResource::OnRep_Depleted() { ApplyDepletionVisuals(); } void AHarvestableResource::ApplyDepletionVisuals() { if (!bDepleted) { SetActorHiddenInGame(false); SetActorEnableCollision(true); // IntactMesh peut etre nul le tout premier coup -- BeginPlay ne l'a pas // encore capture -- auquel cas il n'y a de toute facon rien a reposer. if (IntactMesh && Mesh->GetStaticMesh() != IntactMesh) { Mesh->SetStaticMesh(IntactMesh); } return; } if (DepletedMesh) { Mesh->SetStaticMesh(DepletedMesh); SetActorHiddenInGame(false); // Une souche reste solide : on la garde visable, ce qui permet au coup // suivant de rencontrer quelque chose et de sonner « inefficace » plutot // que de traverser dans le vide. SetActorEnableCollision(true); return; } // Sans mesh de remplacement, la ressource disparait entierement. La collision // PART avec elle : la laisser donnerait un mur invisible en pleine foret. SetActorHiddenInGame(true); SetActorEnableCollision(false); } void AHarvestableResource::Multicast_PlayHarvestFeedback_Implementation(EHarvestFeedback Feedback, FVector_NetQuantize ImpactPoint, FVector_NetQuantizeNormal HitDirection) { PlayHarvestFeedback(Feedback, ImpactPoint, HitDirection); } void AHarvestableResource::PlayHarvestFeedback(EHarvestFeedback Feedback, const FVector& ImpactPoint, const FVector& HitDirection) { if (!HarvestData) { return; } // Un serveur dedie n'a ni haut-parleur ni ecran : les FX y seraient du travail // pur. En serveur d'ecoute la fenetre de l'hote est un client comme un autre, // donc elle passe cette garde. if (IsNetMode(NM_DedicatedServer)) { return; } USoundBase* Sound = nullptr; UNiagaraSystem* Effect = nullptr; switch (Feedback) { case EHarvestFeedback::Hit: Sound = HarvestData->HitSound; Effect = HarvestData->HitEffect; break; case EHarvestFeedback::FinalBlow: // Le son de chute REMPLACE celui du coup, il ne s'y ajoute pas : deux // impacts sur la meme frame s'entendent comme un doublon, pas comme un // evenement plus fort. Sound = HarvestData->DepletionSound ? HarvestData->DepletionSound.Get() : HarvestData->HitSound.Get(); Effect = HarvestData->DepletionEffect ? HarvestData->DepletionEffect.Get() : HarvestData->HitEffect.Get(); break; case EHarvestFeedback::Ineffective: Sound = HarvestData->IneffectiveHitSound; break; } if (Sound) { UGameplayStatics::PlaySoundAtLocation(this, Sound, ImpactPoint); } if (Effect) { // Les copeaux repartent VERS le joueur : -HitDirection. Oriente dans le sens // du coup, ils gicleraient dans le tronc et on ne verrait rien. UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, Effect, ImpactPoint, (-HitDirection).Rotation()); } // Pas de balancement sur un coup qui n'a rien fait : une souche qui oscille // sous un coup inefficace annonce exactement le contraire de ce qui se passe. if (Feedback != EHarvestFeedback::Ineffective) { StartShake(HitDirection); } } void AHarvestableResource::StartShake(const FVector& HitDirection) { if (!HarvestData || HarvestData->HitShakeAngle <= 0.f || HarvestData->HitShakeDuration <= 0.f) { return; } // L'axe de rotation est perpendiculaire au coup ET a la verticale : l'arbre // penche DANS le sens ou on l'a frappe. Le produit vectoriel degenere quand on // frappe a la verticale -- de haut en bas sur un rocher -- d'ou le repli. FVector WorldAxis = FVector::CrossProduct(FVector::UpVector, HitDirection); if (!WorldAxis.Normalize()) { WorldAxis = GetActorRightVector(); } // En espace local : le mesh tourne dans le repere de son parent, et un arbre // pose avec un yaw quelconque pencherait de travers si l'axe restait en monde. ShakeAxis = GetActorTransform().InverseTransformVectorNoScale(WorldAxis).GetSafeNormal(); // Reparti de zero meme si un balancement tournait deja : marteler l'arbre doit // relancer l'oscillation a pleine amplitude, pas l'accumuler. ShakeElapsed = 0.f; SetActorTickEnabled(true); } void AHarvestableResource::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (ShakeElapsed < 0.f || !HarvestData) { SetActorTickEnabled(false); return; } ShakeElapsed += DeltaTime; const float Duration = HarvestData->HitShakeDuration; if (ShakeElapsed >= Duration) { // On repose la rotation d'origine EXACTEMENT plutot que de laisser la // derniere valeur calculee : a un cheveu pres, l'arbre resterait penche, et // vingt coups plus tard il pousserait franchement de travers. Mesh->SetRelativeRotation(BaseMeshRotation); ShakeElapsed = -1.f; SetActorTickEnabled(false); return; } const float Alpha = ShakeElapsed / Duration; // Amortissement au carre plutot que lineaire : l'oscillation meurt vite, ce qui // donne le poids d'un tronc. Lineaire, elle traine et l'arbre parait mou. const float Damping = FMath::Square(1.f - Alpha); const float Angle = HarvestData->HitShakeAngle * Damping * FMath::Sin(2.f * PI * HarvestData->HitShakeFrequency * ShakeElapsed); // Composition par quaternion et pas addition de FRotator : additionner deux // rotations d'Euler n'est une composition correcte que par chance, et l'arbre // pose avec un pitch non nul partirait de travers. const FQuat Offset(ShakeAxis, FMath::DegreesToRadians(Angle)); Mesh->SetRelativeRotation(Offset * BaseMeshRotation.Quaternion()); }