// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "ItemDataAsset.h" #include "HeldItemComponent.generated.h" class AFpsPlayer; class UAnimInstance; 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); /** * Ce qu'a fait PlayUseMontage, du point de vue de l'appelant. * * Trois etats et pas un booleen, parce que l'appelant en tire trois conduites * differentes : un geste qui demarre porte son propre marqueur d'impact et il * n'y a plus qu'a attendre ; un geste deja en cours en enverra un lui aussi ; * une absence de montage, elle, ne produira JAMAIS de marqueur et doit retomber * sur l'effet immediat. Les confondre donne soit un objet sans montage qui ne * fait plus rien du tout, soit un clic martele qui declenche l'effet a chaque * image pendant que le bras, lui, ne rejoue pas. */ UENUM(BlueprintType) enum class EUseMontageResult : uint8 { /** Le geste vient de demarrer : son AnimNotify declenchera l'effet. */ Started, /** Le meme geste tournait deja ; on ne l'a pas relance. */ AlreadyPlaying, /** Rien a jouer -- l'objet n'a pas de UseMontage, ou les bras sont distants. */ NoMontage }; /** * 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 EMBERWILD_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(); /** * Joue le geste du clic gauche avec l'objet en main. * * Appelee par le PAWN au moment de l'input, donc chez le joueur local et * AVANT tout aller-retour reseau : un coup de pioche qui attendrait la * reponse du serveur partirait avec un temps de retard sur le clic, et c'est * la premiere chose qu'on sent dans un jeu. * * Il ne fait QUE l'animation : l'effet de l'objet part du marqueur * UAnimNotify_ItemAction pose dans le montage, pour tomber au moment de * l'impact et pas a l'appui. D'ou le retour a trois etats, que l'appelant * doit lire -- voir EUseMontageResult. * * @return ce qu'il est advenu du geste. */ UFUNCTION(BlueprintCallable, Category = "Held Item") EUseMontageResult PlayUseMontage(); 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"); /** * Geste du clic gauche quand les mains sont VIDES (MTG_Punch). * * Ici et pas sur un DataAsset, pour la raison evidente qu'il n'y a pas * d'objet a qui le demander. Ici et pas sur AFpsPlayer a cote de GrabMontage, * parce que c'est ce composant qui choisit quel geste part au clic : deux * moities de la meme decision dans deux classes finiraient par diverger. * * Il ne sert QUE quand aucun objet n'occupe la case active. Un objet en main * sans UseMontage ne joue rien -- retomber sur le poing ferait boxer le * joueur en mangeant une pomme. */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item") TObjectPtr UnarmedUseMontage; // ------------------------------------------------------------------ // Accroupissement // // Des MONTAGES et pas des etats de la machine, alors que CrouchIdle et // CrouchWalk, eux, sont bien des etats. La frontiere est la meme que partout // ailleurs : ce qui DURE est un etat, ce qui ARRIVE est un montage. Plier les // genoux prend trois dixiemes de seconde et se superpose au cycle en cours. // // Raison technique par-dessus, et elle est decisive : un etat dont la sortie // depend de la fin de l'animation utilise « Automatic Rule Based on Sequence // Player in State », qui a besoin d'un Sequence Player pour lire sa duree. Un // etat qui contiendrait un choix entre deux animations -- avec et sans objet // -- lui retire cette certitude, et il faudrait ecrire des durees en dur dans // les transitions, a re-regler a chaque animation livree. // // Ici et pas sur UItemDataAsset : le geste est GENERIQUE, deux variantes en // tout. Le jour ou un outil voudra la sienne, un champ optionnel sur son // DataAsset primera sur ceux-ci, sans rien casser. // ------------------------------------------------------------------ /** Geste d'accroupissement, mains vides (MTG_EnterCrouch). */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item|Crouch") TObjectPtr UnarmedEnterCrouchMontage; /** Meme geste, un objet en main. Vide = on rejoue la version mains vides. */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item|Crouch") TObjectPtr HeldEnterCrouchMontage; /** Geste de relevee, mains vides (MTG_ExitCrouch). */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item|Crouch") TObjectPtr UnarmedExitCrouchMontage; /** Meme geste, un objet en main. Vide = on rejoue la version mains vides. */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Held Item|Crouch") TObjectPtr HeldExitCrouchMontage; /** * Joue le geste d'accroupissement ou de relevee, selon ce qu'on tient. * * Appelee par AFpsPlayer depuis OnStartCrouch / OnEndCrouch, qui sont deja * surchargees : le pawn sait exactement quand la capsule change de taille, * personne n'a a le deviner. */ UFUNCTION(BlueprintCallable, Category = "Held Item|Crouch") bool PlayCrouchMontage(bool bEntering); 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(); /** * Joue un montage sur les bras du joueur LOCAL, et lui seul. * * @param bSkipIfAlreadyPlaying vrai pour le clic gauche : marteler le bouton * relancerait le geste depuis le debut a chaque image, ce qui donne un * bras qui tremble au lieu de frapper. Faux pour l'equipement, ou * changer d'outil doit couper net le geste precedent. * @return true si un montage a demarre. */ bool PlayLocalArmsMontage(class UAnimMontage* Montage, bool bSkipIfAlreadyPlaying); /** * Branche ou debranche la couche d'animation de l'objet sur les bras. * * Locale elle aussi, et pour la meme raison que le montage : les bras sont en * OnlyOwnerSee. Chez l'hote, lier une couche sur le squelette de bras d'un ami * ferait evaluer un graphe que personne ne rend. * * Idempotente : elle compare a la couche deja liee et ne fait rien si elle est * la bonne. C'est ce qui permet de l'appeler a chaque rafraichissement * d'inventaire sans relancer le graphe. */ void UpdateArmsAnimLayer(); /** * 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; /** * La couche actuellement liee aux bras, ou nulle. * * Indispensable, et pas un simple cache : LinkAnimClassLayers AJOUTE une * couche, elle ne remplace pas la precedente. Sans garder ce qui est en * place pour le delier, passer de la hache a la pioche laisserait les deux * branchees -- et c'est la premiere qui gagnerait sur les fonctions que la * seconde n'implemente pas. */ UPROPERTY(Transient) TSubclassOf LinkedArmsLayer; /** Evite de repeter l'avertissement de socket introuvable a chaque montage. */ bool bLoggedMissingSocket = false; };