[Unity] 3D 구조물 건축기능 구현하기

Lumos Velog·2025년 6월 4일

목표

  • Muck 게임의 건축 시스템

생존/샌드박스 장르 게임에서 자주 볼 수 있는 건축 시스템Muck 게임을 참조하여 고려한 방법들에 대해 정리한다.



과정

개요

  • 미리보기(Preview) 오브젝트를 통해 설치 위치 시각화
  • 설치 가능 여부 판단 후 실제 오브젝트 설치
  • 구조물 간 스냅(Snap) 기능을 통한 정렬된 배치

기본 설치 로직

건축 오브젝트는 실제 설치 오브젝트(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);
    }

이 구조만으로도 단순한 설치 기능은 충분히 가능하다.



스냅 포인트 기반 배치

SnapPoint 찾기

설치된 구조물에 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 축 구분

단순한 거리 비교이기에 이미 설치된 구조물의 스냅포인트를 기준으로 설치할 구조물의 스냅포인트를 구했을때 이런 경우가 생길 수 있다.
그래서 각 SnapPoint는 수직/수평 축을 기준으로 구분된다.


public class BuildingSnapPoint : MonoBehaviour
{
    public enum SnapAxis
    {
        Vertical,
        Horizontal
    }

    public SnapAxis Axis => axis;
    
    [SerializeField] SnapAxis axis;
}


SnapPoint 기준 정렬

같은 축에 있는 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 에 대해 빠르게 이해 할 수 있었다.

0개의 댓글