// Fill out your copyright notice in the Description page of Project Settings. #include "HeldItemComponent.h" #include "Animation/AnimMontage.h" #include "Components/SkeletalMeshComponent.h" #include "Components/StaticMeshComponent.h" #include "FpsPlayer.h" #include "InventoryComponent.h" #include "ItemDataAsset.h" #include "Net/UnrealNetwork.h" UHeldItemComponent::UHeldItemComponent() { // Aucun Tick : le montage est evenementiel. Il ne se declenche qu'au // changement de case active ou de contenu d'inventaire, et les deux sont // deja des delegues. PrimaryComponentTick.bCanEverTick = false; // Obligatoire, et pas seulement pour repliquer HeldItem : sans lui le moteur // JETTE SILENCIEUSEMENT Server_SetHeldItem. Aucun message, aucune erreur -- // l'objet reste simplement invisible chez les autres joueurs. SetIsReplicatedByDefault(true); } void UHeldItemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); // COND_SkipOwner, exactement comme AFpsPlayer::bIsSprinting : le proprietaire // est celui qui a DECIDE de ce changement et l'a deja applique en local. Le // lui renvoyer poserait une valeur ayant un aller-retour de retard, ce qui // fait clignoter l'objet quand on change de case rapidement. DOREPLIFETIME_CONDITION(UHeldItemComponent, HeldItem, COND_SkipOwner); } void UHeldItemComponent::BeginPlay() { Super::BeginPlay(); AFpsPlayer* Player = GetOwningPlayer(); if (!Player) { UE_LOG(LogTemp, Warning, TEXT("UHeldItemComponent : porteur %s n'est pas un AFpsPlayer, aucun objet ne sera montre en main."), *GetNameSafe(GetOwner())); return; } if (UInventoryComponent* Inventory = Player->GetInventoryComponent()) { Inventory->OnSelectedHotbarSlotChanged.AddDynamic(this, &UHeldItemComponent::HandleSelectedHotbarSlotChanged); // Le second abonnement n'est pas une redondance : vider la case active // au glisser-deposer, ou consommer la derniere charge de ce qu'on tient, // ne change pas l'INDEX selectionne. Sans lui, la pomme resterait en main // apres avoir ete mangee. Inventory->OnInventoryChanged.AddDynamic(this, &UHeldItemComponent::HandleInventoryChanged); } // Chez l'hote et en solo, tout est deja en place a cet instant. Chez un // client le controller peut manquer encore : AFpsPlayer rappellera alors // depuis OnRep_Controller, et cette fonction est idempotente. RefreshFromInventory(); } void UHeldItemComponent::OnUnregister() { // Les composants engendres appartiennent a l'acteur et disparaissent avec // lui. On ne lache que nos references, pour ne pas retenir des objets que le // GC doit pouvoir reprendre. FirstPersonHeldMesh = nullptr; ThirdPersonHeldMesh = nullptr; Super::OnUnregister(); } AFpsPlayer* UHeldItemComponent::GetOwningPlayer() const { return Cast(GetOwner()); } void UHeldItemComponent::HandleInventoryChanged() { RefreshFromInventory(); } void UHeldItemComponent::HandleSelectedHotbarSlotChanged(int32 /*NewIndex*/) { RefreshFromInventory(); } void UHeldItemComponent::RefreshFromInventory() { const AFpsPlayer* Player = GetOwningPlayer(); if (!Player) { return; } // Reposee AVANT la garde ci-dessous, et c'est tout l'interet : le passage de // « pas encore controle » a « controle localement » est justement ce qui doit // cacher l'objet en main, et AFpsPlayer rappelle cette fonction depuis // OnRep_Controller. La poser apres la garde ne la ferait jamais tourner chez // celui qui vient de prendre le controle. UpdateLocalVisibility(); // LA garde de ce composant. Chez l'hote, le pawn de chaque ami porte aussi // un inventaire et un UHeldItemComponent : lire la case active d'un pawn // distant y trouverait toujours l'index 0, puisque SelectedHotbarIndex n'est // pas replique. Sans ce test, le serveur remplacerait la hache d'un client // par le contenu de sa premiere case, en boucle. if (!Player->IsLocallyControlled()) { return; } const UInventoryComponent* Inventory = Player->GetInventoryComponent(); if (!Inventory) { return; } const FInventorySlot Selected = Inventory->GetSelectedSlot(); SetHeldItem(Selected.IsEmpty() ? nullptr : Selected.Item.Get()); // Reposee meme quand SetHeldItem est sortie tot faute de changement : chez un // client, les premiers rafraichissements tombent avant que le controller soit // la, donc avant qu'on ait le droit de toucher aux bras. Sans ce rattrapage, // un joueur qui reapparait avec sa pioche deja selectionnee garderait les // cycles mains nues jusqu'a ce qu'il change de case. UpdateArmsAnimLayer(); } void UHeldItemComponent::SetHeldItem(UItemDataAsset* NewItem) { if (HeldItem == NewItem) { return; } HeldItem = NewItem; ApplyHeldVisuals(); // La couche AVANT le montage : le geste de sortie doit se superposer aux // cycles du nouvel outil, pas a ceux qu'on vient de quitter. L'inverse se // voit sur la premiere image, la main partant de la mauvaise pose. UpdateArmsAnimLayer(); PlayEquipMontage(); // En local d'abord, pour que l'objet apparaisse sous les doigts sans attendre // un aller-retour ; le serveur suit. Le sens inverse donnerait une main qui // se remplit avec un temps de retard a chaque coup de molette. if (const AActor* Owner = GetOwner(); Owner && !Owner->HasAuthority()) { Server_SetHeldItem(NewItem); } } void UHeldItemComponent::UpdateArmsAnimLayer() { AFpsPlayer* Player = GetOwningPlayer(); if (!Player || !Player->IsLocallyControlled()) { return; } USkeletalMeshComponent* Arms = Player->GetFirstPersonArms(); if (!Arms) { return; } // Le mesh REELLEMENT monte fait foi, pas le seul HeldItem : un objet dont on // a oublie de cocher bShowInHand ferait sinon jouer les cycles de la pioche // sur une main vide. const TSubclassOf DesiredLayer = (bItemVisibleInHand && HeldItem) ? HeldItem->ArmsAnimLayer : nullptr; if (DesiredLayer == LinkedArmsLayer) { return; } // Delier AVANT de lier, toujours. LinkAnimClassLayers ajoute une couche sans // retirer la precedente : enchainer hache puis pioche laisserait les deux en // place, et la hache l'emporterait sur toute fonction que la pioche // n'implemente pas. Aucun message ne le signale. if (LinkedArmsLayer) { Arms->UnlinkAnimClassLayers(LinkedArmsLayer); } if (DesiredLayer) { Arms->LinkAnimClassLayers(DesiredLayer); } LinkedArmsLayer = DesiredLayer; } void UHeldItemComponent::PlayEquipMontage() { // Ranger un objet ne joue rien : le pack ne livre que des gestes de sortie // d'outil, et un desequipement sans animation ne se remarque pas -- l'objet // suivant arrive dans la foulee avec le sien. if (!HeldItem) { return; } PlayLocalArmsMontage(HeldItem->EquipMontage, /*bSkipIfAlreadyPlaying=*/false); } EUseMontageResult UHeldItemComponent::PlayUseMontage() { // Le test porte sur l'OBJET et pas sur le mesh monte : une pomme n'apparait // pas dans la main (bShowInHand faux) mais elle occupe bien la case active, // et la manger ne doit pas declencher un coup de poing. UAnimMontage* const Montage = HeldItem ? HeldItem->UseMontage.Get() : UnarmedUseMontage.Get(); if (!Montage) { return EUseMontageResult::NoMontage; } // Le geste deja en cours est distingue AVANT l'appel, parce que // PlayLocalArmsMontage rend false dans les deux cas et que l'appelant, lui, // doit les separer : l'un enverra son marqueur d'impact, l'autre jamais. const AFpsPlayer* Player = GetOwningPlayer(); const USkeletalMeshComponent* Arms = Player ? Player->GetFirstPersonArms() : nullptr; const UAnimInstance* ArmsAnim = Arms ? Arms->GetAnimInstance() : nullptr; if (ArmsAnim && ArmsAnim->Montage_IsPlaying(Montage)) { return EUseMontageResult::AlreadyPlaying; } // Chez un pawn distant les bras ne jouent rien (voir PlayLocalArmsMontage) : // aucun marqueur n'en sortira, d'ou NoMontage plutot qu'un etat a part. La // garde IsLocallyControlled() du pawn reste seule juge de ce qui part sur // le reseau. return PlayLocalArmsMontage(Montage, /*bSkipIfAlreadyPlaying=*/true) ? EUseMontageResult::Started : EUseMontageResult::NoMontage; } bool UHeldItemComponent::PlayCrouchMontage(bool bEntering) { UAnimMontage* const Held = bEntering ? HeldEnterCrouchMontage.Get() : HeldExitCrouchMontage.Get(); UAnimMontage* const Unarmed = bEntering ? UnarmedEnterCrouchMontage.Get() : UnarmedExitCrouchMontage.Get(); // Le repli sur la version mains vides est ce qui permet de n'en fournir // qu'une au depart : les gestes « avec objet » arriveront plus tard sans // qu'on ait a repasser dans le code. // // Le test porte sur le MESH monte et pas sur HeldItem, contrairement au clic // gauche : ce qui change la posture des genoux, c'est un objet qu'on voit // dans la main, pas une pomme rangee dans la case active. UAnimMontage* const Montage = (bItemVisibleInHand && Held) ? Held : Unarmed; // Pas de bSkipIfAlreadyPlaying : marteler la touche d'accroupissement doit // relancer le geste, puisque la capsule, elle, change bel et bien de taille // a chaque fois. return PlayLocalArmsMontage(Montage, /*bSkipIfAlreadyPlaying=*/false); } bool UHeldItemComponent::PlayLocalArmsMontage(UAnimMontage* Montage, bool bSkipIfAlreadyPlaying) { if (!Montage) { return false; } AFpsPlayer* Player = GetOwningPlayer(); // Les bras sont en OnlyOwnerSee : chez l'hote, le pawn de chaque ami porte // AUSSI un squelette de bras que personne ne rend. Y jouer un montage ferait // tourner quatre machines a etats pour rien. if (!Player || !Player->IsLocallyControlled()) { return false; } if (bSkipIfAlreadyPlaying) { const USkeletalMeshComponent* Arms = Player->GetFirstPersonArms(); const UAnimInstance* ArmsAnim = Arms ? Arms->GetAnimInstance() : nullptr; if (ArmsAnim && ArmsAnim->Montage_IsPlaying(Montage)) { return false; } } return Player->PlayArmsMontage(Montage) > 0.f; } void UHeldItemComponent::Server_SetHeldItem_Implementation(UItemDataAsset* NewItem) { if (HeldItem == NewItem) { return; } HeldItem = NewItem; // Le serveur rend lui aussi les pawns distants -- l'hote voit ses amis. Sans // cet appel, la hache n'existerait que chez les clients. ApplyHeldVisuals(); } void UHeldItemComponent::OnRep_HeldItem() { ApplyHeldVisuals(); } void UHeldItemComponent::ApplyHeldVisuals() { EnsureHeldMeshComponents(); const UItemDataAsset* Item = HeldItem; // Un objet peut tres bien n'avoir aucune representation en main : c'est le // cas par defaut de toutes les ressources du jeu. On demonte alors, sans que // ce soit une anomalie. const bool bShow = Item != nullptr && Item->bShowInHand && Item->GetHeldMesh() != nullptr; bItemVisibleInHand = bShow; if (FirstPersonHeldMesh) { if (bShow) { Item->ApplyHeldVisualsTo(FirstPersonHeldMesh); FirstPersonHeldMesh->SetRelativeTransform(Item->FirstPersonHeldOffset); } else { FirstPersonHeldMesh->SetStaticMesh(nullptr); } FirstPersonHeldMesh->SetVisibility(bShow); } if (ThirdPersonHeldMesh) { if (bShow) { Item->ApplyHeldVisualsTo(ThirdPersonHeldMesh); ThirdPersonHeldMesh->SetRelativeTransform(Item->ThirdPersonHeldOffset); } else { ThirdPersonHeldMesh->SetStaticMesh(nullptr); } // Et surtout pas SetVisibility(bShow) : chez le joueur qui tient l'objet, // ce mesh doit rester invisible tout en projetant son ombre. UpdateLocalVisibility(); } OnHeldItemChanged.Broadcast(HeldItem); } void UHeldItemComponent::UpdateLocalVisibility() { if (!ThirdPersonHeldMesh) { return; } const AFpsPlayer* Player = GetOwningPlayer(); // Le pawn qu'on controle est le seul dont on ne doit pas voir le corps -- donc // le seul dont l'objet en main se cache. Chez les trois autres joueurs le meme // pawn n'est pas controle localement, et l'outil s'y affiche normalement. // // Relu a CHAQUE passage plutot que pose une fois : chez un client le pawn // arrive par replication AVANT son controller, et IsLocallyControlled() y est // donc faux pendant les premieres images. Sans cette relecture, le joueur // verrait sa propre pioche flotter devant lui pour le reste de la partie. const bool bHideForLocalPlayer = Player != nullptr && Player->IsLocallyControlled(); const bool bHasMesh = ThirdPersonHeldMesh->GetStaticMesh() != nullptr; ThirdPersonHeldMesh->SetVisibility(bHasMesh && !bHideForLocalPlayer); } void UHeldItemComponent::EnsureHeldMeshComponents() { AFpsPlayer* Player = GetOwningPlayer(); if (!Player) { return; } if (!FirstPersonHeldMesh) { FirstPersonHeldMesh = CreateHeldMeshComponent( TEXT("FirstPersonHeldMesh"), Player->GetFirstPersonArms(), FirstPersonSocket, /*bCastWorldShadow=*/false); } if (!ThirdPersonHeldMesh) { ThirdPersonHeldMesh = CreateHeldMeshComponent( TEXT("ThirdPersonHeldMesh"), Player->GetMesh(), ThirdPersonSocket, /*bCastWorldShadow=*/true); } } UStaticMeshComponent* UHeldItemComponent::CreateHeldMeshComponent(FName ComponentName, USkeletalMeshComponent* Parent, FName SocketName, bool bCastWorldShadow) { AActor* Owner = GetOwner(); if (!Owner || !Parent) { return nullptr; } // Un os introuvable n'est pas une erreur pour le moteur : il attache a la // RACINE du composant, en silence, et l'objet apparait plante au milieu du // torse. C'est ce log qui distingue « mal place » de « mal nomme », et la // difference vaut une heure de recherche dans le mauvais fichier. // // Le test du mesh n'est pas superflu : UCharacterAppearanceComponent // REMPLACE le mesh du corps a son BeginPlay, et l'ordre des BeginPlay entre // composants n'est garanti par rien. Sans lui, un avertissement partirait un // coup sur deux pour un os parfaitement valide -- soit exactement le faux // indice que ce log est cense eviter. L'attachement, lui, se moque du // moment : le nom est resolu a chaque lecture de la transform du socket. if (!SocketName.IsNone() && Parent->GetSkeletalMeshAsset() && !Parent->DoesSocketExist(SocketName) && !bLoggedMissingSocket) { UE_LOG(LogTemp, Warning, TEXT("UHeldItemComponent : le socket ou l'os '%s' n'existe pas sur %s. L'objet en main sera attache a la racine du mesh."), *SocketName.ToString(), *GetNameSafe(Parent->GetSkeletalMeshAsset())); bLoggedMissingSocket = true; } UStaticMeshComponent* Held = NewObject(Owner, ComponentName); if (!Held) { return nullptr; } Held->SetupAttachment(Parent, SocketName); // AVANT l'enregistrement, et c'est le piege le plus couteux de ce fichier : // un UStaticMeshComponent cree par NewObject nait en Mobility STATIC, alors // qu'un sous-objet declare dans un Blueprint herite du Movable de son // parent. Le mesh suit pourtant la main sans broncher, ce qui donne // l'illusion que tout va bien -- mais sous Virtual Shadow Maps une primitive // Static part dans le cache d'ombres statiques, qui n'est pas reinvalide // quand elle bouge. Symptome : l'objet est la, son ombre non. Et il n'y a // aucun message pour le dire. Held->SetMobility(EComponentMobility::Movable); // Un objet tenu en main n'est pas une surface de jeu. Sans ca, la hache // bloquerait le trace d'interaction -- qui part exactement d'entre les bras. Held->SetCollisionEnabled(ECollisionEnabled::NoCollision); Held->RegisterComponent(); // Apres l'enregistrement, et pas avant : les setters d'etat de rendu // marquent le proxy « sale », ce qui ne veut rien dire sur un composant qui // n'est pas encore dans le monde. Les poser des deux cotes de // RegisterComponent est le genre de subtilite qui fait qu'un drapeau prend // et l'autre non -- on les pose donc tous ici. // « Visible pour moi seul » se recopie du porteur : c'est ce qui tient les // bras, et un mesh d'arme n'a pas a savoir qu'il les habille. Held->SetOnlyOwnerSee(Parent->bOnlyOwnerSee); // bOwnerNoSee du porteur, en revanche, ne se recopie SURTOUT PAS, alors que // c'est par la que le Mesh du Character se cache lui-meme. Mesure sur ce // projet : cases « Cast Shadow » et « Hidden Shadow » cochees et actives sur // le composant, aucune ombre tant que bOwnerNoSee valait vrai, ombre correcte // des la seconde ou on cache le mesh par « Visible » a la place. // // Autrement dit bCastHiddenShadow n'honore, sur un UStaticMeshComponent, que // la dissimulation par la VISIBILITE -- pas celle par le proprietaire. Le // corps y echappe parce qu'il est un mesh SQUELETTIQUE, qui n'emprunte pas le // meme chemin de relevance dans le renderer. D'ou le partage : le porteur se // cache comme il veut, l'objet en main se cache par UpdateLocalVisibility(). // // Ce n'est pas une perte de fonctionnalite : sur une machine donnee, le seul // pawn dont on est proprietaire est celui qu'on controle, donc les deux // mecanismes cachent exactement les memes objets aux memes joueurs. // L'OMBRE ne se recopie pas non plus -- les deux meshes n'ont pas du tout le // meme role. Celui du corps doit projeter son ombre MEME invisible pour son // proprietaire (bCastHiddenShadow), sinon on voit son propre corps au sol // sans l'outil qu'on tient. Celui des bras ne doit rien projeter du tout : // colle a l'objectif, il barrerait le decor d'une ombre geante. Held->SetCastShadow(bCastWorldShadow); Held->SetCastHiddenShadow(bCastWorldShadow); // Rien a montrer tant qu'aucun objet n'est monte. Held->SetVisibility(false); // Sans ca le composant existe, se rend et s'anime, mais reste INTROUVABLE // dans le Details panel du pawn en PIE : le panneau ne liste que les // sous-objets du template et les composants declares comme composants // d'instance. C'est du confort de debogage, mais c'est ce confort qui // manquait pour diagnostiquer cette ombre. Owner->AddInstanceComponent(Held); UE_LOG(LogTemp, Log, TEXT("UHeldItemComponent : '%s' monte sur '%s' (socket '%s' %s, ombre %s)."), *ComponentName.ToString(), *Parent->GetName(), *SocketName.ToString(), Parent->DoesSocketExist(SocketName) ? TEXT("trouve") : TEXT("INTROUVABLE"), bCastWorldShadow ? TEXT("activee") : TEXT("desactivee")); return Held; }