본문 바로가기

C#,Unity

육각 타일 관련 함수

우선 본문에 담긴 육각 타일 수학은 대부분 다음 링크의 사이트에서 참조하였습니다.

 

https://www.redblobgames.com/grids/hexagons/

 

Red Blob Games: Hexagonal Grids

Amit's guide to math, algorithms, and code for hexagonal grids in games

www.redblobgames.com

해당 사이트에 담긴 내용을 읽으면 큰 무리 없이 육각타일 함수를 만들 수 있을 것이라 생각합니다.

 

목표까지 경로 잇기

public enum HexDir { TopRight,Right,BottomRight,BottomLeft,Left,TopLeft}

 

본 게임 개발 초에는 위와 같은 기능이 없었습니다.

 

절차적 생성 육각 지도를 만드는 데에만 집중하여 제대로 된 이동이 없어 그냥 직진으로 움직일 뿐이었습니다.

 

위 Enum HexDir은 절차적 생성 용도로 0,1,2,3,4,5를 좀 더 직관적으로 사용하기 위한 열거형입니다.

 

  public Vector2Int GetNextCoor(Vector2Int coor,HexDir dir,bool limitgrid)
  {
    int  _y=coor.y%2;
    Vector2Int _mod = Vector2Int.zero;
    if ( _y.Equals(0))
    {
      switch (dir)
      {
        case HexDir.TopRight:
          _mod = new Vector2Int(0, 1);break;
        case HexDir.Right:
          _mod = new Vector2Int(1, 0); break;
        case HexDir.BottomRight:
          _mod = new Vector2Int(0, -1); break;
        case HexDir.BottomLeft:
          _mod = new Vector2Int(-1, -1); break;
        case HexDir.Left:
          _mod = new Vector2Int(-1,0); break;
        case HexDir.TopLeft:
          _mod = new Vector2Int(-1, 1); break;
      }
    }
    else
    {
      switch (dir)
      {
        case HexDir.TopRight:
          _mod = new Vector2Int(1, 1); break;
        case HexDir.Right:
          _mod = new Vector2Int(1, 0); break;
        case HexDir.BottomRight:
          _mod = new Vector2Int(1, -1); break;
        case HexDir.BottomLeft:
          _mod = new Vector2Int(0, -1); break;
        case HexDir.Left:
          _mod = new Vector2Int(-1, 0); break;
        case HexDir.TopLeft:
          _mod = new Vector2Int(0, 1); break;
      }
    }
    Vector2Int _temp = coor + _mod;
    if (!limitgrid) return _temp;

    if (_temp.x < 0) _temp.x = 0;
    if(_temp.y < 0) _temp.y = 0;
    if (_temp.x >= ConstValues.MapSize) _temp.x = ConstValues.MapSize - 1;
    if(_temp.y>=ConstValues.MapSize)_temp.y=ConstValues.MapSize - 1;
    return _temp;
  }
  public Vector2Int GetNextCoor(TileData tile, HexDir dir,bool limitgrid)
  {
    return GetNextCoor(tile.Coordinate, dir, limitgrid);
  }
  public TileData GetNextTile(Vector2Int coor,HexDir dir)
  {
    var _coor=GetNextCoor(coor,dir,true);
    return TileDatas[_coor.x,_coor.y];
  }
  public TileData GetNextTile(TileData tile, HexDir dir)
  {
    var _coor = GetNextCoor(tile.Coordinate, dir,true);
    return TileDatas[_coor.x, _coor.y];
  }

당시 절차적 육각 지도 생성에 필요한 기능은 그게 다였기 때문에, 육각 타일의 좌표는 (x,y) 2차원 좌표로 만족한 상태였습니다.

 

그러나 퀄리티를 높이기 위해 육각 타일 이동 등 기능을 추가하기 위해 제대로 된 육각타일 수학이 필요하게 되었습니다.

 

public class HexGrid
{
  public int q = 0, r = 0, s = 0;
//이하생략

위 사이트에서 배운 축 3개를 사용하는 육각 타일 좌표법입니다.

이상해 보여도 확실합니다.

 

여기서 특징은, q+r+s=0이 항상 유지된다는 점입니다.

위 타일 이미지에서도 확인할 수 있습니다.

  public int GetDistance(HexGrid starthex)
  {
    HexGrid _newhex = new HexGrid(q, r, s) - starthex;
    return Mathf.Max(Mathf.Abs(_newhex.q), Mathf.Abs(_newhex.r), Mathf.Abs(_newhex.s));
  }

 

이 점을 활용하면, 육각 타일끼리의 거리를 손쉽게 도출할수 있습니다.

'두 타일 좌표의 차 좌표에서 가장 높은 절댓값'이 거리입니다.

 

Vector2int -> Hexgrid 변환은 다음과 같습니다.

  public HexGrid(Vector2Int vector2)
  {
    q = vector2.x - (vector2.y - (vector2.y & 1)) / 2;
    r = vector2.y;
    s = -q - r;
  }

육각 좌표를 2차원 좌표로 계산하는데 가장 힘든 이유는, y축이 올라갈수록 지그재그로 배치되어 있는 형태 때문입니다.

 

우선 두 육각 타일 좌표 사이의 경로를 반환하는 함수는 다음과 같습니다.

  public List<HexDir> GetDirectRoute(TileData starttile)
  {
    HexGrid _current = new HexGrid(q-starttile.HexGrid.q,r - starttile.HexGrid.r, s - starttile.HexGrid.s);
    return _current.GetDirectRoute();
  }
  public List<HexDir> GetDirectRoute(HexGrid starthex)
  {
    HexGrid _current = new HexGrid(q- starthex.q, r- starthex.r, s- starthex.s);
    return _current.GetDirectRoute();
  }

A->B가 있으면, B.GetDirectRoute(A) 이렇게 List<HexDir>을 받아옵니다.

 

'타일 리스트'가 아닌 '방향 리스트'를 받아옵니다.

 

그 이유는 다음과 같은데, '방향 리스트'를 활용해 '타일 리스트'를 산출하는 것은 쉽지만, 그 반대는 좀 귀찮기 때문입니다.

 

지도 UI에서는 '타일 리스트' 외에 '방향 리스트' 필드를 활용하는 메소드가 잦아서 이 같은 반환 타입을 결정했습니다.

 

 
 private List<HexDir> GetDirectRoute()
  {
    List<HexDir> _list = new List<HexDir>();
    //Hexdir로 반환하는 이유는, 본 게임의 코드에서는 HexDir을 사용하는 함수가 더 많기 때문입니다.
    //위 GIF에서 나오는 화살표 배치라던지...
    HexGrid _current = new HexGrid(q, r, s);
    //그리고 이 메소드가 실행되는 상황은 '이미 B-A가 진행된 상황'입니다.
    //거리의 차는 구했으니 조각조각 인수분해를 해야 합니다.
    HexGrid _temp = new HexGrid();
    int _value = 100;
    //q+r+s=0이라는 것을 기억하셔야 합니다. 즉 만약 타일이 1칸이라도 움직였다면
    //1+(-1)+0, 2칸을 움직였다면 2+(-1)+(-1) 혹은 역의 꼴이 이루어집니다.
    //q,r,s중 |2| 이상의 값이 있다면 (2,-1,-1)(-1,2,-1)(-1,-1,2)를 모조리 제거해나갑니다.
    //2칸 묶음을 하나하나 제거해나가다 보면 (0,0,0) 혹은 (1,-1,0)의 찌꺼기가 남을 것입니다.
    int _sign = 0;
    HexGrid _dirgrid = new HexGrid();

    while (_value != 0)
    {
      _value = _current.q / 2;
      if (_value != 0)
      {
        _sign = (int)Mathf.Sign(_value);
        for (int i = 0; i < Mathf.Abs(_value); i++)
        {
          _dirgrid.q = 2 * _sign;
          _dirgrid.r = -1 * _sign;
          _dirgrid.s = -1 * _sign;

          _current -= _dirgrid;

          foreach (var _hex in GetDir(_dirgrid))
            _list.Add(_hex);
        }
      }
      _value = _current.r / 2;
      if (_value != 0)
      {
        _sign = (int)Mathf.Sign(_value);
        for (int i = 0; i < Mathf.Abs(_value); i++)
        {
          _dirgrid.q = -1 * _sign;
          _dirgrid.r = 2 * _sign;
          _dirgrid.s = -1 * _sign;

          _current -= _dirgrid;

          foreach (var _hex in GetDir(_dirgrid))
            _list.Add(_hex);
        }

        _value = _current.s / 2;
        if (_value != 0)
        {
          _sign = (int)Mathf.Sign(_value);
          for (int i = 0; i < Mathf.Abs(_value); i++)
          {
            _dirgrid.q = -1 * _sign;
            _dirgrid.r = -1 * _sign;
            _dirgrid.s = 2 * _sign;

            _current -= _dirgrid;

            foreach (var _hex in GetDir(_dirgrid))
              _list.Add(_hex);
          }
        }
      }
    }

 

아래 코드는 인수분해가 끝난 HexDir 목록을 좀 더 깨끗하게 중화시키는 과정입니다.

 

예를 들어 연속으로 (↗)+(↘)의 순서를 가지고 있다면, (→) 하나로 바꿀 수 있지 않습니까?

 

그림을 잘 그리는 편은 아닙니다.

이러한 단순화 과정을 거쳐서 최소 루트를 반환합니다.

 

    foreach (var hex in GetDir(_current))
    {
      _list.Add(hex);
    }
    bool _isoverlap = true;
    while (_isoverlap == true)
    {
      _isoverlap = false;

      if (_list.Contains((HexDir)0) && _list.Contains((HexDir)3))
      {
        _list.Remove((HexDir)0);
        _list.Remove((HexDir)3);
        _isoverlap = true;
      }
      if (_list.Contains((HexDir)1) && _list.Contains((HexDir)4))
      {
        _list.Remove((HexDir)1);
        _list.Remove((HexDir)4);
        _isoverlap = true;
      }
      if (_list.Contains((HexDir)2) && _list.Contains((HexDir)5))
      {
        _list.Remove((HexDir)2);
        _list.Remove((HexDir)5);
        _isoverlap = true;
      }

      if (_list.Contains((HexDir)1) && _list.Contains((HexDir)5))
      {
        _list.Remove((HexDir)1);
        _list.Remove((HexDir)5);

        _list.Add((HexDir)0);
        _isoverlap = true;
      }
      if (_list.Contains((HexDir)0) && _list.Contains((HexDir)2))
      {
        _list.Remove((HexDir)0);
        _list.Remove((HexDir)2);

        _list.Add((HexDir)1);
        _isoverlap = true;
      }
      if (_list.Contains((HexDir)1) && _list.Contains((HexDir)3))
      {
        _list.Remove((HexDir)1);
        _list.Remove((HexDir)3);

        _list.Add((HexDir)2);
        _isoverlap = true;
      }
      if (_list.Contains((HexDir)2) && _list.Contains((HexDir)4))
      {
        _list.Remove((HexDir)2);
        _list.Remove((HexDir)4);

        _list.Add((HexDir)3);
        _isoverlap = true;
      }
      if (_list.Contains((HexDir)3) && _list.Contains((HexDir)5))
      {
        _list.Remove((HexDir)3);
        _list.Remove((HexDir)5);

        _list.Add((HexDir)4);
        _isoverlap = true;
      }
      if (_list.Contains((HexDir)0) && _list.Contains((HexDir)4))
      {
        _list.Remove((HexDir)0);
        _list.Remove((HexDir)4);

        _list.Add((HexDir)5);
        _isoverlap = true;
      }
    }

    return _list;

    //최소단위 받아서 계산
    List<HexDir> GetDir(HexGrid grid)
    {
      List<HexDir> _temp = new List<HexDir>();

      if (grid.q == 2) { _temp.Add((HexDir)1); _temp.Add((HexDir)2); }
      else if (grid.q == -2) { _temp.Add((HexDir)4); _temp.Add((HexDir)5); }
      else if (grid.r == 2) { _temp.Add((HexDir)0); _temp.Add((HexDir)5); }
      else if (grid.r == -2) { _temp.Add((HexDir)2); _temp.Add((HexDir)3); }
      else if (grid.s == 2) { _temp.Add((HexDir)3); _temp.Add((HexDir)4); }
      else if (grid.s == -2) { _temp.Add((HexDir)0); _temp.Add((HexDir)1); }
      else if (grid.q == 0 && grid.r == 1 && grid.s == -1) _temp.Add((HexDir)0);
      else if (grid.q == 1 && grid.r == 0 && grid.s == -1) _temp.Add((HexDir)1);
      else if (grid.q == 1 && grid.r == -1 && grid.s == 0) _temp.Add((HexDir)2);
      else if (grid.q == 0 && grid.r == -1 && grid.s == 1) _temp.Add((HexDir)3);
      else if (grid.q == -1 && grid.r == 0 && grid.s == 1) _temp.Add((HexDir)4);
      else if (grid.q == -1 && grid.r == 1 && grid.s == 0) _temp.Add((HexDir)5);

      return _temp;
    }
  }