From 0ee82ee51f697a075706d364a822580c5077542c Mon Sep 17 00:00:00 2001 From: pyramidmine Date: Wed, 23 Sep 2026 14:26:05 +0900 Subject: [PATCH] =?UTF-8?q?refactor=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=A0=95=EB=A6=AC=20=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=ED=94=84=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 문제 - 테스트가 NewObject로 만든 객체를 파괴하지 않아 에디터에서 테스트를 돌리면 객체가 남음 # 정리 스코프 (TestObjectScope.h) - RunTest 첫 줄에 선언하면 종료 시 등록 객체를 역순으로 EndPlay -> Uninitialize -> MarkAsGarbage - FGCObject로 등록 객체를 붙잡아 테스트 도중 GC를 막고, AddToRoot/KeepAlive 배열을 대체 # 테스트 적용 - 19개 파일의 NewObject를 Scope.New로 치환하고 픽스처 생성 함수에 스코프 인자 추가 - 본문의 직접 Uninitialize/Destroy 호출은 스코프가 대신하므로 제거 [SVN-REV]: 464 --- .../Actor/BoardActor/GimmickModelTests.cpp | 145 ++++++++------- .../Actor/TileMap/TileMapModelTests.cpp | 75 ++++---- .../ArtifactComponentModelTests.cpp | 11 +- .../ArtifactStatModifierTests.cpp | 13 +- .../PushMovementTests.cpp | 53 +++--- .../TeleportMovementTests.cpp | 39 ++-- .../UnitMovementComponentModelTests.cpp | 5 +- .../EquipmentComponentModelTests.cpp | 11 +- .../EquipmentStatModifierTests.cpp | 13 +- .../PassiveComponentModelTests.cpp | 5 +- .../SkillComponentModelTests.cpp | 13 +- .../SkillForcedMoveSyncTests.cpp | 93 +++++----- .../SRPGFramework/BoardActorMoveTests.cpp | 7 +- .../SRPGFramework/BoardActorRotateTests.cpp | 7 +- .../SRPGFramework/EnemyTurnPlannerTests.cpp | 117 ++++++------ .../Singleton/PartyArtifactRestoreTests.cpp | 16 +- .../TAS/Passive/PassiveConditionTests.cpp | 23 ++- .../Passive/TacticalPassiveGenericTests.cpp | 168 +++++++++--------- .../TAS/StatusEffectRoundDurationTests.cpp | 11 +- Source/P_RDTests/TestObjectScope.h | 132 ++++++++++++++ 20 files changed, 587 insertions(+), 370 deletions(-) create mode 100644 Source/P_RDTests/TestObjectScope.h diff --git a/Source/P_RDTests/Actor/BoardActor/GimmickModelTests.cpp b/Source/P_RDTests/Actor/BoardActor/GimmickModelTests.cpp index bf75b8795..35e43af31 100644 --- a/Source/P_RDTests/Actor/BoardActor/GimmickModelTests.cpp +++ b/Source/P_RDTests/Actor/BoardActor/GimmickModelTests.cpp @@ -9,6 +9,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "Actor/BoardActor/GimmickTestsHelper.h" // UMockOverlapGimmickModel, UMockGimmickVictimUnitModel @@ -47,10 +48,10 @@ namespace return nullptr; } - // @brief 기믹 스킬 공통 골격 (단타, 자기 타일 조준, 전 팀 타격) - UStaticSkillData* MakeGimmickSkillDataBase(UObject* Outer) + // @brief 기믹 스킬 공통 골격 (단타, 자기 타일 조준, 전 팀 타격). 스코프에 등록 + UStaticSkillData* MakeGimmickSkillDataBase(UObject* Outer, FTestObjectScope& Scope) { - UStaticSkillData* SkillData = NewObject(Outer); + UStaticSkillData* SkillData = Scope.New(Outer); // 스킬 시전 시 쿨다운 스펙을 만들므로 실제 기믹 DA와 같은 라운드 쿨다운 지정 (미지정 시 스펙이 무효) SkillData->mCooldownEffectClass = UTacticalEffect_RoundCooldown::StaticClass(); SkillData->mSkillAnimationSet.mApplyMotionTags.Add(AnimationTags::Animation_Montage_Skill_Melee_Punch); @@ -65,9 +66,9 @@ namespace } // @brief 기절 트랩 스킬 (기절 태그 1 부여) - UStaticSkillData* MakeStunSkillData(UObject* Outer) + UStaticSkillData* MakeStunSkillData(UObject* Outer, FTestObjectScope& Scope) { - UStaticSkillData* SkillData = MakeGimmickSkillDataBase(Outer); + UStaticSkillData* SkillData = MakeGimmickSkillDataBase(Outer, Scope); TInstancedStruct Layer; Layer.InitializeAs(); @@ -77,9 +78,9 @@ namespace } // @brief 밀치기 발판 스킬 (시전자 방향으로 밀침) - UStaticSkillData* MakePushSkillData(UObject* Outer, int32 PushDistance) + UStaticSkillData* MakePushSkillData(UObject* Outer, FTestObjectScope& Scope, int32 PushDistance) { - UStaticSkillData* SkillData = MakeGimmickSkillDataBase(Outer); + UStaticSkillData* SkillData = MakeGimmickSkillDataBase(Outer, Scope); TInstancedStruct Layer; Layer.InitializeAs(); @@ -97,9 +98,10 @@ namespace UMockUnitMovementComponentModel* Movement = nullptr; }; - // @brief 8x8 타일맵에 기믹과 유닛을 배치하고 스킬/수명을 세팅 + // @brief 8x8 타일맵에 기믹과 유닛을 배치하고 스킬/수명을 세팅 (생성 객체는 스코프에 등록) FGimmickFixture MakeGimmickFixture( UWorld* World, + FTestObjectScope& Scope, const FTileTransform& GimmickTransform, const FTileTransform& UnitTransform, UStaticSkillData* SkillData, @@ -107,11 +109,11 @@ namespace { FGimmickFixture Fixture; - Fixture.TileMap = NewObject(World); + Fixture.TileMap = Scope.New(World); Fixture.TileMap->SetDimensions(8, 8); // 기믹: 타일맵 주입 + 스킬 장착 + 수명 세팅 후 배치 - Fixture.Gimmick = NewObject(World); + Fixture.Gimmick = Scope.New(World); Fixture.Gimmick->Initialize(); Fixture.Gimmick->BeginPlay(); Fixture.Gimmick->SetTileMap(Fixture.TileMap); @@ -120,10 +122,10 @@ namespace Fixture.TileMap->PlaceActor(GimmickTransform, Fixture.Gimmick); // 피해자 유닛: 타일맵 주입 이동 컴포넌트 연결 후 배치 - Fixture.Unit = NewObject(World); + Fixture.Unit = Scope.New(World); Fixture.Unit->Initialize(); Fixture.Unit->BeginPlay(); - Fixture.Movement = NewObject(Fixture.Unit); + Fixture.Movement = Scope.New(Fixture.Unit); Fixture.Movement->SetTileMap(Fixture.TileMap); Fixture.Unit->SetBoardMovementComponentModel(Fixture.Movement); Fixture.TileMap->PlaceActor(UnitTransform, Fixture.Unit); @@ -155,10 +157,10 @@ namespace UMockUnitMovementComponentModel* Movement = nullptr; }; - // @brief 장판 생성/배치 (타일맵 주입 + 스킬 장착 + 라운드 수명 세팅) - UMockPuddleGimmickModel* MakePuddle(UWorld* World, UTileMapModel* TileMap, const FTileTransform& Transform, UStaticSkillData* SkillData, int32 RoundCount) + // @brief 장판 생성/배치 (타일맵 주입 + 스킬 장착 + 라운드 수명 세팅). 스코프에 등록 + UMockPuddleGimmickModel* MakePuddle(UWorld* World, FTestObjectScope& Scope, UTileMapModel* TileMap, const FTileTransform& Transform, UStaticSkillData* SkillData, int32 RoundCount) { - UMockPuddleGimmickModel* Puddle = NewObject(World); + UMockPuddleGimmickModel* Puddle = Scope.New(World); Puddle->Initialize(); Puddle->BeginPlay(); Puddle->SetTileMap(TileMap); @@ -168,9 +170,10 @@ namespace return Puddle; } - // @brief 8x8 타일맵에 장판과 유닛을 배치 + // @brief 8x8 타일맵에 장판과 유닛을 배치 (생성 객체는 스코프에 등록) FPuddleFixture MakePuddleFixture( UWorld* World, + FTestObjectScope& Scope, const FTileTransform& PuddleTransform, const FTileTransform& UnitTransform, UStaticSkillData* SkillData, @@ -178,16 +181,16 @@ namespace { FPuddleFixture Fixture; - Fixture.TileMap = NewObject(World); + Fixture.TileMap = Scope.New(World); Fixture.TileMap->SetDimensions(8, 8); - Fixture.Puddle = MakePuddle(World, Fixture.TileMap, PuddleTransform, SkillData, RoundCount); + Fixture.Puddle = MakePuddle(World, Scope,Fixture.TileMap, PuddleTransform, SkillData, RoundCount); // 피해자 유닛: 타일맵 주입 이동 컴포넌트 연결 후 배치 - Fixture.Unit = NewObject(World); + Fixture.Unit = Scope.New(World); Fixture.Unit->Initialize(); Fixture.Unit->BeginPlay(); - Fixture.Movement = NewObject(Fixture.Unit); + Fixture.Movement = Scope.New(Fixture.Unit); Fixture.Movement->SetTileMap(Fixture.TileMap); Fixture.Unit->SetBoardMovementComponentModel(Fixture.Movement); Fixture.TileMap->PlaceActor(UnitTransform, Fixture.Unit); @@ -195,13 +198,13 @@ namespace return Fixture; } - // @brief 추가 피해자 유닛 생성/배치 (2번째 유닛이 필요한 케이스용) - UMockGimmickVictimUnitModel* MakeVictimUnit(UWorld* World, UTileMapModel* TileMap, const FTileTransform& UnitTransform, UMockUnitMovementComponentModel*& OutMovement) + // @brief 추가 피해자 유닛 생성/배치 (2번째 유닛이 필요한 케이스용). 스코프에 등록 + UMockGimmickVictimUnitModel* MakeVictimUnit(UWorld* World, FTestObjectScope& Scope, UTileMapModel* TileMap, const FTileTransform& UnitTransform, UMockUnitMovementComponentModel*& OutMovement) { - UMockGimmickVictimUnitModel* Unit = NewObject(World); + UMockGimmickVictimUnitModel* Unit = Scope.New(World); Unit->Initialize(); Unit->BeginPlay(); - OutMovement = NewObject(Unit); + OutMovement = Scope.New(Unit); OutMovement->SetTileMap(TileMap); Unit->SetBoardMovementComponentModel(OutMovement); TileMap->PlaceActor(UnitTransform, Unit); @@ -222,6 +225,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FGimmickTriggerTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForGimmickTests(); if (World == nullptr) { @@ -237,10 +242,10 @@ bool FGimmickTriggerTests::RunTest(const FString& Parameters) // (2,2)의 기절 트랩(수명 1)을 (1,2)->(3,2) 경로 도중에 밟음 FGimmickFixture Fixture = MakeGimmickFixture( - World, + World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), FTileTransform(FTileIndex(1, 2), ETileActorDirection::Forward), - MakeStunSkillData(World), + MakeStunSkillData(World, Scope), 1); Fixture.Movement->MoveAlongPath({ FTileIndex(1, 2), FTileIndex(2, 2), FTileIndex(3, 2) }); @@ -259,7 +264,7 @@ bool FGimmickTriggerTests::RunTest(const FString& Parameters) Fixture.TileMap->RemoveActor(Fixture.Unit); UMockUnitMovementComponentModel* SecondMovement = nullptr; - UMockGimmickVictimUnitModel* SecondUnit = MakeVictimUnit(World, Fixture.TileMap, FTileTransform(FTileIndex(1, 2), ETileActorDirection::Forward), SecondMovement); + UMockGimmickVictimUnitModel* SecondUnit = MakeVictimUnit(World, Scope,Fixture.TileMap, FTileTransform(FTileIndex(1, 2), ETileActorDirection::Forward), SecondMovement); SecondMovement->MoveAlongPath({ FTileIndex(1, 2), FTileIndex(2, 2) }); @@ -283,6 +288,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FGimmickLifetimeFilterTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForGimmickTests(); if (World == nullptr) { @@ -297,10 +304,10 @@ bool FGimmickLifetimeFilterTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case1: 수명 -1 트랩은 반복 발동, 사망 없음 ===")); FGimmickFixture Fixture = MakeGimmickFixture( - World, + World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), FTileTransform(FTileIndex(1, 2), ETileActorDirection::Forward), - MakeStunSkillData(World), + MakeStunSkillData(World, Scope), -1); // 첫 번째 유닛 진입 @@ -312,7 +319,7 @@ bool FGimmickLifetimeFilterTests::RunTest(const FString& Parameters) // 두 번째 유닛 진입 UMockUnitMovementComponentModel* SecondMovement = nullptr; - UMockGimmickVictimUnitModel* SecondUnit = MakeVictimUnit(World, Fixture.TileMap, FTileTransform(FTileIndex(1, 2), ETileActorDirection::Forward), SecondMovement); + UMockGimmickVictimUnitModel* SecondUnit = MakeVictimUnit(World, Scope,Fixture.TileMap, FTileTransform(FTileIndex(1, 2), ETileActorDirection::Forward), SecondMovement); SecondMovement->MoveAlongPath({ FTileIndex(1, 2), FTileIndex(2, 2) }); TestTrue(TEXT("[Case1] 두 번째 유닛도 기절"), HasStunTag(SecondUnit)); @@ -323,18 +330,18 @@ bool FGimmickLifetimeFilterTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case2: 다른 Overlay 액터 진입 -> 미발동 ===")); { FGimmickFixture FilterFixture = MakeGimmickFixture( - World, + World, Scope, FTileTransform(FTileIndex(5, 5), ETileActorDirection::Forward), FTileTransform(FTileIndex(1, 1), ETileActorDirection::Forward), - MakeStunSkillData(World), + MakeStunSkillData(World, Scope), 1); // 트랩 타일 위에 다른 Overlay 기믹을 배치 (유닛 레이어가 아니므로 발동하면 안 됨) - UMockOverlapGimmickModel* OtherOverlay = NewObject(World); + UMockOverlapGimmickModel* OtherOverlay = Scope.New(World); OtherOverlay->Initialize(); OtherOverlay->BeginPlay(); OtherOverlay->SetTileMap(FilterFixture.TileMap); - OtherOverlay->GetSkillComponentModel()->SetSkillFrom(TArray>{ MakeStunSkillData(World) }); + OtherOverlay->GetSkillComponentModel()->SetSkillFrom(TArray>{ MakeStunSkillData(World, Scope) }); OtherOverlay->SetRemainingTriggerCount(1); FilterFixture.TileMap->PlaceActor(FTileTransform(FTileIndex(5, 5), ETileActorDirection::Forward), OtherOverlay); @@ -363,6 +370,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FGimmickPlaceAndPushTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForGimmickTests(); if (World == nullptr) { @@ -378,10 +387,10 @@ bool FGimmickPlaceAndPushTests::RunTest(const FString& Parameters) { // 픽스처가 유닛을 트랩 타일 (2,2)에 바로 배치 FGimmickFixture Fixture = MakeGimmickFixture( - World, + World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), - MakeStunSkillData(World), + MakeStunSkillData(World, Scope), 1); TestTrue(TEXT("[Case1] 배치 즉시 기절 부여"), HasStunTag(Fixture.Unit)); @@ -393,10 +402,10 @@ bool FGimmickPlaceAndPushTests::RunTest(const FString& Parameters) { // (3,2)의 발판이 Right(+Y)를 바라봄. 유닛은 (2,2)->(4,2)로 걷는 중 FGimmickFixture Fixture = MakeGimmickFixture( - World, + World, Scope, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Right), FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), - MakePushSkillData(World, 2), + MakePushSkillData(World, Scope, 2), 1); Fixture.Movement->MoveAlongPath({ FTileIndex(2, 2), FTileIndex(3, 2), FTileIndex(4, 2) }); @@ -410,10 +419,10 @@ bool FGimmickPlaceAndPushTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case3: 발판 위 배치 -> 즉시 +Y로 2칸 밀림 ===")); { FGimmickFixture Fixture = MakeGimmickFixture( - World, + World, Scope, FTileTransform(FTileIndex(5, 2), ETileActorDirection::Right), FTileTransform(FTileIndex(5, 2), ETileActorDirection::Forward), - MakePushSkillData(World, 2), + MakePushSkillData(World, Scope, 2), 1); TestTrue(TEXT("[Case3] 배치 즉시 밀린 위치 도착"), Fixture.Unit->GetTileTransform().mIndex == FTileIndex(5, 4)); @@ -437,6 +446,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FGimmickPullThroughTrapTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForGimmickTests(); if (World == nullptr) { @@ -448,9 +459,9 @@ bool FGimmickPullThroughTrapTests::RunTest(const FString& Parameters) } // @brief 시전자 유닛을 배치하고 대상에게 당기기 이펙트 적용 - auto ApplyPull = [World](UTileMapModel* TileMap, const FTileIndex& CasterTile, UMockGimmickVictimUnitModel* Victim, int32 PullDistance) + auto ApplyPull = [World, &Scope](UTileMapModel* TileMap, const FTileIndex& CasterTile, UMockGimmickVictimUnitModel* Victim, int32 PullDistance) { - UMockEnemyUnitModel* Caster = NewObject(World); + UMockEnemyUnitModel* Caster = Scope.New(World); Caster->Initialize(); Caster->BeginPlay(); TileMap->PlaceActor(FTileTransform(CasterTile, ETileActorDirection::Forward), Caster); @@ -467,10 +478,10 @@ bool FGimmickPullThroughTrapTests::RunTest(const FString& Parameters) { // 발판은 경로 밖 (7,7)에 두어 관여하지 않게 함 FGimmickFixture Fixture = MakeGimmickFixture( - World, + World, Scope, FTileTransform(FTileIndex(7, 7), ETileActorDirection::Forward), FTileTransform(FTileIndex(2, 1), ETileActorDirection::Backward), - MakePushSkillData(World, 2), + MakePushSkillData(World, Scope, 2), 1); ApplyPull(Fixture.TileMap, FTileIndex(2, 5), Fixture.Unit, 3); @@ -484,10 +495,10 @@ bool FGimmickPullThroughTrapTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case2: 당기기 경로 위 (2,3) 발판(Left, -Y) -> 발판에서 -Y로 2칸 밀려 (2,1) 도착 ===")); { FGimmickFixture Fixture = MakeGimmickFixture( - World, + World, Scope, FTileTransform(FTileIndex(2, 3), ETileActorDirection::Left), FTileTransform(FTileIndex(2, 1), ETileActorDirection::Backward), - MakePushSkillData(World, 2), + MakePushSkillData(World, Scope, 2), 1); ApplyPull(Fixture.TileMap, FTileIndex(2, 5), Fixture.Unit, 3); @@ -515,6 +526,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FPuddleRoundEndTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForGimmickTests(); if (World == nullptr) { @@ -529,10 +542,10 @@ bool FPuddleRoundEndTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case1: 장판 진입 -> 기절 1, 라운드 끝 -> 기절 2, 수명 2 -> 1 ===")); FPuddleFixture Fixture = MakePuddleFixture( - World, + World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), FTileTransform(FTileIndex(1, 2), ETileActorDirection::Forward), - MakeStunSkillData(World), + MakeStunSkillData(World, Scope), 2); Fixture.Movement->MoveAlongPath({ FTileIndex(1, 2), FTileIndex(2, 2) }); @@ -557,10 +570,10 @@ bool FPuddleRoundEndTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case3: 수명 1 장판 위 유닛, 라운드 끝 -> 발동 후 사망 ===")); { FPuddleFixture LastFixture = MakePuddleFixture( - World, + World, Scope, FTileTransform(FTileIndex(4, 4), ETileActorDirection::Forward), FTileTransform(FTileIndex(4, 4), ETileActorDirection::Forward), - MakeStunSkillData(World), + MakeStunSkillData(World, Scope), 1); LastFixture.Puddle->TriggerRoundEnd(nullptr); @@ -573,10 +586,10 @@ bool FPuddleRoundEndTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case4: 수명 -1 장판은 라운드가 지나도 유지 ===")); { FPuddleFixture InfFixture = MakePuddleFixture( - World, + World, Scope, FTileTransform(FTileIndex(6, 6), ETileActorDirection::Forward), FTileTransform(FTileIndex(6, 6), ETileActorDirection::Forward), - MakeStunSkillData(World), + MakeStunSkillData(World, Scope), -1); InfFixture.Puddle->TriggerRoundEnd(nullptr); @@ -604,6 +617,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FPuddleReplaceTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForGimmickTests(); if (World == nullptr) { @@ -618,19 +633,19 @@ bool FPuddleReplaceTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case1: (2,2) 유닛 위에 장판 배치 -> 즉시 기절 ===")); // 유닛을 먼저 세우고, 그 타일에 장판을 나중에 깖 - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(8, 8); UMockUnitMovementComponentModel* Movement = nullptr; - UMockGimmickVictimUnitModel* Unit = MakeVictimUnit(World, TileMap, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), Movement); + UMockGimmickVictimUnitModel* Unit = MakeVictimUnit(World, Scope,TileMap, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), Movement); const FTileTransform PuddleTransform(FTileIndex(2, 2), ETileActorDirection::Forward); - UMockPuddleGimmickModel* First = MakePuddle(World, TileMap, PuddleTransform, MakeStunSkillData(World), 3); + UMockPuddleGimmickModel* First = MakePuddle(World, Scope,TileMap, PuddleTransform, MakeStunSkillData(World, Scope), 3); TestEqual(TEXT("[Case1] 장판이 깔리자마자 기절 1"), GetStunCount(Unit), 1); /* Case2: 새 장판이 기존 장판을 덮어씀 */ AddInfo(TEXT("=== Case2: 같은 타일에 두 번째 장판 -> 첫 장판 이탈+사망, 새 장판만 잔류 ===")); - UMockPuddleGimmickModel* Second = MakePuddle(World, TileMap, PuddleTransform, MakeStunSkillData(World), 3); + UMockPuddleGimmickModel* Second = MakePuddle(World, Scope,TileMap, PuddleTransform, MakeStunSkillData(World, Scope), 3); const TArray Puddles = TileMap->GetActorsOnTile(PuddleTransform.mIndex, ETileLayerFlag::Overlay); TestEqual(TEXT("[Case2] 타일 위 장판은 1개"), Puddles.Num(), 1); @@ -668,6 +683,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FPuddleRoundEndEventTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForGimmickTests(); if (World == nullptr) { @@ -678,10 +695,10 @@ bool FPuddleRoundEndEventTests::RunTest(const FString& Parameters) return false; } - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(8, 8); - UMockGimmickCombatModel* CombatModel = NewObject(World); + UMockGimmickCombatModel* CombatModel = Scope.New(World); CombatModel->SetTileMap(TileMap); /* Case1: 중복 등록 방지 */ @@ -696,14 +713,14 @@ bool FPuddleRoundEndEventTests::RunTest(const FString& Parameters) /* Case2: 보드 위 모든 장판 일괄 발동 */ AddInfo(TEXT("=== Case2: 장판 3개(유닛 2 + 빈 1), 이벤트 1회 -> 유닛 기절 +1, 수명 전부 -1 ===")); - UMockPuddleGimmickModel* PuddleA = MakePuddle(World, TileMap, FTileTransform(FTileIndex(1, 1), ETileActorDirection::Forward), MakeStunSkillData(World), 3); - UMockPuddleGimmickModel* PuddleB = MakePuddle(World, TileMap, FTileTransform(FTileIndex(5, 5), ETileActorDirection::Forward), MakeStunSkillData(World), 3); - UMockPuddleGimmickModel* PuddleC = MakePuddle(World, TileMap, FTileTransform(FTileIndex(3, 3), ETileActorDirection::Forward), MakeStunSkillData(World), 3); + UMockPuddleGimmickModel* PuddleA = MakePuddle(World, Scope,TileMap, FTileTransform(FTileIndex(1, 1), ETileActorDirection::Forward), MakeStunSkillData(World, Scope), 3); + UMockPuddleGimmickModel* PuddleB = MakePuddle(World, Scope,TileMap, FTileTransform(FTileIndex(5, 5), ETileActorDirection::Forward), MakeStunSkillData(World, Scope), 3); + UMockPuddleGimmickModel* PuddleC = MakePuddle(World, Scope,TileMap, FTileTransform(FTileIndex(3, 3), ETileActorDirection::Forward), MakeStunSkillData(World, Scope), 3); UMockUnitMovementComponentModel* MovementA = nullptr; UMockUnitMovementComponentModel* MovementB = nullptr; - UMockGimmickVictimUnitModel* UnitA = MakeVictimUnit(World, TileMap, FTileTransform(FTileIndex(1, 1), ETileActorDirection::Forward), MovementA); - UMockGimmickVictimUnitModel* UnitB = MakeVictimUnit(World, TileMap, FTileTransform(FTileIndex(5, 5), ETileActorDirection::Forward), MovementB); + UMockGimmickVictimUnitModel* UnitA = MakeVictimUnit(World, Scope,TileMap, FTileTransform(FTileIndex(1, 1), ETileActorDirection::Forward), MovementA); + UMockGimmickVictimUnitModel* UnitB = MakeVictimUnit(World, Scope,TileMap, FTileTransform(FTileIndex(5, 5), ETileActorDirection::Forward), MovementB); CombatModel->TriggerAllRoundEndEvents(); @@ -722,7 +739,5 @@ bool FPuddleRoundEndEventTests::RunTest(const FString& Parameters) TestEqual(TEXT("[Case3] 유닛A 기절 3"), GetStunCount(UnitA), 3); TestEqual(TEXT("[Case3] 장판C 수명 2 -> 1"), PuddleC->GetRemainingRoundCount(), 1); - CombatModel->Destroy(); - return true; } diff --git a/Source/P_RDTests/Actor/TileMap/TileMapModelTests.cpp b/Source/P_RDTests/Actor/TileMap/TileMapModelTests.cpp index 58f1ece03..2876a1305 100644 --- a/Source/P_RDTests/Actor/TileMap/TileMapModelTests.cpp +++ b/Source/P_RDTests/Actor/TileMap/TileMapModelTests.cpp @@ -1,4 +1,4 @@ -/*****************************************************************//** +/*****************************************************************//** * @file TileMapModelTests.cpp * @brief UTileMapModel 타겟범위/영향범위/조준가능(CanAim)/위협범위(GetThreatRanges) 유닛테스트 * @details @@ -11,6 +11,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "SRPGFramework/EnemyTurnPlannerTestsHelper.h" // UMockPlayerUnitModel (점유 판정용 유닛 Mock) @@ -41,17 +42,15 @@ namespace return nullptr; } - // @brief 위협 범위 테스트용 스킬 생성 (KeepAlive에 등록해 GC 방지) - UStaticUnitSkillData* MakeThreatSkill(UWorld* World, TArray& KeepAlive, EAimPattern AimPattern, int32 AimRange, int32 RequiredActionPoint) + // @brief 위협 범위 테스트용 스킬 생성 (스코프에 등록해 GC 방지 + 종료 시 파괴) + UStaticUnitSkillData* MakeThreatSkill(UWorld* World, FTestObjectScope& Scope, EAimPattern AimPattern, int32 AimRange, int32 RequiredActionPoint) { - UStaticUnitSkillData* Skill = NewObject(World); + UStaticUnitSkillData* Skill = Scope.New(World); Skill->mAimPattern = AimPattern; Skill->mAimRange = AimRange; Skill->mCanAimBoardActor = true; Skill->mAimBlockerMask = static_cast(ETileLayerFlag::Obstacle | ETileLayerFlag::Unit); Skill->mRequiredActionPoint = RequiredActionPoint; - Skill->AddToRoot(); - KeepAlive.Add(Skill); return Skill; } } @@ -64,6 +63,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FTileMapModelTargetTilesTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForTileMapTests(); if (World == nullptr) { @@ -75,7 +76,7 @@ bool FTileMapModelTargetTilesTests::RunTest(const FString& Parameters) } // 맵 (8x8): 시전자 C=(0,0) - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(8, 8); const FTileIndex Caster(0, 0); @@ -116,7 +117,7 @@ bool FTileMapModelTargetTilesTests::RunTest(const FString& Parameters) */ AddInfo(TEXT("=== Case3: LineToTarget / 경로 위 점유 -> 경로 불변 ===")); { - UMockPlayerUnitModel* Blocker = NewObject(World); + UMockPlayerUnitModel* Blocker = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(2, 0)), Blocker); const TArray Tiles = TileMap->GetTargetTiles(Caster, FTileIndex(3, 0), ETargetPattern::LineToTarget); @@ -164,6 +165,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FTileMapModelEffectTilesTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForTileMapTests(); if (World == nullptr) { @@ -175,11 +178,11 @@ bool FTileMapModelEffectTilesTests::RunTest(const FString& Parameters) } // 맵 (8x1): 점유 유닛 U=(3,0) - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(8, 1); // 점유 유닛 배치 (Unit 레이어 판정만 필요하니 플레이어 Mock 재사용) - UMockPlayerUnitModel* Blocker = NewObject(World); + UMockPlayerUnitModel* Blocker = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(3, 0)), Blocker); /** @@ -232,6 +235,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FTileMapModelCanAimTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForTileMapTests(); if (World == nullptr) { @@ -249,12 +254,12 @@ bool FTileMapModelCanAimTests::RunTest(const FString& Parameters) * CanAim(원점, 타일) == GetAimableTiles(원점).Contains(타일)을 전수 확인한다. */ // 맵 (7x5): 원점 O=(1,2), 시야/점유 변화를 만들 유닛 U1=(3,2), U2=(2,1) - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(7, 5); - UMockPlayerUnitModel* Unit1 = NewObject(World); + UMockPlayerUnitModel* Unit1 = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(3, 2)), Unit1); - UMockPlayerUnitModel* Unit2 = NewObject(World); + UMockPlayerUnitModel* Unit2 = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(2, 1)), Unit2); const FTileIndex Origin(1, 2); @@ -312,6 +317,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FTileMapModelThreatRangesTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForTileMapTests(); if (World == nullptr) { @@ -322,9 +329,6 @@ bool FTileMapModelThreatRangesTests::RunTest(const FString& Parameters) return false; } - // 유닛테스트 동안 스킬 데이터가 GC 당하지 않도록 걸어두는 장치 - TArray KeepAlive; - /** * Case1: 시전비용 = 행동력 (기획안 예시 A) * -> 이동은 3칸까지 가능하지만, 이동하면 시전 예산이 남지 않아 공격은 제자리에서만 @@ -334,15 +338,15 @@ bool FTileMapModelThreatRangesTests::RunTest(const FString& Parameters) */ AddInfo(TEXT("=== Case1: 시전비용 = 행동력 -> 제자리 시전만 ===")); { - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(5, 5); // 적 유닛 배치 (점유/차폐 제외 판정용 — 레이어만 필요하니 플레이어 Mock 재사용) - UMockPlayerUnitModel* Enemy = NewObject(World); + UMockPlayerUnitModel* Enemy = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(2, 2)), Enemy); // 빈 슬롯(nullptr)이 무시되는지 함께 검증 - const TArray Skills = { nullptr, MakeThreatSkill(World, KeepAlive, EAimPattern::Cross, 1, 3) }; + const TArray Skills = { nullptr, MakeThreatSkill(World, Scope, EAimPattern::Cross, 1, 3) }; TArray MoveTiles; TArray AttackTiles; @@ -366,14 +370,14 @@ bool FTileMapModelThreatRangesTests::RunTest(const FString& Parameters) */ AddInfo(TEXT("=== Case2: 시전비용 1 -> 이동 후 시전으로 공격범위 확장 ===")); { - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(5, 5); // 적 유닛 배치 (점유/차폐 제외 판정용 — 레이어만 필요하니 플레이어 Mock 재사용) - UMockPlayerUnitModel* Enemy = NewObject(World); + UMockPlayerUnitModel* Enemy = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(2, 2)), Enemy); - const TArray Skills = { MakeThreatSkill(World, KeepAlive, EAimPattern::Cross, 2, 1) }; + const TArray Skills = { MakeThreatSkill(World, Scope, EAimPattern::Cross, 2, 1) }; TArray MoveTiles; TArray AttackTiles; @@ -393,16 +397,16 @@ bool FTileMapModelThreatRangesTests::RunTest(const FString& Parameters) */ AddInfo(TEXT("=== Case3: 차단 유닛 -> 통과 불가 + 차폐 ===")); { - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(6, 1); // 적 유닛과 차단 유닛 배치 (레이어만 필요하니 플레이어 Mock 재사용) - UMockPlayerUnitModel* Enemy = NewObject(World); + UMockPlayerUnitModel* Enemy = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(0, 0)), Enemy); - UMockPlayerUnitModel* Blocker = NewObject(World); + UMockPlayerUnitModel* Blocker = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(2, 0)), Blocker); - const TArray Skills = { MakeThreatSkill(World, KeepAlive, EAimPattern::Cross, 2, 1) }; + const TArray Skills = { MakeThreatSkill(World, Scope, EAimPattern::Cross, 2, 1) }; TArray MoveTiles; TArray AttackTiles; @@ -416,11 +420,6 @@ bool FTileMapModelThreatRangesTests::RunTest(const FString& Parameters) TestFalse(TEXT("[Case3] (3,0) 미포함 (차단 유닛 너머 차폐)"), AttackTiles.Contains(FTileIndex(3, 0))); } - // GC 안당하려고 KeepAlive에 매달아놨던 스킬 데이터 연결 해제 -> GC 대상 - for (UObject* Object : KeepAlive) - if (IsValid(Object)) - Object->RemoveFromRoot(); - return true; } @@ -464,6 +463,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FTileMapModelPushPathByDirectionTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForTileMapTests(); if (World == nullptr) { @@ -474,7 +475,7 @@ bool FTileMapModelPushPathByDirectionTests::RunTest(const FString& Parameters) return false; } - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(8, 8); /* Case1: 빈 맵에서 방향대로 끝까지 밀림 */ @@ -489,7 +490,7 @@ bool FTileMapModelPushPathByDirectionTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case2: 점유 칸에 막힘 ===")); { // (2,4)에 차단 유닛 배치 (레이어만 필요하니 플레이어 Mock 재사용) - UMockPlayerUnitModel* Blocker = NewObject(World); + UMockPlayerUnitModel* Blocker = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(2, 4)), Blocker); const TArray Path = TileMap->GetPushPath(FTileIndex(2, 2), ETileActorDirection::Right, 3); @@ -532,6 +533,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FTileMapModelPullPathTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForTileMapTests(); if (World == nullptr) { @@ -542,11 +545,11 @@ bool FTileMapModelPullPathTests::RunTest(const FString& Parameters) return false; } - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(8, 8); // 시전자 자리는 실제처럼 유닛이 점유 (레이어만 필요하니 플레이어 Mock 재사용) - UMockPlayerUnitModel* Puller = NewObject(World); + UMockPlayerUnitModel* Puller = Scope.New(World); /* Case1: 일직선 -> 시전자 바로 앞(면 접촉)까지 */ AddInfo(TEXT("=== Case1: 일직선 (2,1)->(2,5) 시전자 -> (2,4)까지 ===")); @@ -588,7 +591,7 @@ bool FTileMapModelPullPathTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case4: (2,3) 점유 -> (2,2)까지 ===")); { TileMap->PlaceActor(FTileTransform(FTileIndex(2, 5)), Puller); - UMockPlayerUnitModel* Blocker = NewObject(World); + UMockPlayerUnitModel* Blocker = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(2, 3)), Blocker); const TArray Path = TileMap->GetPullPath(FTileIndex(2, 5), FTileIndex(2, 1), 8); diff --git a/Source/P_RDTests/Component/ArtifactComponent/ArtifactComponentModelTests.cpp b/Source/P_RDTests/Component/ArtifactComponent/ArtifactComponentModelTests.cpp index 4061b236c..9c2bae4cd 100644 --- a/Source/P_RDTests/Component/ArtifactComponent/ArtifactComponentModelTests.cpp +++ b/Source/P_RDTests/Component/ArtifactComponent/ArtifactComponentModelTests.cpp @@ -9,6 +9,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "GameplayTagContainer.h" @@ -38,18 +39,20 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FArtifactComponentModelTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag StartTiming = PassiveTiming(TEXT("GameplayAbility.Passive.OnStartTurn")); // 필요한 컴포넌트만 유닛 없이 생성 - UArtifactComponentModel* ArtifactComp = NewObject(); - UPassiveComponentModel* PassiveComp = NewObject(); + UArtifactComponentModel* ArtifactComp = Scope.New(); + UPassiveComponentModel* PassiveComp = Scope.New(); if (!TestNotNull(TEXT("아티팩트 컴포넌트 생성"), ArtifactComp) || !TestNotNull(TEXT("패시브 컴포넌트 생성"), PassiveComp)) { return false; } // 패시브 DA (코드 구성): 제네릭 패시브 + AttackFactor 이펙트 + 수치 5 + OnStartTurn 시점 - UStaticPassiveData* PassiveData = NewObject(); + UStaticPassiveData* PassiveData = Scope.New(); PassiveData->mPassiveClass = UTacticalPassive_Generic::StaticClass(); FPassiveEffectEntry& Effect = PassiveData->mEffects.AddDefaulted_GetRef(); Effect.mEffectClass = UTacticalEffect_AttackFactor_AddBase::StaticClass(); @@ -57,7 +60,7 @@ bool FArtifactComponentModelTests::RunTest(const FString& Parameters) PassiveData->mActivateTimingTag = StartTiming; // 아티펙트 DA (코드 구성): 위 패시브를 연관 - UStaticArtifactData* Artifact = NewObject(); + UStaticArtifactData* Artifact = Scope.New(); Artifact->mStaticPassiveData.Add(TSoftObjectPtr(PassiveData)); // 장착: 패시브가 설치돼야 함 diff --git a/Source/P_RDTests/Component/ArtifactComponent/ArtifactStatModifierTests.cpp b/Source/P_RDTests/Component/ArtifactComponent/ArtifactStatModifierTests.cpp index d093f0472..3ad7d7d63 100644 --- a/Source/P_RDTests/Component/ArtifactComponent/ArtifactStatModifierTests.cpp +++ b/Source/P_RDTests/Component/ArtifactComponent/ArtifactStatModifierTests.cpp @@ -9,6 +9,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "TAS/TASAttributeTestsHelper.h" @@ -52,6 +53,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FArtifactStatModifierTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + // 라이브 월드 확보 (TAS 속성 테스트와 동일 환경) UWorld* World = FindAnyGameWorld(); if (World == nullptr) @@ -79,12 +82,14 @@ bool FArtifactStatModifierTests::RunTest(const FString& Parameters) UTacticalFrameworkModel* FrameworkModel = Cast(RoomInstance->mAliveSubsystemModels.FindRef(UTacticalFrameworkModel::StaticClass())); if (FrameworkModel == nullptr) { - FrameworkModel = NewObject(RoomInstance); + FrameworkModel = Scope.New(RoomInstance); RoomInstance->mAliveSubsystemModels.Add(UTacticalFrameworkModel::StaticClass(), FrameworkModel); + // 테스트가 등록한 것이므로 파괴 전에 등록 해제 + Scope.Defer([RoomInstance]() { RoomInstance->mAliveSubsystemModels.Remove(UTacticalFrameworkModel::StaticClass()); }); } // 속성 컴포넌트 Mock - UTASActorModelMock* MockActorModel = NewObject(World); + UTASActorModelMock* MockActorModel = Scope.New(World); MockActorModel->Initialize(); MockActorModel->BeginPlay(); UAttributeSetComponentModel* AttrComp = MockActorModel->GetAttributeComponent(); @@ -99,7 +104,7 @@ bool FArtifactStatModifierTests::RunTest(const FString& Parameters) TestEqual(TEXT("기본 공격력 10"), AttrComp->GetAttributeCurrentValue(AttackFactor), 10.f); // 아티펙트 DA (코드 구성): 공격력 +5 고유 스탯 - UStaticArtifactData* Artifact = NewObject(); + UStaticArtifactData* Artifact = Scope.New(); FTacticalModifierInfo Mod; Mod.mAttribute = AttackFactor; Mod.mModifierOp = ETacticalModOp::AddBase; @@ -107,7 +112,7 @@ bool FArtifactStatModifierTests::RunTest(const FString& Parameters) Artifact->mStatModifiers.Add(Mod); // 장착: 스탯 이펙트가 자신에게 적용 (패시브 컴포넌트는 불필요 → nullptr) - UArtifactComponentModel* ArtifactComp = NewObject(); + UArtifactComponentModel* ArtifactComp = Scope.New(); const bool bEquipped = ArtifactComp->EquipInternal(Artifact, nullptr, AttrComp); TestTrue(TEXT("장착 성공"), bEquipped); TestEqual(TEXT("장착 후 공격력 15 (기본 10 + 스탯 5)"), AttrComp->GetAttributeCurrentValue(AttackFactor), 15.f); diff --git a/Source/P_RDTests/Component/BoardMovementComponent/PushMovementTests.cpp b/Source/P_RDTests/Component/BoardMovementComponent/PushMovementTests.cpp index 373d131b8..d51b3cc3f 100644 --- a/Source/P_RDTests/Component/BoardMovementComponent/PushMovementTests.cpp +++ b/Source/P_RDTests/Component/BoardMovementComponent/PushMovementTests.cpp @@ -9,6 +9,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "SRPGFramework/EnemyTurnPlannerTestsHelper.h" // UMockEnemyUnitModel @@ -49,20 +50,20 @@ namespace UMockUnitMovementComponentModel* Movement = nullptr; }; - // @brief 8x8 타일맵에 유닛을 배치하고 이동 컴포넌트를 연결 - FPushMovementFixture MakePushMovementFixture(UWorld* World, const FTileTransform& StartTransform) + // @brief 8x8 타일맵에 유닛을 배치하고 이동 컴포넌트를 연결 (생성 객체는 스코프에 등록) + FPushMovementFixture MakePushMovementFixture(UWorld* World, FTestObjectScope& Scope, const FTileTransform& StartTransform) { FPushMovementFixture Fixture; - Fixture.TileMap = NewObject(World); + Fixture.TileMap = Scope.New(World); Fixture.TileMap->SetDimensions(8, 8); - Fixture.Unit = NewObject(World); + Fixture.Unit = Scope.New(World); Fixture.Unit->Initialize(); Fixture.Unit->BeginPlay(); // 유닛을 Outer로 만들어서 GetOwnerModel()(Outer 체인 탐색)이 유닛을 찾게 함 - Fixture.Movement = NewObject(Fixture.Unit); + Fixture.Movement = Scope.New(Fixture.Unit); Fixture.Movement->SetTileMap(Fixture.TileMap); Fixture.TileMap->PlaceActor(StartTransform, Fixture.Unit); @@ -83,6 +84,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FPushMovementBasicTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForPushTests(); if (World == nullptr) { @@ -97,7 +100,7 @@ bool FPushMovementBasicTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case1: PushAlongPath -> 도착, 방향 불변, OnFinished 1회 ===")); { // (2,2)에서 Left를 바라보는 유닛을 +X로 2칸 밀침 - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Left)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Left)); int32 FinishCount = 0; const bool Started = Fixture.Movement->PushAlongPath( @@ -118,7 +121,7 @@ bool FPushMovementBasicTests::RunTest(const FString& Parameters) /* Case2: AP 정산 (일반 이동 차감 vs 밀치기 불변) */ AddInfo(TEXT("=== Case2: 일반 이동 AP 차감 vs 밀치기 AP 불변 ===")); { - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); UAttributeSetComponentModel* AttrComp = Fixture.Unit->GetAttributeComponentModel(); if (TestNotNull(TEXT("[Case2] 속성 컴포넌트"), AttrComp) == false) @@ -142,7 +145,7 @@ bool FPushMovementBasicTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case3: PullAlongPath -> 도착, 방향 불변, AP 불변, 마지막 모드 Pull ===")); { // (5,2)에서 Left를 바라보는 유닛을 -X로 2칸 당김 - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(5, 2), ETileActorDirection::Left)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(5, 2), ETileActorDirection::Left)); UAttributeSetComponentModel* AttrComp = Fixture.Unit->GetAttributeComponentModel(); const float BeforePull = AttrComp->GetAttributeCurrentValue(UUnitAttributeSet::GetActionPointAttribute()); @@ -175,6 +178,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FPushMovementChainTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForPushTests(); if (World == nullptr) { @@ -188,7 +193,7 @@ bool FPushMovementChainTests::RunTest(const FString& Parameters) /* Case1: 걷기 중 함정 등록 -> 경로 교체 */ AddInfo(TEXT("=== Case1: 걷기 중 등록 -> 잔여 걷기 폐기, 밀치기 경로 완주 ===")); { - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); // (3,2)에 밀치기 함정이 있는 상황: 도착 통지에서 +Y 2칸 밀치기 등록 TArray RegisterResults; @@ -221,7 +226,7 @@ bool FPushMovementChainTests::RunTest(const FString& Parameters) /* Case2: 같은 함정 1회 제한 + 새 요청에서 기록 초기화 */ AddInfo(TEXT("=== Case2: 같은 함정 재등록 거부, 새 요청이면 다시 허용 ===")); { - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); // 함정 키를 (3,2)로 고정하고 매 도착마다 등록 시도: 첫 발동만 성공해야 함 TArray RegisterResults; @@ -259,7 +264,7 @@ bool FPushMovementChainTests::RunTest(const FString& Parameters) /* Case3: 밀 곳이 없는 함정 -> 잔여 걷기 폐기, 함정 칸에서 완료 */ AddInfo(TEXT("=== Case3: 제자리 1칸 경로 등록 -> 잔여 걷기 폐기, 함정 칸에서 완료 ===")); { - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); // (3,2)에 밀치기 함정이 있지만 밀릴 곳이 막힌 상황: 도착 통지에서 제자리 1칸 경로 등록 TArray RegisterResults; @@ -326,6 +331,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FPushMovementGuardTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForPushTests(); if (World == nullptr) { @@ -336,7 +343,7 @@ bool FPushMovementGuardTests::RunTest(const FString& Parameters) return false; } - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); /* Case1: 정지 상태 등록 거부 (정지 대상은 PushAlongPath를 쓰라는 신호) */ AddInfo(TEXT("=== Case1: 정지 상태 TryRegisterPendingPush -> false ===")); @@ -385,6 +392,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FPushMovementCancelTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForPushTests(); if (World == nullptr) { @@ -395,7 +404,7 @@ bool FPushMovementCancelTests::RunTest(const FString& Parameters) return false; } - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); // (3,2)로 향하는 스텝이 시작되면 취소 요청 (스텝 연출 종료 시점에 반영됨) FDelegateHandle CancelHandle = Fixture.Unit->OnStartMoveStep.AddLambda( @@ -462,6 +471,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FPushMovementBlockedTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForPushTests(); if (World == nullptr) { @@ -476,10 +487,10 @@ bool FPushMovementBlockedTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case1: 이동 중 다음 칸 점유 -> 직전 칸에서 완료 ===")); { // (5,2)에서 (3,2)까지 당겨지는 유닛 - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(5, 2), ETileActorDirection::Left)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(5, 2), ETileActorDirection::Left)); // 다른 유닛: (4,2) 도착 통지 시점에 목적지 (3,2)를 먼저 차지 (동시에 당겨진 유닛이 먼저 도착한 상황) - UMockEnemyUnitModel* Blocker = NewObject(World); + UMockEnemyUnitModel* Blocker = Scope.New(World); Blocker->Initialize(); Blocker->BeginPlay(); FDelegateHandle BlockHandle = Fixture.Unit->OnEndMoveStep.AddLambda( @@ -511,10 +522,10 @@ bool FPushMovementBlockedTests::RunTest(const FString& Parameters) /* Case2: 시작 전에 첫 칸이 막힘 */ AddInfo(TEXT("=== Case2: 첫 칸 점유 -> 시작 거부, 제자리 ===")); { - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(5, 2), ETileActorDirection::Left)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(5, 2), ETileActorDirection::Left)); // 다른 유닛이 첫 이동 칸 (4,2)에 이미 서 있음 - UMockEnemyUnitModel* Blocker = NewObject(World); + UMockEnemyUnitModel* Blocker = Scope.New(World); Blocker->Initialize(); Blocker->BeginPlay(); Fixture.TileMap->PlaceActor(FTileTransform(FTileIndex(4, 2), ETileActorDirection::Forward), Blocker); @@ -550,6 +561,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FPushMovementStatusEffectTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForPushTests(); if (World == nullptr) { @@ -576,7 +589,7 @@ bool FPushMovementStatusEffectTests::RunTest(const FString& Parameters) /* Case1: 걷는 중 속박 -> 정지 */ AddInfo(TEXT("=== Case1: 걷는 중 속박 -> 그 칸에서 정지, 남은 경로 폐기 ===")); { - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); FDelegateHandle StatusHandle = AddStatusOnArrive(Fixture, EffectTags::GameplayEffect_StatusEffect_RoundDuration_Debuff_Root); int32 FinishCount = 0; @@ -597,7 +610,7 @@ bool FPushMovementStatusEffectTests::RunTest(const FString& Parameters) /* Case2: 밀리는 중 기절 -> 끝까지 밀림 */ AddInfo(TEXT("=== Case2: 밀리는 중 기절 -> 끝까지 밀림 ===")); { - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); FDelegateHandle StatusHandle = AddStatusOnArrive(Fixture, EffectTags::GameplayEffect_StatusEffect_RoundDuration_Debuff_Stun); Fixture.Movement->PushAlongPath({ FTileIndex(2, 2), FTileIndex(3, 2), FTileIndex(4, 2) }); @@ -611,7 +624,7 @@ bool FPushMovementStatusEffectTests::RunTest(const FString& Parameters) /* Case3: 밀리는 중 속박 -> 끝까지 밀림 */ AddInfo(TEXT("=== Case3: 밀리는 중 속박 -> 끝까지 밀림 ===")); { - FPushMovementFixture Fixture = MakePushMovementFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FPushMovementFixture Fixture = MakePushMovementFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); FDelegateHandle StatusHandle = AddStatusOnArrive(Fixture, EffectTags::GameplayEffect_StatusEffect_RoundDuration_Debuff_Root); Fixture.Movement->PushAlongPath({ FTileIndex(2, 2), FTileIndex(3, 2), FTileIndex(4, 2) }); diff --git a/Source/P_RDTests/Component/BoardMovementComponent/TeleportMovementTests.cpp b/Source/P_RDTests/Component/BoardMovementComponent/TeleportMovementTests.cpp index 31d184e2d..bf2f2561c 100644 --- a/Source/P_RDTests/Component/BoardMovementComponent/TeleportMovementTests.cpp +++ b/Source/P_RDTests/Component/BoardMovementComponent/TeleportMovementTests.cpp @@ -9,6 +9,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "SRPGFramework/EnemyTurnPlannerTestsHelper.h" // UMockEnemyUnitModel @@ -49,30 +50,30 @@ namespace UMockUnitMovementComponentModel* Movement = nullptr; }; - // @brief 8x8 타일맵에 유닛을 배치하고 이동 컴포넌트를 연결 - FTeleportFixture MakeTeleportFixture(UWorld* World, const FTileTransform& StartTransform) + // @brief 8x8 타일맵에 유닛을 배치하고 이동 컴포넌트를 연결 (생성 객체는 스코프에 등록) + FTeleportFixture MakeTeleportFixture(UWorld* World, FTestObjectScope& Scope, const FTileTransform& StartTransform) { FTeleportFixture Fixture; - Fixture.TileMap = NewObject(World); + Fixture.TileMap = Scope.New(World); Fixture.TileMap->SetDimensions(8, 8); - Fixture.Unit = NewObject(World); + Fixture.Unit = Scope.New(World); Fixture.Unit->Initialize(); Fixture.Unit->BeginPlay(); // 유닛을 Outer로 만들어서 GetOwnerModel()(Outer 체인 탐색)이 유닛을 찾게 함 - Fixture.Movement = NewObject(Fixture.Unit); + Fixture.Movement = Scope.New(Fixture.Unit); Fixture.Movement->SetTileMap(Fixture.TileMap); Fixture.TileMap->PlaceActor(StartTransform, Fixture.Unit); return Fixture; } - // @brief 오버랩 관측 Mock을 타일에 배치 - UMockOverlapSensorModel* PlaceSensor(const FTeleportFixture& Fixture, const FTileIndex& TileIndex) + // @brief 오버랩 관측 Mock을 타일에 배치 (스코프에 등록) + UMockOverlapSensorModel* PlaceSensor(const FTeleportFixture& Fixture, FTestObjectScope& Scope, const FTileIndex& TileIndex) { - UMockOverlapSensorModel* Sensor = NewObject(Fixture.TileMap); + UMockOverlapSensorModel* Sensor = Scope.New(Fixture.TileMap); Fixture.TileMap->PlaceActor(FTileTransform(TileIndex, ETileActorDirection::Forward), Sensor); return Sensor; } @@ -92,6 +93,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FTeleportMovementBasicTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForTeleportTests(); if (World == nullptr) { @@ -102,9 +105,9 @@ bool FTeleportMovementBasicTests::RunTest(const FString& Parameters) return false; } - FTeleportFixture Fixture = MakeTeleportFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Left)); - UMockOverlapSensorModel* StartSensor = PlaceSensor(Fixture, FTileIndex(2, 2)); - UMockOverlapSensorModel* EndSensor = PlaceSensor(Fixture, FTileIndex(5, 5)); + FTeleportFixture Fixture = MakeTeleportFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Left)); + UMockOverlapSensorModel* StartSensor = PlaceSensor(Fixture, Scope, FTileIndex(2, 2)); + UMockOverlapSensorModel* EndSensor = PlaceSensor(Fixture, Scope, FTileIndex(5, 5)); UAttributeSetComponentModel* AttrComp = Fixture.Unit->GetAttributeComponentModel(); if (TestNotNull(TEXT("속성 컴포넌트"), AttrComp) == false) @@ -154,6 +157,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FTeleportMovementTrapTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForTeleportTests(); if (World == nullptr) { @@ -164,10 +169,10 @@ bool FTeleportMovementTrapTests::RunTest(const FString& Parameters) return false; } - FTeleportFixture Fixture = MakeTeleportFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FTeleportFixture Fixture = MakeTeleportFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); // (5,5) 함정: 진입 통지에서 +Y로 2칸 밀치기 시도 (밀치기 이펙트의 정지 대상 경로 흉내) - UMockOverlapSensorModel* TrapSensor = PlaceSensor(Fixture, FTileIndex(5, 5)); + UMockOverlapSensorModel* TrapSensor = PlaceSensor(Fixture, Scope, FTileIndex(5, 5)); bool MovingAtOverlap = true; bool PushStarted = false; TrapSensor->mOnBeginOverlap.BindLambda([&]() @@ -206,6 +211,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FTeleportMovementGuardTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForTeleportTests(); if (World == nullptr) { @@ -219,9 +226,9 @@ bool FTeleportMovementGuardTests::RunTest(const FString& Parameters) /* Case1: 막힌 타일 */ AddInfo(TEXT("=== Case1: 도착 타일 점유 -> 시작 거부, 제자리 ===")); { - FTeleportFixture Fixture = MakeTeleportFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FTeleportFixture Fixture = MakeTeleportFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); - UMockEnemyUnitModel* Blocker = NewObject(World); + UMockEnemyUnitModel* Blocker = Scope.New(World); Blocker->Initialize(); Blocker->BeginPlay(); Fixture.TileMap->PlaceActor(FTileTransform(FTileIndex(5, 5), ETileActorDirection::Forward), Blocker); @@ -244,7 +251,7 @@ bool FTeleportMovementGuardTests::RunTest(const FString& Parameters) /* Case2: 걷는 중 */ AddInfo(TEXT("=== Case2: 걷는 중 텔레포트 거부, 걷기 종료 후 허용 ===")); { - FTeleportFixture Fixture = MakeTeleportFixture(World, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); + FTeleportFixture Fixture = MakeTeleportFixture(World, Scope, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward)); // 뷰 흉내: 걷기 스텝 배리어를 잡아 걷는 중 상태를 유지 TSharedPtr HeldStepBarrier; diff --git a/Source/P_RDTests/Component/BoardMovementComponent/UnitMovementComponentModelTests.cpp b/Source/P_RDTests/Component/BoardMovementComponent/UnitMovementComponentModelTests.cpp index 537fb0492..83cc8a798 100644 --- a/Source/P_RDTests/Component/BoardMovementComponent/UnitMovementComponentModelTests.cpp +++ b/Source/P_RDTests/Component/BoardMovementComponent/UnitMovementComponentModelTests.cpp @@ -8,6 +8,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "SRPGFramework/EnemyTurnPlannerTestsHelper.h" @@ -47,6 +48,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FUnitRootTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForRootTests(); if (World == nullptr) { @@ -58,7 +61,7 @@ bool FUnitRootTests::RunTest(const FString& Parameters) } // 유닛 생성 (스폰 데이터 없이 도는 플래너용 Mock 재사용) - UMockEnemyUnitModel* Unit = NewObject(World); + UMockEnemyUnitModel* Unit = Scope.New(World); Unit->Initialize(); Unit->BeginPlay(); diff --git a/Source/P_RDTests/Component/EquipmentComponent/EquipmentComponentModelTests.cpp b/Source/P_RDTests/Component/EquipmentComponent/EquipmentComponentModelTests.cpp index 29337db3a..a11207755 100644 --- a/Source/P_RDTests/Component/EquipmentComponent/EquipmentComponentModelTests.cpp +++ b/Source/P_RDTests/Component/EquipmentComponent/EquipmentComponentModelTests.cpp @@ -9,6 +9,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "GameplayTagContainer.h" @@ -38,18 +39,20 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FEquipmentComponentModelTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag StartTiming = PassiveTiming(TEXT("GameplayAbility.Passive.OnStartTurn")); // 컴포넌트 (오너 없이 독립 생성, 내부 시드로 형제 컴포넌트를 직접 주입) - UEquipmentComponentModel* EquipComp = NewObject(); - UPassiveComponentModel* PassiveComp = NewObject(); + UEquipmentComponentModel* EquipComp = Scope.New(); + UPassiveComponentModel* PassiveComp = Scope.New(); if (!TestNotNull(TEXT("장비 컴포넌트 생성"), EquipComp) || !TestNotNull(TEXT("패시브 컴포넌트 생성"), PassiveComp)) { return false; } // 패시브 DA (코드 구성): 제네릭 패시브 + AttackFactor 이펙트 + 수치 5 + OnStartTurn 시점 - UStaticPassiveData* PassiveData = NewObject(); + UStaticPassiveData* PassiveData = Scope.New(); PassiveData->mPassiveClass = UTacticalPassive_Generic::StaticClass(); FPassiveEffectEntry& Effect = PassiveData->mEffects.AddDefaulted_GetRef(); Effect.mEffectClass = UTacticalEffect_AttackFactor_AddBase::StaticClass(); @@ -57,7 +60,7 @@ bool FEquipmentComponentModelTests::RunTest(const FString& Parameters) PassiveData->mActivateTimingTag = StartTiming; // 무기 DA (코드 구성): 위 패시브를 참조 - UStaticEquipmentData* Weapon = NewObject(); + UStaticEquipmentData* Weapon = Scope.New(); Weapon->mStaticPassiveData.Add(TSoftObjectPtr(PassiveData)); // 장착: 패시브가 설치돼야 함 diff --git a/Source/P_RDTests/Component/EquipmentComponent/EquipmentStatModifierTests.cpp b/Source/P_RDTests/Component/EquipmentComponent/EquipmentStatModifierTests.cpp index 0d4230b4f..06d38051c 100644 --- a/Source/P_RDTests/Component/EquipmentComponent/EquipmentStatModifierTests.cpp +++ b/Source/P_RDTests/Component/EquipmentComponent/EquipmentStatModifierTests.cpp @@ -9,6 +9,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "TAS/TASAttributeTestsHelper.h" @@ -52,6 +53,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FEquipmentStatModifierTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + // 라이브 월드 확보 (TAS 속성 테스트와 동일 환경) UWorld* World = FindAnyGameWorld(); if (World == nullptr) @@ -79,12 +82,14 @@ bool FEquipmentStatModifierTests::RunTest(const FString& Parameters) UTacticalFrameworkModel* FrameworkModel = Cast(RoomInstance->mAliveSubsystemModels.FindRef(UTacticalFrameworkModel::StaticClass())); if (FrameworkModel == nullptr) { - FrameworkModel = NewObject(RoomInstance); + FrameworkModel = Scope.New(RoomInstance); RoomInstance->mAliveSubsystemModels.Add(UTacticalFrameworkModel::StaticClass(), FrameworkModel); + // 테스트가 등록한 것이므로 파괴 전에 등록 해제 + Scope.Defer([RoomInstance]() { RoomInstance->mAliveSubsystemModels.Remove(UTacticalFrameworkModel::StaticClass()); }); } // 속성 컴포넌트 모크 (UUnitAttributeSet 부착) - UTASActorModelMock* MockActorModel = NewObject(World); + UTASActorModelMock* MockActorModel = Scope.New(World); MockActorModel->Initialize(); MockActorModel->BeginPlay(); UAttributeSetComponentModel* AttrComp = MockActorModel->GetAttributeComponent(); @@ -99,7 +104,7 @@ bool FEquipmentStatModifierTests::RunTest(const FString& Parameters) TestEqual(TEXT("기본 공격력 10"), AttrComp->GetAttributeCurrentValue(AttackPoint), 10.f); // 무기 DA (코드 구성): 공격력 +5 고유 스탯 - UStaticEquipmentData* Weapon = NewObject(); + UStaticEquipmentData* Weapon = Scope.New(); FTacticalModifierInfo Mod; Mod.mAttribute = AttackPoint; Mod.mModifierOp = ETacticalModOp::AddBase; @@ -107,7 +112,7 @@ bool FEquipmentStatModifierTests::RunTest(const FString& Parameters) Weapon->mStatModifiers.Add(Mod); // 장착: 스탯 이펙트가 자신에게 적용 (패시브 컴포넌트는 불필요 → nullptr) - UEquipmentComponentModel* EquipComp = NewObject(); + UEquipmentComponentModel* EquipComp = Scope.New(); const bool bEquipped = EquipComp->EquipInternal(Weapon, nullptr, AttrComp); TestTrue(TEXT("장착 성공"), bEquipped); TestEqual(TEXT("장착 후 공격력 15 (기본 10 + 스탯 5)"), AttrComp->GetAttributeCurrentValue(AttackPoint), 15.f); diff --git a/Source/P_RDTests/Component/PassiveComponent/PassiveComponentModelTests.cpp b/Source/P_RDTests/Component/PassiveComponent/PassiveComponentModelTests.cpp index 0e959689e..8259864cb 100644 --- a/Source/P_RDTests/Component/PassiveComponent/PassiveComponentModelTests.cpp +++ b/Source/P_RDTests/Component/PassiveComponent/PassiveComponentModelTests.cpp @@ -8,6 +8,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "GameplayTagContainer.h" @@ -32,11 +33,13 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveComponentModelTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag StartTiming = PassiveTiming(TEXT("GameplayAbility.Passive.OnStartApplyingEffect")); const FGameplayTag EndTiming = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndApplyingEffect")); const FGameplayTag UnusedTiming = PassiveTiming(TEXT("GameplayAbility.Passive.OnStartTurn")); - UPassiveComponentModel* Component = NewObject(); + UPassiveComponentModel* Component = Scope.New(); if (!TestNotNull(TEXT("컴포넌트 생성"), Component)) { return false; diff --git a/Source/P_RDTests/Component/SkillComponent/SkillComponentModelTests.cpp b/Source/P_RDTests/Component/SkillComponent/SkillComponentModelTests.cpp index 9a2eec6c0..a1db16d3f 100644 --- a/Source/P_RDTests/Component/SkillComponent/SkillComponentModelTests.cpp +++ b/Source/P_RDTests/Component/SkillComponent/SkillComponentModelTests.cpp @@ -8,6 +8,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "SRPGFramework/EnemyTurnPlannerTestsHelper.h" @@ -39,10 +40,10 @@ namespace return nullptr; } - // @brief 테스트용 더미 스킬 생성. 직업 무관, 시전 비용 0 - UStaticSkillData* MakeDummySkill(UWorld* World) + // @brief 테스트용 더미 스킬 생성. 직업 무관, 시전 비용 0. 스코프에 등록 + UStaticSkillData* MakeDummySkill(UWorld* World, FTestObjectScope& Scope) { - UStaticUnitSkillData* Skill = NewObject(World); + UStaticUnitSkillData* Skill = Scope.New(World); Skill->mJobType = EUnitJobType::Common; Skill->mRequiredActionPoint = 0; // 모션 레이어 없으면 미장착으로 판정하므로 더미 1개 추가 @@ -59,6 +60,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FSkillComponentStatusEffectTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForSkillTests(); if (World == nullptr) { @@ -70,7 +73,7 @@ bool FSkillComponentStatusEffectTests::RunTest(const FString& Parameters) } // 유닛 생성 후 슬롯 0에 더미 스킬 장착 - UMockEnemyUnitModel* Unit = NewObject(World); + UMockEnemyUnitModel* Unit = Scope.New(World); Unit->Initialize(); Unit->BeginPlay(); @@ -86,7 +89,7 @@ bool FSkillComponentStatusEffectTests::RunTest(const FString& Parameters) } SkillComp->SetSkillFrom(TArray>()); - SkillComp->SetSkill(0, MakeDummySkill(World)); + SkillComp->SetSkill(0, MakeDummySkill(World, Scope)); /* [1] 기본 상태: 사용 가능 */ TestTrue(TEXT("기본: 스킬 사용 가능"), SkillComp->CanActiveSkill(0)); diff --git a/Source/P_RDTests/Component/SkillComponent/SkillForcedMoveSyncTests.cpp b/Source/P_RDTests/Component/SkillComponent/SkillForcedMoveSyncTests.cpp index c50745d24..5138b3a44 100644 --- a/Source/P_RDTests/Component/SkillComponent/SkillForcedMoveSyncTests.cpp +++ b/Source/P_RDTests/Component/SkillComponent/SkillForcedMoveSyncTests.cpp @@ -9,6 +9,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "Actor/BoardActor/GimmickTestsHelper.h" // UMockOverlapGimmickModel, UMockGimmickVictimUnitModel @@ -53,10 +54,10 @@ namespace /* 스킬 데이터 */ - // @brief 스킬 공통 골격 (단타, 자기 타일 조준, 전 팀 타격). 영향 범위는 호출자가 지정 - UStaticSkillData* MakeSkillDataBase(UObject* Outer, EEffectPattern EffectPattern, int32 EffectArea) + // @brief 스킬 공통 골격 (단타, 자기 타일 조준, 전 팀 타격). 영향 범위는 호출자가 지정. 스코프에 등록 + UStaticSkillData* MakeSkillDataBase(UObject* Outer, FTestObjectScope& Scope, EEffectPattern EffectPattern, int32 EffectArea) { - UStaticSkillData* SkillData = NewObject(Outer); + UStaticSkillData* SkillData = Scope.New(Outer); // 스킬 시전 시 쿨다운 스펙을 만들므로 라운드 쿨다운 지정 (미지정 시 스펙이 무효) SkillData->mCooldownEffectClass = UTacticalEffect_RoundCooldown::StaticClass(); SkillData->mSkillAnimationSet.mApplyMotionTags.Add(AnimationTags::Animation_Montage_Skill_Melee_Punch); @@ -90,27 +91,27 @@ namespace } // @brief 시전자 스킬: 자기 타일 기준 사각형 2칸 범위 밀치기 (사각형은 차단 레이어와 무관하게 범위 안을 전부 포함하므로 일직선 두 명도 같이 맞음) - UStaticSkillData* MakeAreaPushSkillData(UObject* Outer, int32 PushDistance) + UStaticSkillData* MakeAreaPushSkillData(UObject* Outer, FTestObjectScope& Scope, int32 PushDistance) { - UStaticSkillData* SkillData = MakeSkillDataBase(Outer, EEffectPattern::Square, 2); + UStaticSkillData* SkillData = MakeSkillDataBase(Outer, Scope, EEffectPattern::Square, 2); SkillData->mSkillPhaseLayers[0].mSkillEffectLayers.Add(MakePushLayer(PushDistance)); return SkillData; } // @brief 발판 스킬: 밟은 자리 한 칸, 발판 방향으로 밀치기 - UStaticSkillData* MakeTrapPushSkillData(UObject* Outer, int32 PushDistance) + UStaticSkillData* MakeTrapPushSkillData(UObject* Outer, FTestObjectScope& Scope, int32 PushDistance) { - UStaticSkillData* SkillData = MakeSkillDataBase(Outer, EEffectPattern::Single, 0); + UStaticSkillData* SkillData = MakeSkillDataBase(Outer, Scope, EEffectPattern::Single, 0); SkillData->mSkillPhaseLayers[0].mSkillEffectLayers.Add(MakePushLayer(PushDistance)); return SkillData; } /* 픽스처 */ - // @brief 타일맵 주입 기믹 생성/배치. 시전자 또는 발판으로 사용 - UMockOverlapGimmickModel* MakeGimmick(UWorld* World, UTileMapModel* TileMap, const FTileTransform& Transform, UStaticSkillData* SkillData, int32 TriggerCount) + // @brief 타일맵 주입 기믹 생성/배치. 시전자 또는 발판으로 사용. 스코프에 등록 + UMockOverlapGimmickModel* MakeGimmick(UWorld* World, FTestObjectScope& Scope, UTileMapModel* TileMap, const FTileTransform& Transform, UStaticSkillData* SkillData, int32 TriggerCount) { - UMockOverlapGimmickModel* Gimmick = NewObject(World); + UMockOverlapGimmickModel* Gimmick = Scope.New(World); Gimmick->Initialize(); Gimmick->BeginPlay(); Gimmick->SetTileMap(TileMap); @@ -120,13 +121,13 @@ namespace return Gimmick; } - // @brief 피격 유닛 생성/배치 (타일맵 주입 이동 컴포넌트 연결) - UMockGimmickVictimUnitModel* MakeVictim(UWorld* World, UTileMapModel* TileMap, const FTileTransform& Transform) + // @brief 피격 유닛 생성/배치 (타일맵 주입 이동 컴포넌트 연결). 스코프에 등록 + UMockGimmickVictimUnitModel* MakeVictim(UWorld* World, FTestObjectScope& Scope, UTileMapModel* TileMap, const FTileTransform& Transform) { - UMockGimmickVictimUnitModel* Unit = NewObject(World); + UMockGimmickVictimUnitModel* Unit = Scope.New(World); Unit->Initialize(); Unit->BeginPlay(); - UMockUnitMovementComponentModel* Movement = NewObject(Unit); + UMockUnitMovementComponentModel* Movement = Scope.New(Unit); Movement->SetTileMap(TileMap); Unit->SetBoardMovementComponentModel(Movement); TileMap->PlaceActor(Transform, Unit); @@ -147,13 +148,13 @@ namespace } }; - FSyncFixture MakeSyncFixture(UWorld* World, UStaticSkillData* SkillData) + FSyncFixture MakeSyncFixture(UWorld* World, FTestObjectScope& Scope, UStaticSkillData* SkillData) { FSyncFixture Fixture; - Fixture.TileMap = NewObject(World); + Fixture.TileMap = Scope.New(World); Fixture.TileMap->SetDimensions(8, 8); // 시전자는 오버랩으로 발동하지 않도록 수명 0 - Fixture.Caster = MakeGimmick(World, Fixture.TileMap, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), SkillData, 0); + Fixture.Caster = MakeGimmick(World, Scope, Fixture.TileMap, FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), SkillData, 0); Fixture.SkillComp = Fixture.Caster->GetSkillComponentModel(); return Fixture; } @@ -275,6 +276,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FSkillForcedMoveSyncSimTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForForcedMoveTests(); if (World == nullptr) { @@ -288,8 +291,8 @@ bool FSkillForcedMoveSyncSimTests::RunTest(const FString& Parameters) /* Case1: 기본 */ AddInfo(TEXT("=== Case1: (3,2) 대상 +x로 2칸 밀림 -> (5,2). 순서 이동끝 > 페이즈끝 > 스킬끝 ===")); { - FSyncFixture Fixture = MakeSyncFixture(World, MakeAreaPushSkillData(World, 2)); - UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); + FSyncFixture Fixture = MakeSyncFixture(World, Scope, MakeAreaPushSkillData(World, Scope, 2)); + UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); FEventRecorder Recorder; Recorder.BindSkill(Fixture.SkillComp); @@ -305,10 +308,10 @@ bool FSkillForcedMoveSyncSimTests::RunTest(const FString& Parameters) /* Case2: 함정 연쇄 */ AddInfo(TEXT("=== Case2: 경로 위 (4,2) 발판(+y 1칸) -> (4,3) 도착. 페이즈 끝 시점 위치 == 최종 위치 ===")); { - FSyncFixture Fixture = MakeSyncFixture(World, MakeAreaPushSkillData(World, 2)); - UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); + FSyncFixture Fixture = MakeSyncFixture(World, Scope, MakeAreaPushSkillData(World, Scope, 2)); + UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); // 발판이 Right(+y)를 바라봄. 밀리던 A가 (4,2)를 밟으면 +y로 1칸 재밀림 - UMockOverlapGimmickModel* Trap = MakeGimmick(World, Fixture.TileMap, FTileTransform(FTileIndex(4, 2), ETileActorDirection::Right), MakeTrapPushSkillData(World, 1), 1); + UMockOverlapGimmickModel* Trap = MakeGimmick(World, Scope, Fixture.TileMap, FTileTransform(FTileIndex(4, 2), ETileActorDirection::Right), MakeTrapPushSkillData(World, Scope, 1), 1); FEventRecorder Recorder; Recorder.BindSkill(Fixture.SkillComp); @@ -331,8 +334,8 @@ bool FSkillForcedMoveSyncSimTests::RunTest(const FString& Parameters) /* Case3: 면역 */ AddInfo(TEXT("=== Case3: 강제이동 면역 대상 -> 제자리, 페이즈/스킬 정상 종료 ===")); { - FSyncFixture Fixture = MakeSyncFixture(World, MakeAreaPushSkillData(World, 2)); - UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); + FSyncFixture Fixture = MakeSyncFixture(World, Scope, MakeAreaPushSkillData(World, Scope, 2)); + UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); UnitA->GetAttributeComponentModel()->AddLooseGameplayTag(EffectTags::GameplayEffect_StatusEffect_RoundDuration_Buff_ForcedMovementImmunity); FEventRecorder Recorder; @@ -363,6 +366,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FSkillForcedMoveSyncLiveTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForForcedMoveTests(); if (World == nullptr) { @@ -376,8 +381,8 @@ bool FSkillForcedMoveSyncLiveTests::RunTest(const FString& Parameters) /* Case1: 페이즈가 밀림을 기다림 */ AddInfo(TEXT("=== Case1: 스텝 1 -> (4,2), 스텝 2 -> (5,2) 페이즈 종료, 모션 해제 -> 스킬 종료 ===")); { - FSyncFixture Fixture = MakeSyncFixture(World, MakeAreaPushSkillData(World, 2)); - UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); + FSyncFixture Fixture = MakeSyncFixture(World, Scope, MakeAreaPushSkillData(World, Scope, 2)); + UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); FEventRecorder Recorder; Recorder.BindSkill(Fixture.SkillComp); @@ -410,8 +415,8 @@ bool FSkillForcedMoveSyncLiveTests::RunTest(const FString& Parameters) /* Case2: 연출이 먼저 끝남 */ AddInfo(TEXT("=== Case2: 노티파이 후 모션 먼저 해제 -> 스킬 종료 0. 스텝 다 풀면 페이즈 종료 > 스킬 종료 ===")); { - FSyncFixture Fixture = MakeSyncFixture(World, MakeAreaPushSkillData(World, 2)); - UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); + FSyncFixture Fixture = MakeSyncFixture(World, Scope, MakeAreaPushSkillData(World, Scope, 2)); + UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); FEventRecorder Recorder; Recorder.BindSkill(Fixture.SkillComp); @@ -437,8 +442,8 @@ bool FSkillForcedMoveSyncLiveTests::RunTest(const FString& Parameters) /* Case3: 대기 중 노티파이 무시 */ AddInfo(TEXT("=== Case3: 스텝 보유 중 노티파이 2회 -> 두 번째 무시, 페이즈 종료 1회, 스킬 정상 종료 ===")); { - FSyncFixture Fixture = MakeSyncFixture(World, MakeAreaPushSkillData(World, 2)); - UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); + FSyncFixture Fixture = MakeSyncFixture(World, Scope, MakeAreaPushSkillData(World, Scope, 2)); + UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); FEventRecorder Recorder; Recorder.BindSkill(Fixture.SkillComp); @@ -477,6 +482,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FSkillForcedMoveSyncSequentialTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForForcedMoveTests(); if (World == nullptr) { @@ -490,9 +497,9 @@ bool FSkillForcedMoveSyncSequentialTests::RunTest(const FString& Parameters) /* Case1: 순차 출발 */ AddInfo(TEXT("=== Case1: A (3,2), C (2,3) -> 한 명씩 출발, 둘 다 끝나야 페이즈 종료 ===")); { - FSyncFixture Fixture = MakeSyncFixture(World, MakeAreaPushSkillData(World, 2)); - UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); - UMockGimmickVictimUnitModel* UnitC = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(2, 3), ETileActorDirection::Forward)); + FSyncFixture Fixture = MakeSyncFixture(World, Scope, MakeAreaPushSkillData(World, Scope, 2)); + UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); + UMockGimmickVictimUnitModel* UnitC = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(2, 3), ETileActorDirection::Forward)); FEventRecorder Recorder; Recorder.BindSkill(Fixture.SkillComp); @@ -533,9 +540,9 @@ bool FSkillForcedMoveSyncSequentialTests::RunTest(const FString& Parameters) FTileIndex SimA; FTileIndex SimB; { - FSyncFixture Fixture = MakeSyncFixture(World, MakeAreaPushSkillData(World, 2)); - UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); - UMockGimmickVictimUnitModel* UnitB = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(4, 2), ETileActorDirection::Forward)); + FSyncFixture Fixture = MakeSyncFixture(World, Scope, MakeAreaPushSkillData(World, Scope, 2)); + UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); + UMockGimmickVictimUnitModel* UnitB = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(4, 2), ETileActorDirection::Forward)); Fixture.Cast(); @@ -548,9 +555,9 @@ bool FSkillForcedMoveSyncSequentialTests::RunTest(const FString& Parameters) FTileIndex LiveA; FTileIndex LiveB; { - FSyncFixture Fixture = MakeSyncFixture(World, MakeAreaPushSkillData(World, 2)); - UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); - UMockGimmickVictimUnitModel* UnitB = MakeVictim(World, Fixture.TileMap, FTileTransform(FTileIndex(4, 2), ETileActorDirection::Forward)); + FSyncFixture Fixture = MakeSyncFixture(World, Scope, MakeAreaPushSkillData(World, Scope, 2)); + UMockGimmickVictimUnitModel* UnitA = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(3, 2), ETileActorDirection::Forward)); + UMockGimmickVictimUnitModel* UnitB = MakeVictim(World, Scope, Fixture.TileMap,FTileTransform(FTileIndex(4, 2), ETileActorDirection::Forward)); FStepHolder StepsA; StepsA.Bind(UnitA); @@ -597,6 +604,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FSkillForcedMoveSyncDataValidationTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorldForForcedMoveTests(); if (World == nullptr) { @@ -618,7 +627,7 @@ bool FSkillForcedMoveSyncDataValidationTests::RunTest(const FString& Parameters) /* Case1: 마지막 페이즈 아닌 곳 */ AddInfo(TEXT("=== Case1: 페이즈 2개, Push가 0번 페이즈 -> 경고 ===")); { - UStaticSkillData* SkillData = MakeSkillDataBase(World, EEffectPattern::Single, 0); + UStaticSkillData* SkillData = MakeSkillDataBase(World, Scope, EEffectPattern::Single, 0); SkillData->mSkillPhaseLayers[0].mSkillEffectLayers.Add(MakePushLayer(1)); SkillData->mSkillPhaseLayers.AddDefaulted(); SkillData->mSkillPhaseLayers[1].mSkillEffectLayers.Add(MakeStunLayer()); @@ -629,7 +638,7 @@ bool FSkillForcedMoveSyncDataValidationTests::RunTest(const FString& Parameters) /* Case2: Push 뒤에 다른 이펙트 */ AddInfo(TEXT("=== Case2: 페이즈 1개, Push 뒤에 Stun -> 경고 ===")); { - UStaticSkillData* SkillData = MakeSkillDataBase(World, EEffectPattern::Single, 0); + UStaticSkillData* SkillData = MakeSkillDataBase(World, Scope, EEffectPattern::Single, 0); SkillData->mSkillPhaseLayers[0].mSkillEffectLayers.Add(MakePushLayer(1)); SkillData->mSkillPhaseLayers[0].mSkillEffectLayers.Add(MakeStunLayer()); @@ -639,7 +648,7 @@ bool FSkillForcedMoveSyncDataValidationTests::RunTest(const FString& Parameters) /* Case3: 규칙 준수 */ AddInfo(TEXT("=== Case3: 페이즈 2개, 마지막 페이즈의 마지막이 Push -> 경고 없음 ===")); { - UStaticSkillData* SkillData = MakeSkillDataBase(World, EEffectPattern::Single, 0); + UStaticSkillData* SkillData = MakeSkillDataBase(World, Scope, EEffectPattern::Single, 0); SkillData->mSkillPhaseLayers[0].mSkillEffectLayers.Add(MakeStunLayer()); SkillData->mSkillPhaseLayers.AddDefaulted(); SkillData->mSkillPhaseLayers[1].mSkillEffectLayers.Add(MakeStunLayer()); diff --git a/Source/P_RDTests/SRPGFramework/BoardActorMoveTests.cpp b/Source/P_RDTests/SRPGFramework/BoardActorMoveTests.cpp index 8e0895457..baf393294 100644 --- a/Source/P_RDTests/SRPGFramework/BoardActorMoveTests.cpp +++ b/Source/P_RDTests/SRPGFramework/BoardActorMoveTests.cpp @@ -11,6 +11,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "SRPGFramework/EnemyTurnPlannerTestsHelper.h" // UMockPlayerUnitModel @@ -84,6 +85,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FBoardActorMoveStepBarrierTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorld(); if (World == nullptr) { @@ -97,7 +100,7 @@ bool FBoardActorMoveStepBarrierTests::RunTest(const FString& Parameters) // OnStartMoveStep() 구독자를 없게 해서 시뮬레이션모드 테스트 AddInfo(TEXT("=== Case1: 구독자 없음 -> 시뮬레이션모드 -> 즉시 완료 ===")); { - UMockPlayerUnitModel* Unit = NewObject(World); + UMockPlayerUnitModel* Unit = Scope.New(World); bool bFinished = false; { @@ -116,7 +119,7 @@ bool FBoardActorMoveStepBarrierTests::RunTest(const FString& Parameters) // OnStartMoveStep 구독자를 만들어서 라이브모드 테스트 AddInfo(TEXT("=== Case2: 구독자 있음 -> 라이브모드 -> 구독자가 놓으면 완료 ===")); { - UMockPlayerUnitModel* Unit = NewObject(World); + UMockPlayerUnitModel* Unit = Scope.New(World); // 뷰 역할의 구독자 (Barrier를 보관하고 있어서 참조가 0이 안 되게 유지) TSharedPtr HeldBarrier; diff --git a/Source/P_RDTests/SRPGFramework/BoardActorRotateTests.cpp b/Source/P_RDTests/SRPGFramework/BoardActorRotateTests.cpp index 352fd813d..9d0a49969 100644 --- a/Source/P_RDTests/SRPGFramework/BoardActorRotateTests.cpp +++ b/Source/P_RDTests/SRPGFramework/BoardActorRotateTests.cpp @@ -12,6 +12,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "SRPGFramework/EnemyTurnPlannerTestsHelper.h" // UMockPlayerUnitModel @@ -55,6 +56,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( */ bool FBoardActorRotateTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorld(); if (World == nullptr) { @@ -66,10 +69,10 @@ bool FBoardActorRotateTests::RunTest(const FString& Parameters) } // 타일맵과 유닛 준비 - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(5, 5); - UMockPlayerUnitModel* Unit = NewObject(World); + UMockPlayerUnitModel* Unit = Scope.New(World); TileMap->PlaceActor(FTileTransform(FTileIndex(2, 2), ETileActorDirection::Forward), Unit); // 뷰 역할의 구독자 (발행 횟수와 배리어 수신 기록) diff --git a/Source/P_RDTests/SRPGFramework/EnemyTurnPlannerTests.cpp b/Source/P_RDTests/SRPGFramework/EnemyTurnPlannerTests.cpp index 634a63ec7..8536bcfd9 100644 --- a/Source/P_RDTests/SRPGFramework/EnemyTurnPlannerTests.cpp +++ b/Source/P_RDTests/SRPGFramework/EnemyTurnPlannerTests.cpp @@ -23,6 +23,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "SRPGFramework/EnemyTurnPlannerTestsHelper.h" @@ -86,16 +87,16 @@ namespace return nullptr; } - // @brief 테스트용 일반공격 스킬 생성 (KeepAlive에 등록해 GC 방지) + // @brief 테스트용 일반공격 스킬 생성 (스코프에 등록해 GC 방지 + 종료 시 파괴) // @param EffectPattern 영향 범위 패턴 (Single 조준 스킬은 이 범위로 타격 여부가 결정됨) // @param EffectArea 영향 범위 크기 // @param SkillCost 시전 비용 (플래너 AP 판정용, 0이면 비용 없음) // @param SkillType 스킬 타입 (Attack은 공격 후보, Spell은 자기 버프) - UStaticSkillData* MakeSkill(UWorld* World, TArray& KeepAlive, EAimPattern AimPattern, int32 AimRange, + UStaticSkillData* MakeSkill(UWorld* World, FTestObjectScope& Scope, EAimPattern AimPattern, int32 AimRange, EEffectPattern EffectPattern = EEffectPattern::Single, int32 EffectArea = 0, int32 SkillCost = 0, ESkillType SkillType = ESkillType::Attack) { - UStaticUnitSkillData* Skill = NewObject(World); + UStaticUnitSkillData* Skill = Scope.New(World); Skill->mJobType = EUnitJobType::Common; // 직업 무관 (직업 불일치면 SetSkill이 장착 거부) Skill->mSkillType = SkillType; Skill->mRequiredActionPoint = SkillCost; @@ -108,13 +109,11 @@ namespace Skill->mEffectBlockerMask = static_cast(ETileLayerFlag::Obstacle | ETileLayerFlag::Unit); // 모션 레이어 없으면 FSkillEntry::IsValid()가 미장착으로 판정하므로 더미 1개 추가 Skill->mSkillPhaseLayers.AddDefaulted(); - Skill->AddToRoot(); - KeepAlive.Add(Skill); return Skill; } // @brief 적 유닛의 한 턴을 계획해 커맨드 목록 반환 - // @param KeepAlive SkillComponent가 약참조라 GC 안당하도록 붙잡아두는 장치 + // @param Scope 생성 객체 등록 (GC 방지 + 종료 시 파괴) // @param AimPattern 스킬 조준 패턴 (Cross/Star 직사는 시야 검사 경로를 태움) // @param Slot1AimRange 슬롯 1에 장착할 Square 스킬의 사거리 (0이면 미장착. 다중 스킬 선택 검증용) // @param BlockerIndex 길/시야를 막는 제3의 유닛 배치 좌표 (Invalid면 미배치. 우회 접근 검증용) @@ -130,7 +129,7 @@ namespace // @param Slot1Cooldown 슬롯 1을 쿨다운 상태로 둘지 (쿨다운이면 차순위 검증용) TArray> Plan( UWorld* World, - TArray& KeepAlive, + FTestObjectScope& Scope, EMoveTendency Tendency, int32 MoveRange, int32 AimRange, @@ -153,16 +152,16 @@ namespace bool Slot1Cooldown = false) { // 타일맵 생성 - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(TileMapWidth, TileMapHeight); // 플레이어유닛 생성 (배열 순서가 곧 타겟 인덱스) TArray Players; - UMockPlayerUnitModel* Player = NewObject(World); + UMockPlayerUnitModel* Player = Scope.New(World); Players.Add(Player); // 적유닛 생성 - UMockEnemyUnitModel* Enemy = NewObject(World); + UMockEnemyUnitModel* Enemy = Scope.New(World); Enemy->Initialize(); Enemy->BeginPlay(); Enemy->SetMoveTendency(Tendency); @@ -179,12 +178,12 @@ namespace // 스킬 추가: 일반공격 계열 (버프만 가진 적 검증 시 생략) if (HasAttack == true) { - Enemy->GetSkillComponentModel()->SetSkill(0, MakeSkill(World, KeepAlive, AimPattern, AimRange, EffectPattern, EffectArea, SkillCost)); + Enemy->GetSkillComponentModel()->SetSkill(0, MakeSkill(World, Scope, AimPattern, AimRange, EffectPattern, EffectArea, SkillCost)); } // 슬롯 1 스킬(옵션): 다중 스킬 선택 검증용 (우선순위/쿨다운 지정 가능) if (Slot1AimRange > 0) { - Enemy->GetSkillComponentModel()->SetSkill(1, MakeSkill(World, KeepAlive, EAimPattern::Square, Slot1AimRange)); + Enemy->GetSkillComponentModel()->SetSkill(1, MakeSkill(World, Scope, EAimPattern::Square, Slot1AimRange)); Enemy->SetSkillPriority(1, Slot1Priority); if (Slot1Cooldown == true) { @@ -194,7 +193,7 @@ namespace // 슬롯 2 자기 버프(옵션): Single 조준, 영향 범위 자기 칸 if (Slot2SpellCost > 0) { - Enemy->GetSkillComponentModel()->SetSkill(2, MakeSkill(World, KeepAlive, EAimPattern::Single, 0, + Enemy->GetSkillComponentModel()->SetSkill(2, MakeSkill(World, Scope, EAimPattern::Single, 0, EEffectPattern::Single, 0, Slot2SpellCost, ESkillType::Spell)); Enemy->SetSkillPriority(2, Slot2Priority); } @@ -206,7 +205,7 @@ namespace // 두 번째 플레이어(옵션): 다중 타겟 선택 검증용 if (TileMap->IsValidIndex(SecondPlayerIndex)) { - UMockPlayerUnitModel* SecondPlayer = NewObject(World); + UMockPlayerUnitModel* SecondPlayer = Scope.New(World); Players.Add(SecondPlayer); TileMap->PlaceActor(FTileTransform(SecondPlayerIndex), SecondPlayer); } @@ -214,7 +213,7 @@ namespace // 차단 유닛(옵션): 길/시야를 막는 제3의 유닛 (Unit 레이어 점유만 필요하니 플레이어 Mock 재사용) if (TileMap->IsValidIndex(BlockerIndex)) { - UMockPlayerUnitModel* Blocker = NewObject(World); + UMockPlayerUnitModel* Blocker = Scope.New(World); TileMap->PlaceActor(FTileTransform(BlockerIndex), Blocker); } @@ -226,7 +225,7 @@ namespace } // @brief 이동 스킬 하나만 가진 적 유닛의 한 턴을 계획해 커맨드 목록 반환 - // @param KeepAlive SkillComponent가 약참조라 GC 안당하도록 붙잡아두는 장치 + // @param Scope 생성 객체 등록 (GC 방지 + 종료 시 파괴) // @param ActionPoint 적 유닛의 행동력 // @param AimRange 이동 스킬의 조준 사거리 (Square 패턴, 빈 타일만 조준) // @param SkillCost 이동 스킬의 시전 비용 @@ -235,7 +234,7 @@ namespace // @param Priority 이동 스킬의 대상우선순위 (기준 타겟 선정 검증용) TArray> PlanMoveSkill( UWorld* World, - TArray& KeepAlive, + FTestObjectScope& Scope, int32 ActionPoint, int32 AimRange, int32 SkillCost, @@ -247,18 +246,18 @@ namespace EEnemyTargetPriority Priority = EEnemyTargetPriority::Nearest) { // 타일맵 생성 - UTileMapModel* TileMap = NewObject(World); + UTileMapModel* TileMap = Scope.New(World); TileMap->SetDimensions(TileMapWidth, TileMapHeight); // 적유닛 생성 (시전 불가 시 접근 이동 검증을 위해 근거리 성향) - UMockEnemyUnitModel* Enemy = NewObject(World); + UMockEnemyUnitModel* Enemy = Scope.New(World); Enemy->Initialize(); Enemy->BeginPlay(); Enemy->SetMoveTendency(EMoveTendency::MoveClose); Enemy->GetAttributeComponentModel()->ApplyModToAttribute(UEnemyUnitAttributeSet::GetActionPointAttribute(), ETacticalModOp::Override, ActionPoint); // 이동 스킬 생성: 점유 타일은 조준 불가, 대상우선순위는 스킬 DA 값으로 지정 - UStaticUnitSkillData* MoveSkill = CastChecked(MakeSkill(World, KeepAlive, EAimPattern::Square, AimRange, + UStaticUnitSkillData* MoveSkill = CastChecked(MakeSkill(World, Scope, EAimPattern::Square, AimRange, EEffectPattern::Single, 0, SkillCost, ESkillType::Move)); MoveSkill->mCanAimBoardActor = false; MoveSkill->mTargetPolicy.mPriority = Priority; @@ -274,7 +273,7 @@ namespace TArray Players; for (const FTileIndex& PlayerIndex : PlayerIndexes) { - UMockPlayerUnitModel* Player = NewObject(World); + UMockPlayerUnitModel* Player = Scope.New(World); Players.Add(Player); TileMap->PlaceActor(FTileTransform(PlayerIndex), Player); } @@ -282,7 +281,7 @@ namespace // 차단 유닛 배치 (Unit 레이어 점유만 필요하니 플레이어 Mock 재사용, 타겟 목록에는 넣지 않음) for (const FTileIndex& BlockerIndex : BlockerIndexes) { - UMockPlayerUnitModel* Blocker = NewObject(World); + UMockPlayerUnitModel* Blocker = Scope.New(World); TileMap->PlaceActor(FTileTransform(BlockerIndex), Blocker); } @@ -336,6 +335,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + UWorld* World = GetAnyGameWorld(); if (World == nullptr) { @@ -346,9 +347,6 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) return false; } - // 유닛테스트 동안 SkillComponent가 GC 당하지 않도록 걸어두는 장치 - TArray KeepAlive; - /** * 케이스 레이블: CaseX-Y * X = 이동성향 (1:원거리/MoveAway, 2:등거리/HoldRange, 3:근거리/MoveClose) @@ -365,7 +363,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case1-1: 원거리(MoveAway), 조준 가능 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveAway, + World, Scope, EMoveTendency::MoveAway, 6, 3, 8, 2, FTileIndex(2, 1), FTileIndex(1, 0)); @@ -391,7 +389,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case1-2: 원거리(MoveAway) / 조준 불가능 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveAway, + World, Scope, EMoveTendency::MoveAway, 4, 3, 10, 3, FTileIndex(0, 1), FTileIndex(9, 1)); @@ -407,7 +405,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case1-3: 원거리(MoveAway) / 직사 / 일직선 후퇴 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveAway, + World, Scope, EMoveTendency::MoveAway, 6, 4, 8, 1, FTileIndex(1, 0), FTileIndex(0, 0), @@ -434,7 +432,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case2-1: 등거리(HoldRange) / 조준 가능 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::HoldRange, + World, Scope, EMoveTendency::HoldRange, 3, 1, 6, 3, FTileIndex(2, 1), FTileIndex(3, 1)); @@ -451,7 +449,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case2-2: 등거리(HoldRange) / 조준 불가능 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::HoldRange, + World, Scope, EMoveTendency::HoldRange, 4, 1, 10, 3, FTileIndex(0, 1), FTileIndex(9, 1)); @@ -467,7 +465,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case3-1: 근거리(MoveClose) / 조준 가능 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 3, 1, 6, 3, FTileIndex(2, 1), FTileIndex(3, 1)); @@ -489,7 +487,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case3-2: 근거리(MoveClose) / 이동 후에도 조준 불가능 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 4, 1, 10, 3, FTileIndex(0, 1), FTileIndex(9, 1)); @@ -506,7 +504,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case3-3: 근거리(MoveClose) / 몹에 막힘 / 우회 접근 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 2, 4, 8, 2, FTileIndex(2, 1), FTileIndex(0, 1), @@ -525,7 +523,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case4-1: 다중 스킬 / 동순위 랜덤 선택 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::HoldRange, + World, Scope, EMoveTendency::HoldRange, 3, 1, 6, 3, FTileIndex(2, 1), FTileIndex(3, 1), @@ -548,7 +546,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case4-2: 다중 스킬 / 우선순위 높은 슬롯 선택 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::HoldRange, + World, Scope, EMoveTendency::HoldRange, 3, 1, 6, 3, FTileIndex(2, 1), FTileIndex(3, 1), @@ -579,7 +577,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case4-3: 다중 스킬 / 최우선 슬롯 쿨다운이면 차순위 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::HoldRange, + World, Scope, EMoveTendency::HoldRange, 3, 1, 6, 3, FTileIndex(2, 1), FTileIndex(3, 1), @@ -613,7 +611,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case4-4: 다중 스킬 / 최우선 슬롯 사거리 밖이면 스킬 유지하고 접근 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 2, 5, 10, 3, FTileIndex(0, 1), FTileIndex(4, 1), @@ -639,7 +637,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case5-1: 다중 플레이어 / 최근접 타겟 선택 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::HoldRange, + World, Scope, EMoveTendency::HoldRange, 3, 1, 6, 3, FTileIndex(2, 1), FTileIndex(3, 1), @@ -665,7 +663,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case5-2: 다중 플레이어 / 시전 가능한 타겟 우선 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 2, 1, 7, 1, FTileIndex(2, 0), FTileIndex(0, 0), @@ -697,7 +695,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case5-3: 다중 플레이어 / 균형 후퇴 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveAway, + World, Scope, EMoveTendency::MoveAway, 2, 3, 7, 1, FTileIndex(2, 0), FTileIndex(0, 0), @@ -728,7 +726,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case5-4: 다중 플레이어 / 최근접 접근 폴백 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 2, 1, 10, 3, FTileIndex(4, 1), FTileIndex(9, 1), @@ -747,7 +745,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case6-1: 속박 / 제자리 조준 가능 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveAway, + World, Scope, EMoveTendency::MoveAway, 6, 3, 8, 2, FTileIndex(2, 1), FTileIndex(1, 0), @@ -769,7 +767,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case6-2: 속박 / 사거리 밖 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 6, 1, 8, 2, FTileIndex(6, 1), FTileIndex(0, 0), @@ -791,7 +789,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case6-3: 속박 / 비용 있는 스킬 시전 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::HoldRange, + World, Scope, EMoveTendency::HoldRange, 6, 1, 6, 3, FTileIndex(2, 1), FTileIndex(3, 1), @@ -816,7 +814,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case7-1: Single 조준 / 영향 범위로 인접 타격 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::HoldRange, + World, Scope, EMoveTendency::HoldRange, 3, 0, 6, 3, FTileIndex(2, 1), FTileIndex(3, 1), @@ -844,7 +842,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case7-2: Single 조준 / 출발 칸 너머 타격 / 자기 차폐 회귀 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveAway, + World, Scope, EMoveTendency::MoveAway, 6, 0, 8, 1, FTileIndex(1, 0), FTileIndex(0, 0), @@ -876,7 +874,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case7-3: Single 조준 / 영향 범위 밖 / 접근 폴백 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 4, 0, 10, 3, FTileIndex(0, 1), FTileIndex(9, 1), @@ -897,7 +895,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case8-1: 공격 가능 / 버프 우선순위 낮음 / 공격 확정 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::HoldRange, + World, Scope, EMoveTendency::HoldRange, 3, 1, 6, 3, FTileIndex(2, 1), FTileIndex(3, 1), @@ -931,7 +929,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case8-2: 버프 우선순위 높음 / 비용 떼고 성향 이동 후 버프 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 4, 1, 10, 3, FTileIndex(0, 1), FTileIndex(3, 1), @@ -970,7 +968,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case8-4: 공격 확정 / 사거리 밖 / 버프로 안 바꾸고 접근 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 3, 1, 10, 3, FTileIndex(0, 1), FTileIndex(9, 1), @@ -997,7 +995,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case8-5: 공격 확정 / 시전 비용 부족 / 버프 없이 선점이동 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveClose, + World, Scope, EMoveTendency::MoveClose, 4, 1, 10, 3, FTileIndex(0, 1), FTileIndex(3, 1), @@ -1031,7 +1029,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case8-3: 버프만 보유 / 성향 이동 후 버프 ===")); { const TArray> Commands = Plan( - World, KeepAlive, EMoveTendency::MoveAway, + World, Scope, EMoveTendency::MoveAway, 4, 1, 8, 1, FTileIndex(2, 0), FTileIndex(0, 0), @@ -1074,7 +1072,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case9-1: 이동 스킬 / 빈 타일 조준 ===")); { const TArray> Commands = PlanMoveSkill( - World, KeepAlive, + World, Scope, /*ActionPoint*/6, /*AimRange*/3, /*SkillCost*/2, 8, 1, FTileIndex(0, 0), @@ -1098,7 +1096,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case9-2: 이동 스킬 / 점유 타일 제외 ===")); { const TArray> Commands = PlanMoveSkill( - World, KeepAlive, + World, Scope, /*ActionPoint*/6, /*AimRange*/3, /*SkillCost*/2, 8, 2, FTileIndex(0, 0), @@ -1123,7 +1121,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case9-3: 이동 스킬 / 경로 없음 / 맨해튼 거리 비교 ===")); { const TArray> Commands = PlanMoveSkill( - World, KeepAlive, + World, Scope, /*ActionPoint*/6, /*AimRange*/3, /*SkillCost*/2, 8, 1, FTileIndex(0, 0), @@ -1147,7 +1145,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case9-4: 이동 스킬 / 대상우선순위 Farthest ===")); { const TArray> Commands = PlanMoveSkill( - World, KeepAlive, + World, Scope, /*ActionPoint*/6, /*AimRange*/3, /*SkillCost*/2, 9, 1, FTileIndex(4, 0), @@ -1172,7 +1170,7 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) AddInfo(TEXT("=== Case9-5: 이동 스킬 / 빈 타일 없음 / 접근 폴백 ===")); { const TArray> Commands = PlanMoveSkill( - World, KeepAlive, + World, Scope, /*ActionPoint*/3, /*AimRange*/0, /*SkillCost*/1, 6, 1, FTileIndex(0, 0), @@ -1180,10 +1178,5 @@ bool FEnemyTurnPlannerTests::RunTest(const FString& Parameters) CheckApproachNoCast(*this, Commands, TEXT("Case9-5"), FTileIndex(3, 0)); } - // GC 안당하려고 KeepAlive에 마달아놨던 SkillComponent 연결 해제 -> GC 대상 - for (UObject* Object : KeepAlive) - if (IsValid(Object)) - Object->RemoveFromRoot(); - return true; } diff --git a/Source/P_RDTests/Singleton/PartyArtifactRestoreTests.cpp b/Source/P_RDTests/Singleton/PartyArtifactRestoreTests.cpp index f0822c1f8..8478a0557 100644 --- a/Source/P_RDTests/Singleton/PartyArtifactRestoreTests.cpp +++ b/Source/P_RDTests/Singleton/PartyArtifactRestoreTests.cpp @@ -9,6 +9,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "GameplayTagContainer.h" #include "Engine/Engine.h" @@ -51,6 +52,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPartyArtifactRestoreTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + /* 월드와 프레임워크 모델 확보 (파티 연결 시 속성 초기화에 필요) */ UWorld* World = GetRestoreTestWorld(); if (TestNotNull(TEXT("유효한 월드"), World) == false) @@ -71,9 +74,9 @@ bool FPartyArtifactRestoreTests::RunTest(const FString& Parameters) const FPrimaryAssetId ArtifactId(TEXT("Artifact"), TEXT("DA_Artifact_A001_ChaliceOfLife")); /* 파티와 구성원 2명 생성 (복원 경로처럼 월드 소속으로 초기화) */ - UPartyModel* PartyModel = NewObject(World); - UMockPartyMemberModel* MemberA = NewObject(World); - UMockPartyMemberModel* MemberB = NewObject(World); + UPartyModel* PartyModel = Scope.New(World); + UMockPartyMemberModel* MemberA = Scope.New(World); + UMockPartyMemberModel* MemberB = Scope.New(World); for (UMockPartyMemberModel* Member : { MemberA, MemberB }) { Member->SetStaticSpawnData(KnightData); @@ -82,7 +85,7 @@ bool FPartyArtifactRestoreTests::RunTest(const FString& Parameters) PartyModel->Initialize(); /* 영구데이터에 아티팩트 시드 후 동기화 (셋째 슬롯은 빈 자리) */ - UPartyArtifactRestoreTestData* PersistData = NewObject(); + UPartyArtifactRestoreTestData* PersistData = Scope.New(); PersistData->SeedArtifact(ArtifactId); TArray> Players = { MemberA, MemberB, nullptr }; PersistData->Sync(PartyModel, Players); @@ -102,10 +105,5 @@ bool FPartyArtifactRestoreTests::RunTest(const FString& Parameters) /* 빈 슬롯은 그대로 */ TestNull(TEXT("셋째 슬롯 비어 있음"), PartyModel->GetPlayerUnitModel(2)); - /* 정리 */ - PartyModel->Uninitialize(); - MemberA->Uninitialize(); - MemberB->Uninitialize(); - return true; } diff --git a/Source/P_RDTests/TAS/Passive/PassiveConditionTests.cpp b/Source/P_RDTests/TAS/Passive/PassiveConditionTests.cpp index cdcb7f879..cf48bddac 100644 --- a/Source/P_RDTests/TAS/Passive/PassiveConditionTests.cpp +++ b/Source/P_RDTests/TAS/Passive/PassiveConditionTests.cpp @@ -11,6 +11,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "TAS/Passive/PassiveCondition.h" @@ -22,10 +23,10 @@ namespace { - // HP / MaxHP / 타일 위치를 가진 스냅샷 생성 - UBoardCombatTargetSnapshotData* MakeSnapshot(float HP, float MaxHP, const FTileIndex& Tile) + // HP / MaxHP / 타일 위치를 가진 스냅샷 생성 (스코프에 등록) + UBoardCombatTargetSnapshotData* MakeSnapshot(FTestObjectScope& Scope, float HP, float MaxHP, const FTileIndex& Tile) { - UBoardCombatTargetSnapshotData* Snapshot = NewObject(); + UBoardCombatTargetSnapshotData* Snapshot = Scope.New(); Snapshot->mAttributes.Add(UCombatTargetAttributeSet::GetHPAttribute(), HP); Snapshot->mAttributes.Add(UCombatTargetAttributeSet::GetMaxHPAttribute(), MaxHP); Snapshot->mTileTransform.mIndex = Tile; @@ -98,8 +99,10 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveConditionResolveTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + // 소유자: HP 30 / MaxHP 100, 디버프 약화 1 + 취약 2 - UBoardCombatTargetSnapshotData* Owner = MakeSnapshot(30.f, 100.f, FTileIndex(0, 0)); + UBoardCombatTargetSnapshotData* Owner = MakeSnapshot(Scope, 30.f, 100.f, FTileIndex(0, 0)); Owner->mEffectCounts.Add(EffectTags::GameplayEffect_StatusEffect_RoundDuration_Debuff_Weakness, 1); Owner->mEffectCounts.Add(EffectTags::GameplayEffect_StatusEffect_RoundDuration_Debuff_Vulnerability, 2); Owner->mEffectCounts.Add(EffectTags::GameplayEffect_StatusEffect_RoundDuration_Buff_Vigor, 1); @@ -158,11 +161,13 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveConditionSpatialTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + // 소유자 (0,0), 타겟 0 (2,3), 타겟 1 (1,0) FPassiveActivateContext Ctx; - Ctx.mOwnerSnapshot = MakeSnapshot(100.f, 100.f, FTileIndex(0, 0)); - Ctx.mTargetSnapshots.Add(MakeSnapshot(100.f, 100.f, FTileIndex(2, 3))); - Ctx.mTargetSnapshots.Add(MakeSnapshot(100.f, 100.f, FTileIndex(1, 0))); + Ctx.mOwnerSnapshot = MakeSnapshot(Scope, 100.f, 100.f, FTileIndex(0, 0)); + Ctx.mTargetSnapshots.Add(MakeSnapshot(Scope, 100.f, 100.f, FTileIndex(2, 3))); + Ctx.mTargetSnapshots.Add(MakeSnapshot(Scope, 100.f, 100.f, FTileIndex(1, 0))); FDynamicPassiveData_Generic State; float Value = 0.f; @@ -214,9 +219,11 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveConditionEvaluateAllTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + // 소유자 HP 30 / MaxHP 100, 카운터 6 FPassiveActivateContext Ctx; - Ctx.mOwnerSnapshot = MakeSnapshot(30.f, 100.f, FTileIndex(0, 0)); + Ctx.mOwnerSnapshot = MakeSnapshot(Scope, 30.f, 100.f, FTileIndex(0, 0)); FDynamicPassiveData_Generic State; State.mCounter = 6; diff --git a/Source/P_RDTests/TAS/Passive/TacticalPassiveGenericTests.cpp b/Source/P_RDTests/TAS/Passive/TacticalPassiveGenericTests.cpp index 249f14bec..f911eec66 100644 --- a/Source/P_RDTests/TAS/Passive/TacticalPassiveGenericTests.cpp +++ b/Source/P_RDTests/TAS/Passive/TacticalPassiveGenericTests.cpp @@ -8,6 +8,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "TAS/TASAttributeTestsHelper.h" #include "TAS/Passive/PassiveTestsHelper.h" @@ -58,8 +59,8 @@ namespace return FGameplayTag::RequestGameplayTag(FName(TagName)); } - // 보드액터 목 생성 - UMockBoardActorModel* MakeMockActor(FAutomationTestBase& Test) + // 보드액터 목 생성 (스코프에 등록) + UMockBoardActorModel* MakeMockActor(FAutomationTestBase& Test, FTestObjectScope& Scope) { UWorld* World = GetAnyGameWorld(); if (World == nullptr) @@ -87,11 +88,13 @@ namespace UTacticalFrameworkModel* FrameworkModel = Cast(RoomInstance->mAliveSubsystemModels.FindRef(UTacticalFrameworkModel::StaticClass())); if (FrameworkModel == nullptr) { - FrameworkModel = NewObject(RoomInstance); + FrameworkModel = Scope.New(RoomInstance); RoomInstance->mAliveSubsystemModels.Add(UTacticalFrameworkModel::StaticClass(), FrameworkModel); + // 테스트가 등록한 것이므로 파괴 전에 등록 해제 + Scope.Defer([RoomInstance]() { RoomInstance->mAliveSubsystemModels.Remove(UTacticalFrameworkModel::StaticClass()); }); } - UMockBoardActorModel* Actor = NewObject(World); + UMockBoardActorModel* Actor = Scope.New(World); Actor->Initialize(); Actor->BeginPlay(); return Actor; @@ -145,10 +148,10 @@ namespace return Condition; } - // Generic용 DA 조립: 효과 1개 기본형. 조건/캡처/추가 효과는 호출부에서 채움 - UStaticPassiveData* MakeGenericData(FGameplayTag Activate, FGameplayTag Deactivate, TSubclassOf EffectClass, const FPassiveOperand& Magnitude) + // Generic용 DA 조립: 효과 1개 기본형. 조건/캡처/추가 효과는 호출부에서 채움. 스코프에 등록 + UStaticPassiveData* MakeGenericData(FTestObjectScope& Scope, FGameplayTag Activate, FGameplayTag Deactivate, TSubclassOf EffectClass, const FPassiveOperand& Magnitude) { - UStaticPassiveData* Data = NewObject(); + UStaticPassiveData* Data = Scope.New(); Data->mActivateTimingTag = Activate; Data->mDeactivateTimingTag = Deactivate; @@ -159,10 +162,10 @@ namespace return Data; } - // Generic 패시브 생성 - UTacticalPassive_Generic* MakeGenericPassive(UStaticPassiveData* Data) + // Generic 패시브 생성 (스코프에 등록) + UTacticalPassive_Generic* MakeGenericPassive(FTestObjectScope& Scope, UStaticPassiveData* Data) { - UTacticalPassive_Generic* Passive = NewObject(); + UTacticalPassive_Generic* Passive = Scope.New(); Passive->SetStaticData(Data); return Passive; } @@ -188,7 +191,9 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericBasicTest::RunTest(const FString& Parameters) { - UMockBoardActorModel* Actor = MakeMockActor(*this); + FTestObjectScope Scope; + + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return false; @@ -202,8 +207,8 @@ bool FPassiveGenericBasicTest::RunTest(const FString& Parameters) const FGameplayTag OnEndRoom = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndRoom")); // 조건 없이 힐 배율 1.25를 룸 동안 유지하는 DA - UStaticPassiveData* Data = MakeGenericData(OnStartRoom, OnEndRoom, UTacticalEffect_HealFactor_MultiplyAdditive::StaticClass(), MakeConstOperand(1.25f)); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UStaticPassiveData* Data = MakeGenericData(Scope, OnStartRoom, OnEndRoom, UTacticalEffect_HealFactor_MultiplyAdditive::StaticClass(), MakeConstOperand(1.25f)); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); FPassiveActivateContext Ctx; Ctx.mOwner = Actor; @@ -236,7 +241,9 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericConditionTest::RunTest(const FString& Parameters) { - UMockBoardActorModel* Actor = MakeMockActor(*this); + FTestObjectScope Scope; + + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return false; @@ -250,12 +257,12 @@ bool FPassiveGenericConditionTest::RunTest(const FString& Parameters) const FGameplayTag OnEndTurn = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndTurn")); // 체력이 절반 이하면 방어도 4를 주는 DA - UStaticPassiveData* Data = MakeGenericData(OnEndTurn, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), MakeConstOperand(4.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnEndTurn, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), MakeConstOperand(4.f)); Data->mConditions.Add(MakeCondition( MakeAttrOperand(UCombatTargetAttributeSet::GetHPAttribute(), EPassiveOperandSource::Self), EPassiveCompareOp::LessEqual, MakeAttrOperand(UCombatTargetAttributeSet::GetMaxHPAttribute(), EPassiveOperandSource::Self, 0.5f))); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); // HP 80: 조건 거짓이라 미발동 { @@ -293,7 +300,9 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericCounterTest::RunTest(const FString& Parameters) { - UMockBoardActorModel* Actor = MakeMockActor(*this); + FTestObjectScope Scope; + + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return false; @@ -310,7 +319,7 @@ bool FPassiveGenericCounterTest::RunTest(const FString& Parameters) // 매 3번째 스킬 사용에 공격 보너스를 주는 DA auto MakeCounterData = [&]() -> UStaticPassiveData* { - UStaticPassiveData* Data = MakeGenericData(OnStartUsingSkill, OnEndUsingSkill, UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnStartUsingSkill, OnEndUsingSkill, UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); Data->mConditions.Add(MakeCondition(MakeCounterOperand(), EPassiveCompareOp::ModuloZero, MakeConstOperand(3.f))); return Data; }; @@ -321,7 +330,7 @@ bool FPassiveGenericCounterTest::RunTest(const FString& Parameters) // 8회 반복: 3의 배수 회차만 발동 { - UTacticalPassive_Generic* Passive = MakeGenericPassive(MakeCounterData()); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, MakeCounterData()); for (int32 Hit = 1; Hit <= 8; ++Hit) { const bool bExpectFire = (Hit % 3 == 0); @@ -339,7 +348,7 @@ bool FPassiveGenericCounterTest::RunTest(const FString& Parameters) { UStaticPassiveData* Data = MakeCounterData(); Data->mCounterResetTimingTag = OnEndTurn; - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); // 발동 없이 2회 진행 for (int32 Hit = 1; Hit <= 2; ++Hit) @@ -368,7 +377,7 @@ bool FPassiveGenericCounterTest::RunTest(const FString& Parameters) MakeAttrOperand(UCombatTargetAttributeSet::GetHPAttribute(), EPassiveOperandSource::Self), EPassiveCompareOp::Less, MakeConstOperand(50.f))); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); // Self 조건은 소유자 스냅샷으로 판정하므로 HP 변경 시마다 스냅샷 갱신 FPassiveActivateContext SnapshotCtx; @@ -408,6 +417,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericCaptureTest::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag OnStartUsingSkill = PassiveTiming(TEXT("GameplayAbility.Passive.OnStartUsingSkill")); const FGameplayTag OnEndUsingSkill = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndUsingSkill")); @@ -415,7 +426,7 @@ bool FPassiveGenericCaptureTest::RunTest(const FString& Parameters) // bDamaged: 캡처와 발동 사이에 체력을 깎을지 auto RunCase = [&](bool bDamaged) -> float { - UMockBoardActorModel* Actor = MakeMockActor(*this); + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return -1.f; @@ -425,7 +436,7 @@ bool FPassiveGenericCaptureTest::RunTest(const FString& Parameters) Comp->SetAttributeBaseValue(UCombatTargetAttributeSet::GetHPAttribute(), 80.f); // 스킬 시작에 HP를 캡처하고, 종료에 현재 HP와 비교하는 DA - UStaticPassiveData* Data = MakeGenericData(OnEndUsingSkill, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), MakeConstOperand(4.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnEndUsingSkill, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), MakeConstOperand(4.f)); Data->mCaptureTimingTag = OnStartUsingSkill; FPassiveCaptureEntry Capture; Capture.mKey = FName(TEXT("HP")); @@ -435,7 +446,7 @@ bool FPassiveGenericCaptureTest::RunTest(const FString& Parameters) MakeAttrOperand(UCombatTargetAttributeSet::GetHPAttribute(), EPassiveOperandSource::Self), EPassiveCompareOp::Less, MakeCapturedOperand(FName(TEXT("HP")), EPassiveOperandSource::Self))); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); // 스냅샷을 새로 떠서 Ctx를 구성 auto MakeCtx = [&]() -> FPassiveActivateContext @@ -483,11 +494,13 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericMagnitudeTest::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag OnEndTurn = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndTurn")); // 속성값 수치: 공격력만큼 방어도를 얻는 DA { - UMockBoardActorModel* Actor = MakeMockActor(*this); + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return false; @@ -495,9 +508,9 @@ bool FPassiveGenericMagnitudeTest::RunTest(const FString& Parameters) UAttributeSetComponentModel* Comp = Actor->GetAttributeComponentModel(); Comp->SetAttributeBaseValue(UCombatTargetAttributeSet::GetAttackFactorAttribute(), 3.f); - UStaticPassiveData* Data = MakeGenericData(OnEndTurn, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), + UStaticPassiveData* Data = MakeGenericData(Scope, OnEndTurn, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), MakeAttrOperand(UCombatTargetAttributeSet::GetAttackFactorAttribute(), EPassiveOperandSource::Self)); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); // 수치 계산이 스냅샷을 읽으므로 소유자 스냅샷 필요 FPassiveActivateContext Ctx; @@ -511,7 +524,7 @@ bool FPassiveGenericMagnitudeTest::RunTest(const FString& Parameters) // 효과 2개: 방어도 +4와 체력 +5 동시 적용 { - UMockBoardActorModel* Actor = MakeMockActor(*this); + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return false; @@ -520,12 +533,12 @@ bool FPassiveGenericMagnitudeTest::RunTest(const FString& Parameters) Comp->SetAttributeBaseValue(UCombatTargetAttributeSet::GetMaxHPAttribute(), 100.f); Comp->SetAttributeBaseValue(UCombatTargetAttributeSet::GetHPAttribute(), 50.f); - UStaticPassiveData* Data = MakeGenericData(OnEndTurn, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), MakeConstOperand(4.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnEndTurn, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), MakeConstOperand(4.f)); FPassiveEffectEntry Second; Second.mEffectClass = UTacticalEffect_HP::StaticClass(); Second.mMagnitude = MakeConstOperand(5.f); Data->mEffects.Add(Second); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); FPassiveActivateContext Ctx; Ctx.mOwner = Actor; @@ -551,14 +564,16 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericTargetTest::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag OnStartTurn = PassiveTiming(TEXT("GameplayAbility.Passive.OnStartTurn")); const FGameplayTag OnEndTurn = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndTurn")); // 효과 대상: Self면 소유자만, Targets면 타겟 전부 { - UMockBoardActorModel* Player = MakeMockActor(*this); - UMockBoardActorModel* Target1 = MakeMockActor(*this); - UMockBoardActorModel* Target2 = MakeMockActor(*this); + UMockBoardActorModel* Player = MakeMockActor(*this, Scope); + UMockBoardActorModel* Target1 = MakeMockActor(*this, Scope); + UMockBoardActorModel* Target2 = MakeMockActor(*this, Scope); if (Player == nullptr || Target1 == nullptr || Target2 == nullptr) { return false; @@ -577,9 +592,9 @@ bool FPassiveGenericTargetTest::RunTest(const FString& Parameters) // Self: 소유자만 +5 { - UStaticPassiveData* Data = MakeGenericData(OnStartTurn, OnEndTurn, UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnStartTurn, OnEndTurn, UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); Data->mEffectTarget = EPassiveEffectTarget::Self; - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); DriveTiming(Passive, OnStartTurn, Ctx); TestEqual(TEXT("Self 발동: 소유자 15"), PlayerComp->GetAttributeCurrentValue(UCombatTargetAttributeSet::GetAttackFactorAttribute()), 15.f); @@ -590,9 +605,9 @@ bool FPassiveGenericTargetTest::RunTest(const FString& Parameters) // Targets: 타겟 전부 +5, 소유자는 그대로 { - UStaticPassiveData* Data = MakeGenericData(OnStartTurn, OnEndTurn, UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnStartTurn, OnEndTurn, UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); Data->mEffectTarget = EPassiveEffectTarget::Targets; - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); DriveTiming(Passive, OnStartTurn, Ctx); TestEqual(TEXT("Targets 발동: 타겟1 15"), TargetComp1->GetAttributeCurrentValue(UCombatTargetAttributeSet::GetAttackFactorAttribute()), 15.f); @@ -608,9 +623,9 @@ bool FPassiveGenericTargetTest::RunTest(const FString& Parameters) { auto RunCase = [&](EPassiveTargetQuantifier Quantifier, float HP1, float HP2) -> float { - UMockBoardActorModel* Player = MakeMockActor(*this); - UMockBoardActorModel* Target1 = MakeMockActor(*this); - UMockBoardActorModel* Target2 = MakeMockActor(*this); + UMockBoardActorModel* Player = MakeMockActor(*this, Scope); + UMockBoardActorModel* Target1 = MakeMockActor(*this, Scope); + UMockBoardActorModel* Target2 = MakeMockActor(*this, Scope); if (Player == nullptr || Target1 == nullptr || Target2 == nullptr) { return -1.f; @@ -622,14 +637,14 @@ bool FPassiveGenericTargetTest::RunTest(const FString& Parameters) TargetComp1->SetAttributeBaseValue(UCombatTargetAttributeSet::GetAttackFactorAttribute(), 10.f); TargetComp2->SetAttributeBaseValue(UCombatTargetAttributeSet::GetAttackFactorAttribute(), 10.f); - UStaticPassiveData* Data = MakeGenericData(OnStartTurn, FGameplayTag(), UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnStartTurn, FGameplayTag(), UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); Data->mEffectTarget = EPassiveEffectTarget::Targets; Data->mTargetQuantifier = Quantifier; Data->mConditions.Add(MakeCondition( MakeAttrOperand(UCombatTargetAttributeSet::GetHPAttribute(), EPassiveOperandSource::Target), EPassiveCompareOp::Less, MakeConstOperand(50.f))); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); FPassiveActivateContext Ctx; Ctx.mOwner = Player; @@ -650,7 +665,7 @@ bool FPassiveGenericTargetTest::RunTest(const FString& Parameters) // Infinite 효과 2개: 동시 적용, 재발동 갱신, 해제까지 { - UMockBoardActorModel* Actor = MakeMockActor(*this); + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return false; @@ -659,12 +674,12 @@ bool FPassiveGenericTargetTest::RunTest(const FString& Parameters) Comp->SetAttributeBaseValue(UCombatTargetAttributeSet::GetAttackFactorAttribute(), 10.f); Comp->SetAttributeBaseValue(UCombatTargetAttributeSet::GetHealFactorAttribute(), 1.f); - UStaticPassiveData* Data = MakeGenericData(OnStartTurn, OnEndTurn, UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnStartTurn, OnEndTurn, UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); FPassiveEffectEntry Second; Second.mEffectClass = UTacticalEffect_HealFactor_MultiplyAdditive::StaticClass(); Second.mMagnitude = MakeConstOperand(1.25f); Data->mEffects.Add(Second); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); FPassiveActivateContext Ctx; Ctx.mOwner = Actor; @@ -702,12 +717,14 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericNoTargetTest::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag OnEndApplyingEffect = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndApplyingEffect")); // 소유자 AttackFactor 10, HP 30으로 두고 빈 타겟 컨텍스트로 발동. 반환값은 발동 후 소유자 AttackFactor auto RunCase = [&](const TArray& Conditions) -> float { - UMockBoardActorModel* Player = MakeMockActor(*this); + UMockBoardActorModel* Player = MakeMockActor(*this, Scope); if (Player == nullptr) { return -1.f; @@ -716,9 +733,9 @@ bool FPassiveGenericNoTargetTest::RunTest(const FString& Parameters) PlayerComp->SetAttributeBaseValue(UCombatTargetAttributeSet::GetAttackFactorAttribute(), 10.f); PlayerComp->SetAttributeBaseValue(UCombatTargetAttributeSet::GetHPAttribute(), 30.f); - UStaticPassiveData* Data = MakeGenericData(OnEndApplyingEffect, FGameplayTag(), UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnEndApplyingEffect, FGameplayTag(), UTacticalEffect_AttackFactor_AddBase::StaticClass(), MakeConstOperand(5.f)); Data->mConditions = Conditions; - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); // 타겟과 타겟 스냅샷을 비워둔 컨텍스트 FPassiveActivateContext Ctx; @@ -769,6 +786,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericTagTest::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag OnStartTurn = PassiveTiming(TEXT("GameplayAbility.Passive.OnStartTurn")); const FGameplayTag OnEndTurn = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndTurn")); @@ -777,7 +796,7 @@ bool FPassiveGenericTagTest::RunTest(const FString& Parameters) // 상태이상 스택: 수치가 스택 수로 들어감 { - UMockBoardActorModel* Actor = MakeMockActor(*this); + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return false; @@ -786,8 +805,8 @@ bool FPassiveGenericTagTest::RunTest(const FString& Parameters) TestFalse(TEXT("적용 전: 취약 태그 없음"), Comp->HasMatchingGameplayTag(VulnerabilityTag)); // 취약 3스택을 주는 DA - UStaticPassiveData* Data = MakeGenericData(OnEndTurn, FGameplayTag(), UTacticalEffect_GetVulnerability::StaticClass(), MakeConstOperand(3.f)); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UStaticPassiveData* Data = MakeGenericData(Scope, OnEndTurn, FGameplayTag(), UTacticalEffect_GetVulnerability::StaticClass(), MakeConstOperand(3.f)); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); FPassiveActivateContext Ctx; Ctx.mOwner = Actor; @@ -801,7 +820,7 @@ bool FPassiveGenericTagTest::RunTest(const FString& Parameters) // Infinite 태그 부여: 해제 시 태그 회수 { - UMockBoardActorModel* Actor = MakeMockActor(*this); + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return false; @@ -810,8 +829,8 @@ bool FPassiveGenericTagTest::RunTest(const FString& Parameters) TestFalse(TEXT("적용 전: 취약 태그 없음"), Comp->HasMatchingGameplayTag(VulnerabilityTag)); // GrantedTags 목 이펙트를 턴 동안 유지하는 DA - UStaticPassiveData* Data = MakeGenericData(OnStartTurn, OnEndTurn, UMockGrantedTagTacticalEffect::StaticClass(), MakeConstOperand(1.f)); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UStaticPassiveData* Data = MakeGenericData(Scope, OnStartTurn, OnEndTurn, UMockGrantedTagTacticalEffect::StaticClass(), MakeConstOperand(1.f)); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); FPassiveActivateContext Ctx; Ctx.mOwner = Actor; @@ -841,6 +860,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericStatusStackTest::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag OnStartTurn = PassiveTiming(TEXT("GameplayAbility.Passive.OnStartTurn")); const FGameplayTag OnEndTurn = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndTurn")); const FTacticalEffectQuery StrengthQuery = FTacticalEffectQuery::MakeQuery_MatchAnyEffectTags(FGameplayTagContainer(EffectTags::GameplayEffect_StatusEffect_Infinite_Buff_Strength)); @@ -848,12 +869,12 @@ bool FPassiveGenericStatusStackTest::RunTest(const FString& Parameters) // 완력 버프 3스택을 턴 동안 유지하는 DA auto MakeStrengthData = [&]() -> UStaticPassiveData* { - return MakeGenericData(OnStartTurn, OnEndTurn, UTacticalEffect_Buff_Strength::StaticClass(), MakeConstOperand(3.f)); + return MakeGenericData(Scope, OnStartTurn, OnEndTurn, UTacticalEffect_Buff_Strength::StaticClass(), MakeConstOperand(3.f)); }; // 다른 출처의 2스택 위에 3스택을 얹고, 해제하면 3스택만 빠지는지 { - UMockBoardActorModel* Actor = MakeMockActor(*this); + UMockBoardActorModel* Actor = MakeMockActor(*this, Scope); if (Actor == nullptr) { return false; @@ -866,7 +887,7 @@ bool FPassiveGenericStatusStackTest::RunTest(const FString& Parameters) Comp->ApplyTacticalEffectSpecToSelf(*OtherSpec); TestEqual(TEXT("적용 전: 다른 출처 2스택"), Comp->GetAggregatedStackCount(StrengthQuery), 2); - UTacticalPassive_Generic* Passive = MakeGenericPassive(MakeStrengthData()); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, MakeStrengthData()); FPassiveActivateContext Ctx; Ctx.mOwner = Actor; Ctx.mTargets.Add(Actor); @@ -878,25 +899,6 @@ bool FPassiveGenericStatusStackTest::RunTest(const FString& Parameters) TestEqual(TEXT("해제: 패시브 3스택만 빠져 2스택"), Comp->GetAggregatedStackCount(StrengthQuery), 2); } - // 면역 대상에게는 걸리지 않는지 - { - UMockBoardActorModel* Actor = MakeMockActor(*this); - if (Actor == nullptr) - { - return false; - } - UAttributeSetComponentModel* Comp = Actor->GetAttributeComponentModel(); - Comp->AddLooseGameplayTag(EffectTags::GameplayEffect_ActorState_Immunity); - - UTacticalPassive_Generic* Passive = MakeGenericPassive(MakeStrengthData()); - FPassiveActivateContext Ctx; - Ctx.mOwner = Actor; - Ctx.mTargets.Add(Actor); - - DriveTiming(Passive, OnStartTurn, Ctx); - TestEqual(TEXT("면역: 0스택"), Comp->GetAggregatedStackCount(StrengthQuery), 0); - } - return true; } @@ -912,13 +914,15 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FPassiveGenericCapturedTargetsTest::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + const FGameplayTag OnStartApplyingEffect = PassiveTiming(TEXT("GameplayAbility.Passive.OnStartApplyingEffect")); const FGameplayTag OnEndUsingSkill = PassiveTiming(TEXT("GameplayAbility.Passive.OnEndUsingSkill")); - UMockBoardActorModel* Owner = MakeMockActor(*this); - UMockBoardActorModel* Target1 = MakeMockActor(*this); - UMockBoardActorModel* Target2 = MakeMockActor(*this); - UMockBoardActorModel* Target3 = MakeMockActor(*this); + UMockBoardActorModel* Owner = MakeMockActor(*this, Scope); + UMockBoardActorModel* Target1 = MakeMockActor(*this, Scope); + UMockBoardActorModel* Target2 = MakeMockActor(*this, Scope); + UMockBoardActorModel* Target3 = MakeMockActor(*this, Scope); if (Owner == nullptr || Target1 == nullptr || Target2 == nullptr || Target3 == nullptr) { return false; @@ -931,7 +935,7 @@ bool FPassiveGenericCapturedTargetsTest::RunTest(const FString& Parameters) Target3->GetAttributeComponentModel()->SetAttributeBaseValue(UCombatTargetAttributeSet::GetHPAttribute(), 80.f); // 이펙트 적용 시점에 대상 HP를 캡처하고, 스킬 종료 시 캡처한 대상 중 HP가 줄어든 게 있으면 방어도 4를 얻는 DA - UStaticPassiveData* Data = MakeGenericData(OnEndUsingSkill, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), MakeConstOperand(4.f)); + UStaticPassiveData* Data = MakeGenericData(Scope, OnEndUsingSkill, FGameplayTag(), UTacticalEffect_Defense::StaticClass(), MakeConstOperand(4.f)); Data->mCaptureTimingTag = OnStartApplyingEffect; Data->mActivateOnCapturedTargets = true; FPassiveCaptureEntry CaptureEntry; @@ -942,7 +946,7 @@ bool FPassiveGenericCapturedTargetsTest::RunTest(const FString& Parameters) MakeAttrOperand(UCombatTargetAttributeSet::GetHPAttribute(), EPassiveOperandSource::Target), EPassiveCompareOp::Less, MakeCapturedOperand(FName(TEXT("HP")), EPassiveOperandSource::Target))); - UTacticalPassive_Generic* Passive = MakeGenericPassive(Data); + UTacticalPassive_Generic* Passive = MakeGenericPassive(Scope, Data); // 페이즈 Ctx: 스킬 대상들과 스냅샷 auto Capture = [&](std::initializer_list Targets) diff --git a/Source/P_RDTests/TAS/StatusEffectRoundDurationTests.cpp b/Source/P_RDTests/TAS/StatusEffectRoundDurationTests.cpp index 5c4e85af1..17839fb7e 100644 --- a/Source/P_RDTests/TAS/StatusEffectRoundDurationTests.cpp +++ b/Source/P_RDTests/TAS/StatusEffectRoundDurationTests.cpp @@ -20,6 +20,7 @@ *********************************************************************/ #include "P_RDTests.h" +#include "TestObjectScope.h" #include "Misc/AutomationTest.h" #include "TASAttributeTestsHelper.h" #include "GameplayTagType.h" @@ -75,6 +76,8 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( bool FStatusEffectRoundDurationTests::RunTest(const FString& Parameters) { + FTestObjectScope Scope; + /* 1. 월드 및 FrameworkModel 환경 준비 */ UWorld* World = FindAnyGameWorld(); @@ -102,15 +105,17 @@ bool FStatusEffectRoundDurationTests::RunTest(const FString& Parameters) UTacticalFrameworkModel* FrameworkModel = Cast(RoomInstance->mAliveSubsystemModels.FindRef(UTacticalFrameworkModel::StaticClass())); if (FrameworkModel == nullptr) { - FrameworkModel = NewObject(RoomInstance); + FrameworkModel = Scope.New(RoomInstance); RoomInstance->mAliveSubsystemModels.Add(UTacticalFrameworkModel::StaticClass(), FrameworkModel); + // 테스트가 등록한 것이므로 파괴 전에 등록 해제 + Scope.Defer([RoomInstance]() { RoomInstance->mAliveSubsystemModels.Remove(UTacticalFrameworkModel::StaticClass()); }); } const FGameplayTag StunTag = EffectTags::GameplayEffect_StatusEffect_RoundDuration_Debuff_Stun; /* 테스트 1: 다른 유닛만 턴을 받은 라운드에서도 기절 스택이 라운드당 1씩 감소 */ { - UTASActorModelMock* MockActorModel = NewObject(World); + UTASActorModelMock* MockActorModel = Scope.New(World); MockActorModel->Initialize(); MockActorModel->BeginPlay(); @@ -174,7 +179,7 @@ bool FStatusEffectRoundDurationTests::RunTest(const FString& Parameters) /* 테스트 2: 라운드가 진행되지 않으면 기절 스택이 줄지 않음 */ { - UTASActorModelMock* MockActorModel = NewObject(World); + UTASActorModelMock* MockActorModel = Scope.New(World); MockActorModel->Initialize(); MockActorModel->BeginPlay(); diff --git a/Source/P_RDTests/TestObjectScope.h b/Source/P_RDTests/TestObjectScope.h new file mode 100644 index 000000000..516e193bb --- /dev/null +++ b/Source/P_RDTests/TestObjectScope.h @@ -0,0 +1,132 @@ +/*****************************************************************//** + * @file TestObjectScope.h + * @brief 테스트용 UObject 생성/정리 스코프 + * @details + * RunTest 안에서 만든 UObject를 등록해 두었다가 스코프 종료 시 일괄 파괴. + * 등록된 객체는 스코프가 살아 있는 동안 GC에서 보호됨 (FGCObject). + * 파괴는 등록 역순. UObjectModel 계열은 인게임 파괴 경로와 같이 + * EndPlay(액터만) -> Uninitialize 후 MarkAsGarbage. + * @author 이문환 + * @date 2026-09-23 + *********************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/GCObject.h" +#include "Actor/ActorModel.h" + +/** + * @brief 테스트용 UObject 생성/정리 스코프 + * @details RunTest 첫 줄에 로컬로 선언. New로 만든 객체는 이른 return을 포함한 + * 모든 종료 경로에서 파괴됨. 테스트 본문은 EndPlay/Uninitialize를 직접 부르지 않음 + */ +class FTestObjectScope : public FGCObject +{ +public: + FTestObjectScope() = default; + ~FTestObjectScope(); + + // 복사 금지 (파괴가 두 번 일어나지 않도록) + FTestObjectScope(const FTestObjectScope&) = delete; + FTestObjectScope& operator=(const FTestObjectScope&) = delete; + +public: + // @brief 객체 생성 + 등록 (NewObject 대체) + template + T* New(UObject* Outer = GetTransientPackage(), UClass* Class = nullptr); + + // @brief 외부에서 만든 객체 등록 (DuplicateObject 등) + template + T* Add(T* Object); + + // @brief 파괴 직전에 실행할 정리 작업 등록 (등록 역순 실행) + void Defer(TFunction Task); + +public: + // FGCObject: 등록 객체를 참조로 보고해 테스트 도중 GC 방지 + void AddReferencedObjects(FReferenceCollector& Collector) override; + FString GetReferencerName() const override; + +private: + // @brief 등록된 객체 (등록 순서 유지) + TArray> mObjects; + // @brief 파괴 전 실행할 정리 작업 + TArray> mDeferred; +}; + +// @brief 객체 생성 + 등록 (NewObject 대체) +template +T* FTestObjectScope::New(UObject* Outer, UClass* Class) +{ + T* Object = Class != nullptr ? NewObject(Outer, Class) : NewObject(Outer); + return Add(Object); +} + +// @brief 외부에서 만든 객체 등록 (DuplicateObject 등) +template +T* FTestObjectScope::Add(T* Object) +{ + if (Object != nullptr) + { + mObjects.Add(Object); + } + return Object; +} + +// @brief 파괴 직전에 실행할 정리 작업 등록 (등록 역순 실행) +inline void FTestObjectScope::Defer(TFunction Task) +{ + mDeferred.Add(MoveTemp(Task)); +} + +// @brief 등록 역순으로 정리 작업 실행 후 객체 파괴 +inline FTestObjectScope::~FTestObjectScope() +{ + // 1. 파괴 전 정리 작업 (등록 해제 등) 역순 실행 + for (int32 Index = mDeferred.Num() - 1; Index >= 0; --Index) + { + mDeferred[Index](); + } + mDeferred.Reset(); + + // 2. 객체 파괴 (등록 역순: 컴포넌트 -> 유닛 -> 타일맵 순으로 내려감) + for (int32 Index = mObjects.Num() - 1; Index >= 0; --Index) + { + UObject* Object = mObjects[Index]; + if (IsValid(Object) == false) + { + continue; + } + + // 인게임 파괴 경로와 동일 + if (UObjectModel* Model = Cast(Object)) + { + if (UActorModel* ActorModel = Cast(Model)) + { + ActorModel->EndPlay(); + } + Model->Uninitialize(); + } + + // 외부에서 루트에 매단 객체는 먼저 풀어야 GC 대상이 됨 + if (Object->IsRooted()) + { + Object->RemoveFromRoot(); + } + Object->MarkAsGarbage(); + } + mObjects.Reset(); +} + +// @brief 테스트 도중 GC가 돌아도 살아남게 GC 참조로 등록 +inline void FTestObjectScope::AddReferencedObjects(FReferenceCollector& Collector) +{ + Collector.AddReferencedObjects(mObjects); +} + +// @brief GC 디버그 출력용 참조자 이름 +inline FString FTestObjectScope::GetReferencerName() const +{ + return TEXT("FTestObjectScope"); +}