- Muck 게임의 건축 시스템
생존/샌드박스 장르 게임에서 자주 볼 수 있는 건축 시스템을 Muck 게임을 참조하여 고려한 방법들에 대해 정리한다.
건축 오브젝트는 실제 설치 오브젝트(BuildingObj)와 프리뷰용(PreviewObj)으로 구성되며, Ray를 통해 플레이어가 바라보는 지점에 배치된다.

//BuildingObject.cs
public void Init()
{
buildingObj.SetActive(false);
previewObj.SetActive(true);
}
public void UpdateToBuildingState(bool isBuildable)
=> previewObj.SetActive(isBuildable);
public void Built()
{
buildingObj.SetActive(true);
}
//BuildingSystem.cs
public void UpdateBuildingObject()
{
if (_buildingObject == null)
{
return;
}
_isBuildable = false;
Ray ray = _camera.ScreenPointToRay(new Vector2(Screen.width / 2f, Screen.height / 2f));
if (Physics.Raycast(ray, out RaycastHit hit, RayDistance))
{
_buildingObject.transform.position = hit.point;
_isBuildable = true;
}
_buildingObject.UpdateToBuildingState(_isBuildable);
}

이 구조만으로도 단순한 설치 기능은 충분히 가능하다.
설치된 구조물에 Ray가 닿았을 때, 가장 가까운 SnapPoint를 찾는다.

//BuildingObject.cs
public BuildingSnapPoint GetSnapPointClosestHit(Vector3 hitPoint)
{
BuildingSnapPoint snapPoint = null;
float tempDist = float.MaxValue;
foreach (var item in snapPoints)
{
float compareDist = Vector3.Distance(item.transform.position, hitPoint);
if (compareDist < tempDist)
{
tempDist = compareDist;
snapPoint = item;
}
}
return snapPoint;
}

단순한 거리 비교이기에 이미 설치된 구조물의 스냅포인트를 기준으로 설치할 구조물의 스냅포인트를 구했을때 이런 경우가 생길 수 있다.
그래서 각 SnapPoint는 수직/수평 축을 기준으로 구분된다.
public class BuildingSnapPoint : MonoBehaviour
{
public enum SnapAxis
{
Vertical,
Horizontal
}
public SnapAxis Axis => axis;
[SerializeField] SnapAxis axis;
}
같은 축에 있는 SnapPoint끼리 비교해 가장 멀리 떨어진 쪽을 기준으로 스냅한다.
//BuildingObject.cs
public BuildingSnapPoint GetSnapPointClosestTargetPoint(BuildingSnapPoint targetPoint)
{
BuildingSnapPoint tempSnapPoint = null;
float tempDist = float.MinValue;
foreach (var item in snapPoints)
{
if (item.Axis == targetPoint.Axis)
{
float compareDist = Vector3.Distance(item.transform.localPosition, targetPoint.transform.localPosition);
if (compareDist > tempDist)
{
tempDist = compareDist;
tempSnapPoint = item;
}
}
}
return tempSnapPoint;
}
설치 방향에 따라 스냅 방향을 보정하기 위해 Ray의 방향 벡터를 활용한다.
//BuildingObject.cs
if (item.Axis == BuildingSnapPoint.SnapAxis.Vertical)
{
if (lookDir.y > 0 && item.transform.localPosition.y < 0 ||
lookDir.y < 0 && item.transform.localPosition.y > 0)
continue;
}
//BuildingSystem.cs
public void UpdateBuildingObject()
{
if (_buildingObject == null)
{
return;
}
_isBuildable = false;
Ray ray = _camera.ScreenPointToRay(new Vector2(Screen.width / 2f, Screen.height / 2f));
if (Physics.Raycast(ray, out RaycastHit hit, RayDistance))
{
_buildingObject.transform.position = hit.point;
if (hit.rigidbody != null)
{
Snap(hit, ray.direction.normalized);
}
_isBuildable = true;
}
_buildingObject.UpdateToBuildingState(_isBuildable);
}
void Snap(RaycastHit hit, Vector3 rayDirNormalized)
{
if (hit.rigidbody.TryGetComponent(out BuildingObject targetObject))
{
if (targetObject.IsSnappable && _buildingObject.IsSnappable)
{
BuildingSnapPoint targetSnapPoint = targetObject.GetSnapPointClosestHit(hit.point);
BuildingSnapPoint curSnapPoint
= _buildingObject.GetSnapPointClosestTargetPoint(targetSnapPoint, rayDirNormalized);
if (curSnapPoint != null)
{
Vector3 offset = targetSnapPoint.transform.position - curSnapPoint.transform.position;
_buildingObject.transform.position += offset;
}
}
}
}

Ray 방향 벡터를 활용해 플레이어 위치에 따른 스냅 방향을 정교하게 조절할 수 있다.
정확도를 높이기 위해 단순 거리 비교 외에도 방향성을 고려한 논리가 필요했는데 구현 과정 속에서 Ray 와 아직 익숙치 않은 3D 의 Vector 에 대해 빠르게 이해 할 수 있었다.