본문 바로가기
📒개발일지

[📒개발일지] 1. Unity Rule tile(룰 타일)을 자동 로딩해보자

by 김굴치 2025. 4. 17.

유니티에서 룰타일 에셋을 불러오려면

  1. 이미지 파일 로드
  2. 임포트 세팅 (ppu, compression 등)
  3. Sprite editor를 이용한 slicing
  4. Rule 설정

이런 과정이 필요한데요. 이 과정이 생각보다 매우 귀찮습니다. 게다가 저는 룰 종류가 47가지라 override rule tile을 사용한다손 처도 손으로 이걸 하는건 너무 귀찮겠죠. 그래서 타일에셋 불러오기를 자동화 해보는게 오늘 목표입니다.

미리보기

※ 룰타일/타일맵 기초


1. 메뉴추가

커스텀 메뉴를 추가하는 방법은 다음과 같습니다

public class RuleTileBuilder
{
	[MenuItem("Tools/Build HamRuleTile from PNG")]
	public static void BuildHamRuleTile()
	{
		...
	}
}

이렇게 하면 상단 메뉴바에 "Tools->Build HamRuleTile From PNG" 메뉴가 생기고
메뉴를 선택하면 BuildHamRuleTile 함수가 실행됩니다.

2. 파일 선택

이제 유저에게 파일 선택창을 보여주고 선택한 파일의 경로를 읽어와야겠죠?

    string path = EditorUtility.OpenFilePanel("Select PNG", "", "png");
    string fileName = Path.GetFileNameWithoutExtension(path);
    if (string.IsNullOrEmpty(path))
        return;
    string assetPath = FileUtil.GetProjectRelativePath(path);

이렇게 하면 .png 형식의 파일 선택할 수 있는 익스플로러 창이 나오고 선택한 파일의 경로를 얻어옵니다.
또 저희는 Unity AssetImporter을 사용해 텍스처를 로드해야 하니 asset path 상대경로로 변환도 해줬습니다.

3. 텍스처 로드

저희에게 필요한건 타일의 스프라이트입니다. 여기서 스프라이트란 텍스처의 특정 영역(대부분 직사각형)을 나타내는 에셋을 말합니다(유니티에서 그렇습니다). 그러니 텍스처를 먼저 로드해야겠네요.

    TextureImporter textureImporter = AssetImporter.GetAtPath(assetPath) as TextureImporter;
    if (textureImporter == null)
    {
        Debug.LogError("Selected file is not a valid texture.");
        return;
    }
    textureImporter.spriteImportMode = SpriteImportMode.Multiple;
    textureImporter.isReadable = true;
    textureImporter.textureType = TextureImporterType.Sprite;
    textureImporter.spritePixelsPerUnit = 16;
    textureImporter.filterMode = FilterMode.Point;
    textureImporter.textureCompression = TextureImporterCompression.Uncompressed;
    AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);

    Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);

텍스처 임포터를 만들고 임포트 세팅을 해줍니다.
여기서 임포트 세팅은 유니티 에디터에서 이미지를 선택하면 Inspector창에 나오는 설정들을 말합니다.

원랜 손으로 하나하나 바꿔야 돼서 매우 귀찮다

4. 스프라이트 슬라이싱

이제 텍스처가 있으니 스프라이트를 만들 수 있겠네요.
기존에 유니티 에디터에서 스프라이트를 만드는 방법은 스프라이트 에디터를 이용하는겁니다. 위의 인스펙터 창 중간에 있는 "Open Sprite Editor"를 누르면 스프라이트 에디터 창이 나옵니다.

"Slice"메뉴에서 텍스처를 여러 스프라이트로 쪼개면 스프라이트 에셋이 생성됩니다.

이 일련의 과정을 자동화 해봅시다.

    // slice to tiles
    int textureWidth = texture.width;
    int textureHeight = texture.height;
    int tileWidth = 16;
    int tileHeight = 16;

    var factory = new SpriteDataProviderFactories();
    factory.Init();
    var dataProvider = factory.GetSpriteEditorDataProviderFromObject(texture);
    dataProvider.InitSpriteEditorDataProvider();

    var spriteRects = new List<SpriteRect>();
    for (int y = 0; y < textureHeight; y += tileHeight)
    {
        for (int x = 0; x < textureWidth; x += tileWidth)
        {
            SpriteRect spriteData = new SpriteRect
            {
                name = fileName + "_" + "(" + (y / tileHeight) * (textureWidth / tileWidth) + ","+ (x / tileWidth) + ")",
                spriteID = GUID.Generate(),
                rect = new Rect(x, y, tileWidth, tileHeight),
            };
            spriteRects.Add(spriteData);
        }
    }
    dataProvider.SetSpriteRects(spriteRects.ToArray());
    dataProvider.Apply();
    // refresh the asset database
    var assetImporter = dataProvider.targetObject as AssetImporter;
    assetImporter.SaveAndReimport();
    AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);

저는 유니티6를 사용중인데 옛날 버전에선을 다른방법(SpriteMetaData)을 사용합니다.

아까 로드한 텍스처에 대한 Data provider를 할당하고, 스프라이트 정보를 담은 SpriteRect를 채워 데이터 프로바이더에 공급하면 스프라이트가 생성됩니다. 저희가 해야할건 스프라이트 정보를 담고있는 SpriteRect를 채우는거겠네요.
SpriteData 정의를 보면 프로퍼티가 꽤 많은데, 저희에게 가장 중요한건 스프라이트가 나타내는 텍스처 영역을 정의하는 rect프로퍼티 입니다. 제가 만든 룰타일에 경우 항상 같은 포맷으로 정의 돼 있기 때문에 같은 크기의 grid로 자르도록 했습니다.
텍스처 포맷에 따라 스프라이트를 만드는 로직(ex. 잘 사용하지 않는 특정 색상(마젠타)으로 스프라이트를 경계 표시해두는 방법)을 추가하는 것도 재밌겠네요.
 
새롭게 만든 스프라이트들이 적용될 수 있도록 에셋데이터베이스를 refresh 하는것도 잊지 마세용.
 
이제 스프라이트들이 에셋데이터베이스에 저장됐습니다. 그럼 다시 로드해 와야겠죠?

    // laod sprites
    Object[] assets = AssetDatabase.LoadAllAssetsAtPath(assetPath);

    List<Sprite> sprites = new List<Sprite>();
    foreach (var asset in assets)
    {
        if (asset is Sprite sprite)
        {
            sprites.Add(sprite);
        }
    }

    if (sprites.Count == 0)
    {
        Debug.LogError("No sprites found in PNG.");
        return;
    }

드디어 룰타일을 만들 준비가 끝났습니다.

5. 룰타일 생성

룰타일을 생성하는 코드는 생각보다 간단합니다. RuleTile 오브젝트를 할당해 룰 정보를 채워주기만 하면 됩니다.

    // build rule tile
    RuleTile ruleTile = ScriptableObject.CreateInstance<RuleTile>();
    ruleTile.m_DefaultSprite = sprites[0];

    int THIS = 1;
    int EMTPY = 2;
    for (int i = 0; i < RULE_LIST.GetLength(0); i++)
    {
        // define rules
        RuleTile.TilingRule rule = new RuleTile.TilingRule();
        rule.m_Sprites = new Sprite[] { sprites[i] };
        rule.m_NeighborPositions = new List<Vector3Int>();
        rule.m_Neighbors = new List<int>();
        for (int j = 0; j < RULE_LIST.GetLength(1); j++)
        {
            switch (RULE_LIST[i, j])
            {
                case 0: // empty
                    rule.m_NeighborPositions.Add(RULE_POSITIONS[j]);
                    rule.m_Neighbors.Add(EMTPY);
                    break;
                case 1: // this
                    rule.m_NeighborPositions.Add(RULE_POSITIONS[j]);
                    rule.m_Neighbors.Add(THIS);
                    break;
                case 2: // any
                    break;
                default:
                    break;
            }
        }

        ruleTile.m_TilingRules.Add(rule);
    }
    // save rule tile
    string savePath = $"Assets/_MAIN/Resources/TileGraphic/{fileName}.asset";
    AssetDatabase.CreateAsset(ruleTile, savePath);
    AssetDatabase.SaveAssets();
    AssetDatabase.Refresh();

이제 익숙하실거 같습니다. 새로운 부분이라면 TilingRule을 RULE_LIST와 RULE_POSITIONS 배열을 이용해 설정하는 부분이겠네요.
타일링 룰은 이웃위치(List<Vector3Int> NeighborPosition)와 이웃(List<int> Neighbor)으로 정의됩니다.

만약 이런 룰이 있다면
이웃위치 : [ (-1,0,0) , (1,0,0) , (0,-1,0) , (0,1,0) ] 이웃 : [ 1 , 2 , 1 , 1 ] ※(1 : empty 2 : filled) 이런식입니다.
저는 주변 8개의 위치에 대해 룰을 정의할거라 RULE_POSITION 배열을 이렇게 만들었습니다.

static Vector3Int[] RULE_POSITIONS = new Vector3Int[]
{
	new(-1, 1, 0),
	new(0, 1, 0),
	new(1, 1, 0),
	new(-1, 0, 0),
	new(1, 0, 0),
	new(-1, -1, 0),
	new(0, -1, 0),
	new(1, -1, 0)
};

RULE_LIST는 그냥 노가다입니다. 타일셋 이미지에 위치한 타일 순서대로 룰을 정의하면 됩니다.

static int[,] RULE_LIST = new int[,]
{
	// 0 : empty 1 : filled 2 : any
	// row 0							
	{ 0,0,0, 0,0, 0,0,0 }, // (0, 0) 
	{ 2,0,2, 0,1, 2,0,2 }, // (0, 1) 
	{ 2,0,2, 1,1, 2,0,2 }, // (0, 2) 
	{ 2,0,2, 1,0, 2,0,2 }, // (0, 3) 
    
	...
    
	{ 2,0,2, 0,1, 2,1,1 }, // (3, 8)
	{ 0,1,0, 1,1, 1,1,1 }, // (3, 9)
	{ 2,0,2, 1,1, 1,1,1 }, // (3,10)
	{ 2,0,2, 1,0, 1,1,2 }, // (3,11)
};

any라는건 "empty든 filled든 상관 없다" 입니다. 이웃위치와 이웃을 추가안하면 됩니다.

인덱스는 이미지의 왼쪽 아래부터 시작합니다. 룰의 개수가 적다면 직접 쓰는것도 괜찮지만 저처럼 룰이 많다면 유니티 에디터에서 Rule tile을 하나 만들고 그 룰타일의 neighbor정보를 RULE_LIST에 저장하면 쉽게 리스트를 만들 수 있습니다.


자, 이제 빌더로 만든 룰타일이 제대로 동작하는지 확인만 해보면 되겠네요.
단순한 룰타일의 경우는 룰타일 Inspector에 표시되는 프리뷰로 확인이 가능하지만, 저는 룰이 복잡해 프리뷰가 모든 경우의 수를 커버하지 못하기 때문에 직접 그려보았습니다.

잘되는듯

더 다양한 경우에도 잘 동작하는지 확인해야하니 더 넓은 지형에 적용해봅시다.

잘된다

다른 종류의 타일끼리(돌-흙)도 룰이 잘 적용되는게 보이실텐데요. 유니티 기본 RuleTile을 사용하면 원래 서로 다른 타일은 인식 못해서 이어지지 않습니다. 나중에 이 기능도 일지에 추가해 보겠습니다.


오늘은 룰타일 불러오기 기능을 구현해보았습니다. 앞으로도 소개할만한 재밌는 기능이 있으면 일지에 추가해보겠습니다.
감사합니다.