자동화 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[실패 유형 집계]
먼저 정할 것: 성공 조건과 실패 조건
테스트가 모호하면 결과도 모호해진다. 예를 들어 출구가 있는 던전이라면 성공 조건은 단순히 좌표가 움직였다는 사실이 아니라 제한 시간 안에 출구 트리거를 통과한 것으로 둔다.
실패는 가능한 한 구체적으로 나눈다.
- 경로 없음: 생성된 그래프에서 시작점과 출구가 연결되지 않았다.
- 이동 정체: 일정 시간 동안 목표까지의 거리가 줄지 않는다.
- 상태 전이 오류: 문을 열었지만 내비게이션 경로가 갱신되지 않는다.
- 예외 또는 비정상 종료: 테스트 도중 예외가 발생하거나 씬이 예상치 않게 다시 로드된다.
- 시간 초과: 경로는 존재하지만 제한 스텝 안에 목표에 도달하지 못한다.
이 분류는 실패 건수를 집계하고 조사 순서를 정하는 데 사용한다. 경로 없음은 생성 로직 문제일 수 있고 이동 정체는 내비게이션이나 충돌 설정 문제일 수 있지만, 실패 분류만으로 원인이나 담당 팀을 확정하지 않는다.

탐색 경로와 정체 후보를 표현한 설명용 이미지다. 실제 Unity 실행 결과나 공식 디버그 화면을 입증하는 자료는 아니다.
맵 생성 시드는 반드시 기록한다
절차적 생성 던전은 시드를 저장하지 않으면 실패를 다시 만들기 어렵다. 실패 보고서에는 최소한 다음 정보를 포함하는 편이 좋다.
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에서 반복 테스트 루프 만들기
아래는 Unity 2022.3 API를 기준으로 한 반복 루프 예시다. DungeonFactory, DungeonTestAgent, DungeonTestResult, TestResultWriter는 프로젝트에서 구현할 타입이며 Unity 기본 API가 아니다. Build는 생성·내비게이션 준비가 완료된 맵을 반환하고, Initialize는 이동 상태와 진행량 관측을 초기화하며, HasReachedExit는 출구 통과 결과를 보존한다고 가정한다. 비동기 준비가 필요한 프로젝트는 준비 완료 대기를 추가해야 한다.
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()
{
if (totalRuns <= 0 || maxStepsPerRun <= 0 ||
float.IsNaN(stepInterval) || float.IsInfinity(stepInterval) || stepInterval <= 0f ||
dungeonFactory == null || agent == null || resultWriter == null)
{
Debug.LogError("Invalid soak-test configuration.");
yield break;
}
// 맵 생성기가 UnityEngine.Random 상태를 바꿔도 시드 선택에 영향이 없게 한다.
var seedSource = new System.Random(184923);
for (var runIndex = 0; runIndex < totalRuns; runIndex++)
{
var seed = seedSource.Next();
var result = new DungeonTestResult(runIndex + 1, seed);
try
{
var dungeon = dungeonFactory.Build(seed);
agent.Initialize(dungeon.Start, dungeon.Exit);
for (var step = 0; step < maxStepsPerRun; step++)
{
if (!agent.HasReachedExit) agent.Tick();
if (agent.HasReachedExit)
{
result.MarkSuccess(step + 1);
break;
}
if (agent.HasNotMadeProgressFor(3f))
{
result.MarkFailure("NoProgressTimeout", step + 1, agent.Position);
break;
}
yield return new WaitForSecondsRealtime(stepInterval);
}
if (!result.IsFinished)
{
result.MarkFailure("StepLimitExceeded", maxStepsPerRun, agent.Position);
}
resultWriter.Write(result);
}
finally
{
dungeonFactory.DisposeCurrentDungeon();
}
}
}
}
시드 선택용 System.Random은 게임의 전역 난수 상태와 분리한다. 다만 런타임 버전이 달라지면 같은 생성자 시드로 같은 수열이 나온다고 가정하지 말고 실제 선택한 맵 시드 목록을 저장한다. Microsoft Random 설명
Tick()은 이동 명령이나 목적지 갱신을 수행하는 프로젝트 메서드다. 이 예는 규칙 기반 에이전트 루프이며 학습 모델을 구현하지 않는다. 입력·화면 해상도를 통과하는 테스트가 필요하면 별도 시나리오로 추가한다. 기록하는 스텝 수는 반복 루프의 판정 횟수다. WaitForSecondsRealtime은 스케일되지 않은 시간으로 대기하지만 실제 게임 이동은 프로젝트의 시간 설정을 따른다. 따라서 멈춤 상태를 시험하지 않는다면 timeScale 등도 실행 조건으로 고정해야 한다. Unity 대기 API
이 루프는 경로 단절을 직접 검사하거나 예외를 실패 결과로 변환하지 않는다. 생성·이동·기록 중 예외는 테스트를 중단시키므로 외부 실행기가 실패 로그와 프로세스 종료 코드를 보존해야 한다. DisposeCurrentDungeon은 생성 실패 후에도 호출할 수 있어야 한다. 종료·코루틴 취소 때의 정리도 실제 Unity 생명주기에 맞춰 구현하고 검증한다.
막힘은 위치가 아니라 진행량으로 판단한다
벽 앞에서 충돌하는 경우만 막힘이 아니다. 에이전트가 두 지점을 왕복하거나 닫힌 문 앞에서 계속 재경로를 계산하는 경우도 있다. 따라서 위치 변화량만 보지 말고 목표까지의 거리 변화도 함께 기록한다.
// DungeonTestAgent 내부에 넣을 관측 코드.
// Position과 _targetPosition은 에이전트의 현재 위치와 목표 위치다.
private float _bestDistanceToTarget;
private double _lastProgressTime;
private void ResetProgressObserver()
{
_bestDistanceToTarget = Vector3.Distance(Position, _targetPosition);
_lastProgressTime = Time.realtimeSinceStartupAsDouble;
}
public bool HasNotMadeProgressFor(float seconds)
{
if (float.IsNaN(seconds) || float.IsInfinity(seconds) || seconds <= 0f)
{
throw new System.ArgumentOutOfRangeException(nameof(seconds));
}
UpdateProgress();
return Time.realtimeSinceStartupAsDouble - _lastProgressTime >= seconds;
}
private void UpdateProgress()
{
var currentDistance = Vector3.Distance(Position, _targetPosition);
if (currentDistance < _bestDistanceToTarget - 0.1f)
{
_bestDistanceToTarget = currentDistance;
_lastProgressTime = Time.realtimeSinceStartupAsDouble;
}
}
Initialize에서 위치와 목표를 설정한 뒤 ResetProgressObserver를 호출해야 한다. 목표가 바뀌어도 관측을 초기화한다. 위치·목표는 유한한 좌표라는 전제이며 잘못된 좌표나 이동 실패는 별도 오류로 처리해야 한다. 경과 시간에는 게임 시간 배율의 영향을 받지 않는 Time.realtimeSinceStartupAsDouble을 사용한다.
0.1은 프로젝트 거리 단위의 예시이며 물리 흔들림을 진전으로 세지 않기 위한 기준이다. 이 코드는 출구까지의 직선거리 최솟값이 줄어드는지만 본다. 정상적인 우회·문 열기 대기·새 경로 탐색도 정체 후보로 분류할 수 있으므로, 경로를 따라 남은 거리와 상호작용 상태를 함께 조사하고 기준을 조정한다. NoProgressTimeout은 막힘의 확정 진단이 아니다.
실패 당시의 증거를 남긴다
실패 원인을 조사할 수 있도록 다음 정보를 함께 저장하는 방안을 검토한다. 이 정보가 조사 시간을 얼마나 줄이는지는 측정하지 않았다.
- 생성 시드와 맵 버전
- 에이전트의 최근 위치 목록
- 현재 목표와 계산된 경로의 코너 목록
- 주변 충돌체 또는 문 상태
- 실패 직전 화면 캡처
- 예외 스택 트레이스
최근 위치 20–50개는 버퍼 크기의 예시다. 샘플 간격과 보존 시간도 지정해야 움직임을 해석할 수 있다. 경로가 비었을 때는 요청 대기·갱신 시점·탐색 실패를 구분하고, 경로가 있는데 움직이지 않을 때는 충돌체·이동 제어·게임 상태를 함께 점검한다.

진단 항목을 배치한 설명용 이미지이며 실제 1,000회 실행 기록이나 측정된 성능 수치를 제시하는 화면은 아니다.
결과는 성공률보다 실패 유형으로 읽는다
가령 1,000회 중 995회 성공했다면 관측 성공 비율은 99.5%다. 이것이 모든 맵의 성공 확률이나 남은 오류 수를 뜻하지는 않는다. 실패한 5회가 같은 문 프리팹과 관련된다면 공통 조건을 조사하고, 서로 다른 시드여도 공통 원인이 없는지 살펴본다. 시드가 다르다는 이유만으로 메모리 누수나 비결정적 물리 처리가 원인이라고 추론할 수는 없다.
결과를 집계할 때는 최소한 실패 유형별 횟수와 대표 시드를 정리한다.
| 실패 유형 | 확인할 값 | 우선 점검 대상 |
|---|---|---|
| PathNotFound | 시작점과 출구의 연결 여부 | 맵 생성 그래프, NavMesh |
| NoProgressTimeout | 거리 감소와 위치 이력 | 충돌체, 문 상태, 이동 제어 |
| StepLimitExceeded | 우회 거리와 경로 길이 | 경로 비용, 목표 선택 |
| Exception | 스택 트레이스 | 초기화, null 참조, 이벤트 해제 |
동일한 시드에서 항상 재현되는 오류는 먼저 고정한다. 재현되지 않는 오류는 실행 환경, 프레임 시간, 비동기 로딩 완료 순서 같은 추가 데이터를 기록한 뒤 좁혀 가는 편이 낫다.
운영 팁
검증 대상을 나눠 실행 비용을 비교한다. 순수 그래프 연결성은 렌더링 없이 검사하는 방안을 검토하고, 실제 이동·충돌·동적 내비게이션은 해당 런타임 조건에서 반복한다. 편집기에서 연결된 그래프라고 판정해도 실제 플레이 경로의 통과 가능성까지 입증하는 것은 아니다.
과거에 실패했던 시드와 조건을 회귀 테스트 묶음으로 유지하고 새 무작위 시드를 추가한다. 회귀 테스트를 통과해도 다른 조건에 오류가 없다는 보장은 없다.
이 자동화 루프는 정한 성공·실패 기준과 실행 조건만 검사한다. 던전 생성 시드, 명확한 판정 기준, 실패 기록을 갖춰 반복 실행 결과를 재검토할 수 있게 하고, 사람의 탐색·조작 경험은 별도 테스트로 확인한다.



