S5-T1 : comparaison CoupleSim (sandbox) / LPShowCoupleSubsystem (jeu)
- Fiche : S5-T1 | Role : A-CART | Modele prevu :
claude-sonnet-5-5-high (M2)
- Mode : lecture seule. Aucun fichier de code modifie, seul ce document est cree.
- Amont :
S4/S4-CART.md (decoupage CoupleSim), 21_Sandbox_Addon.md (S5 : "LPShowCoupleSubsystem reste dans LifePlay ; aligner le contrat avec l'addon").
0. Sources lues
| Cote |
Fichier |
Lignes |
| Sandbox |
D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\Source\LPSandbox\LPCoupleSim.cpp |
477 |
| Sandbox (membres, constantes) |
LPPairSandbox.h (l. 100-129, 196-203), LPSandboxRoomBuilder.cpp (l. 117-226), LPSandboxInteraction.cpp (l. 61-101) |
|
| Sandbox (cible API) |
Plugins\LPSandbox\Source\LPAnimMeasure\ (LPAnimMeasure.h = seulement LogLPAnimMeasure), LPAnimMeasure.Build.cs |
|
| Jeu |
Source/LifePlay/LPShowCoupleSubsystem.h (77) et .cpp (1048) |
|
Remarque : LPCoupleSim n'a pas de header propre dans la sandbox. Les methodes sont celles de ALPPairSandboxStage / ALPPairSandboxCharacter declarees dans LPPairSandbox.h.
LPAnimMeasure ne depend que de Core, CoreUObject, Engine (+ Json en prive) et ne doit jamais dependre de LifePlay ni de LPSandbox. LifePlay.Build.cs depend deja publiquement de LPAnimMeasure (l. 46), ce qui rend un header partage utilisable des deux cotes sans changer les .Build.cs.
1. Ce qui est identique (a ne pas dupliquer plus longtemps)
| Element |
Sandbox |
Jeu |
| Transformation mesh vers acteur |
FTransform(FRotator(0,-90,0), FVector(0,0,-CapsuleHalf)) (l. 195) |
idem (l. 886, MeshToActor) |
| Echantillonnage de pose |
lambda Accumulate (l. 197-249) |
AddPose (l. 270-320) |
| Details de l'echantillonnage |
8 pas a Length*Step/8, Length = max(PlayLength, 0.01), os animes via GetCompressedTrackToSkeletonMapTable, GetBoneTransform(..., false), rejet NaN, tous les os du squelette de reference |
identique, caractere par caractere hors le 0.01f / 0.01 |
| Padding anime |
FVector(12, 12, 6) |
FVector(12, 12, 6) |
| Plafond de taille mesuree |
GetSize().GetMax() > 800 : rejet |
> 800.0 : rejet |
| Recalage bas de boite |
Extent.Z = max(20, Extent.Z - Cut*0.5), Center.Z += Cut*0.5 |
identique (formule), seul le seuil change (voir 2.2) |
| Composant boite |
UBoxComponent outer = Lead, attache a la racine du Lead, objet ECC_Pawn, toutes reponses Ignore, WorldDynamic = Block, cache, sans navigation |
identique |
| Soudure de base |
AttachToActor(Lead, SnapToTargetNotIncludingScale), relatif location/rotation = zero |
identique |
| Mesh des deux corps |
relatif (0,0,-Half), rotation (0,-90,0) |
identique |
| Canal des balayages |
ECC_Pawn |
ECC_Pawn |
2. Tableau des ecarts
2.1 Constantes
| # |
Constante |
Sandbox |
Jeu |
Ecart / risque |
| C1 |
Demi-hauteur de capsule |
CapsuleHalf du CDO Person (RoomBuilder l. 124-125), 88 par defaut du membre (LPPairSandbox.h l. 203), reecrite sur le corps a chaque FitCoupleBox |
capsule du Lead, GetUnscaledCapsuleHalfHeight() si > 1, sinon 88.f (l. 762, 884) |
Meme valeur nominale, source differente. A passer en parametre unique. |
| C2 |
Rayon de capsule |
CapsuleRadius, 20 par defaut ; reecrit sur les deux corps |
non utilise (la capsule du jeu n'est pas redimensionnee) |
Sandbox seule force la taille. |
| C3 |
Vitesse du couple |
250.f * dt dans MoveCouple (l. 142) |
aucune (le Blueprint deplace le Lead) |
Specifique sandbox, hors API. |
| C4 |
Plancher de boite |
18 en Z monde : Bottom = PlayerSpot.Z + Center.Z - Extent.Z, clamp si < 18.f (l. 259-260) |
Bottom = Center.Z - Extent.Z en local, clamp si < -Half (l. 898-899) |
Sandbox : fond a 18 - PlayerSpot.Z = -90 local (PlayerSpot.Z = Half + 20 = 108), soit 2 cm sous la plante. Jeu : fond a -Half = -88 (pile a la plante). Ecart de 2 cm. |
| C5 |
Marge pas de 2 pour le sol |
Z = CapsuleHalf + 20 (RoomBuilder l. 204) : le sol sandbox est a z = +10 |
inconnue (le jeu n'a pas ce sol) |
Explique le 18 de C4 ; pas de contrepartie jeu. |
| C6 |
Distance max de recherche du partenaire |
aucune (joueur + partenaire fixes) |
ShowReach = 400.0 (l. 35) |
Specifique jeu (selection du couple). |
| C7 |
Rayon de recherche du mobilier de la chambre |
non applicable |
4000.0 (l. 632) |
Specifique jeu. |
| C8 |
Nombre d'iterations de balayage |
2 (Step < 2, l. 157) |
3 (Step < 3, l. 977) |
Ecart. |
| C9 |
Seuil "marchable" |
Hit.Normal.Z > 0.6f : le pas est accepte (l. 163) |
Normal.Z > 0.6f pour les lits uniquement (l. 989, 993) ; > 0.6f aussi dans le test de normale horizontale (l. 1016, abs(Z) <= 0.6f) |
Meme 0.6 mais sens different. |
| C10 |
Profondeur de penetration ignoree |
Hit.bStartPenetrating : mouvement stoppe |
PenetrationDepth < 1.f ignoree (l. 1002) |
Ecart. |
| C11 |
Seuil de mouvement |
Wish.IsNearlyZero() |
Delta.IsNearlyZero(), Base.Equals(Now, 0.1f) |
Meme esprit, tolerances differentes. |
| C12 |
Ancre de pelvis du corps vivant |
non applicable (pas de corps vivant) |
140 cm autour de pelvis / hip (l. 832) |
Specifique jeu. |
| C13 |
Nom du composant |
CoupleBox (l. 268) |
LPShowCoupleBox (l. 853, 908) |
Cosmetique ; utile pour les captures, a unifier en une constante. |
2.2 Boite et extents
| # |
Sujet |
Sandbox |
Jeu |
Ecart / risque |
| E1 |
Source de mesure |
uniquement les animations : <Pair>_F et <Pair>_M sous DazAnimRoot, chemin "%s/%s%s.%s%s" (l. 199) |
deux chemins : (a) FitToBodies (os vivants des deux meshes, chaque tick tant qu'il reussit, l. 776-873) ; (b) FitBox sur les animations seulement si (a) echoue et si la cle de paire n'a pas deja ete essayee (l. 429-438) |
Le jeu mesure d'abord les corps reels, la sandbox ne le peut pas. Le comportement jeu est donc plus fin, la sandbox prevoit le cas (b). |
| E2 |
Noms de clips de la paire |
_F et _M fixes |
PairSibling : _F/_M et _0/_1 ; retrait des clips idle ; completion de paire depuis le dossier |
Sandbox ne gere pas _0/_1. |
| E3 |
Padding anime |
(12, 12, 6) |
(12, 12, 6) |
Identique. |
| E4 |
Padding corps vivants |
non applicable |
(4, 4, 4), puis max(.., 8) sur X/Y/Z (l. 846-849) |
Specifique jeu. |
| E5 |
Plafond corps vivants |
non applicable |
GetSize().GetMax() > 400.0 : rejet (l. 841) ; la mesure anime tombe a 800 |
Deux plafonds dans le jeu. |
| E6 |
Os exclus (corps vivants) |
non applicable |
hand, finger, foot, toe, twist, breast, hair, eye, jaw, weapon, ik (l. 822-829) |
La sandbox mesure TOUS les os en anime (mains, pieds, ik compris) ; le jeu les exclut en vivant mais pas en anime. La meme pose peut donner deux boites differentes. |
| E7 |
Recalage bas |
clamp dans le monde a 18 (voir C4), toujours applique |
clamp a -Half, applique seulement dans FitBox (pas dans FitToBodies) |
La boite vivante du jeu peut depasser sous la plante. |
| E8 |
Recalcul |
a chaque Apply() (toggles, PlayPair) |
vivant : a chaque tick ; anime : une fois par Key de paire ; FittedPair / RejectedPair sont remis a zero dans Release |
Frequence differente ; pas un probleme de parite tant que le resultat est a peu pres stable. |
| E9 |
Rotation |
SetRelativeRotation(Zero) |
idem |
Identique. |
| E10 |
Etat de collision du composant a la creation |
defaut du composant, puis bCapsulesOn ? QueryAndPhysics : NoCollision |
QueryAndPhysics a la creation et a chaque ajustement |
Le jeu n'a pas de bascule "boite coupee". |
| E11 |
Journal |
Sandbox boite %s : centre %s, etendue %s (l. 298) |
Show couple : boite ... (l. 923) |
Les deux formats sont lisibles, mais pas homogenes pour un diff automatique. |
| E12 |
Debug draw |
DrawDebugBox Vert / Rouge par bCapsulesOn (LPPairSandbox.cpp l. 565-567) |
DrawDebugBox vert tous les ticks (l. 440-443) |
Comparable. |
2.3 Balayage (sweep) du couple
| # |
Sujet |
Sandbox (MoveCouple, l. 130-186) |
Jeu (KeepTogether, l. 927-1048) |
| S1 |
Qui bouge |
la sandbox deplace elle-meme le Lead (AddActorWorldOffset) |
le Blueprint a deja deplace le Lead ; le sous-systeme reconstruit un point sur sans collision (LastSafe) et teleporte |
| S2 |
Declencheur |
clavier, via MoveCouple |
OnWorldPostActorTick (post tick acteurs), chaque frame |
| S3 |
Demarrage |
Box->GetComponentLocation() |
BoxAt(LastSafe) : transforme le decalage relatif de la boite par la rotation courante du Lead |
| S4 |
Fonction |
SweepSingleByChannel |
SweepMultiByChannel |
| S5 |
Forme |
MakeBox(Box->GetScaledBoxExtent()) |
idem |
| S6 |
Rotation |
Box->GetComponentQuat() |
idem pour le balayage ; Who->GetActorQuat() pour le point de depart |
| S7 |
Ignores |
Lead, Partner |
Lead, Partner, tous les ACharacter, tous les acteurs nommes Wall / Wall_* (coque, l. 955-975) |
| S8 |
Iterations |
2 |
3 |
| S9 |
Marches |
Normal.Z > 0.6 : pas complet accepte |
pas de marche generique ; seuls les lits (voir 2.4) sont franchissables |
| S10 |
Depart en penetration |
arret net (bStartPenetrating : break, pas de mouvement) |
depenetration : on prend le hit le plus profond (>= 1), Exit = Normal * Depth, Base += Exit, reboucle ; si la normale est quasi horizontale et que Wish y pointe, le mouvement est projete sur le plan |
| S11 |
Glissement |
VectorPlaneProject(Wish*(1-Time), Normal), puis Wish.Z = 0 |
VectorPlaneProject(Wish*(1-Time), Normal) sans Z = 0 |
| S12 |
Z |
verrouille a PlayerSpot.Z si bCapsulesOn (l. 180-185) |
laisse libre (le Blueprint gere le sol) |
| S13 |
Application |
AddActorWorldOffset(.., false) |
SetActorLocation(Base, false, nullptr, TeleportPhysics) |
| S14 |
Etat de reprise |
aucun |
LastSafe memorise ; Engage l'initialise a la position du Lead |
| S15 |
Journal |
aucun |
Show couple : bloque par %s normal %s profondeur %.0f (une fois par blocage) ; coque ... ignoree |
| S16 |
Coque |
les 4 murs sont BlockAllDynamic donc bloquent le couple |
la coque est ignoree par nom (Wall) : sinon le couple restait coince |
Constat : les deux algorithmes sont des variantes du meme slide-and-project, mais le jeu est un sur-ensemble (depenetration, lits, coque) et la sandbox est volontairement simple. Un test de parite (S5-T3) devra donc tourner sur le meme algorithme pour que la sandbox prevoie le jeu.
2.4 Meubles Pawn et lits
| # |
Sujet |
Sandbox |
Jeu |
| F1 |
Meubles de la chambre |
Furniture[] : UStaticMeshComponent crees par la sandbox, Pawn = Block des la creation (RoomBuilder l. 146) |
Room du GI : acteurs lus par reflexion (proprietes objet / tableaux de Room, acteurs attaches, rayon 4000), hors ACharacter |
| F2 |
Bascule |
bFurnitureBlocks : SetCollisionResponseToChannel(ECC_Pawn, Block/Ignore) sur tout Furniture[] (Apply, l. 82-85), touche 1 |
BlockRoomPawn : tout primitif (hors mesh squelettique, hors NoCollision) dont la reponse Pawn est Ignore passe a Block ; l'ancien etat est memorise dans SavedFurniture et restaure par Release (l. 646-693, 722-728) |
| F3 |
Reponse "Overlap" |
non traitee |
comptee (Overlap) mais non modifiee (seul Ignore est inverse) |
| F4 |
Meubles "d'usage" (assis / couche / utilise / travail) |
non traites |
IsUseFurniture : existence de `/Game/Heavy/New2/Daily/-f-0-<sit |
| F5 |
Lits |
aucun traitement |
IsBedMesh : nom contient bed, ou fnt_ext011, fnt_new045, fnt_new235. Hors penetration, Normal.Z > 0.6 et Wish.Z < -1 : on pose le couple dessus. En penetration avec Normal.Z > 0.6 et Wish.Z < 0 : on annule le Z et on ignore le lit |
| F6 |
Assis / couches vs capsules |
non traites |
LetSittersPass : pour tout ACharacter dont l'anim contient -sit, -lie, -use, -work ou sitting : capsule WorldDynamic = Ignore (memorise puis restaure dans SavedSeats) |
| F7 |
Debrayage global |
touches 1 / 2 |
CVar lp.ShowCouple (1 : nouveau, 0 : ancien chemin Blueprint) |
2.5 Soudure (weld) et drapeaux
| # |
Drapeau / action |
Sandbox (Weld + Apply + FitCoupleBox.ResetBody) |
Jeu (Engage + Weld + Release) |
| W1 |
Mouvement du partenaire |
StopMovementImmediately, DisableMovement, SetComponentTickEnabled(false) a chaque Weld |
pareil, mais une seule fois dans Engage (l. 451-461) ; ancien etat sauvegarde (FSavedMove : mode, gravite, tick) |
| W2 |
Restauration |
jamais (la sandbox detache plus tard dans BeginInteraction en coupant juste les mouvements) |
Release : SetComponentTickEnabled, SetMovementMode, GravityScale, detache en KeepWorldTransform (l. 710-720) |
| W3 |
Capsule du partenaire |
SetCollisionEnabled(NoCollision) a chaque Weld |
non touchee : le sous-systeme exige CapsuleOff (capsule deja coupee par le Blueprint) pour reconnaitre un acteur du show (l. 98-102, 375) |
| W4 |
Capsule du Lead |
Apply : NoCollision, DisableMovement, GravityScale = 0, teleport a PlayerSpot |
non touchee (idem : CapsuleOff requis) ; aucune gravite forcee |
| W5 |
Taille des capsules |
ResetBody : SetCapsuleSize(CapsuleRadius, CapsuleHalf, true) sur Lead et Partner |
jamais redimensionnees |
| W6 |
Placement des meshes |
seulement dans FitCoupleBox / ApplyBody |
a chaque tick (Weld l. 760-774) |
| W7 |
Pied du mesh mannequin |
ApplyBody : -Half - MinZ (MinZ = bas des bounds du mesh Mannequin) ; mais FitCoupleBox.ResetBody ecrase par -Half (incoherence interne a la sandbox) |
-Half uniquement |
| W8 |
Attache |
if Parent != Lead attache puis relatif zero ; a chaque Weld |
idem, a chaque tick |
| W9 |
Reglages "Interaction" |
BeginInteraction : detache, capsule Lead QueryAndPhysics, Block sur WorldStatic et WorldDynamic, gravite 1, MaxWalkSpeed = 180, MOVE_Walking, boite NoCollision |
n'existe pas : le jeu rend la main via Release |
2.6 Toggles de canaux
| # |
Toggle |
Sandbox |
Jeu |
| T1 |
ECC_Pawn des meubles |
bFurnitureBlocks (touche 1), sans restauration par composant |
BlockRoomPawn (Ignore -> Block, memorise) / Release (restaure) / OpenUseFurniture (Block -> Ignore, non restaure) |
| T2 |
Collision de la boite |
bCapsulesOn (touche 2) : QueryAndPhysics / NoCollision |
toujours QueryAndPhysics tant que le show est branche ; boite detruite par Release |
| T3 |
ECC_WorldDynamic des capsules |
Block en Interaction pour le Lead (BeginInteraction) ; sinon profil copie de Person (ConfigureCapsule) |
Ignore pour les capsules assises, memorise puis restaure |
| T4 |
ECC_WorldDynamic de la boite |
Block |
Block |
| T5 |
Profil de collision de capsule |
copie complete du CDO Person : profil, type d'objet, tableau de reponses (ConfigureCapsule, l. 36-55) |
non touche |
| T6 |
Murs |
BlockAllDynamic (RoomBuilder l. 170) |
coque Wall* ignoree dans le balayage (pas de changement de canal) |
3. Principaux ecarts comportementaux (resume)
- Mesure des extents : le jeu mesure d'abord les os vivants (anchor pelvis 140 cm, os exclus, plafond 400, pad 4/min 8) ; la sandbox ne mesure que les animations (tous os, pad 12/12/6, plafond 800). Le jeu retombe sur la mesure anime si la vivante echoue.
- Plancher : -88 (jeu) contre -90 (sandbox) en local, et seule la voie anime du jeu clampe.
- Sweep : 2 iterations simple / depart en penetration = arret, contre 3 iterations multi / depenetration, lits, coque ignoree, tous les
ACharacter ignores.
- Weld : le jeu sauvegarde et restaure (mode, gravite, tick) ; la sandbox ne restaure rien et force aussi capsules, gravite et tailles. Le jeu depend d'un etat deja coupe par le Blueprint.
- Pawn : sandbox = interrupteur global du tableau
Furniture ; jeu = inversion Ignore -> Block memorisee, ouverture des meubles d'usage, capsules assises ouvertes.
- Hors sujet de la sandbox : lits, meubles d'usage, assis/couches, selection du couple,
lp.ShowCouple.
4. API commune proposee : LPCoupleSim.h dans LPAnimMeasure
4.1 Principes
- Header
Plugins/LPSandbox/Source/LPAnimMeasure/LPCoupleSim.h, namespace LPCoupleSim, exports LPANIMMEASURE_API. Aucun changement de .Build.cs (les deux modules dependent deja de LPAnimMeasure, qui n'utilise que Core, CoreUObject, Engine).
- Nom d'implementation :
LPCoupleSimCore.cpp (et non LPCoupleSim.cpp) pour eviter une collision de nom avec LPSandbox/LPCoupleSim.cpp (autre module, autres intermediaires, mais confusion d'IDE et de diff).
- Trois niveaux, du plus pur au plus lie a Unreal :
- Pur (aucun
UObject, aucun UWorld) : constantes, FTransform, FBox, FVector. Testables sans monde.
- Lectures d'assets (
const UAnimSequence*, const USkeletalMeshComponent*) : sans effet de bord.
- Effets de bord (composants, canaux) : fonctions fines, chacune avec etat sauvegarde / restaure.
- Le sous-systeme jeu et la sandbox appellent les memes fonctions avec des options differentes, pas avec deux copies de l'algorithme.
4.2 Constantes et parametres
namespace LPCoupleSim
{
// Valeurs communes (jeu = reference) ; la sandbox peut les surcharger par option.
inline constexpr double DefaultCapsuleHalf = 88.0;
inline constexpr int32 PoseSamples = 8;
inline constexpr float WalkableNormalZ = 0.6f;
inline constexpr float MinPenetrationDepth = 1.f;
inline constexpr float MoveEpsilon = 0.1f;
struct FBoxFitParams
{
double CapsuleHalf = DefaultCapsuleHalf;
// Mesures par animation (voies sandbox et FitBox du jeu)
FVector AnimPad = FVector(12.0, 12.0, 6.0);
double AnimMaxSize = 800.0;
// Mesures par corps vivants (FitToBodies du jeu)
FVector LivePad = FVector(4.0, 4.0, 4.0);
FVector LiveMinExtent = FVector(8.0, 8.0, 8.0);
double LiveMaxSize = 400.0;
double LiveAnchorRadius = 140.0;
// Fond de boite en Z local de l'acteur (la plante = -CapsuleHalf).
double FloorLocalZ = -DefaultCapsuleHalf;
double MinExtentZ = 20.0;
};
struct FBoxFit
{
FVector Center = FVector::ZeroVector;
FVector Extent = FVector::ZeroVector;
bool bValid = false;
};
}
4.3 Fonctions pures (sans UObject)
namespace LPCoupleSim
{
// Transformation commune mesh -> acteur (rotation -90 yaw, decalage -CapsuleHalf).
LPANIMMEASURE_API FTransform MeshToActor(double CapsuleHalf);
// Espace composant a partir de locales + parents (remplace les deux boucles dupliquees).
LPANIMMEASURE_API void ComposeComponentSpace(
const TArray<int32>& Parents, const TArray<FTransform>& Local, TArray<FTransform>& OutComponent);
// Ajoute les positions d'os (espace composant) dans la boite, via MeshToActor.
LPANIMMEASURE_API void AccumulatePoints(
const TArray<FTransform>& Component, const FTransform& MeshToActor, FBox& InOutBounds);
// Os exclus de la mesure vivante (hand, finger, foot, toe, twist, breast, hair, eye, jaw, weapon, ik).
LPANIMMEASURE_API bool IsExcludedLiveBone(const FName BoneName);
// Mesure vivante : ancre pelvis/hip, rayon, os exclus. Nom des os fournis par l'appelant.
LPANIMMEASURE_API void AccumulateLiveBones(
const TArray<FName>& BoneNames, const TArray<FTransform>& Component, const FTransform& MeshToActor,
const FBoxFitParams& Params, FBox& InOutBounds);
// Transforme une boite brute en boite finale (padding, plafond, recalage du fond).
LPANIMMEASURE_API FBoxFit FitFromAnimBounds(const FBox& Bounds, const FBoxFitParams& Params);
LPANIMMEASURE_API FBoxFit FitFromLiveBounds(const FBox& Bounds, const FBoxFitParams& Params);
// Resolution de glissement : cas pur, pour tests unitaires et pour les deux algorithmes.
LPANIMMEASURE_API FVector SlideWish(const FVector& Wish, const FVector& Normal, float TimeFraction, bool bFlattenZ);
}
FitFromAnimBounds reprend la logique commune (pad, plafond 800, fond clampe : Bottom = Center.Z - Extent.Z, si < FloorLocalZ alors Cut = FloorLocalZ - Bottom, Extent.Z = max(MinExtentZ, Extent.Z - Cut*0.5), Center.Z += Cut*0.5).
FloorLocalZ : jeu = -Half (-88), sandbox actuellement 18 - PlayerSpot.Z (-90). Valeur de reference proposee : -Half (jeu), la sandbox s'aligne (a valider en S5-T3 avec LPPairCollisionTest).
4.4 Lectures d'assets (sans effet de bord)
namespace LPCoupleSim
{
// Echantillonne une AnimSequence (8 pas, os animes, GetBoneTransform raw=false) et ajoute au boite.
// Remplace Accumulate (sandbox) et AddPose (jeu).
LPANIMMEASURE_API bool AccumulateAnimBounds(
const UAnimSequence* Anim, const FTransform& MeshToActor, FBox& InOutBounds,
int32 Samples = PoseSamples);
// Os vivants d'un SkeletalMeshComponent (espace composant -> acteur, rotation incluse).
LPANIMMEASURE_API void AccumulateLiveBody(
const USkeletalMeshComponent* Mesh, const FBoxFitParams& Params, FBox& InOutBounds);
// Paire F/M ou 0/1 d'un clip, dans son dossier (remplace PairSibling cote jeu ; la sandbox
// l'utilise pour _F/_M et gagne _0/_1).
LPANIMMEASURE_API UAnimSequence* PairSibling(const UAnimSequence* Anim);
LPANIMMEASURE_API bool IsIdleClip(const UAnimSequence* Anim);
}
Note : LPPoseMeasure::Sample n'est pas un bon remplacement : il echantillonne tout le squelette et appelle GetBoneTransform(..., true) (donnees brutes) alors que les deux copies actuelles utilisent false et seulement les os animes. Garder le comportement des copies pour la parite.
4.5 Boite, sweep, soudure, canaux (effets de bord)
namespace LPCoupleSim
{
// --- Boite ---
// Cree (ou reutilise) la boite, nom et profil communs. bEnabled pilote QueryAndPhysics / NoCollision.
LPANIMMEASURE_API UBoxComponent* EnsureBox(
ACharacter* Lead, UBoxComponent*& InOutBox, FName Name, bool bEnabled);
LPANIMMEASURE_API void ApplyBoxFit(UBoxComponent& Box, const FBoxFit& Fit);
// --- Sweep ---
struct FSweepOptions
{
int32 MaxIterations = 3; // sandbox : 2 aujourd'hui
bool bFlattenZ = false; // sandbox : true
bool bAcceptWalkableStep = false; // sandbox : true (Normal.Z > 0.6)
bool bDepenetrate = true; // sandbox : false (arret net)
bool bIgnoreAllCharacters = true; // sandbox : false
TFunction<bool(const UPrimitiveComponent*)> IsWalkableSupport; // jeu : IsBedMesh
TFunction<bool(const AActor*)> IsIgnoredShell; // jeu : nom "Wall" / "Wall_*"
};
struct FSweepResult
{
FVector Location = FVector::ZeroVector; // position acteur retenue
FString Blocker; // dernier bloqueur (journal)
bool bMoved = false;
};
LPANIMMEASURE_API FSweepResult ResolveCoupleMove(
UWorld& World, const UBoxComponent& Box, const ACharacter& Lead, const ACharacter* Partner,
const FVector& From, const FVector& Wish, const FSweepOptions& Options);
// --- Soudure ---
struct FWeldOptions
{
bool bFreezePartnerMovement = true;
bool bPartnerCapsuleOff = false; // sandbox : true, jeu : false (deja coupe par le Blueprint)
bool bLeadCapsuleOff = false; // sandbox : true
bool bLeadGravityZero = false; // sandbox : true
bool bResizeCapsules = false; // sandbox : true
bool bPlaceMeshes = true;
double CapsuleHalf = DefaultCapsuleHalf;
float CapsuleRadius = 20.f;
};
struct FSavedMove // remplace FSavedMove (jeu) ; la sandbox en beneficie a BeginInteraction
{
TWeakObjectPtr<UCharacterMovementComponent> Move;
TEnumAsByte<EMovementMode> Mode = MOVE_None;
float Gravity = 1.f;
bool bTick = true;
};
LPANIMMEASURE_API FSavedMove SaveAndFreeze(ACharacter& Body);
LPANIMMEASURE_API void RestoreMove(FSavedMove& Saved);
LPANIMMEASURE_API void Weld(ACharacter& Lead, ACharacter& Partner, const FWeldOptions& Options);
LPANIMMEASURE_API void Unweld(ACharacter& Partner); // DetachFromActor(KeepWorldTransform)
// --- Canaux (meubles, capsules) ---
struct FSavedResponse
{
TWeakObjectPtr<UPrimitiveComponent> Comp;
ECollisionResponse Previous = ECR_Ignore;
ECollisionChannel Channel = ECC_Pawn;
};
// Inverse From -> To pour un canal, avec memorisation pour restaurer. Retourne le nombre modifie.
LPANIMMEASURE_API int32 FlipResponse(
TArrayView<UPrimitiveComponent* const> Comps, ECollisionChannel Channel,
ECollisionResponse From, ECollisionResponse To, TArray<FSavedResponse>& Saved);
LPANIMMEASURE_API void RestoreResponses(TArray<FSavedResponse>& Saved);
}
4.6 Mapping des appelants
| Fonction commune |
Appelant sandbox (LPCoupleSim.cpp) |
Appelant jeu (LPShowCoupleSubsystem) |
MeshToActor, AccumulateAnimBounds, FitFromAnimBounds |
FitCoupleBox (remplace la lambda Accumulate) |
FitBox (remplace AddPose) |
AccumulateLiveBody, FitFromLiveBounds |
aucun (la sandbox peut ajouter un mode "corps vivants" pour tester le chemin jeu) |
FitToBodies |
PairSibling, IsIdleClip |
optionnel |
CollectShowAnims |
EnsureBox, ApplyBoxFit |
FitCoupleBox |
FitBox, FitToBodies |
ResolveCoupleMove |
MoveCouple (options sandbox) |
KeepTogether (options jeu) |
SaveAndFreeze, RestoreMove, Weld, Unweld |
Weld, BeginInteraction |
Engage, Weld, Release |
FlipResponse, RestoreResponses |
Apply (touche 1) |
BlockRoomPawn, LetSittersPass, Release |
| Reste local |
PlayIdle, ConfigureCapsule, corps Quaternius, RefreshPose, PlayWalk, vitesse 250 |
selection du couple (SexPersons, AP1/AP2, ShowReach), IsBedMesh, IsUseFurniture, OpenUseFurniture, CVar lp.ShowCouple, reflexion GI / Room |
5. Risques
- Parite du sweep : tout alignement des options sandbox sur celles du jeu (3 iterations, depenetration, coque) change le comportement de la sandbox. A valider avec
LPPairCollisionTest avant de supprimer l'ancien code.
- Boite vivante : la sandbox n'a pas de corps vivants au moment du calcul (le mesh est place a la volee). Pour prevoir le jeu, ajouter un mode "vivant" optionnel cote sandbox, sinon l'ecart E1/E6 subsiste.
- Plancher : -90 / -88 : la boite sandbox descend 2 cm plus bas. Verifier les lits et le seuil de marche.
GetBoneTransform raw : conserver false pour la parite ; ne pas basculer sur LPPoseMeasure::Sample.
- Reflexion GI / Room : reste dans le jeu (
ObjectActor, ObjectProperty, ArrayProperty, BlockRoomPawn). Ne pas la deplacer dans LPAnimMeasure (aucun Blueprint GI/Room cote addon).
FSavedMove : le jeu restaure, la sandbox non. Utiliser la meme structure impose de decider si BeginInteraction doit restaurer le mode du partenaire (aujourd'hui il ne coupe que le mouvement).
- Nom de fichier :
LPCoupleSim.cpp existe deja cote LPSandbox ; utiliser LPCoupleSimCore.cpp cote LPAnimMeasure.
- Unity build :
LPAnimMeasure utilise PCHUsage = UseExplicitOrSharedPCHs ; ajouter les includes (Components/BoxComponent.h, GameFramework/Character.h, Animation/AnimSequence.h) dans le .cpp, pas dans le header (declarations anticipees class UBoxComponent; class ACharacter; ...).
6. Ordre de migration recommande (S5-T2 et suivants)
LPCoupleSim.h + LPCoupleSimCore.cpp : niveaux pur et lecture d'assets (MeshToActor, ComposeComponentSpace, AccumulateAnimBounds, FitFromAnimBounds). Brancher FitBox (jeu) et FitCoupleBox (sandbox) ; comparer les journaux boite ... centre ... etendue avant / apres sur une meme paire (ecart attendu : seulement le fond, 2 cm).
FSavedMove, Weld, Unweld, FlipResponse, RestoreResponses. Brancher Engage / Release / BlockRoomPawn / LetSittersPass (jeu), puis Weld / Apply (sandbox).
ResolveCoupleMove avec les options jeu ; ajouter les options sandbox ; garder l'ancien MoveCouple jusqu'a validation.
AccumulateLiveBody + FitFromLiveBounds (jeu) ; option sandbox "corps vivants".
- Validation : build Win64 Development du jeu et du plugin, test
LPPairCollisionTest, comparaison des journaux Show couple et Sandbox boite.
7. Points non verifies
- Les captures
ShowCouple du jeu n'ont pas ete rejouees : toutes les valeurs ci-dessus viennent de la lecture du code.
Docs/Migration/etat/sandbox/S5/ n'existait pas avant ce document.