Unity Tilemap

2022년 06월 30일
제작기간 2022년 06월 30일
태그 Unity

이번에 플랫포머 게임을 만들면서 맵을 제작할 때, Tilemap을 사용했다. 팔레트를 이용해서 타일을 그리는 기능과, 맵 전체의 타일에 대한 세부 조정이 굉장히 간편하다 여겨져서 타일맵을 사용하기로 했다.

그러나 모든 맵을 게임 오브젝트 단위로 관리하는 것은 불편하다고 여겨서 맵 데이터를 저장하고 로드하여 그려주는 식으로 맵 시스템을 만들고 싶었다. 그를 위한 스크립트 몇 개를 포스팅하려고 한다.

세팅

Tilemap은 2D 타일 어셋을 관리하는 컴포넌트다. 타일맵을 생성하면 Grid라는 이름의 부모오브젝트 하위에 Tilemap 오브젝트가 생성된다. Grid 컴포넌트는 지정된 레이아웃에 맞게 하위 타일등의 오브젝트의 위치를 변환하고 정렬하는 역할을 한다.

플랫포머의 맵이기 때문에 Collider 세팅이 필요하다. Tilemap Collider 2D는 Tilemap의 타일들 모양에 맞게 Collider를 그려준다. 런타임에 타일이 변경될 경우, LateUpdate를 통해 업데이트된다. Tilemap Collider 2D에도 Used By Composite 이라는 값이 있다. 해당 값을 true로 변경하면 타일별로 나눠진 콜라이더를 하나의 콜라이더로 통합해준다.

스크립트를 통한 타일 배치

타일맵의 데이터를 json으로 관리하기 위해 타일맵 데이터 Writer와 맵 Generator를 구현했다. 데이터 작성은 JsonUtility를 사용했기 때문에 데이터 구조만 간단히 소개한다.

{
    "mapId": "000",
    "tileData": [
        {
            "id": "tileid",
            "x": -10,
            "y": -5
        }
    ]
}

맵이 하나가 아니기 때문에 별도로 맵 id 지정이 필요했고, 타일 데이터는 타일을 그리기 위한 최소한의 정보를 담게 했다. 여기서 tileData의 id는 타일 오브젝트의 이름이다.

Tile Palette

타일 팔레트는 Grid와 Tilemap을 가지고 있는 프리팹이고, 유니티 에디터의 Tile Palette 윈도우에서 간편하게 편집할 수 있다. 스크립트를 통해 타일맵에서 타일을 가져와야하기 때문에 타일 팔레트 오브젝트에 타일을 서치하는 컴포넌트를 추가했다.

public class TilePalette : MonoBehaviour
{
    [SerializeField]
    private Tilemap _tilemap;

    public TileBase GetTile(string id)
    {
        var tiles = _tilemap.GetTilesBlock(_tilemap.cellBounds);
        for (int i = 0; i < tiles.Length; i++)
        {
            if (tiles[i].name.Equals(id))
            {
                return tiles[i] as TileBase;
            }
        }
        // return default
        return tiles[0] as TileBase;
    }
}

팔레트의 타일맵에서 전체 범위의 모든 타일을 가져와 타일 이름을 서치한다. 모든 타일 오브젝트는 한 폴더에서 관리하고 있어서 이름은 유니크한 아이디가 된다.

Tilemap 생성

public class TilemapGenerator : MonoBehaviour
{
    [SerializeField]
    private Tilemap _tilemap;
    [SerializeField]
    private TilePalette _palette;

    public void GenerateMap(string mapId)
    {
        TilemapData mapData = MapStaticData.GetMapData(mapId);
        _tilemap.ClearAllTiles();

        foreach (var data in mapData.TileData)
        {
            var tile = _pallette.GetTile(data.Id);
            _tilemap.SetTile(data.Position, tile);
            if (tile is CustomTile)
            {
                // 커스텀 타일의 경우 지정된 오브젝트를 사용하기 위해 기존 타일의 색을 지움
                _tilemap.SetColor(data.Position, Color.clear);
            }
        }
    }

    public void ClearMap()
    {
        _tilemap.ClearAllTiles();
    }
}

[CustomEditor(typeof(TilemapGenerator))]
public class TilemapGeneratorrEditor : Editor
{
    private string _mapId;
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        _mapId = GUILayout.TextField(_mapId);
        if (GUILayout.Button("Generate map"))
        {
            if (string.IsNullOrEmpty(_mapId))
            {
                Debug.LogError("Map ID를 입력하고 생성해주세요.");
                return;
            }
            (target as TilemapGenerator).GenerateMap(_mapId);
        }
        if (GUILayout.Button("Clear map"))
        {
            (target as TilemapGenerator).ClearMap();
        }
    }
}

TilemapData는 가장 처음에 언급한 맵 데이터를 가지고 있는 클래스다. 맵 데이터 인스턴스를 가져와서 타일을 전부 세팅한다. 맵 생성 확인은 플레이 모드 외에서도 자주 할 예정이라 커스텀 에디터를 붙였다.

처음에는 tile.color을 변경하는 방식으로 커스텀 타일 스프라이트의 색을 지웠으나, 이 방식은 타일의 색을 영구적으로 바꾸기 때문에 맵을 그리고 확인하는 과정이 번거로워졌다. 조금 더 찾아본 결과, 타일맵의 SetColor을 통해 해당 위치의 타일 색을 변경할 수 있음을 알았다. 단, 타일 세팅에서 FlagsLock Color로 되어있는 경우, 초기 색으로 Lock이 걸리기 때문에 다른 플래그로 변경해줄 필요가 있다.

Tilemap 데이터 저장

foreach (var pos in _tileMap.cellBounds.allPositionsWithin)
{
    var tile = _tileMap.GetTile(pos);
    if (tile != null)
    {
        tileDataList.Add(new TileData(tile.name, pos));
    }
}

포지션을 통해 타일을 가져오는 방식으로 데이터를 가져왔다.

타일 맵 그리기

붉은색이 시작 지점, 하얀 점이 골이나 중간지점 등을 의미하는 깃발이다.

커스텀 타일

발판 타일만 있는 것은 아니기 때문에, 게임 오브젝트로 생성되어야하는 타일도 있었다. 깃발(골, 중간지점…)과 시작 지점이 이런 타일에 해당한다. 따로 위치를 지정하는 방법도 있지만, 타일을 그리며 함께 그리는 것이 이상적이라 여겨서 타일로 취급하기로 했다.

타일에는 Instanced Game Object를 지정할 수 있다. 타일이 배치되면서 해당 오브젝트를 타일 위치에 생성하는데, 이를 이용하여 깃발 등의 오브젝트를 배치했다.

Instanced Game Object는 Debug 모드로 봐야 인스펙터창에서 접근할 수 있다. 번거롭기 때문에 Tile을 상속받는 클래스를 만들어 커스텀 타일을 만들었다.

Tile은 Scriptable Object이기 때문에 상속받은 클래스들은 생성 menu가 따로 필요하다.

[CreateAssetMenu(fileName = "FlagTile", menuName = "Tiles/FlagTile", order = 0)]
public class FlagTile : CustomTile
{
    // ...
}

깃발 타일은 위와 같이 만들었다. 커스텀 타일들은 모두 공통적으로 필요한 기능이 있지만, 다르게 작동하는 기능도 필요하기에 CustomTile을 상속받게 했다. CreateAssetMenu 어트리뷰트는 Assets/Create 메뉴에 menuName에 해당되는 계층을 만들어 오브젝트를 생성할 수 있게 해준다.

후기

간만에 플랫포머를 만지게 되어서 너무 재밌다! 타일맵이 정말 생소해서 시간이 좀 걸리기는 했지만 어느정도 틀은 나왔고 금방 게임을 보여줄 수 있을 것 같다. 어서 소개하고 싶다. 두근두근👍

게임