개처럼개발한다
[Unity] 방 꾸미기(마이룸) 기능 구현(1) - Mesh 타일 생성 본문
↓안 봐도 되는 사담
예전에 어느 프로젝트를 진행하면서 간단하게 진행했다가 사정상 멈췄던(사실상 끝난..) 방꾸미기 기능을 다른 프로젝트에서 운 좋게 진행 할 수 있게 되었다! 그래서 그 때보다 더 발전된 실력으로 진행해보는 방꾸미기 기능.
근데 방 꾸미기 게임, 흔히 마이룸이라고 하는 류의 게임은 인기가 많은 편인데, 게임 개발 관련해서는 자료가 잘 안나오는 편이다. 왜지?
마이룸에서 타일을 생성하는 부분에 대한 포스팅이다. 중간에 Mesh에 대한 설명도 나온다.
마이룸 꾸미기에서 가장 처음 개발되어야하는 것은 당연 "타일"이다.
그 타일 위에 가구들이 배치, 회전등이 가능해야한다.
그저 오브젝트를 갖다가 놓는 자리에 배치할 수도 있겠지만, 그렇게 되면 이상하게 공중에 가구가 놓여지는 경우나, 여러개가 겹쳐지는, 디테일들을 잡을 수 없기에 가구들은 정확히 차지하는 크기 만큼 타일 위에 놓여야하며, 놓여진 곳에는 못 놓아지게 해야한다. 여기서 중요한 역할을 하는 것이 바로 타일이다.
타일은 클래스를 생성하여 관리하고자 한다.
public class Tile : MonoBehaviour
{
public int x { get; private set; }
public int y { get; private set; }
public bool isBlock = false;
public void Set(int x, int y)
{
this.x = x;
this.y = y;
}
}
각각의 타일 객체들은 좌표를 갖는다(x,y) 그리고 보다 디테일한 처리를 위해 유니티의 Collider기능이 아닌 isBlock이라는 bool 변수만을 두어 이 타일 위에 지금 가구가 놓여졌는지, 즉, 막혔는지 안막혔는지를 체크해줄 것이다.
이 Tile 클래스는 유니티 씬에서 생성되는 타일 오브젝트의 컴포넌트가 된다.
타일 클래스를 만들었다면 이제 이 타일 객체들을 생성시키는 스크립트를 만들어보자.
↓전체 스크립트
public class Tiles : MonoBehaviour
{
private int _width = 5;
private int _length = 5;
private Tile[,] _floorTiles;
private List<GameObject> _tileObject = new List<GameObject>();
private int _autoIncrementF = 1;
//타일 한칸의 대각선 가로와 세로의 크기
private const float _TILE_WIDTH = 0.5f;
private const float _TILE_HEIGHT = 0.25f;
[HideInInspector] public bool isOnTiled = false;
private GameObject _floorGroup;
public void Initialize(int currentWidth, int currentlength)
{
_width = currentWidth;
_length = currentlength;
_floorTiles = new Tile[_width, _length];
_floorGroup = GameObject.Find("FloorTiles");
// 전체적으로 그려진 타일 그리드의 중심 계산
float gridHeight = (_width + _length) * _TILE_HEIGHT / 2;
Vector3 gridOffset = new Vector3(0, -gridHeight / 2, 0);
// generate a grid.
for (int x = 0; x < _width; x++)
{
for (int y = 0; y < _length; y++)
{
_floorTiles[x, y] = GenerateTile(x, y, gridOffset);
}
}
}
private Tile GenerateTile(int x, int y, Vector3 gridOffset)
{
GameObject obj = Manager.Resource.InstantiateSceneResource("Tile", gameObject.transform);
obj.name = "Tile" + _autoIncrementF++;
Mesh mesh = new Mesh();
mesh.name = "tileMesh";
mesh.vertices = new Vector3[]
{
new Vector3(-0.25f, 0, 0),
new Vector3(0, 0.125f, 0),
new Vector3(0.25f, 0, 0),
new Vector3(0, -0.125f, 0)
};
mesh.triangles = new int[] { 0, 1, 2, 0, 2, 3 };
obj.GetComponent<MeshFilter>().mesh = mesh;
obj.GetComponent<MeshCollider>().sharedMesh = mesh;
// 타일의 너비, 높이를 기준으로 위치 조정
float posX = (y - x) * (_TILE_WIDTH / 2);
float posY = (x + y) * (_TILE_HEIGHT / 2);
obj.transform.localPosition = new Vector3(posX, posY, 0) + gridOffset;
obj.transform.SetParent(_floorGroup.transform);
var tile = obj.GetComponent<Tile>();
tile.Set(x, y);
_tileObject.Add(obj);
return tile;
}
public Tile GetTileByCoordinate(Furniture furniture, int x, int y)
{
if(x < 0 || y < 0)
return null;
if (x >= _width || y >= _length){
return null;
return _floorTiles[x, y];
}
public void DestroyTiled()
{
foreach(var tile in _tileObject)
{
Destroy(tile);
}
}
}
Initialize()
public void Initialize(int currentWidth, int currentlength)
{
_width = currentWidth;
_length = currentlength;
_floorTiles = new Tile[_width, _length];
_floorGroup = GameObject.Find("FloorTiles");
// 전체적으로 그려진 타일 그리드의 중심 계산
float gridHeight = (_width + _length) * _TILE_HEIGHT / 2;
Vector3 gridOffset = new Vector3(0, -gridHeight / 2, 0);
// generate a grid.
for (int x = 0; x < _width; x++)
{
for (int y = 0; y < _length; y++)
{
_floorTiles[x, y] = GenerateTile(x, y, gridOffset);
}
}
}
1. 외부에서 받아오는 width, length 크기 정보를 통해 그리드를 생성할 것이다.
2. 바닥 타일 Tile[, ] 이차원 배열 _floorTiles를 width, length 크기만큼 생성
3. 타일 오브젝트가 씬의 가운데에 배치 될 수 있게 gridOffset도 설정해준다. -> girdHeight가 다 그려진 gird의 대각선 세로의 길이이다. girdOffset은 그 세로의 가운데 좌표가 된다.
4. GenerateTile을 이용해 타일 오브젝트를 생성하고, 그 오브젝트의 Tile 객체를 _floorTiles에 저장한다.
GenerateTile()
private Tile GenerateTile(int x, int y, Vector3 gridOffset)
{
GameObject obj = Manager.Resource.InstantiateSceneResource("Tile", gameObject.transform);
obj.name = "Tile" + _autoIncrementF++;
Mesh mesh = new Mesh();
mesh.name = "tileMesh";
mesh.vertices = new Vector3[]
{
new Vector3(-0.25f, 0, 0),
new Vector3(0, 0.125f, 0),
new Vector3(0.25f, 0, 0),
new Vector3(0, -0.125f, 0)
};
mesh.triangles = new int[] { 0, 1, 2, 0, 2, 3 };
obj.GetComponent<MeshFilter>().mesh = mesh;
obj.GetComponent<MeshCollider>().sharedMesh = mesh;
// 타일의 너비, 높이를 기준으로 위치 조정
float posX = (y - x) * (_TILE_WIDTH / 2);
float posY = (x + y) * (_TILE_HEIGHT / 2);
obj.transform.localPosition = new Vector3(posX, posY, 0) + gridOffset;
obj.transform.SetParent(_floorGroup.transform);
var tile = obj.GetComponent<Tile>();
tile.Set(x, y);
_tileObject.Add(obj);
return tile;
}
여기가 오늘의 하이라이트라고 볼 수 있다.
타일은 유니티의 Mesh를 이용해 그려줄 것이다.
유니티에서 mesh와 관련된 설명은 https://docs.unity3d.com/kr/560/Manual/AnatomyofaMesh.html
위 링크에서도 자세히 확인 할 수 있다. 요컨대 3D공간에 삼각형들로 구성된 배열이다.
타일을 생성하고 가구가 배치될 곳의 마우스로 드래그를 하며 옮길 때, 마우스 포인터에 Ray를 그려 RayCast를 통해 충돌이 감지된 타일을 가져와 가구의 위치를 그 타일의 위치로 옮겨지는 것처럼 보이게 할 생각인데 이때를 Mesh Collider가 필요하다.
- Mesh
mesh는 3D 그래픽을 다루는 데 사용되는 클래스이다. mesh 객체는 정점(vertices), 삼각형 면(triangles), 텍스처 좌표 등을 포함하는 그래픽 데이터를 나타낸다. 여기서 Mesh Collider 컴포넌트를 붙이게 되면, Mesh가 그려진형태 대로 Mesh Collider가 생성된다. Mesh Collider 이외에도 다른 여러 Collider가 존재하지만, Mesh Collider 이외의 콜라이더는 내가 원하는대로 모양을 동적으로 생성하지 못한다.
Mesh를 통해 타일을 그리고자 한 이유는 보다 더 정확한 타일 처리를 하기 위해서이다.
내가 만들 방의 타일은 다이아몬드 형태로 2.5D라고도 불리는 Isometric 스타일로 표현할 생각이다. 단순 평면 사각형이나 원이 아닌 기울어진 타일이거나 비대칭, 다이아몬드와 같은 형태라면 BoxCollider와 같은 단순한 콜라이더는 정확한 판정이 어려울 것이라 판단했다. 또한, 나중에 업데이트 되면서 충분히 Z축도 건들게 되는 날이 올 것 같다고 생각 되어, 3D 공간에서 가장 적합한 Collider인 Mesh Collider를 선택한 것이다.
Mesh mesh = new Mesh();
mesh.vertices = new Vector3[] { new Vector3(-2, 0, 0), new Vector3(0, 1, 0), new Vector3(2, 0, 0), new Vector3(0, -1, 0) };
mesh.triangles = new int[] { 1, 2, 3, 0, 1, 3 };
이게 내가 생성할 타일의 기본 Mesh형태이다. 프로젝트 요구에 따라 크기가 변경되었지만, 기본적으로 크기로 하자면 위와 같다.
이 부분은 Mesh 클래스로 객체를 생성하고, 정점(verticles)과 삼각형(triangles) 모양을 정의해주고 있다. 좌표상에서 각각 (-2, 0) (0, 1) (2, 0) (0, -1) 부분에 정점을 만들고 이 정점을 1->2->3 (삼각형 1개) 0->1->3 (삼각형 나머지) 이렇게 시계방향으로 면을 만들어주겠다는 말이다.



허접한 그림판 이지만 이런 느낌이다! 참고로 meshRenderer는 시계방향으로 그리지 않으면 모습이 안나타난다고 한다. 주의 해두자 meshRenderer는 사각형, 사다리꼴, 심지어 원도 가능하긴 하다. 단 삼각형 모양을 여러개 붙여 만들어진 모양이다.
obj.GetComponent<MeshFilter>().mesh = mesh;
obj.GetComponent<MeshCollider>().sharedMesh = mesh;
// 타일의 너비, 높이를 기준으로 위치 조정
float posX = (y - x) * (_TILE_WIDTH / 2);
float posY = (x + y) * (_TILE_HEIGHT / 2);
obj.transform.localPosition = new Vector3(posX, posY, 0) + gridOffset;
obj.transform.SetParent(_floorGroup.transform);
var tile = obj.GetComponent<Tile>();
tile.Set(x, y);
_tileObject.Add(obj);
return tile;
이제 생성한 게임 오브젝트에 이 mesh를 할당해주면 된다. MeshFilter 컴포넌트를 붙여 mesh에 할당해주면 되는데, 나의 경우는 프리팹으로 미리 만들어진 오브젝트를 Instantiate할 것이라 이미 MeshFilter가 붙어있기에 GetComponent로 가져와주었다. 다음 중요한 MeshCollider를 붙여주고, 타일의 위치와 씬 계층에서의 배치 등 디테일한 작업들도 완성해주면 된다.
맨 처음 만든 Tile 컴포넌트도 붙여 좌표까지 생성해주면 타일 그리기는 끝!
포스팅 하다가 든 생각인데, 배경 없는 Sprite를 통해 PolygonCollider2D를 이용하는 방법도 가능할 것 같다는 생각이 들었다. 게다가 Mesh를 그린 후 MeshCollider를 붙이는 것보다 속도 등의 성능 측면에서도 더 효율적이라고 생각한다.
물론 3D공간에서 Z축까지 신경쓰게 되는 일이 온다면 Mesh가 맞는 거지만, 현재로써는 예정이 없기 때문에 sprite에 PolygonCollider2D로,, 수정을 해야하나 말아야하나 고민이 된다.
'Unity > 게임개발' 카테고리의 다른 글
[Unity2D] 캐릭터를 따라다니는 카메라 구현 (0) | 2024.08.25 |
---|