유니티 부트캠프 8기/Ch05. Unity 게임 개발 숙련

Grid 기반의 건축 시스템 4. pivot을 잡고 원래대로 되돌리기, 회전연산

Imperor 2025. 3. 19. 03:21

 

    /// <summary>
    /// gridPosition은 preview 오브젝트의 Grid 좌표의 기준점(0도 회전 기준 왼쪽 아래)으로 마우스 커서가 있는 그리드의 좌표이다
    /// objectSize는 preview오브젝트의 크기
    /// angle으로 넘어오는건 TempInputManager.angle으로 회전각
    /// </summary>
    /// <param name="gridPosition"></param>
    /// <param name="objectSize"></param>
    /// <returns></returns>
    public bool CanPlaceObjAt(Vector3Int gridPosition, Vector2Int objectSize, float angle)
    {
        /// gridPosition는 그리드의 좌표(월드좌표 아니다)
        List<Vector3Int> positionToOccupy = CalculatePositions(gridPosition, objectSize);
        foreach (var pos in positionToOccupy)   /// pos는 Vector3Int값으로 그리드의 좌표
        {
            /// 해당 그리드 좌표에 다른 오브젝트 점유하고 있는 영역이 있는지 확인한다★
            /// placedObject는 PlacedObjectData를 저장하고있으며
            /// PlacedObjectData는 
            /// PlacedObject가 생성된 GameObject를 저장하고 있다
            /// 기존에는 pos를 대입하면 해당 pos가 있는 게임오브젝트를 리턴하기 때문에
            /// 수정한 다음에도 GameObject를 가져와야한다
            if (placedObjects.ContainsKey(pos))
            {
                // 기존 오브젝트 데이터 가져오기
                PlacementData existingData = placedObjects[pos];

                // 기존 오브젝트의 회전값과 현재 회전값의 차이 계산
                float angleDiff = (existingData.angle - angle) % 360;

                // 기존 오브젝트의 기준점 (첫 번째 좌표 사용)
                //Vector3 pivot = existingData.occupiedPositions[0];
                /// Vector3Int로 변환
                //Vector3Int pivot = Vector3Int.RoundToInt(existingData.occupiedPositions[0]);
                Vector3Int pivot = Vector3Int.RoundToInt(existingData.occupiedPositions[0]);
                /// pivot이 (0,0,0) 이 안나오는데?
                /// pivot이 (0,0,0) 불변이므로 그냥 대입하고 시작하는게 나을 수 있다

                // 회전 차이를 적용하여 기존 오브젝트의 좌표를 변환
                Vector3Int rotatedPos = RotatePosition(pos, pivot, angleDiff);

                // 변환된 좌표가 현재 배치하려는 위치와 충돌하는지 확인
                if (positionToOccupy.Contains(rotatedPos))
                {
                    return false; // 충돌 발생 → 배치 불가능
                }
                return false;
            }
        }
        return true;
    }

 

빨간 부분의 그리드 좌표가 어떻게 되는지 알아보니

(2, 0, 1), (2, 0, 2)가 되었다

 

그리드 좌표가 다음처럼 나왔다

해석하자

이는 (2, 0, 1) 까지는 월드기반의 그리드 좌표계로 이동하고 (검은색)

그 내부에서는 로컬 기반의 좌표계로 이동하는 것을 알 수 있다 (노란색)

 

시계방향으로 90도 회전하기 전에 (2, 0, 1), (2, 0, 2)에는 브릿지를 건설했으므로 건물이 있는게 맞지만

이것이 시계방향으로 90도 회전한 후에도 (2, 0, 1), (2, 0, 2) 에 있는 것처럼 보이니 문제다

따라서 (2, 0, 1), (2, 0, 2)를 -90도 회전시킨 다음 내려놓는 곳과 다시 비교해야 제대로 된 비교를 할 수 있다

 

-90도가 나왔으므로 (2,0,1) 기준으로 -90도씩 돌려야 바뀐 좌표계에서 바른 좌표가 된다

 

회전중심인 pivot이 (2,0,1)로 바르게 나왔다!

 

그런데 첫번째 격자의 그리드 좌표가 (2,0,1)이다

이제 회전했을때 붙는 격자가 회전이 안되는 이유를 알았다

회전축과 같으니까 회전한 결과가 자신이 되는것이다

그러니 이 격자는 무조건 다른 오브젝트가 있다고 표시되는것이다

 

pivot을 옮겨서 만든다

 

(2.5, 0, 2)인 이유를 알아보자

로컬좌표를 따라가기 때문에 중심좌표가 그렇게 되는것이다

이걸 기준으로 -90도 회전하면

(2,0,2) 가 된다고 하는데, 맞는지 살펴본다

pos에 들어있는 값은 (2,0,1)

pivot은 (2.5, 0, 2)

angleDif은 -90도니까 반시계로 90도 회전한다 

 

스샷은 생략하지만... 좌표가 이상한곳으로가는데??

아니다.. 정리하고 다시 생각해보자

 

현재 격자의 좌표는 그리드의 왼쪽 아래 정점을 기준으로 하고 있다

이걸 x와 z방향으로 0.5f씩 더한다

로컬좌표계이므로 위치관계는 회전상태와 상관없이 똑같다

0.5f 씩 더하면 보기에도 직관적이며(격자의 중점이니까)

90도의 몇배수를 회전하든 오차가 생기지 않는다는 큰 장점이 있다

 

주의할 것은 -90도 회전 연산을 해야하는데 각도가 float니까 오차가 존재

 

회전할때는 pivot을 원점으로 평행이동하고 회전한 뒤에 다시 pivot만큼 평행이동한다

이제 만들어보자

 

CanPlaceObjAt 메서드에서 

RotatePosition에서 얻은 좌표를 가지고 겹치는 곳이 있는지 판단한다

    /// <summary>
    /// gridPosition은 preview 오브젝트의 Grid 좌표의 기준점(0도 회전 기준 왼쪽 아래)으로 마우스 커서가 있는 그리드의 좌표이다
    /// objectSize는 preview오브젝트의 크기
    /// angle으로 넘어오는건 TempInputManager.angle으로 회전각
    /// </summary>
    /// <param name="gridPosition"></param>
    /// <param name="objectSize"></param>
    /// <returns></returns>
    public bool CanPlaceObjAt(Vector3Int gridPosition, Vector2Int objectSize, float angle)
    {
        /// gridPosition는 그리드의 좌표(월드좌표 아니다)
        List<Vector3Int> positionToOccupy = CalculatePositions(gridPosition, objectSize);
        foreach (var pos in positionToOccupy)   /// pos는 Vector3Int값으로 그리드의 좌표
        {
            /// 해당 그리드 좌표에 다른 오브젝트 점유하고 있는 영역이 있는지 확인한다★
            /// placedObject는 PlacedObjectData를 저장하고있으며
            /// PlacedObjectData는 
            /// PlacedObject가 생성된 GameObject를 저장하고 있다
            /// 기존에는 pos를 대입하면 해당 pos가 있는 게임오브젝트를 리턴하기 때문에
            /// 수정한 다음에도 GameObject를 가져와야한다
            if (placedObjects.ContainsKey(pos))
            {
                // 기존 오브젝트 데이터 가져오기
                PlacementData existingData = placedObjects[pos];

                // 기존 오브젝트의 회전값과 현재 회전값의 차이 계산
                float angleDiff = (existingData.angle - angle) % 360;

                // pivot을 기존 occupiedPositions[0]을 기준으로 설정
                Vector3Int pivot = Vector3Int.RoundToInt(existingData.occupiedPositions[0]);

                Vector3 originPos = new Vector3(pos.x, pos.y, pos.z);
                // RotatePosition에서는 pos의 x, z를 0.5씩 더하여 중심을 맞춘 후 회전 적용
                Vector3Int rotatedPos = RotatePosition(originPos, pivot, angleDiff);

                // 변환된 좌표가 현재 배치하려는 위치와 충돌하는지 확인
                if (positionToOccupy.Contains(rotatedPos))
                {
                    return false; // 충돌 발생 → 배치 불가능
                }
            }
        }
        return true;
    }

 

 

    /// <summary>
    /// 매개변수로 들어오는 Vector3Int 는 grid의 좌표
    /// 회전 차이를 적용하여 기존 pos를 변환(축의 회전각을 맞춘다면 실제로 있어야 할 자리는 어디인지 구하기 위해)
    /// </summary>
    /// <param name="pos"></param>
    /// <param name="pivot"></param>
    /// <param name="rotationDiff"></param>
    /// <returns></returns>
    private Vector3Int RotatePosition(Vector3 pos, Vector3 pivot, float angleDiff)
    {
        // 중심을 보정하여 로컬 좌표 변환
        // RotatePosition에서는 pos의 x, z를 0.5씩 더하여 중심을 맞춘 후 회전 적용
        Vector3 adjustedPos = new Vector3(pos.x + 0.5f, pos.y, pos.z + 0.5f);
        Vector3 localPos = adjustedPos - pivot;

        // 회전 행렬 계산 (Y축 기준)
        float rad = Mathf.Deg2Rad * angleDiff;
        float cos = Mathf.Cos(rad);
        float sin = Mathf.Sin(rad);

        /// 90도 단위 회전이므로, 부동소수점 오차를 처리
        if (Mathf.Abs(cos) < 1e-6f) cos = 0f;
        if (Mathf.Abs(sin) < 1e-6f) sin = 0f;

        if (Mathf.Abs(cos - 1f) < 1e-6f) cos = 1f;
        if (Mathf.Abs(sin - 1f) < 1e-6f) sin = 1f;
        if (Mathf.Abs(cos + 1f) < 1e-6f) cos = -1f;
        if (Mathf.Abs(sin + 1f) < 1e-6f) sin = -1f;

        float rotatedX = cos * localPos.x + sin * localPos.z;
        float rotatedZ = -sin * localPos.x + cos * localPos.z;

        // 다시 월드 좌표로 변환하고, 그리드 기준으로 정수 변환
        Vector3 newPos = pivot + new Vector3(rotatedX, localPos.y, rotatedZ);

        return new Vector3Int(
            Mathf.FloorToInt(newPos.x),  // RoundToInt 대신 FloorToInt 사용
            Mathf.RoundToInt(newPos.y),
            Mathf.FloorToInt(newPos.z)
        );
    }

 

회전해도 놓을 수 있다!!

 


하지만

 

새로운 문제가...

이젠 원래 있던 곳에 겹쳐지는 문제가 있다

 

positionToOccupy에는 내가 들고 있는 preview오브젝트와 걸쳐 있는 그리드 좌표들이 있다 

(2,0,2), (2,0,3)이 들어있다

현재 회전축을 90도 회전한 상태이다

 

placedObjects에는 이미 이곳에 놓여 있는 오브젝트와 걸쳐 있는 그리드 좌표들이 있다

(2,0,1), (2,0,2)이 들어 있다

회전축을 0도 회전한 상태이다

 

 

(2,0,2)가 있지만 이걸 같은 좌표로 인식하면 안된다. 좌표축이 다르다

실제로 겹치는 부분은 positionToOccupy의 (2,0,2) 그리드와 placedObjects의 (2,0,1) 그리드이다

 

이전 경우처럼 비어있는 부분을 90도 회전해야 하는 경우는 아니다

이를 구분해야한다

 

자세히 보니

이전 문제는

회전축이 같다

 

이번 문제는

회전축이 다르다 

 

즉, 회전축이 같으면 회전을 해서 원래 위치를 비교하고 

겹치는 그리드의 회전축이 다르면 그대로 처리한다

 

그리드가 겹치려면 회전축이 무조건 달라야 한다

좌표가 다르니까 그 둘을 맞게 해야한다

회전상태에 따라 다를 수 있다

① 두 그리드의 회전축이 90도 차이날 때

② 두 그리드의 회전축이 180도 차이날 때

③ 두 그리드의 회전축이 270도 차이날 때

 

위의 3가지 경우에 회전축이 어떤 모양으로 떨어지게 되는지 살펴보자

주의할 것은 preview가 보이는 좌표계가 현재의 좌표계라는 사실이다

 

① 두 그리드의 회전축이 90도 차이날 때

x에서 1 차이, z는 0차이가 나면 90도 차이

오브젝트 (2,0,1), 회전 0도

프리뷰 (2,0,2), 회전 90도

 

 

② 두 그리드의 회전축이 180도 차이날 때

x, z 모두 1만큼 차이가 나면 180도 차이

오브젝트 (2,0,1), 회전 0도

프리뷰 (3,0,2), 회전 180도

 

③ 두 그리드의 회전축이 270도 차이날 때

x에서 0차이, z에서 1차이가 나면 270도 차이

오브젝트 (2,0,1), 회전 0도

프리뷰 (3,0,1), 회전 90도

 

    /// <summary>
    /// gridPosition은 preview 오브젝트의 Grid 좌표의 기준점(0도 회전 기준 왼쪽 아래)으로 마우스 커서가 있는 그리드의 좌표이다
    /// objectSize는 preview오브젝트의 크기
    /// angle으로 넘어오는건 TempInputManager.angle으로 회전각
    /// </summary>
    /// <param name="gridPosition"></param>
    /// <param name="objectSize"></param>
    /// <returns></returns>
    public bool CanPlaceObjAt(Vector3Int gridPosition, Vector2Int objectSize, float angle)
    {
        /// gridPosition는 그리드의 좌표(월드좌표 아니다)
        List<Vector3Int> positionToOccupy = CalculatePositions(gridPosition, objectSize);
        foreach (var pos in positionToOccupy)   /// pos는 Vector3Int값으로 그리드의 좌표
        {
            /// 해당 그리드 좌표에 다른 오브젝트 점유하고 있는 영역이 있는지 확인한다★
            /// placedObject는 PlacedObjectData를 저장하고있으며
            /// PlacedObjectData는 
            /// PlacedObject가 생성된 GameObject를 저장하고 있다
            /// 기존에는 pos를 대입하면 해당 pos가 있는 게임오브젝트를 리턴하기 때문에
            /// 수정한 다음에도 GameObject를 가져와야한다
            if (placedObjects.ContainsKey(pos))
            {
                // 기존 오브젝트 데이터 가져오기
                PlacementData existingData = placedObjects[pos];

                // 기존 오브젝트의 회전값과 현재 회전값의 차이 계산
                float angleDiff = (existingData.angle - angle) % 360;

                /// pivot을 기존 occupiedPositions[0]을 기준으로 설정
                /// occupiedPositions[1] 부터는 회전축이 될 수 없다!
                Vector3Int pivot = Vector3Int.RoundToInt(existingData.occupiedPositions[0]);

                ///// 회전축이 같은 경우
                if (pivot == pos)
                {
                    Vector3 originPos = new Vector3(pos.x, pos.y, pos.z);
                    // RotatePosition에서는 pos의 x, z를 0.5씩 더하여 중심을 맞춘 후 회전 적용
                    Vector3Int rotatedPos = RotatePosition(originPos, pivot, angleDiff);

                    // 변환된 좌표가 현재 배치하려는 위치와 충돌하는지 확인
                    if (positionToOccupy.Contains(rotatedPos))
                    {
                        return false; // 충돌 발생 → 배치 불가능
                    }
                }
                else /// 회전축이 다른 경우
                {
                    // 음수 각도 처리: -360 ~ 360 사이의 값을 0 ~ 360 사이로 변환
                    if (angleDiff < 0)
                    {
                        angleDiff += 360;  // 음수일 경우 360을 더하여 양수로 변환
                    }
                    // 회전 차이에 따라 위치를 변경
                    Vector3Int offset = Vector3Int.zero;
                    switch ((int)angleDiff)
                    {
                        case 90:
                            offset = new Vector3Int(1, 0, 0); // x가 1 차이
                            break;
                        case 180:
                            offset = new Vector3Int(1, 0, 1); // x와 z가 1씩 차이
                            break;
                        case 270:
                            offset = new Vector3Int(0, 0, 1); // z가 1 차이
                            break;
                        default:
                            // 기본적으로 0도인 경우, 회전 없이 원래 위치
                            offset = Vector3Int.zero;
                            break;
                    }

                    // 기존 오브젝트의 회전 차이에 따른 새로운 위치를 계산
                    Vector3Int adjustedPos = pos + offset;

                    // 변환된 좌표가 현재 배치하려는 위치와 충돌하는지 확인
                    if (positionToOccupy.Contains(adjustedPos))
                    {
                        return false; // 충돌 발생 → 배치 불가능
                    }
                }
            }
        }
        return true;
    }

 

 

저번에 실패했지만 월드로 변환가능하지 않을까 생각했다

일단 해보자

 

 

(2,0,2) 와 (1,0,3)에 주목하자

로컬좌표계인데?

 

안되네...

 

아무래도 Grid 좌표평면을 부모로 하는것도 쉽지는 않은 방법이니

그냥 기존의 방법으로 하자

 

시간이 너무 많이 걸릴 것 같다

 

브랜치만 따로 팠고, 다시 기존의 방식으로 작업했다