// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "HeldItemComponent.generated.h" class AFpsPlayer; class UItemDataAsset; class UStaticMeshComponent; /** Diffuse a chaque changement d'objet en main, nul compris. L'UI et l'anim s'y abonnent. */ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHeldItemChanged, UItemDataAsset*, NewItem); /** * Montre dans la main l'objet occupant la case active de la barre rapide. * * Un systeme = un composant, comme le reste du projet. Celui-ci ne decide de * rien et ne consomme rien : il regarde l'inventaire et pose deux meshes. Ce que * l'objet FAIT quand on clique reste dans AFpsPlayer::PerformUseItem. * * Deux meshes et pas un, parce qu'il y a deux corps a habiller : * * - les BRAS (FirstPersonArms), visibles du seul proprietaire, attaches a la * camera. C'est la hache qu'on voit soi-meme. * - le CORPS (le Mesh du Character), invisible pour son proprietaire mais vu * par les trois autres joueurs -- et dont on percoit sa propre ombre. * * Les bras recopient du porteur leur « visible pour moi seul ». Le mesh du corps, * lui, se cache par la VISIBILITE chez le seul joueur qui controle ce pawn -- * bOwnerNoSee y couperait son ombre, voir UpdateLocalVisibility(). * * ------------------------------------------------------------------ * Reseau : c'est le CLIENT qui pilote, toujours * ------------------------------------------------------------------ * * UInventoryComponent::SelectedHotbarIndex n'est PAS replique -- c'est une * intention purement locale, et il n'y a aucune raison de la mettre sur le * reseau. Le serveur ne peut donc pas deviner ce que le joueur a en main : lire * l'inventaire d'un pawn distant y trouverait toujours la case 0. * * Ce qui se replique est donc l'OBJET lui-meme, pas l'index. Le proprietaire * l'applique en local et l'annonce ; le serveur l'enregistre et le rediffuse aux * autres. Exactement le schema de bIsSprinting, COND_SkipOwner compris : renvoyer * a l'emetteur une valeur qui a un aller-retour de retard ne peut que faire * clignoter ce qu'il tient deja. */ UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent)) class SURVIVAL_PROJET_API UHeldItemComponent : public UActorComponent { GENERATED_BODY() public: UHeldItemComponent(); virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; /** L'objet actuellement montre dans la main. Nul = les mains sont vides. */ UFUNCTION(BlueprintPure, Category = "Held Item") UItemDataAsset* GetHeldItem() const { return HeldItem; } /** * Vrai quand un mesh est effectivement monte dans la main. * * Different de `GetHeldItem() != nullptr`, et la nuance compte pour * l'animation : la plupart des ressources du jeu n'ont pas bShowInHand, donc * les tenir ne met rien dans la paume. Se fier a l'objet ferait jouer la * posture « objet en main » sur des mains vides, ce qui se voit tout de suite. */ UFUNCTION(BlueprintPure, Category = "Held Item") bool HasItemInHand() const { return bItemVisibleInHand; } /** * Relit la case active et met la main a jour. * * Sans effet ailleurs que chez le joueur local : ce composant est le seul du * projet dont la SOURCE de verite vive uniquement sur la machine du * proprietaire. Idempotente, donc appelable a la volee. */ UFUNCTION(BlueprintCallable, Category = "Held Item") void RefreshFromInventory(); UPROPERTY(BlueprintAssignable, Category = "Held Item") FOnHeldItemChanged OnHeldItemChanged; /** * Os ou socket des BRAS auquel l'objet s'accroche. * * C'est un OS du rig et pas un socket taille sur mesure, d'ou les offsets * par objet dans UItemDataAsset : creer un socket par outil demanderait de * rouvrir le squelette a chaque nouvel objet, alors que l'offset se regle * dans le DataAsset qu'on est deja en train d'ecrire. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held Item") FName FirstPersonSocket = TEXT("RightHand_Holder_JTN"); /** Os de la main droite du mannequin UE5, porteur du mesh 3e personne. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held Item") FName ThirdPersonSocket = TEXT("hand_r"); protected: virtual void BeginPlay() override; virtual void OnUnregister() override; private: /** * L'objet en main. Un pointeur d'asset se replique comme n'importe quelle * reference d'objet -- c'est deja ce que fait FInventorySlot::Item, et c'est * pour ca qu'on peut se permettre d'envoyer l'objet plutot qu'un index. * * COND_SkipOwner : le proprietaire vient de l'appliquer en local, le lui * renvoyer ecraserait son etat courant par une valeur en retard. */ UPROPERTY(ReplicatedUsing = OnRep_HeldItem) TObjectPtr HeldItem; UFUNCTION() void OnRep_HeldItem(); /** Point d'entree unique : applique en local, puis annonce au serveur. */ void SetHeldItem(UItemDataAsset* NewItem); UFUNCTION(Server, Reliable) void Server_SetHeldItem(UItemDataAsset* NewItem); /** Monte ou demonte les deux meshes selon HeldItem. Tourne sur TOUTES les machines. */ void ApplyHeldVisuals(); /** * Joue le geste de sortie d'outil sur les bras du joueur LOCAL. * * Appelee depuis SetHeldItem et pas depuis ApplyHeldVisuals, alors que les * deux disent « l'objet en main a change » : ApplyHeldVisuals tourne sur * toutes les machines (OnRep, serveur), et un geste de bras n'a de sens que * chez celui qui les voit. */ void PlayEquipMontage(); /** * Cache le mesh du CORPS chez le joueur qui controle ce pawn, et lui seul. * * Par la VISIBILITE et non par bOwnerNoSee : sur un UStaticMeshComponent, * bCastHiddenShadow n'honore que la premiere -- cache par le proprietaire, * l'objet perd son ombre en meme temps que son mesh. Detail dans le .cpp. */ void UpdateLocalVisibility(); /** * Cree les deux composants de mesh s'ils manquent. * * A la demande et pas dans le constructeur : le composant n'a alors aucune * hypothese sur l'ordre de creation des sous-objets d'AFpsPlayer, et un pawn * qui ne tient jamais rien ne paie pas deux composants pour rien. */ void EnsureHeldMeshComponents(); /** * Cree un mesh d'objet en main attache a Parent, ou nullptr si impossible. * * @param bCastWorldShadow l'objet projette-t-il une ombre dans le monde. * Vrai pour la main du corps, faux pour les bras -- un mesh colle a * l'objectif projetterait une ombre enorme en travers du decor. */ UStaticMeshComponent* CreateHeldMeshComponent(FName ComponentName, class USkeletalMeshComponent* Parent, FName SocketName, bool bCastWorldShadow); /** Les deux delegues d'inventaire menent ici. */ UFUNCTION() void HandleInventoryChanged(); UFUNCTION() void HandleSelectedHotbarSlotChanged(int32 NewIndex); /** Le pawn porteur, ou nullptr si le composant vit ailleurs. */ AFpsPlayer* GetOwningPlayer() const; /** Mesh vu par le seul proprietaire, accroche aux bras. */ UPROPERTY(Transient) TObjectPtr FirstPersonHeldMesh; /** Mesh vu par les autres joueurs, accroche a la main du corps. */ UPROPERTY(Transient) TObjectPtr ThirdPersonHeldMesh; /** * Un mesh est-il reellement monte en main, en cache. * * Recopie de la decision prise par ApplyHeldVisuals plutot que recalculee : * l'AnimInstance la lit a chaque frame, et surtout deux facons de repondre a * la meme question finiraient par diverger. */ bool bItemVisibleInHand = false; /** Evite de repeter l'avertissement de socket introuvable a chaque montage. */ bool bLoggedMissingSocket = false; };