자동화 QA 에이전트로 던전 맵 1,000회 반복 탐색하기

자동화 QA 에이전트로 던전 맵 1,000회 반복 탐색하기

플레이 가능한 던전 맵을 자동으로 1,000회 실행해 막힘, 경로 단절, 상태 전이 오류를 찾는 방법을 정리한다. 핵심은 이동 성공 여부가 아니라 재현 가능한 실패 기록을 남기는 데 있다.

왜 1,000회 반복 실행이 필요한가

던전 맵의 오류는 한 번의 플레이로 드러나지 않는 경우가 많다. 무작위 방 배치, 몬스터 위치, 문 잠금 상태, 드롭 아이템, 프레임 지연이 겹치면 특정 순서에서만 캐릭터가 멈추거나 목표에 도달하지 못한다.

자동화 QA 에이전트의 목적은 사람처럼 맵을 잘 플레이하는 것이 아니다. 같은 규칙으로 맵을 반복 탐색하고 실패한 조건을 충분히 남겨 개발자가 다시 실행할 수 있게 만드는 것이다.

flowchart TD
    A[시드 선택] --> B[던전 생성]
    B --> C[에이전트 탐색]
    C --> D{목표 도달 또는 제한 초과}
    D -->|도달| E[성공 지표 기록]
    D -->|실패| F[실패 원인과 상태 저장]
    E --> G[다음 반복]
    F --> G
    G --> H{1,000회 완료?}
    H -->|아니오| A
    H -->|예| I[실패 유형 집계]

먼저 정할 것: 성공 조건과 실패 조건

테스트가 모호하면 결과도 모호해진다. 예를 들어 출구가 있는 던전이라면 성공 조건은 단순히 좌표가 움직였다는 사실이 아니라 제한 시간 안에 출구 트리거를 통과한 것으로 둔다.

실패는 가능한 한 구체적으로 나눈다.

  • 경로 없음: 생성된 그래프에서 시작점과 출구가 연결되지 않았다.
  • 이동 정체: 일정 시간 동안 목표까지의 거리가 줄지 않는다.
  • 상태 전이 오류: 문을 열었지만 내비게이션 경로가 갱신되지 않는다.
  • 예외 또는 비정상 종료: 테스트 도중 예외가 발생하거나 씬이 예상치 않게 다시 로드된다.
  • 시간 초과: 경로는 존재하지만 제한 스텝 안에 목표에 도달하지 못한다.

이 분류는 나중에 실패 건수를 세는 용도만이 아니다. 어떤 팀이 수정해야 하는지 빠르게 판단하는 기준이 된다. 경로 없음은 생성 로직 문제일 수 있고 이동 정체는 내비게이션이나 충돌 설정 문제일 수 있다.

던전 방과 복도 위에 자동화 에이전트의 탐색 경로 및 정체 지점이 표시된 화면

맵 생성 시드는 반드시 기록한다

절차적 생성 던전은 시드를 저장하지 않으면 실패를 다시 만들기 어렵다. 실패 보고서에는 최소한 다음 정보를 포함하는 편이 좋다.

seed: 184923
run: 317
mapVersion: 0.8.4
agentPosition: (12.4, 0.0, -8.7)
targetPosition: (30.0, 0.0, -4.0)
state: MovingToExit
failure: NoProgressTimeout

mapVersion도 중요하다. 같은 시드라도 방 배치 알고리즘이나 프리팹 크기가 바뀌면 전혀 다른 맵이 생성될 수 있다. 빌드 번호나 커밋 해시를 함께 남기면 더 좋다.

Unity에서 반복 테스트 루프 만들기

아래 예시는 테스트 모드에서 던전을 생성하고 에이전트가 출구에 도달했는지 검사하는 단순한 구조다. 실제 프로젝트에서는 DungeonFactory, INavigationAgent, ITestResultSink처럼 인터페이스로 분리하면 교체와 단위 테스트가 쉬워진다.

using System;
using System.Collections;
using UnityEngine;

public sealed class DungeonSoakTest : MonoBehaviour
{
    [SerializeField] private int totalRuns = 1000;
    [SerializeField] private int maxStepsPerRun = 2000;
    [SerializeField] private float stepInterval = 0.05f;

    [SerializeField] private DungeonFactory dungeonFactory;
    [SerializeField] private DungeonTestAgent agent;
    [SerializeField] private TestResultWriter resultWriter;

    private IEnumerator Start()
    {
        for (var run = 1; run <= totalRuns; run++)
        {
            var seed = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
            var dungeon = dungeonFactory.Build(seed);

            agent.Initialize(dungeon.Start, dungeon.Exit);
            var result = new DungeonTestResult(run, seed);

            for (var step = 0; step < maxStepsPerRun; step++)
            {
                agent.Tick();

                if (agent.HasReachedExit)
                {
                    result.MarkSuccess(step);
                    break;
                }

                if (agent.HasNotMadeProgressFor(3f))
                {
                    result.MarkFailure("NoProgressTimeout", step, agent.Position);
                    break;
                }

                yield return new WaitForSeconds(stepInterval);
            }

            if (!result.IsFinished)
            {
                result.MarkFailure("StepLimitExceeded", maxStepsPerRun, agent.Position);
            }

            resultWriter.Write(result);
            dungeonFactory.DisposeCurrentDungeon();
        }
    }
}

여기서 Tick()은 입력을 흉내 내는 메서드일 수도 있고 내비게이션 목적지를 갱신하는 메서드일 수도 있다. 중요한 점은 테스트가 화면 해상도나 실제 키 입력에 의존하지 않도록 만드는 것이다. 가능하면 게임 규칙 계층의 명령을 호출하고 별도 테스트에서 입력 처리까지 검증한다.

막힘은 위치가 아니라 진행량으로 판단한다

벽 앞에서 충돌하는 경우만 막힘이 아니다. 에이전트가 두 지점을 왕복하거나 닫힌 문 앞에서 계속 재경로를 계산하는 경우도 있다. 따라서 위치 변화량만 보지 말고 목표까지의 거리 변화도 함께 기록한다.

public bool HasNotMadeProgressFor(float seconds)
{
    if (Time.time - _lastProgressTime < seconds)
    {
        return false;
    }

    var currentDistance = Vector3.Distance(Position, _targetPosition);
    return currentDistance >= _bestDistanceToTarget - 0.1f;
}

private void UpdateProgress()
{
    var currentDistance = Vector3.Distance(Position, _targetPosition);

    if (currentDistance < _bestDistanceToTarget - 0.1f)
    {
        _bestDistanceToTarget = currentDistance;
        _lastProgressTime = Time.time;
    }
}

허용 오차를 두지 않으면 물리 흔들림이나 좁은 복도에서의 미세한 이동을 진전으로 잘못 해석할 수 있다. 반대로 너무 큰 값을 쓰면 정상적인 우회 경로를 막힘으로 판정할 수 있으므로 맵의 타일 크기와 이동 속도에 맞춰 조정해야 한다.

실패 당시의 증거를 남긴다

로그 한 줄만으로는 원인을 찾기 어려울 때가 많다. 실패가 발생하면 다음 정보를 함께 저장하면 조사 시간이 크게 줄어든다.

  • 생성 시드와 맵 버전
  • 에이전트의 최근 위치 목록
  • 현재 목표와 계산된 경로의 코너 목록
  • 주변 충돌체 또는 문 상태
  • 실패 직전 화면 캡처
  • 예외 스택 트레이스

특히 최근 위치 20~50개는 왕복 이동과 진동을 확인하는 데 유용하다. 경로 코너가 비어 있는데 목표가 남아 있다면 경로 탐색 문제일 가능성이 높고 경로는 있는데 움직이지 않는다면 충돌체나 이동 제어 문제를 의심할 수 있다.

실패 시드, 에이전트 위치 이력, 목표 거리 변화, 경로 상태를 함께 보여 주는 자동화 테스트 결과 화면

결과는 성공률보다 실패 유형으로 읽는다

1,000회 중 995회 성공이라는 숫자는 좋아 보일 수 있다. 하지만 실패한 5회가 모두 같은 문 프리팹에서 발생한다면 수정 우선순위는 높다. 반대로 서로 다른 시드에서 한 번씩만 발생했다면 메모리 누수, 초기화 순서, 비결정적 물리 처리처럼 다른 원인을 살펴야 한다.

결과를 집계할 때는 최소한 실패 유형별 횟수와 대표 시드를 정리한다.

실패 유형확인할 값우선 점검 대상
PathNotFound시작점과 출구의 연결 여부맵 생성 그래프, NavMesh
NoProgressTimeout거리 감소와 위치 이력충돌체, 문 상태, 이동 제어
StepLimitExceeded우회 거리와 경로 길이경로 비용, 목표 선택
Exception스택 트레이스초기화, null 참조, 이벤트 해제

동일한 시드에서 항상 재현되는 오류는 먼저 고정한다. 재현되지 않는 오류는 실행 환경, 프레임 시간, 비동기 로딩 완료 순서 같은 추가 데이터를 기록한 뒤 좁혀 가는 편이 낫다.

운영 팁

처음부터 1,000회를 모두 무거운 플레이 모드로 돌릴 필요는 없다. 맵 연결성 검사처럼 렌더링이 필요 없는 검증은 빠른 편집기 테스트로 수만 회 실행하고 이동과 충돌이 필요한 검증만 플레이 모드 또는 CI 빌드에서 반복한다.

또한 무작위 시드만 쓰면 이미 고친 실패를 놓칠 수 있다. 과거에 실패했던 시드 목록을 회귀 테스트 묶음으로 유지하고 그 뒤에 새 무작위 시드를 추가하는 방식이 안정적이다.

자동화 QA 에이전트는 사람 테스트를 대체하지 않는다. 대신 사람이 반복하기 지루하고 재현하기 어려운 조건을 꾸준히 검사한다. 던전 생성 시드, 명확한 실패 기준, 충분한 실패 기록을 갖추면 1,000회 반복은 단순한 횟수가 아니라 맵 안정성을 확인하는 실용적인 안전망이 된다.

#Unity#자동화 QA#게임 테스트#AI 에이전트#C#

계속 읽어보기

이런 글은 어떠세요?

< Back to Logs