(Feat) Add Harvetable

This commit is contained in:
2026-08-16 19:35:27 +02:00
parent d6f46f32af
commit 2c272c11b9
41 changed files with 1759 additions and 71 deletions
+116 -21
View File
@@ -19,6 +19,7 @@
#include "CraftingComponent.h"
#include "FootstepComponent.h"
#include "FpsPlayerController.h"
#include "HarvestingComponent.h"
#include "HeadBobComponent.h"
#include "HeldItemComponent.h"
#include "InteractionComponent.h"
@@ -115,6 +116,7 @@ AFpsPlayer::AFpsPlayer()
AppearanceComponent = CreateDefaultSubobject<UCharacterAppearanceComponent>(TEXT("AppearanceComponent"));
HeldItemComponent = CreateDefaultSubobject<UHeldItemComponent>(TEXT("HeldItemComponent"));
FootstepComponent = CreateDefaultSubobject<UFootstepComponent>(TEXT("FootstepComponent"));
HarvestingComponent = CreateDefaultSubobject<UHarvestingComponent>(TEXT("HarvestingComponent"));
// En vue FPS on ne veut pas voir son propre corps (mesh 3e personne),
// mais on garde son ombre.
@@ -786,52 +788,113 @@ void AFpsPlayer::TriggerHeldItemAction()
return;
}
// Le trace part d'ICI, chez celui qui frappe, et sans condition sur ce qu'il
// tient. Deux raisons de ne pas le reserver aux outils : une ressource dont
// RequiredTool vaut None se cueille a mains nues, et lire l'inventaire local
// pour trier ferait dependre le trace d'un etat qui peut avoir une frame de
// retard. Le cout est un unique balayage spherique par CLIC -- pas par frame.
FVector ImpactPoint;
FVector HitDirection;
AActor* SwingTarget = HarvestingComponent
? HarvestingComponent->TraceSwingTarget(ImpactPoint, HitDirection)
: nullptr;
// 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(PendingUseSlotIndex);
Server_UseItem(PendingUseSlotIndex, SwingTarget, ImpactPoint, HitDirection);
return;
}
PerformUseItem(PendingUseSlotIndex);
PerformUseItem(PendingUseSlotIndex, SwingTarget, ImpactPoint, HitDirection);
}
void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex)
void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex, AActor* SwingTarget,
FVector_NetQuantize ImpactPoint, FVector_NetQuantizeNormal HitDirection)
{
PerformUseItem(SlotIndex);
PerformUseItem(SlotIndex, SwingTarget, ImpactPoint, HitDirection);
}
void AFpsPlayer::PerformUseItem(int32 SlotIndex)
void AFpsPlayer::PerformUseItem(int32 SlotIndex, AActor* SwingTarget, const FVector& ImpactPoint,
const FVector& HitDirection)
{
if (!HasAuthority() || !InventoryComponent->GetSlots().IsValidIndex(SlotIndex))
if (!HasAuthority())
{
return;
}
const FInventorySlot Selected = InventoryComponent->GetSlots()[SlotIndex];
if (Selected.IsEmpty())
// Un slot invalide ou vide n'est PAS une sortie : c'est le cas des mains nues,
// qui frappent quand meme -- un buisson de baies se cueille au poing. La
// version precedente sortait ici, quand seuls les consommables agissaient.
const FInventorySlot* Selected = InventoryComponent->GetSlots().IsValidIndex(SlotIndex)
? &InventoryComponent->GetSlots()[SlotIndex]
: nullptr;
const UItemDataAsset* Item = (Selected && !Selected->IsEmpty()) ? Selected->Item.Get() : nullptr;
if (!Item)
{
PerformToolSwing(nullptr, INDEX_NONE, SwingTarget, ImpactPoint, HitDirection);
return;
}
if (Item->bIsConsumable)
{
// 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);
return;
}
// Tout le reste frappe : les outils avec leur puissance, les autres objets
// comme un poing. Un objet qui n'est ni consommable ni outil ne peut donc rien
// contre un arbre -- AHarvestableResource::AcceptsTool l'y refuse -- mais
// cueille les ressources a mains nues, ce qui est exactement ce qu'on veut :
// tenir une pomme n'empeche pas de ramasser des baies.
PerformToolSwing(Item, SlotIndex, SwingTarget, ImpactPoint, HitDirection);
}
void AFpsPlayer::PerformToolSwing(const UItemDataAsset* Tool, int32 SlotIndex, AActor* SwingTarget,
const FVector& ImpactPoint, const FVector& HitDirection)
{
if (!HarvestingComponent || !SwingTarget)
{
return;
}
const UItemDataAsset* Item = Selected.Item;
const EHarvestResult Result = HarvestingComponent->ApplySwing(SwingTarget, Tool, ImpactPoint, HitDirection);
// 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)
// SEULS les deux resultats qui portent usent l'outil. Un mauvais outil, une
// souche ou un coup dans le vide n'en consomment aucune charge : abimer une
// hache en tapant un rocher serait une punition que rien a l'ecran n'explique.
if (Result != EHarvestResult::Harvested && Result != EHarvestResult::Depleted)
{
return;
}
// Les valeurs de restauration sont appliquees a CHAQUE utilisation.
SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore);
if (!Tool || !Tool->IsTool() || SlotIndex == INDEX_NONE)
{
return;
}
// Un outil laisse a MaxUses = 1 disparaitrait au PREMIER coup : ConsumeUse
// retire alors l'exemplaire entier. On refuse plutot que de faire s'evaporer
// la hache que le joueur vient de fabriquer, et on le dit -- le symptome, lui,
// se lit comme « ma hache a disparu » et se cherche tres longtemps.
if (Tool->MaxUses <= 1)
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : l'outil %s a MaxUses = 1, il casserait au premier coup. Usure ignoree."),
*GetNameSafe(Tool));
return;
}
// 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);
}
@@ -940,6 +1003,11 @@ bool AFpsPlayer::DropFromInventory(UInventoryComponent* Inventory, int32 SlotInd
}
void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
{
DropAtFeet(Item, Quantity, RemainingUses);
}
void AFpsPlayer::DropAtFeet(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
{
if (!Item || Quantity <= 0)
{
@@ -948,7 +1016,7 @@ void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32
if (!DroppedPickupClass)
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sortis de la fabrication sont perdus."),
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sont perdus."),
Quantity, *GetNameSafe(Item));
return;
}
@@ -960,16 +1028,43 @@ void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32
// 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.
// une cloison qu'on ne regarde meme pas. Le meme raisonnement vaut pour le
// surplus de recolte -- le joueur regarde l'arbre, pas le sol.
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."),
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s."),
Quantity, *GetNameSafe(Item));
}
}
int32 AFpsPlayer::GiveItemOrDrop(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
{
if (!Item || Quantity <= 0 || !InventoryComponent)
{
return 0;
}
// Garde-fou et pas simple precaution : engendrer un pickup chez un client
// donnerait un objet visible de lui seul, efface au premier nettoyage reseau.
// L'appelant normal -- AHarvestableResource -- est deja cote serveur.
if (!HasAuthority())
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer::GiveItemOrDrop appelee sans autorite pour %s, ignoree."),
*GetNameSafe(Item));
return 0;
}
const int32 Rejected = InventoryComponent->AddItem(Item, Quantity, RemainingUses);
if (Rejected > 0)
{
DropAtFeet(Item, Rejected, RemainingUses);
}
return Quantity - Rejected;
}
void AFpsPlayer::DropAllItems()
{
// Declenche par la mort, qui sera arbitree par le serveur. La garde est la
@@ -0,0 +1,71 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "HarvestableDataAsset.h"
#include "ItemDataAsset.h"
#if WITH_EDITOR
#include "Misc/DataValidation.h"
#endif
#define LOCTEXT_NAMESPACE "HarvestableData"
int32 FHarvestDrop::Roll() const
{
if (!Item)
{
return 0;
}
// Le jet de chance d'abord : une ligne a 30 % qui echoue ne doit pas consommer
// de tirage de quantite, sinon deux tables identiques a la chance pres ne
// donneraient pas la meme suite -- sans consequence en jeu, mais deroutant a
// deboguer avec une graine fixee.
if (Chance < 1.f && FMath::FRand() > Chance)
{
return 0;
}
// Max borne par Min plutot que l'inverse : un designer qui saisit 3 puis 1
// obtient 3, jamais un intervalle vide qui rendrait la ligne muette sans rien
// dire. RandRange est inclusif des deux cotes.
return FMath::RandRange(MinQuantity, FMath::Max(MinQuantity, MaxQuantity));
}
#if WITH_EDITOR
EDataValidationResult UHarvestableDataAsset::IsDataValid(FDataValidationContext& Context) const
{
EDataValidationResult Result = Super::IsDataValid(Context);
// Le symptome d'une ligne vide est fourbe : l'arbre se recolte, s'abat, et
// rend simplement moins que prevu. Rien dans le log, rien a l'ecran.
auto ValidateTable = [&Context, &Result](const TArray<FHarvestDrop>& Table, const FText& TableName)
{
for (int32 Index = 0; Index < Table.Num(); ++Index)
{
if (!Table[Index].Item)
{
Context.AddError(FText::Format(
LOCTEXT("DropNoItem", "{0}[{1}] has no 'Item' assigned: this line would silently yield nothing."),
TableName, FText::AsNumber(Index)));
Result = EDataValidationResult::Invalid;
}
}
};
ValidateTable(PerHitDrops, LOCTEXT("PerHitTable", "Per Hit Drops"));
ValidateTable(DepletionDrops, LOCTEXT("DepletionTable", "Depletion Drops"));
// Une ressource sans aucun butin est frappable, s'epuise, et ne donne rien.
// C'est legitime pour un decor destructible ; ca ne l'est presque jamais ici.
if (PerHitDrops.IsEmpty() && DepletionDrops.IsEmpty())
{
Context.AddWarning(LOCTEXT("NoDropsAtAll", "This harvestable has no drops at all: harvesting it will consume tool durability for nothing."));
}
return Result;
}
#endif
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,358 @@
// 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<UStaticMeshComponent>(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<FLifetimeProperty>& 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<FHarvestDrop>& 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());
}
@@ -0,0 +1,147 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "HarvestingComponent.h"
#include "DrawDebugHelpers.h"
#include "Engine/World.h"
#include "FpsPlayer.h"
#include "GameFramework/Controller.h"
#include "GameFramework/Pawn.h"
#include "HarvestableResource.h"
#include "ItemDataAsset.h"
UHarvestingComponent::UHarvestingComponent()
{
// Aucun Tick : ce composant ne travaille qu'au moment du coup, une poignee de
// fois par minute. C'est toute la difference avec UInteractionComponent, qui
// doit entretenir un prompt a l'ecran.
PrimaryComponentTick.bCanEverTick = false;
// Pas de SetIsReplicatedByDefault : aucune RPC ne vit ici, elles sont sur le
// pawn. Voir le commentaire de classe.
}
bool UHarvestingComponent::GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const
{
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
if (!OwnerPawn)
{
return false;
}
// Par le controller et pas par la camera : GetPlayerViewPoint renvoie
// exactement ce que le joueur a a l'ecran, secousses et balancement de tete
// compris. Le coup part donc pile du centre du viseur.
if (const AController* OwnerController = OwnerPawn->GetController())
{
OwnerController->GetPlayerViewPoint(OutLocation, OutRotation);
return true;
}
return false;
}
AActor* UHarvestingComponent::TraceSwingTarget(FVector& OutImpactPoint, FVector& OutDirection) const
{
OutImpactPoint = FVector::ZeroVector;
OutDirection = FVector::ForwardVector;
FVector ViewLocation;
FRotator ViewRotation;
if (!GetViewPoint(ViewLocation, ViewRotation))
{
return nullptr;
}
OutDirection = ViewRotation.Vector();
const FVector TraceEnd = ViewLocation + OutDirection * SwingRange;
FCollisionQueryParams Params(SCENE_QUERY_STAT(HarvestSwingTrace), /*bTraceComplex=*/false, GetOwner());
FHitResult Hit;
const bool bHit = GetWorld()->SweepSingleByChannel(
Hit,
ViewLocation,
TraceEnd,
FQuat::Identity,
SwingChannel,
FCollisionShape::MakeSphere(SwingRadius),
Params);
#if ENABLE_DRAW_DEBUG
if (bDrawDebug)
{
const FColor Color = bHit ? FColor::Green : FColor::Red;
DrawDebugLine(GetWorld(), ViewLocation, TraceEnd, Color, false, 1.5f, 0, 1.f);
if (bHit)
{
DrawDebugSphere(GetWorld(), Hit.ImpactPoint, SwingRadius, 12, Color, false, 1.5f);
}
}
#endif
if (!bHit)
{
return nullptr;
}
// ImpactPoint et pas Location : Location est le centre de la sphere balayee a
// l'arret, donc a SwingRadius de la surface. Les copeaux en jailliraient a
// vingt-cinq centimetres du tronc, en plein air.
OutImpactPoint = Hit.ImpactPoint;
return Hit.GetActor();
}
float UHarvestingComponent::GetSwingPower(const UItemDataAsset* Tool) const
{
// Un objet qui n'est pas un outil frappe comme un poing : tenir une pomme ne
// doit ni empecher de cueillir une baie, ni aider a abattre un arbre.
return (Tool && Tool->IsTool()) ? Tool->HarvestPower : UnarmedHarvestPower;
}
EHarvestResult UHarvestingComponent::ApplySwing(AActor* Target, const UItemDataAsset* Tool,
const FVector& ImpactPoint, const FVector& HitDirection)
{
AActor* Owner = GetOwner();
if (!Owner || !Owner->HasAuthority())
{
return EHarvestResult::Invalid;
}
AHarvestableResource* Resource = Cast<AHarvestableResource>(Target);
if (!IsValid(Resource))
{
// Le cas normal, et de loin : la plupart des coups tombent sur le sol, un
// mur, ou rien du tout. Ce n'est pas une erreur, juste un coup dans le vide.
return EHarvestResult::Invalid;
}
// Le client a vise il y a un aller-retour : entre-temps il a bouge, et la
// position que le serveur connait de lui est en retard. On revalide donc, avec
// la meme portee elargie par ServerRangeTolerance.
//
// La distance se mesure a la BOITE ENGLOBANTE de la cible et pas a son origine :
// le pivot d'un arbre de pack est au SOL, donc frapper le tronc a deux metres de
// haut mesurerait une distance bien plus grande que la portee du coup. Le
// symptome serait un grand arbre impossible a abattre pour un client, et
// parfaitement normal chez l'hote.
const FBox TargetBounds = Resource->GetComponentsBoundingBox(/*bNonColliding=*/true);
const float MaxDistanceSq = FMath::Square(SwingRange * ServerRangeTolerance);
if (TargetBounds.ComputeSquaredDistanceToPoint(Owner->GetActorLocation()) > MaxDistanceSq)
{
UE_LOG(LogTemp, Verbose, TEXT("UHarvestingComponent : %s a frappe %s, hors de portee cote serveur."),
*GetNameSafe(Owner), *GetNameSafe(Resource));
return EHarvestResult::Invalid;
}
AFpsPlayer* Harvester = Cast<AFpsPlayer>(Owner);
if (!Harvester)
{
return EHarvestResult::Invalid;
}
return Resource->TryHarvest(Harvester, Tool, GetSwingPower(Tool), ImpactPoint, HitDirection);
}