// Portage UE 5.5 - voir LPPairCollisionTestCommandlet.h #include "LPPairCollisionTestCommandlet.h" #include "AssetRegistry/AssetData.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Components/CapsuleComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Engine/World.h" #include "GameFramework/Character.h" #include "GameFramework/CharacterMovementComponent.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "PhysicsEngine/BodySetup.h" #include "PreviewScene.h" DEFINE_LOG_CATEGORY_STATIC(LogLPPairCollision, Log, All); namespace { struct FProp { FString Name; FVector Location = FVector::ZeroVector; FRotator Rotation = FRotator::ZeroRotator; FVector Scale = FVector::OneVector; }; struct FProbe { bool bBlocked = false; bool bStartInside = false; FString Hit; float Moved = 0.f; }; float Token(const FString& Part, const TCHAR* Key) { const FString Needle = FString::Printf(TEXT("%s="), Key); const int32 At = Part.Find(Needle); return At == INDEX_NONE ? 0.f : FCString::Atof(*Part + At + Needle.Len()); } bool LoadWorldFile(const FString& Path, FVector& RoomScale, TArray& Props) { TArray Lines; if (!FFileHelper::LoadFileToStringArray(Lines, *Path)) { return false; } bool bWantName = false; int32 Slash = 0; FString Name; for (FString Line : Lines) { Line.TrimStartAndEndInline(); if (Line.IsEmpty()) { continue; } if (Line == TEXT("*****")) { bWantName = true; Slash = 0; Name.Reset(); continue; } if (bWantName) { Name = Line; bWantName = false; continue; } if (Line == TEXT("///")) { ++Slash; continue; } if (Name.IsEmpty() && Line.StartsWith(TEXT("X="))) { RoomScale = FVector(Token(Line, TEXT("X")), Token(Line, TEXT("Y")), Token(Line, TEXT("Z"))); continue; } if (Slash < 2 || !Line.StartsWith(TEXT("X="))) { continue; } TArray Parts; Line.ParseIntoArray(Parts, TEXT(";"), true); if (Parts.Num() < 3) { continue; } FProp Prop; Prop.Name = Name; Prop.Location = FVector(Token(Parts[0], TEXT("X")), Token(Parts[0], TEXT("Y")), Token(Parts[0], TEXT("Z"))); Prop.Rotation = FRotator(Token(Parts[1], TEXT("P")), Token(Parts[1], TEXT("Y")), Token(Parts[1], TEXT("R"))); Prop.Scale = FVector(Token(Parts[2], TEXT("X")), Token(Parts[2], TEXT("Y")), Token(Parts[2], TEXT("Z"))); Props.Add(Prop); Slash = 0; } return Props.Num() > 0 && !RoomScale.IsNearlyZero(); } int32 BodyElems(UStaticMeshComponent* Comp) { const UBodySetup* Body = Comp ? Comp->GetBodySetup() : nullptr; return Body ? Body->AggGeom.GetElementCount() : 0; } void SetPawnResponse(const TArray& Meshes, const ECollisionResponse Response) { for (UStaticMeshComponent* Mesh : Meshes) { Mesh->SetCollisionResponseToChannel(ECC_Pawn, Response); } } void ApplyBodies(ACharacter* A, ACharacter* B, const bool bOn, const bool bIgnoreEachOther) { auto Apply = [bOn](ACharacter* Who) { UCapsuleComponent* Cap = Who->GetCapsuleComponent(); Cap->SetCollisionEnabled(bOn ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision); if (UCharacterMovementComponent* Move = Who->GetCharacterMovement()) { Move->GravityScale = bOn ? 1.f : 0.f; } Cap->ClearMoveIgnoreActors(); }; Apply(A); Apply(B); if (bIgnoreEachOther) { A->GetCapsuleComponent()->IgnoreActorWhenMoving(B, true); B->GetCapsuleComponent()->IgnoreActorWhenMoving(A, true); } A->GetCapsuleComponent()->RecreatePhysicsState(); B->GetCapsuleComponent()->RecreatePhysicsState(); } FProbe Probe(ACharacter* Who, const FVector& From, const FVector& Delta) { Who->SetActorLocation(From, false, nullptr, ETeleportType::TeleportPhysics); UCapsuleComponent* Cap = Who->GetCapsuleComponent(); const FVector Start = Cap->GetComponentLocation(); FHitResult Hit; Cap->MoveComponent(Delta, Cap->GetComponentQuat(), true, &Hit); FProbe Result; Result.Moved = FVector::Dist(Start, Cap->GetComponentLocation()); Result.bStartInside = Hit.bStartPenetrating; Result.bBlocked = Result.Moved < Delta.Size() - 2.f; if (Hit.bBlockingHit || Hit.bStartPenetrating) { Result.Hit = GetNameSafe(Hit.GetActor()); if (const UPrimitiveComponent* Comp = Hit.GetComponent()) { const UStaticMeshComponent* Mesh = Cast(Comp); const FString MeshName = (Mesh && Mesh->GetStaticMesh()) ? Mesh->GetStaticMesh()->GetName() : Comp->GetName(); Result.Hit += TEXT("/") + MeshName; } } Who->SetActorLocation(From, false, nullptr, ETeleportType::TeleportPhysics); return Result; } bool FindPairSpot(UWorld* World, float Radius, float HalfHeight, FVector& OutA, FVector& OutB) { const FCollisionShape Shape = FCollisionShape::MakeCapsule(Radius, HalfHeight); FCollisionQueryParams Query(SCENE_QUERY_STAT(LPPairSpot), false); const float Gap = Radius * 2.f + 40.f; const float Z = HalfHeight + 20.f; for (float Y = -700.f; Y <= 700.f; Y += 80.f) { for (float X = -700.f; X <= 700.f; X += 80.f) { const FVector A(X, Y, Z); const FVector B(X + Gap, Y, Z); const bool bA = World->OverlapBlockingTestByChannel(A, FQuat::Identity, ECC_WorldStatic, Shape, Query); const bool bB = World->OverlapBlockingTestByChannel(B, FQuat::Identity, ECC_WorldStatic, Shape, Query); if (!bA && !bB) { OutA = A; OutB = B; return true; } } } return false; } } ULPPairCollisionTestCommandlet::ULPPairCollisionTestCommandlet() { IsClient = false; IsServer = false; IsEditor = true; LogToConsole = true; } int32 ULPPairCollisionTestCommandlet::Main(const FString& Params) { TArray Tokens, Switches; TMap ParamsMap; ParseCommandLine(*Params, Tokens, Switches, ParamsMap); const FString WorldFile = ParamsMap.FindRef(TEXT("World")).IsEmpty() ? FPaths::Combine(FPaths::ProjectDir(), TEXT("Content/Modules/vin_Base/World/work___vin01.lpworld")) : ParamsMap.FindRef(TEXT("World")); const FString OutPath = ParamsMap.FindRef(TEXT("Out")).IsEmpty() ? FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Logs/portage/LPPairCollisionTest.txt")) : ParamsMap.FindRef(TEXT("Out")); FVector RoomScale = FVector::OneVector; TArray Props; if (!LoadWorldFile(WorldFile, RoomScale, Props)) { UE_LOG(LogLPPairCollision, Error, TEXT("Chambre illisible : %s"), *WorldFile); return 1; } FAssetRegistryModule& Assets = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); Assets.Get().SearchAllAssets(true); TArray MeshAssets; Assets.Get().GetAssetsByClass(UStaticMesh::StaticClass()->GetClassPathName(), MeshAssets, true); TMap MeshPath; for (const FAssetData& Data : MeshAssets) { const FString Name = Data.AssetName.ToString(); const FString Path = FString::Printf(TEXT("%s.%s"), *Data.PackageName.ToString(), *Name); const bool bFurniture = Path.Contains(TEXT("/Furniture/")) || Path.Contains(TEXT("/Heavy/")); if (!MeshPath.Contains(Name) || bFurniture) { MeshPath.Add(Name, Path); } } if (!GEngine) { UE_LOG(LogLPPairCollision, Error, TEXT("Pas de moteur, scene d'essai impossible.")); return 1; } FPreviewScene::ConstructionValues SceneValues; SceneValues.SetCreateDefaultLighting(false).SetEditor(false).SetTransactional(false).SetForceMipsResident(false); FPreviewScene Scene(SceneValues); UWorld* World = Scene.GetWorld(); FActorSpawnParameters Spawn; Spawn.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; Spawn.ObjectFlags |= RF_Transient; AActor* Room = World->SpawnActor(AActor::StaticClass(), FTransform::Identity, Spawn); USceneComponent* Root = NewObject(Room, TEXT("RoomRoot")); Room->SetRootComponent(Root); Root->RegisterComponent(); Root->SetWorldScale3D(RoomScale); TArray Furniture; UStaticMeshComponent* Wall = nullptr; double WallArea = 0.0; int32 Missing = 0; int32 WithoutBody = 0; for (const FProp& Prop : Props) { const FString* Path = MeshPath.Find(Prop.Name); UStaticMesh* Mesh = Path ? LoadObject(nullptr, **Path) : nullptr; if (!Mesh) { ++Missing; UE_LOG(LogLPPairCollision, Warning, TEXT("Maillage absent : %s"), *Prop.Name); continue; } UStaticMeshComponent* Comp = NewObject(Room); Comp->SetStaticMesh(Mesh); Comp->SetupAttachment(Root); Comp->SetRelativeLocation(Prop.Location); Comp->SetRelativeRotation(Prop.Rotation); Comp->SetRelativeScale3D(Prop.Scale); Comp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); Comp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); Comp->RegisterComponent(); Furniture.Add(Comp); if (BodyElems(Comp) == 0) { ++WithoutBody; } const FVector Extent = Comp->Bounds.GetBox().GetExtent(); const double Area = double(Extent.X) * double(Extent.Y); if ((Prop.Name.StartsWith(TEXT("fnt_new14")) || Prop.Name.StartsWith(TEXT("fnt_new18"))) && Area > WallArea) { Wall = Comp; WallArea = Area; } } if (!Wall && Furniture.Num() > 0) { Wall = Furniture[0]; } UStaticMesh* CubeMesh = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); UStaticMeshComponent* Cube = nullptr; if (CubeMesh) { AActor* CubeActor = World->SpawnActor(AActor::StaticClass(), FTransform::Identity, Spawn); Cube = NewObject(CubeActor, TEXT("Cube")); CubeActor->SetRootComponent(Cube); Cube->SetStaticMesh(CubeMesh); Cube->SetRelativeLocation(FVector(3000.f, 0.f, 100.f)); Cube->SetRelativeScale3D(FVector(1.5f)); Cube->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); Cube->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); Cube->RegisterComponent(); Furniture.Add(Cube); } UClass* PersonClass = LoadObject(nullptr, TEXT("/Game/BPs/Person.Person_C")); const ACharacter* PersonDefaults = PersonClass ? Cast(PersonClass->GetDefaultObject()) : nullptr; const UCapsuleComponent* PersonCap = PersonDefaults ? PersonDefaults->GetCapsuleComponent() : nullptr; auto MakeBody = [&](const FVector& At) -> ACharacter* { ACharacter* Who = World->SpawnActor(ACharacter::StaticClass(), At, FRotator::ZeroRotator, Spawn); if (!Who) { return nullptr; } UCapsuleComponent* Cap = Who->GetCapsuleComponent(); if (PersonCap) { Cap->SetCapsuleSize(PersonCap->GetUnscaledCapsuleRadius(), PersonCap->GetUnscaledCapsuleHalfHeight()); Cap->SetCollisionProfileName(PersonCap->GetCollisionProfileName()); Cap->SetCollisionObjectType(PersonCap->GetCollisionObjectType()); Cap->SetCollisionResponseToChannels(PersonCap->GetCollisionResponseToChannels()); } Cap->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); Cap->RecreatePhysicsState(); return Who; }; auto ResponseText = [](const UCapsuleComponent* Cap, const ECollisionChannel Channel) -> const TCHAR* { switch (Cap->GetCollisionResponseToChannel(Channel)) { case ECR_Ignore: return TEXT("Ignore"); case ECR_Overlap: return TEXT("Overlap"); case ECR_Block: return TEXT("Block"); default: return TEXT("?"); } }; const FVector Park(0.f, 0.f, 8000.f); ACharacter* A = MakeBody(Park); ACharacter* B = MakeBody(Park + FVector(200.f, 0.f, 0.f)); if (!A || !B || !Cube || !Wall) { UE_LOG(LogLPPairCollision, Error, TEXT("Scene incomplete (personnes %d, cube %d, mur %d, meubles %d)."), (A ? 1 : 0) + (B ? 1 : 0), Cube ? 1 : 0, Wall ? 1 : 0, Furniture.Num()); return 1; } const float Radius = A->GetCapsuleComponent()->GetScaledCapsuleRadius(); const float Half = A->GetCapsuleComponent()->GetScaledCapsuleHalfHeight(); FVector SpotA, SpotB; const bool bInside = FindPairSpot(World, Radius, Half, SpotA, SpotB); if (!bInside) { SpotA = FVector(0.f, 0.f, 4000.f); SpotB = SpotA + FVector(Radius * 2.f + 40.f, 0.f, 0.f); } const FBox WallBox = Wall->Bounds.GetBox(); const FVector WallCenter = WallBox.GetCenter(); FVector WallStart(WallBox.Max.X + Radius + 30.f, WallCenter.Y, FMath::Clamp(Half + 20.f, WallBox.Min.Z, WallBox.Max.Z)); const FVector WallDelta = FVector(WallCenter.X, WallCenter.Y, WallStart.Z) - WallStart; const FBox CubeBox = Cube->Bounds.GetBox(); const FVector CubeStart(CubeBox.Max.X + Radius + 30.f, CubeBox.GetCenter().Y, CubeBox.GetCenter().Z); const FVector CubeDelta = FVector(CubeBox.GetCenter().X, CubeStart.Y, CubeStart.Z) - CubeStart; const FVector TowardB = (SpotB - SpotA).GetSafeNormal() * 80.f; struct FCase { const TCHAR* Name; bool bBlockPawn; bool bCapsulesOn; bool bIgnore; bool bExpectOther; bool bExpectSolid; }; const FCase Cases[] = { { TEXT("Bureau actuel, capsules allumees"), false, true, false, true, false }, { TEXT("Scene actuelle, capsules coupees"), false, false, false, false, false }, { TEXT("Meubles bloquent, capsules encore coupees"), true, false, false, false, false }, { TEXT("Principes : capsules allumees, ignore mutuel, meubles bloquent"), true, true, true, false, true }, { TEXT("Meubles bloquent, sans ignorer l'autre"), true, true, false, true, true }, }; TArray Report; Report.Add(FString::Printf(TEXT("Chambre : %s"), *WorldFile)); Report.Add(FString::Printf(TEXT("Echelle racine : %s"), *RoomScale.ToString())); Report.Add(FString::Printf(TEXT("Meubles poses : %d, absents : %d, sans volume simple : %d"), Furniture.Num() - (Cube ? 1 : 0), Missing, WithoutBody)); if (Furniture.Num() > (Cube ? 1 : 0)) { UStaticMeshComponent* Sample = Furniture[0]; Report.Add(FString::Printf(TEXT("Exemple %s : position monde %s, echelle monde %s, volumes %d"), *Sample->GetStaticMesh()->GetName(), *Sample->GetComponentLocation().ToString(), *Sample->GetComponentScale().ToString(), BodyElems(Sample))); } Report.Add(FString::Printf(TEXT("Mur d'essai : %s, boite %s"), Wall->GetStaticMesh() ? *Wall->GetStaticMesh()->GetName() : TEXT("?"), *WallBox.ToString())); Report.Add(FString::Printf(TEXT("Personnes : capsules moteur, rayon %.1f, demi-hauteur %.1f, profil %s, type %d, corps physique %s. CDO Person : %s"), Radius, Half, *A->GetCapsuleComponent()->GetCollisionProfileName().ToString(), int32(A->GetCapsuleComponent()->GetCollisionObjectType()), A->GetCapsuleComponent()->IsPhysicsStateCreated() ? TEXT("oui") : TEXT("non"), PersonCap ? TEXT("lu") : TEXT("introuvable, valeurs moteur"))); Report.Add(FString::Printf(TEXT("Reponses de la capsule Person : Pawn=%s WorldStatic=%s WorldDynamic=%s"), ResponseText(A->GetCapsuleComponent(), ECC_Pawn), ResponseText(A->GetCapsuleComponent(), ECC_WorldStatic), ResponseText(A->GetCapsuleComponent(), ECC_WorldDynamic))); const bool bPersonBlocksPawn = A->GetCapsuleComponent()->GetCollisionResponseToChannel(ECC_Pawn) == ECR_Block; Report.Add(bPersonBlocksPawn ? TEXT("La capsule Person bloque le canal Pawn.") : TEXT("La capsule Person ne bloque pas le canal Pawn : deux Person se traversent deja.")); Report.Add(FString::Printf(TEXT("Emplacement du couple : %s -> %s (%s)"), *SpotA.ToString(), *SpotB.ToString(), bInside ? TEXT("dans la chambre") : TEXT("au-dessus, chambre trop occupee"))); Report.Add(TEXT("Deplacement = balayage de la capsule (le meme que la marche). L'animation n'est pas jouee.")); Report.Add(TEXT("")); auto ForcePawnProfile = [](ACharacter* Who) { UCapsuleComponent* Cap = Who->GetCapsuleComponent(); Cap->SetCollisionProfileName(TEXT("Pawn")); Cap->SetCollisionObjectType(ECC_Pawn); Cap->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); Cap->RecreatePhysicsState(); }; bool bPrinciples = true; bool bRoom = true; bool bForced = true; auto RunCases = [&](const TCHAR* Series, const bool bForcePawn) { if (bForcePawn) { ForcePawnProfile(A); ForcePawnProfile(B); Report.Add(TEXT("")); Report.Add(TEXT("--- Profil Pawn du moteur (les deux capsules se bloquent, pour isoler IgnoreActorWhenMoving) ---")); } for (const FCase& Case : Cases) { SetPawnResponse(Furniture, Case.bBlockPawn ? ECR_Block : ECR_Ignore); ApplyBodies(A, B, Case.bCapsulesOn, Case.bIgnore); if (bForcePawn && Case.bCapsulesOn) { ForcePawnProfile(A); ForcePawnProfile(B); if (Case.bIgnore) { A->GetCapsuleComponent()->IgnoreActorWhenMoving(B, true); B->GetCapsuleComponent()->IgnoreActorWhenMoving(A, true); } } B->SetActorLocation(SpotB, false, nullptr, ETeleportType::TeleportPhysics); const FProbe Other = Probe(A, SpotA, TowardB); B->SetActorLocation(Park, false, nullptr, ETeleportType::TeleportPhysics); const FProbe Solid = Probe(A, CubeStart, CubeDelta); const FProbe RoomHit = Probe(A, WallStart, WallDelta); const bool bExpectOther = (bForcePawn || bPersonBlocksPawn) ? Case.bExpectOther : false; const bool bOtherOk = Other.bBlocked == bExpectOther && !Other.bStartInside; const bool bWallOk = RoomHit.bBlocked == Case.bExpectSolid && !RoomHit.bStartInside; if (!bForcePawn) { bPrinciples &= bOtherOk; bRoom &= bWallOk; } else { bForced &= bOtherOk && bWallOk; } const FString Line = FString::Printf( TEXT("%s | %s | autre:%s (attendu %s, %.0f cm, %s) | cube:%s (attendu %s, %.0f cm, %s) | meuble:%s (attendu %s, %.0f cm, %s) | %s"), Series, Case.Name, Other.bBlocked ? TEXT("bloque") : TEXT("passe"), bExpectOther ? TEXT("bloque") : TEXT("passe"), Other.Moved, *Other.Hit, Solid.bBlocked ? TEXT("bloque") : TEXT("passe"), Case.bExpectSolid ? TEXT("bloque") : TEXT("passe"), Solid.Moved, *Solid.Hit, RoomHit.bBlocked ? TEXT("bloque") : TEXT("passe"), Case.bExpectSolid ? TEXT("bloque") : TEXT("passe"), RoomHit.Moved, *RoomHit.Hit, (bOtherOk && bWallOk) ? TEXT("OK") : TEXT("ECART")); Report.Add(Line); UE_LOG(LogLPPairCollision, Warning, TEXT("%s"), *Line); } }; RunCases(TEXT("Person"), false); RunCases(TEXT("Pawn"), true); Report.Add(TEXT("")); Report.Add(FString::Printf(TEXT("Profil Person : %s"), bPrinciples ? TEXT("TIENT") : TEXT("NE TIENT PAS"))); Report.Add(FString::Printf(TEXT("Profil Pawn force, ignore mutuel : %s"), bForced ? TEXT("TIENT") : TEXT("NE TIENT PAS"))); Report.Add(FString::Printf(TEXT("Meubles du bureau : %s"), bRoom ? TEXT("TIENT") : TEXT("NE TIENT PAS"))); Report.Add(TEXT("Le cube du moteur est mesure a part. Le verdict porte sur les meubles de la chambre et sur les deux capsules.")); UE_LOG(LogLPPairCollision, Warning, TEXT("Person : %s. Pawn force : %s. Meubles : %s."), bPrinciples ? TEXT("TIENT") : TEXT("NE TIENT PAS"), bForced ? TEXT("TIENT") : TEXT("NE TIENT PAS"), bRoom ? TEXT("TIENT") : TEXT("NE TIENT PAS")); FFileHelper::SaveStringArrayToFile(Report, *OutPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); UE_LOG(LogLPPairCollision, Warning, TEXT("Rapport : %s"), *OutPath); return (bPrinciples && bRoom && bForced) ? 0 : 2; }