우선 본문에 담긴 육각 타일 수학은 대부분 다음 링크의 사이트에서 참조하였습니다.
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;
}
}'C#,Unity' 카테고리의 다른 글
| Unity UI - 깔끔한 사이즈 변경을 도와주는 Image Type (0) | 2024.05.06 |
|---|---|
| Unity UI - 이미지 영역을 제한해주는 Mask (0) | 2024.05.04 |
| Unity UI - Content Size Fitter 응용: 텍스트 정보 창 만들기 (0) | 2024.05.03 |
| Unity UI - 부드러운 전환을 도와주는 AnimationCurve (0) | 2024.05.03 |
| Unity UI - 투명도와 가리기를 도와주는 CanvasGroup (0) | 2024.05.03 |