LifePlay-Revival/Source/LifePlay/LPOutdoorRoomSubsystem.cpp
edujiro5972 8084622450 LifePlay UE 5.8 : état de développement au 2026-10-19
Tickets : REP1-T4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 22:57:42 +02:00

543 lines
20 KiB
C++

// Portage UE 5.5 - voir LPOutdoorRoomSubsystem.h
#include "LPOutdoorRoomSubsystem.h"
#include "Blueprint/UserWidget.h"
#include "Components/BoxComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/SceneComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "EngineUtils.h"
#include "GameFramework/Actor.h"
#include "GameFramework/Character.h"
#include "GameFramework/InputSettings.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerInput.h"
#include "LPKeyboardSubsystem.h"
#include "PhysicsEngine/BodySetup.h"
#include "UObject/UObjectIterator.h"
#include "LifePlay.h"
#include "UObject/UnrealType.h"
namespace
{
// Interieurs : 0,2 a 0,4 ; exterieurs : 0,01. Le seuil laisse une marge des deux cotes.
constexpr double OutdoorScaleZ = 0.05;
const FName RoomPropertyName(TEXT("Room"));
const FName CeilingPropertyName(TEXT("WallT"));
// S-F4 : plancher des personnages (BoxComponent du Blueprint Room) et echelle Z de reference d'un interieur.
const FName FloorPropertyName(TEXT("Box"));
constexpr double IndoorScaleZ = 0.2;
// S-F4b : decors exterieurs (fnt_ext*) dont la collision simple englobe la zone de marche ; examines toutes les 0,5 s.
const TCHAR* const OutdoorDecorPrefix = TEXT("fnt_ext");
constexpr float DecorScanSeconds = 0.5f;
// S-F5 : seuil d'enfoncement sous le dessus de Box, examens immobiles exiges, depart de la descente de la capsule (bas a
// 30 cm au-dessus de Box : le plan de marche fnt_nav01, 9 cm au-dessus de Box, est trouve ; un banc ou un etal ne l'est pas).
constexpr double SunkDepth = 15.0;
constexpr int32 SunkScans = 3;
constexpr double LiftProbe = 30.0;
// S-F5j : examens immobiles exiges pour le joueur (0 ; 0,5 ; 1 ; 1,5 s).
constexpr int32 PlayerSunkScans = 4;
// S-F5k : distance minimale entre deux personnes relevees, et ecart applique le long du grand axe de la piece.
constexpr double LiftSpacing = 50.0;
// S-F5p : apres l'entree dans une piece exterieure, balayage a chaque tick pendant FastScanSeconds ; saut de position au-dela de
// JumpDistance entre deux balayages = personne posee par le Blueprint (teleportation).
constexpr float FastScanSeconds = 3.f;
constexpr double JumpDistance = 50.0;
// Memes menus que LPDirectControlSubsystem (MenuClasses), dont la boite de scene Scene_C.
const TCHAR* const PlayerMenuClasses[] = {
TEXT("Journal_C"), TEXT("MainMenu_C"), TEXT("CityMenu_C"), TEXT("StatMenu_C"),
TEXT("ContactMenu_C"), TEXT("Inventory_C"), TEXT("CharGen_C"), TEXT("ModManager_C"),
TEXT("BodyShapeMenu_C"), TEXT("Scene_C")
};
template <typename T>
T* GetObjectProperty(const UObject* Container, FName Name)
{
const FObjectPropertyBase* Prop = Container ? CastField<FObjectPropertyBase>(Container->GetClass()->FindPropertyByName(Name)) : nullptr;
return Prop ? Cast<T>(Prop->GetObjectPropertyValue_InContainer(Container)) : nullptr;
}
bool ReadBoolProperty(const UObject* Container, const TCHAR* Name)
{
const FBoolProperty* Prop = Container ? CastField<FBoolProperty>(Container->GetClass()->FindPropertyByName(Name)) : nullptr;
return Prop && Prop->GetPropertyValue_InContainer(Container);
}
// S-F5j : le joueur pilote-t-il (une touche de MoveForward / MoveRight enfoncee) ou un menu, ou la boite de scene, est-il a l'ecran ?
bool PlayerBusy(const UGameInstance* GI, UWorld* World)
{
const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
const UInputSettings* Settings = UInputSettings::GetInputSettings();
if (!GI || !PC || !PC->PlayerInput || !Settings)
{
return true;
}
for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings())
{
if ((Map.AxisName == FName(TEXT("MoveForward")) || Map.AxisName == FName(TEXT("MoveRight")))
&& (Map.Key.IsDigital() ? PC->PlayerInput->IsPressed(Map.Key) : !FMath::IsNearlyZero(PC->PlayerInput->GetKeyValue(Map.Key))))
{
return true;
}
}
if (const ULPKeyboardSubsystem* Keys = GI->GetSubsystem<ULPKeyboardSubsystem>())
{
if (Keys->IsMenuOpen())
{
return true;
}
}
for (TObjectIterator<UUserWidget> It; It; ++It)
{
const UUserWidget* Widget = *It;
if (!Widget || Widget->HasAnyFlags(RF_ClassDefaultObject) || Widget->GetGameInstance() != GI || !Widget->IsInViewport())
{
continue;
}
const ESlateVisibility Visibility = Widget->GetVisibility();
if (Visibility == ESlateVisibility::Collapsed || Visibility == ESlateVisibility::Hidden)
{
continue;
}
const FString ClassName = Widget->GetClass()->GetName();
for (const TCHAR* Menu : PlayerMenuClasses)
{
if (ClassName == Menu)
{
return true;
}
}
}
return false;
}
// GI.PersonsUsedThisScene contient-il cet acteur (personne prise par la scene en cours) ?
bool UsedInScene(const UObject* GI, const AActor* Person)
{
const FArrayProperty* Prop = GI ? CastField<FArrayProperty>(GI->GetClass()->FindPropertyByName(TEXT("PersonsUsedThisScene"))) : nullptr;
const FObjectPropertyBase* Inner = Prop ? CastField<FObjectPropertyBase>(Prop->Inner) : nullptr;
if (!Inner)
{
return false;
}
FScriptArrayHelper Array(Prop, Prop->ContainerPtrToValuePtr<void>(GI));
for (int32 Index = 0; Index < Array.Num(); ++Index)
{
if (Inner->GetObjectPropertyValue(Array.GetRawPtr(Index)) == Person)
{
return true;
}
}
return false;
}
}
void ULPOutdoorRoomSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPOutdoorRoomSubsystem::Tick));
}
void ULPOutdoorRoomSubsystem::Deinitialize()
{
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
RestoreFloor();
RestoreDecor();
Super::Deinitialize();
}
bool ULPOutdoorRoomSubsystem::Tick(float DeltaTime)
{
const AActor* Room = GetObjectProperty<AActor>(GetGameInstance(), RoomPropertyName);
USceneComponent* Root = IsValid(Room) ? Room->GetRootComponent() : nullptr;
USceneComponent* Ceiling = IsValid(Room) ? GetObjectProperty<USceneComponent>(Room, CeilingPropertyName) : nullptr;
// Plafond masque precedemment mais qui n'est plus celui de la piece courante : le rendre.
if (HiddenCeiling.IsValid() && HiddenCeiling.Get() != Ceiling)
{
HiddenCeiling->SetHiddenInGame(false);
HiddenCeiling.Reset();
}
UBoxComponent* Floor = IsValid(Room) ? GetObjectProperty<UBoxComponent>(Room, FloorPropertyName) : nullptr;
if (ThickenedFloor.IsValid() && ThickenedFloor.Get() != Floor)
{
RestoreFloor();
}
if (!Root)
{
return true;
}
const bool bOutdoor = Root->GetRelativeScale3D().Z < OutdoorScaleZ;
if (Floor)
{
UpdateFloor(Floor, bOutdoor);
}
if (bOutdoor)
{
// S-F5p : nouvelle piece exterieure (echelle differente, ou retour de l'interieur) : balayage rapide, positions oubliees.
const FVector Scale = Root->GetRelativeScale3D();
if (!Scale.Equals(OutdoorScale))
{
OutdoorScale = Scale;
FastScanTime = FastScanSeconds;
LastPositions.Reset();
}
const bool bFast = FastScanTime > 0.f;
FastScanTime = FMath::Max(0.f, FastScanTime - DeltaTime);
DecorScanDelay += DeltaTime;
if (DecorScanDelay >= DecorScanSeconds)
{
DecorScanDelay = 0.f;
IgnoreOutdoorDecor(Room);
LiftSunkPersons(Room, Floor);
}
else if (bFast)
{
LiftSunkPersons(Room, Floor);
}
}
else
{
OutdoorScale = FVector::ZeroVector;
FastScanTime = 0.f;
LastPositions.Reset();
if (IgnoredDecor.Num() > 0)
{
RestoreDecor();
}
SunkPersons.Reset();
}
if (!Ceiling)
{
return true;
}
if (bOutdoor && !Ceiling->bHiddenInGame)
{
Ceiling->SetHiddenInGame(true);
HiddenCeiling = Ceiling;
UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure (echelle Z %.3f) : plafond WallT masque"), Root->GetRelativeScale3D().Z);
}
else if (!bOutdoor && HiddenCeiling.Get() == Ceiling)
{
Ceiling->SetHiddenInGame(false);
HiddenCeiling.Reset();
UE_LOG(LogLifePlay, Log, TEXT("Piece interieure (echelle Z %.3f) : plafond WallT rendu visible"), Root->GetRelativeScale3D().Z);
}
return true;
}
void ULPOutdoorRoomSubsystem::UpdateFloor(UBoxComponent* Floor, const bool bOutdoor)
{
if (!bOutdoor)
{
if (ThickenedFloor.Get() == Floor)
{
RestoreFloor();
}
return;
}
if (ThickenedFloor.Get() == Floor)
{
return;
}
// Box n'a pas d'echelle propre dans Room : son epaisseur monde vaut 2 x demi-hauteur x echelle Z de la racine.
const double ScaleZ = Floor->GetComponentScale().Z;
if (ScaleZ <= UE_KINDA_SMALL_NUMBER || !FMath::IsNearlyEqual(Floor->GetRelativeScale3D().Z, 1.0))
{
return;
}
const FVector Extent = Floor->GetUnscaledBoxExtent();
const double TargetZ = Extent.Z * IndoorScaleZ / ScaleZ;
if (TargetZ <= Extent.Z)
{
return;
}
FloorExtent = Extent;
FloorLocation = Floor->GetRelativeLocation();
FloorMobility = Floor->Mobility;
if (FloorMobility != EComponentMobility::Movable)
{
// Un composant Static refuse SetRelativeLocation en jeu.
Floor->SetMobility(EComponentMobility::Movable);
}
// Dessus inchange : relatif Z + demi-hauteur reste egal ; seul le dessous descend.
Floor->SetBoxExtent(FVector(Extent.X, Extent.Y, TargetZ), true);
Floor->SetRelativeLocation(FVector(FloorLocation.X, FloorLocation.Y, FloorLocation.Z + Extent.Z - TargetZ));
ThickenedFloor = Floor;
UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure (echelle Z %.3f) : plancher Box epaissi a %.1f cm (demi-hauteur %.1f -> %.1f)"),
ScaleZ, 2.0 * TargetZ * ScaleZ, Extent.Z, TargetZ);
}
void ULPOutdoorRoomSubsystem::RestoreFloor()
{
if (UBoxComponent* Floor = ThickenedFloor.Get())
{
Floor->SetBoxExtent(FloorExtent, true);
Floor->SetRelativeLocation(FloorLocation);
if (Floor->Mobility != FloorMobility)
{
Floor->SetMobility(FloorMobility);
}
UE_LOG(LogLifePlay, Log, TEXT("Plancher Box rendu a sa demi-hauteur d'origine %.1f"), FloorExtent.Z);
}
ThickenedFloor.Reset();
}
void ULPOutdoorRoomSubsystem::IgnoreOutdoorDecor(const AActor* Room)
{
// Depuis CHG-145, Room::addObject met chaque objet en WorldDynamic / Pawn Block ; la capsule des personnages bloque
// WorldDynamic, et la collision simple d'un decor exterieur (un volume, pas le pave) les emprisonne (STG-SF4b-DIAG.md).
IgnoredDecor.RemoveAll([](const TWeakObjectPtr<UPrimitiveComponent>& Comp) { return !Comp.IsValid(); });
SolidDecor.RemoveAll([](const TWeakObjectPtr<UPrimitiveComponent>& Comp) { return !Comp.IsValid(); });
TInlineComponentArray<UStaticMeshComponent*> Meshes;
Room->GetComponents(Meshes);
int32 Added = 0;
for (UStaticMeshComponent* Mesh : Meshes)
{
const UStaticMesh* Asset = Mesh ? Mesh->GetStaticMesh() : nullptr;
if (!Asset || !Asset->GetName().StartsWith(OutdoorDecorPrefix) || IgnoredDecor.Contains(Mesh) || SolidDecor.Contains(Mesh)
|| Mesh->GetCollisionResponseToChannel(ECC_Pawn) != ECR_Block)
{
continue;
}
// Variante facades : collision complexe (triangles du pave et des facades), pas de volume piege : le decor reste solide.
const UBodySetup* Body = Asset->GetBodySetup();
if (Body && Body->CollisionTraceFlag == CTF_UseComplexAsSimple)
{
SolidDecor.Add(Mesh);
UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : decor %s (%s) en collision complexe, laisse en Pawn Block"), *Mesh->GetName(), *Asset->GetName());
continue;
}
Mesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
IgnoredDecor.Add(Mesh);
++Added;
}
if (Added > 0)
{
UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %d decor(s) fnt_ext en Pawn Ignore (%d en tout)"), Added, IgnoredDecor.Num());
}
}
void ULPOutdoorRoomSubsystem::RestoreDecor()
{
int32 Restored = 0;
for (const TWeakObjectPtr<UPrimitiveComponent>& Comp : IgnoredDecor)
{
if (UPrimitiveComponent* Prim = Comp.Get())
{
Prim->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);
++Restored;
}
}
if (IgnoredDecor.Num() > 0)
{
UE_LOG(LogLifePlay, Log, TEXT("Decors fnt_ext rendus en Pawn Block : %d (sur %d, les autres detruits avec la piece)"), Restored, IgnoredDecor.Num());
}
IgnoredDecor.Reset();
SolidDecor.Reset();
DecorScanDelay = 0.f;
}
void ULPOutdoorRoomSubsystem::LiftSunkPersons(const AActor* Room, const UBoxComponent* Floor)
{
const UGameInstance* GI = GetGameInstance();
UWorld* World = Room ? Room->GetWorld() : nullptr;
if (!Floor || !World || ReadBoolProperty(GI, TEXT("SexOn")))
{
SunkPersons.Reset();
return;
}
const AActor* Player = GetObjectProperty<AActor>(GI, TEXT("PlayerCharacter"));
// Dessus de Box en monde (Box sans rotation dans Room ; inchange par S-F4).
const double FloorTop = Floor->GetComponentLocation().Z + Floor->GetScaledBoxExtent().Z;
TMap<TWeakObjectPtr<AActor>, TPair<FVector, int32>> Seen;
for (auto Logged = UnplacedLogged.CreateIterator(); Logged; ++Logged)
{
if (!Logged->IsValid())
{
Logged.RemoveCurrent();
}
}
for (auto Known = LastPositions.CreateIterator(); Known; ++Known)
{
if (!Known.Key().IsValid())
{
Known.RemoveCurrent();
}
}
for (TActorIterator<ACharacter> It(World); It; ++It)
{
ACharacter* Person = *It;
UCapsuleComponent* Capsule = Person ? Person->GetCapsuleComponent() : nullptr;
const bool bPlayer = Person == Player;
if (!Capsule || Person->IsHidden() || (!bPlayer && !Person->GetClass()->GetName().StartsWith(TEXT("Person_C")))
|| Capsule->GetCollisionEnabled() == ECollisionEnabled::NoCollision
|| IsValid(GetObjectProperty<UObject>(Person, TEXT("OccupyingFnt"))) || UsedInScene(GI, Person))
{
continue;
}
const double HalfHeight = Capsule->GetScaledCapsuleHalfHeight();
const FVector Location = Person->GetActorLocation();
const double Bottom = Location.Z - HalfHeight;
// S-F5p : personne posee depuis le balayage precedent (premiere vue dans la piece, ou saut de plus de JumpDistance).
const FVector* Previous = LastPositions.Find(Person);
const bool bJustPlaced = !Previous || FVector::Dist(*Previous, Location) > JumpDistance;
LastPositions.Add(Person, Location);
if (Bottom >= FloorTop - SunkDepth || Person->GetVelocity().Size2D() > 1.0)
{
if (Bottom >= FloorTop - SunkDepth)
{
// S-F5m : episode termine (personne a hauteur), une nouvelle ligne sera permise.
UnplacedLogged.Remove(Person);
}
continue;
}
// Immobile : meme position (a 1 cm pres) sur SunkScans examens consecutifs.
const TPair<FVector, int32>* Before = SunkPersons.Find(Person);
const int32 Scans = (Before && FVector::Dist(Before->Key, Location) < 1.0) ? Before->Value + 1 : 1;
// S-F5p : une personne qui vient d'etre posee (immobile, garde de vitesse ci-dessus) est relevee des ce premier examen.
if (!bJustPlaced && Scans < (bPlayer ? PlayerSunkScans : SunkScans))
{
Seen.Add(Person, TPair<FVector, int32>(Location, Scans));
continue;
}
// S-F5j : jamais le joueur qui pilote, ni sous un menu ou la boite de scene (examen repris a zero).
if (bPlayer && PlayerBusy(GI, World))
{
continue;
}
// S-F5k : autre personne visible a moins de LiftSpacing en 2D ? Alors essayer d'abord un point ecarte le long du grand axe
// de la piece (du cote oppose a elle), puis de l'autre cote, puis sur place.
const AActor* Near = nullptr;
for (TActorIterator<ACharacter> Other(World); Other; ++Other)
{
const ACharacter* OtherPerson = *Other;
const UCapsuleComponent* OtherCapsule = OtherPerson ? OtherPerson->GetCapsuleComponent() : nullptr;
if (OtherPerson && OtherPerson != Person && OtherCapsule && !OtherPerson->IsHidden()
&& OtherCapsule->GetCollisionEnabled() != ECollisionEnabled::NoCollision
&& FVector::Dist2D(OtherPerson->GetActorLocation(), Location) < LiftSpacing)
{
Near = OtherPerson;
break;
}
}
TArray<FVector, TInlineAllocator<3>> Candidates;
if (Near)
{
const FVector Scale = Room->GetRootComponent() ? Room->GetRootComponent()->GetComponentScale() : FVector::OneVector;
FVector Axis = (FMath::Abs(Scale.Y) >= FMath::Abs(Scale.X)) ? Room->GetActorRightVector() : Room->GetActorForwardVector();
Axis.Z = 0.0;
Axis = Axis.GetSafeNormal();
const double Side = FVector::DotProduct(Location - Near->GetActorLocation(), Axis) >= 0.0 ? 1.0 : -1.0;
Candidates.Add(Location + Axis * (Side * LiftSpacing));
Candidates.Add(Location - Axis * (Side * LiftSpacing));
}
Candidates.Add(Location);
// Descente de la capsule depuis le dessus de Box + LiftProbe : premier sol qu'elle rencontre, avec ses propres reponses.
FCollisionQueryParams Query(TEXT("LPLiftSunk"), false, Person);
FCollisionResponseParams Responses(Capsule->GetCollisionResponseToChannels());
FHitResult Hit;
bool bPlaced = false;
FVector Target = Location;
// Descente en un point : vrai si la capsule trouve un sol (ou rien : dessus de Box) sans partir en recouvrement.
FHitResult InPlace;
auto TryPlace = [&](const FVector& Candidate) -> bool
{
const FVector Start(Candidate.X, Candidate.Y, FloorTop + HalfHeight + LiftProbe);
const FVector End(Candidate.X, Candidate.Y, FloorTop + HalfHeight - 2.0);
Hit = FHitResult();
double TargetZ = FloorTop + HalfHeight + 2.0;
if (World->SweepSingleByChannel(Hit, Start, End, Capsule->GetComponentQuat(), Capsule->GetCollisionObjectType(), Capsule->GetCollisionShape(), Query, Responses))
{
if (Hit.bStartPenetrating)
{
if (Candidate.Equals(Location))
{
InPlace = Hit;
}
return false;
}
TargetZ = Hit.Location.Z + 1.0;
}
Target = FVector(Candidate.X, Candidate.Y, TargetZ);
return true;
};
for (const FVector& Candidate : Candidates)
{
if (TryPlace(Candidate))
{
bPlaced = true;
break;
}
}
// S-F5m (STG-COMMUN8-DIAG A) : point d'arrivee colle a un decor solide (facade en collision complexe depuis COMMUN-6) : la
// descente part en recouvrement. On essaie alors : S-F5n, le point donne par la normale de depenetration (partie horizontale,
// profondeur + 2 cm) ; puis 50 cm le long du petit axe vers le centre de la piece (loin de la facade), 50 cm de chaque cote le
// long du grand axe, et 50 cm le long du petit axe de l'autre cote.
bool bMoved = false;
if (!bPlaced)
{
TArray<FVector, TInlineAllocator<5>> Escapes;
FVector Push = InPlace.Normal;
Push.Z = 0.0;
if (InPlace.bStartPenetrating && Push.SizeSquared() > 0.01)
{
Escapes.Add(Location + Push.GetSafeNormal() * (static_cast<double>(InPlace.PenetrationDepth) + 2.0));
}
const FVector Scale = Room->GetRootComponent() ? Room->GetRootComponent()->GetComponentScale() : FVector::OneVector;
const bool bLongIsY = FMath::Abs(Scale.Y) >= FMath::Abs(Scale.X);
FVector Long = bLongIsY ? Room->GetActorRightVector() : Room->GetActorForwardVector();
FVector Short = bLongIsY ? Room->GetActorForwardVector() : Room->GetActorRightVector();
Long.Z = 0.0;
Short.Z = 0.0;
Long = Long.GetSafeNormal();
Short = Short.GetSafeNormal();
const double Inward = FVector::DotProduct(Room->GetActorLocation() - Location, Short) >= 0.0 ? 1.0 : -1.0;
Escapes.Add(Location + Short * (Inward * LiftSpacing));
Escapes.Add(Location + Long * LiftSpacing);
Escapes.Add(Location - Long * LiftSpacing);
Escapes.Add(Location - Short * (Inward * LiftSpacing));
for (const FVector& Candidate : Escapes)
{
if (TryPlace(Candidate))
{
bPlaced = true;
bMoved = true;
break;
}
}
}
if (!bPlaced)
{
if (!UnplacedLogged.Contains(Person))
{
UnplacedLogged.Add(Person);
UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %s%s enfonce de %.1f cm, aucun point libre (depart dans %s, S-F5m)"),
bPlayer ? TEXT("joueur ") : TEXT(""), *Person->GetName(), FloorTop - Bottom,
InPlace.GetComponent() ? *InPlace.GetComponent()->GetName() : TEXT("-"));
}
continue;
}
UnplacedLogged.Remove(Person);
Person->SetActorLocation(Target, false, nullptr, ETeleportType::TeleportPhysics);
++LiftCount;
UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %s%s releve de %.1f cm (bas de capsule %.1f, dessus Box %.1f, sol %s) ; %d releve(s) en tout"),
bPlayer ? TEXT("joueur ") : TEXT(""), *Person->GetName(), Target.Z - Location.Z, Bottom, FloorTop, Hit.GetComponent() ? *Hit.GetComponent()->GetName() : TEXT("Box"), LiftCount);
if (bMoved)
{
UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %s ecarte de %.1f cm (depart dans %s, S-F5m)"),
*Person->GetName(), FVector::Dist2D(Target, Location), InPlace.GetComponent() ? *InPlace.GetComponent()->GetName() : TEXT("-"));
}
else if (Near)
{
UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %s ecarte de %.1f cm (voisin %s a %.1f cm, S-F5k)"),
*Person->GetName(), FVector::Dist2D(Target, Location), *Near->GetName(), FVector::Dist2D(Near->GetActorLocation(), Location));
}
}
SunkPersons = MoveTemp(Seen);
}