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

Grid 기반의 건축 시스템 3. scale이 -값이 되었을 때 발생하는 문제

Imperor 2025. 3. 18. 18:15

scale이 -가 되면, 격자가 이상하게 그려지고, 영역 판단도 이상하게 한다

 

회전의 전제

1. position을 바꾸지 않는다 (회전축 고정)

2. rotation을 바꾸지 않는다 (좌표 고정)

3. scale이 음수가 될 수 있어야 한다

 

라고 생각했었다...

 

하지만 이대로는 해결할 수 없다

셋 중 하나를 깨야 한다

rotation을 하는 방법을 선택했다

 

rotation을 하면 좌표축이 회전하지만, 격자와 오브젝트가 완벽하게 일치하게 된다

좌표축이 회전한 상태로 오브젝트를 놓는다면

좌표축들이 서로 다른 회전상태에서 놓은 물체를 어떻게 비교하냐는 문제가 발생한다

 

1번 회전한 상태에서 (1,0,1)에 놓은 물체와

2번 회전한 상태에서 (1,0,1)에 물체를 놓으려고 하면 (1,0,1)에는 이미 물체가 있으므로 물체를 놓을 수 없다

 

축의 회전 상태를 고려하지 않고 좌표만 저장해서 그런 것이다

 

좌표축을 회전한다면, 물체들은 축의 어떤 회전상태일때 어떤 좌표인지 함께 저장해야한다

그래야 제대로 측정이 가능하다

 

그걸 어떻게 저장하느냐가 문제다

 

 

우선 영역비교를 어디서 하는지 찾아보자

평소에 마우스 커서를 올려놓을 때는

PlacementSystem.Update → PlacementState.UpdateState → PlacementState.CheckPlacementValidity → GridData.CanPlaceObjAt → GridData.CalculatePositions

순으로 들어가서 영역비교를 하고 있다

 

오브젝트를 내려놓을 때는 

TempInputManager.Update → TempInputManager.OnClicked에 저장된 PlacementSystem.PlaceStructure → PlacementState.OnAction에서 ObjectPlacer.PlaceObject → 생성 후 객체의 인덱스를 데이터베이스에 추가(이때 gridPosition을 저장한다!)   다시 PlacementState.OnAction까지 빠뎌나온 다음 GridData.AddObjectAt를 호출하여 내려놓은 오브젝트의 영역을 추가한다

이때 회전각을 고려하여 저장해야한다. 그래야 나중에 회전각 연산을 통해 비교할 수 있기 때문이다

 

그렇다면 ObjectPlacer의 placedGameObjects는 왜 있는걸까?

프리팹을 저장하고 있긴 한데, AddObjectAt에서 저장하면 여기에는 저장할 필요가 없지 않나??

 

 

저장할 때 gridPosition과 현재 축의 회전각을 같이 저장하면 될 것 같다

어디에서 좌표축의 회전을 고려하여 비교연산을 하면 좋은지 찾아보고 생각해보자

 

 

우선, 커서를 옮길 때 이미 가능한 영역 표시가 되어야 하므로 

PlacementState.CheckPlacementValidity

GridData.CanPlaceObjAt

GridData.CalculatePositions

 

이 셋을 중심으로 살펴보자

 

먼저 PlacementSystem에서 마우스 좌표를 grid 좌표로 변환하여

        Vector3Int gridPosition = grid.WorldToCell(mousePos);   // 마우스가 있는 위치를 3d로 변환하여 그리드의 어느 격자 내에 있는지 알아낸다
        /// gridPosition은 grid의 좌표(월드 좌표가 아니다)

 

PlacementState.UpdateState를 호출한다

매개변수로 grid 좌표를 사용한다(월드 좌표가 아님에 주의 ★)

        // grid 내에서 커서가 이동하면 연산하지 않는다
        if (lastDetectedPosition != gridPosition)
        {
            buildingState.UpdateState(gridPosition);    /// gridPosition은 월드좌표가 아님에 주의한다
            lastDetectedPosition = gridPosition;
        }

 

 

PlacementState.UpdateState에서는 

grid좌표를 매개변수로 CheckPlacementValidity를 호출한다

    /// <summary>
    /// preview 오브젝트가 점유하고 있는 그리드의 영역을 표시한다
    /// </summary>
    /// <param name="gridPosition"></param>
    public void UpdateState(Vector3Int gridPosition)
    {
        /// gridPosition는 그리드의 좌표
        /// selectedObjIndex는 UI에서 선택한 인덱스(preview 오브젝트)
        bool placementValidity = CheckPlacementValidity(gridPosition, selectedObjIndex);

        previewSystem.UpdatePosition(grid.CellToWorld(gridPosition), placementValidity);
    }

 

 

 

PlacementState.CheckPlacementValidity

    /// <summary>
    /// preview 오브젝트가 차지하는 그리드의 종류를 판단한다
    /// </summary>
    /// <param name="gridPosition"></param>
    /// <param name="selectedObjIndex"></param>
    /// <returns></returns>
    private bool CheckPlacementValidity(Vector3Int gridPosition, int selectedObjIndex)
    {
        GridData selectedData = database.objectsData[selectedObjIndex].ID == 0 ? tree : buildingData;

        /// gridPosition는 그리드의 좌표
        /// database.objectsData[selectedObjIndex]는 preview 오브젝트
        /// CanPlaceObjAt는 마우스가 있는 gridPosition에 preview오브젝트를 놓을 수 있으면 true를 리턴
        return selectedData.CanPlaceObjAt(gridPosition, database.objectsData[selectedObjIndex].Size);
    }

 

 

GridData.CanPlaceObjAt에서 해당 그리드에 오브젝트가 있는지 비교한다 ★

    /// <summary>
    /// gridPosition은 preview 오브젝트의 Grid 좌표의 기준점(0도 회전 기준 왼쪽 아래)으로 마우스 커서가 있는 그리드의 좌표이다
    /// objectSize는 preview오브젝트의 크기
    /// </summary>
    /// <param name="gridPosition"></param>
    /// <param name="objectSize"></param>
    /// <returns></returns>
    public bool CanPlaceObjAt(Vector3Int gridPosition, Vector2Int objectSize)
    {
        /// gridPosition는 그리드의 좌표(월드좌표 아니다)
        List<Vector3Int> positionToOccupy = CalculatePositions(gridPosition, objectSize);
        foreach (var pos in positionToOccupy)
        {
            /// 해당 그리드 좌표에 다른 오브젝트가 있는지 확인한다★
            /// 로직을 바꾼다
            if (placedObjects.ContainsKey(pos))
            {
                /// 
                return false;
            }
        }
        return true;
    }

foreach문 안에서 비교하게된다 

positionToOccupy는 

 

그리드 영역을 비교하기위해 

GridData.CalculatePositions 에서는 preview 오브젝트가 점유하고있는 그리드 좌표들을 리스트에 저장하여 리턴

    /// <summary>
    /// gridPosition은 preview 오브젝트의 Grid 좌표의 기준점(0도 회전 기준 왼쪽 아래)으로 마우스 커서가 있는 그리드의 좌표이다
    /// objectSize는 preview 오브젝트의 크기
    /// </summary>
    /// <param name="gridPosition"></param>
    /// <param name="objectSize"></param>
    /// <returns></returns>
    private List<Vector3Int> CalculatePositions(Vector3Int gridPosition, Vector2Int objectSize)
    {
        List<Vector3Int> returnVal = new List<Vector3Int>();
        // object의 왼쪽 아래 기준
        /// preview 오브젝트가 점유하고있는 grid좌표를 returnVal에 담아서 리턴한다 ★
        for (int x = 0; x < objectSize.x; x++)
        {
            for (int y = 0; y < objectSize.y; y++)
            {
                returnVal.Add(gridPosition + new Vector3Int(x, 0, y));
            }
        }
        return returnVal;
    }

 

 


PlacementState.OnAction에서 ObjectPlacer.PlaceObject를 호출한다

    public void OnAction(Vector3Int gridPosition)
    {
        // PlaceStructure

        /// 영역검사할때도 격자의 회전을 반영해야한다
        bool placementValidity = CheckPlacementValidity(gridPosition, selectedObjIndex);
        if (placementValidity == false)
        {
            soundFeedback.PlaySound(SoundType.wrongPlacement);
            return;
        }
        soundFeedback.PlaySound(SoundType.Place);
        /// 오브젝트 생성(오브젝트 정보는 previewSystem.previewObject가 가지고 있다)
        /// grid 좌표를 world좌표로 환산한 값을 대입하니까 PlaceObject에서 다루는 것은 월드 좌표다
        int index = objectPlacer.PlaceObject(database.objectsData[selectedObjIndex].Prefab, grid.CellToWorld(gridPosition));

        // 이 객체의 인덱스를 데이터에 추가
        GridData selectedData = database.objectsData[selectedObjIndex].ID == 0 ? tree : buildingData;   // 나무와 건물을 구분한다
        selectedData.AddObjectAt(gridPosition,
            database.objectsData[selectedObjIndex].Size,
            database.objectsData[selectedObjIndex].ID,
            index);

        previewSystem.UpdatePosition(grid.CellToWorld(gridPosition), false);
    }

 

 

오브젝트 생성은 ObjectPlacer.PlaceObject에서 한다

/// <summary>
/// 건물 오브젝트를 맵에 추가
/// </summary>
public class ObjectPlacer : MonoBehaviour
{
    [SerializeField]
    private List<GameObject> placedGameObjects = new List<GameObject>(); // 생성된 게임오브젝트 저장

    public TempInputManager tempInputManager;  // angle을 가져온다

    public int PlaceObject(GameObject prefab, Vector3 position)
    {
        /// tempInputManager에서 회전각을 가져온다
        /// 이건 preview오브젝트가 회전한 각이기도 하지만, 
        /// 회전축이 회전한 각이기도 하다
        float angle = tempInputManager.angle; 

        GameObject newObject = Instantiate(prefab);
        newObject.transform.position = position; // 오브젝트를 그리드 좌표를 월드 좌표로 환산한 값으로 이동
        newObject.transform.rotation = Quaternion.Euler(0, angle, 0); // Y축 기준으로 회전

        placedGameObjects.Add(newObject);   // 생성한 게임오브젝트 저장
        return placedGameObjects.Count - 1;
    }
}

하지만 여기에는 프리팹만 저장하고 있다

프리팹을 저장할때 프리팹의 position, rotation은 저장이 된다

다만 축의 회전상태는 저장이 안된다

전에 언급했듯이

회전을 한다고 해서 프리팹의 position이 변하지 않는 것이 가장 큰 문제다 ★

대신에 rotation이 변한다

프리팹을 저장하면 rotation 정보는 확실히 남으므로

TempInputManager에서 angle을 가져올 필요가 없다

그러니 그냥 프리팹만 저장한다 ★

 

그 다음 빠져나가서 PlacementState.OnAction으로 다시 돌아간 다음

GridData.AddObjectAt를 호출한다

GridData.AddObjectAt에서는 이 객체의 인덱스를 추가하는데

이때 회전정보 또한 같이 저장해야한다

 


나중에 비교할때는 

현재를 기준으로 하면 안된다

다음 스샷에서 볼 수 있듯이

회전하더라도 축이 함께 90도씩 회전하므로 상대적인 위치가 같아서

없어도 있다고 나온다!

 

현재 마우스의 위치에 따른 grid좌표에 무엇인가 있다고 나오면

축의 현재 회전각(TempInputManager.angle)과 

그 오브젝트의 저장 당시의 angle을 비교해서 그 회전각만큼 돌려놓고 다시 비교해야한다 ★

position은 같지만 grid좌표로 연산하면 다른 값이 나올테니 비교가 가능해질것 ★

 

그러면 회전하는 방향에 대해 생각하자

현재 축이 270도라고 가정하자

저장한 회전각이 0도라면 0-270 = -270 회전해야한다

저장한  회전각이 90도라면 90-270 = -180 회전해야한다

저장한 회전각이 180도라면 -90 회전해야한다

저장한 회전각이 270도라면 270-270 = 0 회전해야한다

(-는 반시계방향이다. R 입력을 하여 축을 시계방향으로 하고 있으므로, 반대방향으로 하는게 복원의 의미가 있어서 헷갈리지 않는다)

 

 

이어서 계속