(Feat) Add footsteps
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
// 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<FLifetimeProperty>& 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<AFpsPlayer>(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());
|
||||
}
|
||||
|
||||
void UHeldItemComponent::SetHeldItem(UItemDataAsset* NewItem)
|
||||
{
|
||||
if (HeldItem == NewItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HeldItem = NewItem;
|
||||
ApplyHeldVisuals();
|
||||
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::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 || !HeldItem->EquipMontage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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. La garde est deja tenue par
|
||||
// RefreshFromInventory, on la repose ici parce qu'un futur appelant
|
||||
// l'oublierait.
|
||||
if (!Player || !Player->IsLocallyControlled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Player->PlayArmsMontage(HeldItem->EquipMontage);
|
||||
}
|
||||
|
||||
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<UStaticMeshComponent>(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;
|
||||
}
|
||||
Reference in New Issue
Block a user