레벨 에디터, 도트 툴, 밸런스 스프레드시트. 게임 하나를 만들다 보면 Unity 밖에서 데이터를 뱉어내는 외부 도구가 점점 늘어난다. 문제는 이 도구들이 내놓는 json, tmx, 커스텀 바이너리 파일을 Unity가 곧바로 쓸 수 있는 형태로 들고 있지 않다는 것이다. 매번 에디터 메뉴를 눌러 변환 스크립트를 돌리거나, 누군가 원본 파일을 고칠 때마다 손으로 재변환하는 식이면 금세 사고가 난다. "그 레벨 데이터 최신 버전 맞아?"라는 질문이 나오는 순간 이미 늦었다.
이 문제를 가장 깔끔하게 푸는 Unity 기능이 ScriptedImporter다. 특정 확장자를 가진 파일을 프로젝트에 넣는 순간, Unity가 알아서 그 파일을 네이티브 에셋(ScriptableObject, Mesh, Texture 등)으로 변환해 준다. import 한 번이 곧 변환 파이프라인 전체가 되는 셈이다. 원본을 고치면 자동으로 다시 변환되고, 결과물은 AssetDatabase가 캐싱한다.
그런데 ScriptedImporter는 강력한 만큼 보일러플레이트가 많고, AssetImportContext를 다루는 규칙도 까다롭다. 그래서 이번 글에서는 임포터 작성 자체를 AI 코딩 에이전트(Claude Code, Cursor 등)에게 맡기는 워크플로우를 정리한다. 포맷 스펙만 정확히 주면 임포터 코드와 검증 테스트까지 한 번에 뽑아낼 수 있다.
ScriptedImporter는 AssetPostprocessor와 무엇이 다른가
둘을 헷갈리면 자동화 설계가 처음부터 어긋난다. AssetPostprocessor는 Unity가 이미 인식하는 에셋(png, fbx, wav 등)의 import 과정을 가로채 압축 포맷이나 리그 설정 같은 옵션을 조정한다. 즉 "Unity가 아는 파일의 설정을 손보는" 도구다.
반면 ScriptedImporter는 Unity가 전혀 모르는 확장자를 통째로 새 에셋 타입으로 정의한다. leveldata, dialogue, stats 같은 임의의 확장자를 직접 만들고, 그 안의 내용을 게임 데이터로 변환하는 책임을 진다. 핵심 동사가 "변환"이라면 답은 거의 항상 ScriptedImporter다.
AI 에이전트에게 줄 명세
AI에게 임포터를 맡길 때 결과 품질은 명세의 정확도에 비례한다. 막연하게 "JSON을 ScriptableObject로 바꿔줘"라고 하면 의존성 처리나 예외 처리가 빠진 코드가 나온다. 다음 네 가지를 반드시 못 박아 준다.
역할: Unity 6 에셋 파이프라인 엔지니어.
목표: 확장자 .leveldata (UTF-8 JSON)를 LevelData
ScriptableObject로 변환하는 ScriptedImporter 작성.
입력 샘플:
{ "levelName":"stage_01", "width":16, "height":12, "tiles":[0,1,1,0] }
대상 타입: LevelData { string levelName; Vector2Int size; int[] tiles; }
요구사항:
1. UnityEditor.AssetImporters.ScriptedImporter 상속.
2. 파싱 실패 시 ctx.LogImportError 호출 후 안전 종료(예외 throw 금지).
3. ctx.AddObjectToAsset + ctx.SetMainObject 로 메인 에셋 등록.
4. EditMode 테스트 동봉: 샘플 파일을 임시 경로에 쓰고
AssetDatabase.ImportAsset 후 필드 값 검증.
요구사항 2번과 3번이 특히 중요하다. 임포트 중 예외를 그냥 던지면 에디터 콘솔이 빨갛게 도배되고, SetMainObject를 빠뜨리면 프로젝트 창에서 에셋이 회색 빈 아이콘으로 뜬다. 사람이 자주 까먹는 부분일수록 명세에 박아 두어야 AI도 챙긴다.
생성된 임포터 코드
위 명세로 받은 결과물의 핵심이다. JsonUtility로 역직렬화한 뒤 ScriptableObject 인스턴스를 만들어 컨텍스트에 등록하는 흐름이 그대로 보인다.
using System.IO;
using UnityEngine;
using UnityEditor.AssetImporters; // Unity 2020.2+ 네임스페이스
[System.Serializable]
public class LevelDataJson
{
public string levelName;
public int width;
public int height;
public int[] tiles;
}
[ScriptedImporter(version: 2, ext: "leveldata")]
public class LevelDataImporter : ScriptedImporter
{
public override void OnImportAsset(AssetImportContext ctx)
{
LevelDataJson parsed;
try
{
parsed = JsonUtility.FromJson<LevelDataJson>(
File.ReadAllText(ctx.assetPath));
}
catch (System.Exception e)
{
ctx.LogImportError($"leveldata 파싱 실패: {e.Message}");
return;
}
var asset = ScriptableObject.CreateInstance<LevelData>();
asset.levelName = parsed.levelName;
asset.size = new Vector2Int(parsed.width, parsed.height);
asset.tiles = parsed.tiles;
ctx.AddObjectToAsset("level", asset);
ctx.SetMainObject(asset);
}
}
LevelData는 평범한 ScriptableObject이고 CreateAssetMenu 어트리뷰트가 필요 없다. 에셋을 만드는 주체가 메뉴가 아니라 임포터이기 때문이다. version 인자를 2로 올린 점도 눈여겨보자. 임포트 로직을 바꿨을 때 이 숫자를 올리면 Unity가 기존 에셋을 강제로 재임포트한다.
변환을 믿으려면 테스트가 필요하다
자동 변환의 무서운 점은 조용히 틀린다는 것이다. 필드 하나를 잘못 매핑해도 컴파일은 멀쩡히 되고, 게임을 한참 돌려봐야 "왜 타일이 90도 돌아가 있지"를 깨닫는다. 그래서 임포터와 테스트는 한 묶음으로 받아야 한다.
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using System.IO;
public class LevelDataImporterTests
{
const string Dir = "Assets/__ImportTest";
const string Path = Dir + "/sample.leveldata";
[SetUp]
public void Setup()
{
Directory.CreateDirectory(Dir);
File.WriteAllText(Path,
"{\"levelName\":\"stage_01\",\"width\":16,\"height\":12,\"tiles\":[0,1,1,0]}");
AssetDatabase.ImportAsset(Path,
ImportAssetOptions.ForceSynchronousImport);
}
[Test]
public void Imports_AsLevelData_WithCorrectSize()
{
var asset = AssetDatabase.LoadAssetAtPath<LevelData>(Path);
Assert.IsNotNull(asset);
Assert.AreEqual("stage_01", asset.levelName);
Assert.AreEqual(new Vector2Int(16, 12), asset.size);
Assert.AreEqual(4, asset.tiles.Length);
}
[TearDown]
public void Cleanup() => AssetDatabase.DeleteAsset(Dir);
}
이 테스트가 초록불이면, 원본 포맷이 바뀌어 임포터가 깨지는 순간 CI가 바로 잡아준다. AI가 만든 변환 코드를 믿을 수 있는 근거가 여기서 나온다.
AI 출력에서 반드시 손봐야 하는 곳
에이전트 결과물을 그대로 머지하기 전에 다음 네 가지는 사람이 직접 확인한다. AI가 가장 자주 빠뜨리는 함정들이다.
첫째, 외부 의존성이 있으면 ctx.DependsOnSourceAsset(otherPath)를 호출해야 한다. 예를 들어 타일셋 텍스처를 참조하는 레벨이라면, 그 텍스처가 바뀌었을 때 레벨도 재임포트되도록 의존성을 명시해야 캐시가 꼬이지 않는다.
둘째, SetMainObject 누락 여부를 본다. 빠지면 에셋이 프로젝트 창에서 열리지 않는다. 셋째, 깨진 입력에 대한 예외 처리가 throw가 아니라 LogImportError로 되어 있는지 확인한다. 넷째, 로직을 수정할 때마다 version 숫자를 올렸는지 본다. 이걸 안 올리면 코드는 새것인데 에셋은 옛날 변환 결과를 그대로 들고 있는 유령 버그가 생긴다.
정리
ScriptedImporter는 외부 도구의 포맷을 게임 데이터로 바꾸는 작업을 import 한 번으로 압축해 준다. 보일러플레이트가 많다는 단점은 AI 코딩 에이전트로 상쇄할 수 있는데, 핵심은 명세의 정확도다. 입력 샘플, 대상 타입, 예외 처리 규칙, 검증 테스트를 명세에 못 박으면 임포터 한 벌을 통째로 뽑아낼 수 있다. 마지막으로 의존성 선언, SetMainObject, LogImportError, version 갱신 이 네 가지만 사람이 검수하면, 외부 포맷이 바뀌어도 프로젝트에 떨어뜨리기만 하면 알아서 변환되는 안정적인 파이프라인이 완성된다.
'AI 보조 게임 개발' 카테고리의 다른 글
| AI 에이전트로 Unity BatchMode 점검 봇 만들기: 씬 품질 리포트를 매일 자동 생성하기 (1) | 2026.07.03 |
|---|---|
| AI에게 Unity 성능 실험 루프 맡기기: 로그 수집부터 패치 후보까지 (0) | 2026.07.01 |
| 게임 코드에 AI 리뷰어 붙이기: Update 루프의 GC 할당을 매 커밋 자동으로 잡는 워크플로우 (0) | 2026.06.24 |
| AI 에이전트에게 세이브 데이터 마이그레이션 맡기기: 버전 변환 코드와 회귀 테스트를 한 번에 (0) | 2026.06.19 |
| Unity 에디터를 Claude Code에 연결하기: MCP 브리지로 AI가 씬을 직접 읽고 고치게 만들기 (0) | 2026.06.17 |