Sparta Unreal 부트캠프 108일차

정찬호·2026년 5월 6일

코딩 테스트

프로그래머스 - 점 찍기

2024-07-23에 정답 판전을 받았던 문제입니다.
최대한 코드를 보지 않고 풀어야 겠네요.

이전 코드

#include <string>
#include <vector>
#include <cmath>
using namespace std;

long long solution(int k, int d) {
    long long answer = 0;
    
    for(int i=0;i<=d;i+=k)
    {
        answer+=(long long)floor(sqrt(pow(d,2)-pow(i,2))/k)+1;
    }
    return answer;
}

코드를 길이로만 보았을 때 엄청 짧은 것을 보아 다른 사람의 코드를 참고했던 것 같습니다.

2차원 좌표 평면에서 원저과의 거리가 d이하이고 k의 배수인 곳에 점 찍고 개수를 세는 건데..
음수 좌표는 생각을 안해도 되니 그건 또 편하겠네요.

중복이 없어야 하고 결국 중요한 건 점의 개수이니

(ak) ^ 2 + (bk) ^ 2 의 제곱근이 d 이하인 모든 좌표
x 축만으로 나아간 경우의 최대 a를 구한뒤
a를 하나씩 감소시키면서 b를 증가시킨 경우의 거리 d 이하의 좌표 값을 구하면 되려나?
a나 b의 최대는 d/k를 하면 나올 테고

1차 코드

#include <string>
#include <vector>
#include <cmath>
using namespace std;

long long solution(int k, int d) {
    long long answer = 0;
    
    for(int i = d/k; i >= 0; i--)
    {
        for(int j = 0; j <= d/k; j++)
        {
            if(pow(i * k, 2) + pow(j * k, 2) <= (long long)d * d)
            {
                answer++;
            }
            else
            {
                break;
            }
        }
    }
    return answer;
}

O(n^2)라 시간초과가 발생합니다.
그래도 정확성 81.3%를 받았으니 시간 복잡도를 줄이면 어찌 해결이 될 것 같습니다.

2차 코드

#include <string>
#include <vector>
#include <cmath>
using namespace std;

long long solution(int k, int d) {
    long long answer = 0;
    
    for(int i = d/k; i >= 0; i--)
    {
        answer += sqrt(d * d - pow(i * k, 2)) / k + 1;
    }
    return answer;
}

1차 반복문으로 풀 꼼수를 생각해 보았습니다.
그냥 전체 거리에서 a*i를 뺀 만큼의 거리 / k 로 구하면 되는 거 아니가? b = 0 일 대를 고려해서 + 1 해주면 되는 거고..
안 되네요.

이전 오답 받은 거에 비슷하게 있나 찾아보니 같은게 있던 군요. 과거나 지금이나 저는 저인가 봅니다.

모르겠다. 정답 코드를 보겠습니다.
아.. 아!!! long long으로 캐스팅 해주는 거 빼먹었구나!!!
또 다른게 floor로 버림 안 해준거? 이거 자동 캐스팅 될 때 올림 처리되나?

정답 코드

#include <string>
#include <vector>
#include <cmath>
using namespace std;

long long solution(int k, int d) {
    long long answer = 0;
    
    for(int i = d/k; i >= 0; i--)
    {
        answer += (long long)floor(sqrt(pow(d, 2) - pow(i * k, 2)) / k) + 1;
    }
    return answer;
}

알아보니 올림이 아니라 부동 소수점의 미세한 오차로 인해 형변환 시 컴파일러 최적화나 레지스터 정밀도에 따라 예상과 다른 값이 나올 가능성이 있다고 합니다.



1. QA 프로젝트 진행

1-1. AI 감지 버그 분석 및 수정 — AI-001

S-019(시각), S-022(청각) 테스트 중 AI가 플레이어를 전혀 인식하지 못하는 버그를 발견했습니다. 관련 소스 파일 5개(AO_AIControllerBase.cpp, AO_AggressiveAICtrl.cpp, AO_AggressiveAIBase.cpp, AO_AICharacterBase.cpp, AO_WerewolfController.cpp)를 읽으며 원인을 추적했습니다.

핵심 원인은 AO_AIControllerBase.cppBeginPlay()에서 발견됐습니다.

// BeginPlay() 내부 — 바인딩 블록이 비어 있음
if (AIPerceptionComponent)
{
    // ← 아무것도 없음
}

OnTargetPerceptionUpdated 함수는 정의되어 있었지만, 델리게이트에 실제로 연결하는 코드가 빠져 있었기 때문에 Perception 시스템이 자극을 감지해도 콜백 체인(OnTargetPerceptionUpdatedOnPlayerDetectedOnNoiseHeard)이 전혀 호출되지 않았습니다. 시각·청각 감지가 동시에 작동하지 않는 증상과 정확히 일치합니다.

// 수정 — 바인딩 한 줄 추가
AIPerceptionComponent->OnTargetPerceptionUpdated.AddDynamic(
    this, &AAO_AIControllerBase::OnTargetPerceptionUpdated);

수정 후 AI가 플레이어를 따라오는 것을 확인했고, 청각 테스트를 위해 OnNoiseHeard에 임시 로그도 추가했습니다. AI-002(청각 감지가 되지 않는 버그)는 AI-001과 동일한 버그이였으므로 아예 제거를 했습니다.


1-2. 피격 후 영구 무적·어빌리티 차단 버그 — COMBAT-001

S-007 테스트 중 AI에게 1회 피격 이후 추가 피격도 안 되고 스프린트·점프까지 막히는 버그가 발생했다.

AO_GameplayAbility_HitReact.cpp를 확인해보니 ActivateAbility에서 UAbilityTask_PlayMontageAndWait 태스크를 생성하고 델리게이트 바인딩까지는 했지만, ReadyForActivation()을 호출하지 않아 태스크가 시작되지 않았다.

// 버그 — ReadyForActivation() 누락
MontageTask->OnCompleted.AddDynamic(...);
MontageTask->OnBlendOut.AddDynamic(...);
MontageTask->OnCancelled.AddDynamic(...);
MontageTask->OnInterrupted.AddDynamic(...);
// ← ReadyForActivation() 없음

태스크가 시작되지 않으니 OnCompleted/OnCancelled가 발동되지 않고, EndAbility()도 호출되지 않아 InvulnerableEffectClass(무적)와 BlockAbilitiesEffectClass(어빌리티 차단) GE가 영구적으로 유지됐다.

// 수정
MontageTask->OnInterrupted.AddDynamic(...);
MontageTask->ReadyForActivation(); // ← 추가

UAbilityTask는 델리게이트 바인딩만으로는 동작하지 않고 ReadyForActivation()을 명시적으로 호출해야 한다는 것을 이번에 확실히 알았다. 바인딩 전에 호출하면 콜백이 실행되지 않을 수 있으니 반드시 바인딩 완료 후에 호출해야 한다.


1-3. 체력 0에서 사망 미발동 버그 — COMBAT-002

S-008 테스트 중 체력이 0이 되어도 플레이어가 계속 조작 가능한 버그를 발견했다.

AO_PlayerCharacter_AttributeSet.cppPostGameplayEffectExecute()를 확인해보니 사망 분기 조건이 if (NewHealth < 0.f)였다.

const float NewHealth = GetHealth();  // 최솟값 0.f (PreAttributeChange에서 클램프됨)
if (NewHealth < 0.f)                  // 항상 false
{
    OnPlayerDeath.Broadcast();        // 절대 호출되지 않음
}

PreAttributeChange()에서 체력을 FMath::Clamp(NewValue, 0.f, MaxHealth)로 먼저 클램프하기 때문에, PostGameplayEffectExecute() 시점에서 GetHealth()의 최솟값은 이미 0.f다. < 0.f 조건은 절대 성립하지 않아 OnPlayerDeath가 발동되지 않았다.

// 수정
if (FMath::IsNearlyZero(NewHealth))

PreAttributeChangePostGameplayEffectExecute의 실행 순서를 제대로 이해하지 못하면 이런 실수가 생긴다. 어트리뷰트 값을 클램프하는 위치와 그 이후 조건 분기의 기준값이 무엇인지 항상 같이 봐야 한다는 걸 배웠다.


1-4. S-007 테스트 케이스 수정

S-007 테스트 중 화면 비네트·플래시 피드백이 없는 것을 발견했습니다. 관련 코드를 확인해보니 C++ 구현 자체가 존재하지 않았네요. 처음부터 구현이 안 된 기능을 테스트 케이스에 넣어둔 것이므로, 잘못된 쪽은 테스트 케이스였습니다.

프로젝트의 실제 구현이 정답이다. 코드에 없는 기능이 테스트 케이스에 있다면 테스트 케이스가 잘못된 것이다.

비네트·플래시 관련 절차와 기대 결과를 S-007에서 제거했습니다.


1-5. 버그 리포트 갱신

CHAR-001(Crouch 바인딩 누락), CHAR-002(Lockout 조건 반전), CHAR-003(커스터마이징 미반영), CHAR-004(Jump 태그 타이포), INV-001(nullptr 역참조 크래시), TRAIN-001(CanInteraction 하드코딩) 총 6개 항목이 수정 완료됐는데, BugReport.md에 반영이 안 된 상태였네요. 모두 수정 완료로 갱신하고 원인 분석과 수정 코드도 채워 넣었습니다.

profile
게임 개발 지망생입니다.

0개의 댓글