530 likes | 697 Views
게임을 위한 테스트 주도 개발 : 무엇을 , 왜 그리고 어떻게 ?. Noel Llopis Senior Architect High Moon Studios 번역 : 박일 http://ParkPD.egloos.com 도움 : 김기웅 http://betterways.tistory.com/. 1. 테스트 주도 개발 ( TDD) 이란 ? 2. 우리는 TDD 를 어떻게 사용했는가 3. TDD 와 게임 4. 우리가 얻은 교훈들 5. 결론. 1. 테스트 주도 개발 (TDD) 이란 ?
E N D
게임을 위한 테스트 주도 개발:무엇을, 왜 그리고 어떻게? Noel Llopis Senior Architect High Moon Studios 번역 : 박일 http://ParkPD.egloos.com 도움 : 김기웅 http://betterways.tistory.com/
1. 테스트 주도 개발(TDD)이란? • 2. 우리는 TDD를 어떻게 사용했는가 • 3. TDD와 게임 • 4. 우리가 얻은 교훈들 • 5. 결론
1. 테스트 주도 개발(TDD)이란? • (그리고 어쩌다 이것을 도입하게 되었는가에 대하여) • 2. 우리는 TDD를 어떻게 사용했는가 • 3. TDD와 게임 • 4. 우리가 얻은 교훈들 • 5. 결론
define G(n) int n(int t, int q, int d) #define X(p,t,s) (p>=t&&p<(t+s)&&(p-(t)&1023)<(s&1023)) #define U(m) *((signed char *)(m)) #define F if(!--q){ #define I(s) (int)main-(int)s #define P(s,c,k) for(h=0; h>>14==0; h+=129)Y(16*c+h/1024+Y(V+36))&128>>(h&7)?U(s+(h&15367))=k:k G (B) { Z; F D = E (Y (V), C = E (Y (V), Y (t + 4) + 3, 4, 0), 2, 0); Y (t + 12) = Y (t + 20) = i; Y (t + 24) = 1; Y (t + 28) = t; Y (t + 16) = 442890; Y (t + 28) = d = E (Y (V), s = D * 8 + 1664, 1, 0); for (p = 0; j < s; j++, p++) U (d + j) = i == D | j < p ? p--, 0 : (n = U (C + 512 + i++)) < ' ' ? p |= n * 56 - 497, 0 : n; } n = Y (Y (t + 4)) & 1; F U (Y (t + 28) + 1536) |= 62 & -n; M U (d + D) = X (D, Y (t + 12) + 26628, 412162) ? X (D, Y (t + 12) + 27653, 410112) ? 31 : 0 : U (d + D); for (; j < 12800; j += 8) P (d + 27653 + Y (t + 12) + ' ' * (j & ~511) + j % 512, U (Y (t + 28) + j / 8 + 64 * Y (t + 20)), 0); } F if (n) { D = Y (t + 28); if (d - 10) U (++Y (t + 24) + D + 1535) = d; else { for (i = D; i < D + 1600; i++) U (i) = U (i + 64); Y (t + 24) = 1; E (Y (V), i - 127, 3, 0); } } else Y (t + 20) += ((d >> 4) ^ (d >> 5)) - 3; } }
TDD의 순환 과정 테스트 실패 테스트 통과 테스트 통과 불과 몇 분밖에 걸리지 않는다. 테스트 작성 코드 작성 TEST (ShieldLevelStartsFull) { Shield shield; CHECK_EQUAL (Shield::kMaxLevel, shield.GetLevel()); } Shield::Shield() : m_level (Shield::kMaxLevel) { } 체크 인 체크 인 리팩토링
장점: 즉각적인 피드백 마일스톤: ~2개월 주기: 2~4주 야간구축: 1일 자동 구축: ~1시간 TDD : 30회 / 3~4분
TDD != 단위 검사TDD != 테스팅 전략 TDD == 개발 기법
1. 테스트 주도 개발(TDD)이란? • 2. 우리는 TDD를 어떻게 사용했는가 • 3. TDD와 게임 • 4. 우리가 얻은 교훈들 • 5. 결론
캐릭터+ 방패 Character Damage(x) Shield Damage(x) class Character { IShield* m_shield;public: Character(); void Damage(float amount); float GetHealth() const; };
3가지 검사 방법 • 반환되는 값을 검사하기 • 상태를 검사하기 • 객체간의 상호작용을 검사하기
반환되는 값 검사 방패 검사하기 방패 bool Damage() 데미지? TEST (ShieldCanBeDamagedIfFull) { } Shield shield; CHECK (shield.Damage()); “ShieldLevelStartsFull에서 오류 발생: 100을 예상했지만, 0이 나옴.”
상태 검사 방패 방패 검사하기 Damage(200) GetLevel() 0? TEST (LevelCannotBeDamagedBelowZero) { } Shield shield; shield.Damage(200); CHECK_EQUAL (0, shield.GetLevel());
어디에 검사를 삽입하는 게 좋을까? • TestGame.exe (Game.lib와 연결됨) • #ifdef UNIT_TESTS • GameTests.DLL • GameTests.upk
검사를 작성하기 • 새로운 검사를 추가하기 손쉽도록, 단위 검사 프레임워크를 사용하라 • UnitTest++는 게임에 꽤 적합하다
상호작용 검사(초기에 문제가 될 수 있다.) 캐릭터 검사하기 캐릭터 Damage() 방패 화려한 방패 *m_shield GetHealth() 390? TEST(CharacterUsesShieldToAbsorbDamage){ Character character(400); character.Damage(100); CHECK_EQUAL(390, character.GetHealth());}
class IShield { public: virtual float Damage(float amount) = 0; } 가짜 객체가, 검사 대상인 해당 단위(unit)의 외부에 위치한 객체를 대신한다. class FancyShield : public IShield { public: float Damage(float amount) { … }; } class MockShield : public IShield { public: float damagePassedIn; float damageToReturn; float Damage(float amount) { damagePassedIn = amount; return damageToReturn; } }
Mock을 사용해서 검사하기 캐릭터 Damage() *m_shield 매개 변수들은 정확한가? 반환된 데미지가 정확하게 사용되는가? GetHealth() 캐릭터 검사하기 가짜 방패 TEST(CharacterUsesShieldToAbsorbDamage){ } MockShield mockShield = new MockShield; mockShield->damageToReturn = 10; Character character(400, mockShield); character.Damage(200); CHECK_EQUAL(200, mockShield->damagePassedIn); CHECK_EQUAL(390, character.GetHealth());
누가 알겠어 검사 검사중인 코드 검사 검사중인 코드 하위 시스템 C 하위 시스템 B 하위 시스템 A 부엌의 싱크대 고양이가 끄집어낸 어떤 것 최상의 관행: 근처의 코드만 검사하기
최상의 관행: 간결한 검사 TEST(ActorDoesntMoveIfPelvisBodyIsInSamePositionAsPelvisAnim) { component = ConstructObject<UAmpPhysicallyDrivableSkeletalComponent>(); component->physicalPelvisHandle = NULL; component->SetOwner(owner); component->SkeletalMesh = skelMesh; component->Animations = CreateReadable2BoneAnimSequenceForAmpRagdollGetup(component, skelMesh, 10.0f, 0.0f); component->PhysicsAsset = physicsAsset; component->SpaceBases.AddZeroed(2); component->InitComponentRBPhys(false); component->LocalToWorld = FMatrix::Identity; const FVector actorPos(100,200,300); const FVector pelvisBodyPositionWS(100,200,380); const FTranslationMatrix actorToWorld(actorPos); owner->Location = actorPos; component->ConditionalUpdateTransform(actorToWorld); INT pelvisIndex = physicsAsset->CreateNewBody(TEXT("Bone1")); URB_BodySetup* pelvisSetup = physicsAsset->BodySetup(pelvisIndex); FPhysAssetCreateParams params = GetGenericCreateParamsForAmpRagdollGetup(); physicsAsset->CreateCollisionFromBone( pelvisSetup, skelMesh, 1, params, boneThings); URB_BodyInstance* pelvisBody = component->PhysicsAssetInstance->Bodies(0); NxActor* pelvisNxActor = pelvisBody->GetNxActor(); SetRigidBodyPositionWSForAmpRagdollGetup(*pelvisNxActor, pelvisBodyPositionWS); component->UpdateSkelPose(0.016f); component->RetransformActorToMatchCurrrentRoot(TransformManipulator()); const float kTolerance(0.002f); FMatrix expectedActorMatrix; expectedActorMatrix.SetIdentity(); expectedActorMatrix.M[3][0] = actorPos.X; expectedActorMatrix.M[3][1] = actorPos.Y; expectedActorMatrix.M[3][2] = actorPos.Z; const FMatrix actorMatrix = owner->LocalToWorld(); CHECK_ARRAY2D_CLOSE(expectedActorMatrix.M, actorMatrix.M, 4, 4, kTolerance); } TEST (ShieldStartsAtInitialLevel) { ShieldComponent shield(100); CHECK_EQUAL (100, shield.GetLevel()); } TEST (ShieldTakesDamage) { ShieldComponent shield(100); shield.Damage(30); CHECK_EQUAL (70, shield.GetLevel()); } TEST (LevelCannotDropBelowZero) { ShieldComponent shield(100); shield.Damage(200); CHECK_EQUAL (0, shield.GetLevel()); }
최상의 관행: 신속한 검사 Slow Test(24 > 20 ms): CheckSpotOverlapIsHandledCorrectly1Test Slow Test(25 > 20 ms): CheckSpotOverlapIsHandledCorrectly2Test Slow Test(24 > 20 ms): DeleteWaveEventFailsIfEventDoesntExistInCueTest Slow Test(22 > 20 ms): CanGetObjectsInBrowserListPackageTest Slow Test(48 > 20 ms): HmAddActorCallsCreateActorTest Slow Test(74 > 20 ms): HmReplaceActorDoesNothingIfEmptySelectionTest Slow Test(57 > 20 ms): HmReplaceActorWorksIfTwoActorsSelectedTest Slow Test(26 > 20 ms): ThrowExceptionWhenTrackIndexOutOfRangeTest 1923회 검사의 총소요 시간: 4.83초. 26회의 느린 검사에 소요된 시간: 2.54초.
최상의 관행: 신속한 검사 Debug 상태에서 TestDebugServer의 단위 검사 실행중.. 검사 116번 실행 실패한 검사 없음.소요 시간: 0.016초. Debug 상태에서 TestStreams의 단위 검사 실행중... 검사 138번 실행 실패한 검사 없음.소요 시간: 0.015초. Debug 상태에서 TestMath의 단위 검사 실행중... 검사 245번 실행 실패한 검사 없음.소요 시간: 0.001초. 단위 검사 실행중... 검사 184번 실행 실패한 검사 없음.소요 시간: 0.359초.
최상의 관행: 비의존적인 검사 g_CollisionWorldSingleton
1. 테스트 주도 개발(TDD)이란? • 2. 우리는 TDD를 어떻게 사용했는가 • 3. TDD와 게임 • 4. 우리가 얻은 교훈들 • 5. 결론
미들웨어까지 포함해서 검사하기 Havok RenderWare Unreal Novodex OpenGL DirectX
1. TDD 란? • 2. TDD 사용법 • 3. TDD와 게임 • 4. 우리가 얻은 교훈들 • 5. 결론
예시: 공격형 인공지능 function TestEnemyChoosesLightAttack() { FightingComp = new(self) class'FightingComponent'; FightingComp.AddAttack(LightAttack); FightingComp.AddAttack(HeavyAttack); enemy.AttachComponent(FightingComp); enemy.FightingComponent = FightingComp; enemy.FindPlayerPawn = MockFindPlayerPawn; enemy.ShouldMeleeAttack = MockShouldAttack; ShouldMeleeAttackReturn = true; enemy.Tick(0.666); CheckObjectsEqual(LightAttack, FightingComp.GetCurrentAttack()); }
예시: 캐릭터의 행동 TEST_F( CharacterFixture, SupportedWhenLeapAnimationEndsTransitionsRunning ) { LandingState state(CharacterStateParameters(&character), AnimationIndex::LeapLanding); state.Enter(input); input.deltaTime = character.GetAnimationDuration( AnimationIndex::LeapLanding ) + kEpsilon; character.supported = true; CharacterStateOutput output = state.Update( input ); CHECK_EQUAL(std::string("TransitionState"), output.nextState->GetClassInfo().GetName()); const TransitionState& transition = *output.nextState; CHECK_EQUAL(std::string("RunningState"), transition.endState->GetClassInfo().GetName()); }
교훈#3: 검사 횟수는 프로젝트 진행의 척도가 될 수 있다
교훈#7: TDD 도입하기 위험할수록, 대가가 크다 (High risk – High reward)