TIL -13

Chu_uhC·2023년 10월 29일
0

TIL

목록 보기
13/16
post-thumbnail

📄 23.10.31✍ 최종 프로젝트. 건설 기능 계획

✨ 건물과 건설 기능 이번이 두 번째이다.
처음 만들었을 때는 가장 부족한 부분이 UI(버튼)의 재사용성과 모델들의 값이 문제였다.
UI는 함수 실행과 Sprite가 생각보다 하드 코딩된 점과 모델들의 값들은 SO로 관리되고있었지만 그 기능 자체가 상당히 불편하였다.
이런 부족한 부분들까지 채워서 이번 두 번째 도전을 시작해보자.

📌 빌딩 클래스는 최소 공약수 느낌으로 기능별로 나누고 건물 = 클래스는 최대한 피할 것이다.

📌 최대한 InterfaceOverride하여 관리할 것이지만 세분화된다면 언제든
     전략 패턴으로 리팩토링!

📌 MVC패턴의 문제점인 Controller 스크립트의 거대화는 Handler로 공통적이고
     큰 기능들은 나눠 볼 것!

📌 건설 기능은 3인칭이기에 카메라의 Ray를 활용하여 연출할 것
     청사진이나 회전도 반드시 넣어볼 것

🎉 한 장 요약



📄 23.11.01✍ 건설 - 그리드와 스냅

그리드(Grid) : 사전적 의미는 격자, 우리는 RTS나 시뮬레이션 게임을 할 때 건물이 칸에 맞춰서 지어지는 경우가 많은데 그 때 네모 칸들이 그리드의 셀(Cell)이다.

무튼 이 그리드 기능을 기반으로 여러가지 세부 기능들도 추가할 예정이다.
그중에서도 오늘 작성해볼 기능은...

그리드 & 스냅 : 그리드를 세팅하고 마우스 포지션으로 셀 위치 불러오기


🔨 그리드 세팅

선 그리드는 X,Y 축을 기반으로 만들어져 있기 때문에 원하는 방식으로 표현하기 위해서는 Cell Swizzle(컴퓨터 그래픽)을 이용하여 셀의 좌표를 재정렬할 필요가 있다.
그래서 그리드의 Y -> Z로 바꾸기위해서는 XYZ -> XZY로 변경 해줄 것이다.

📌 현재는 YZX로 하여도 큰 문제는 없던데 격자 크기가 차이가 나거나 타일맵을
     사용한다면 주의가 필요할듯하다.


🔨 그리드 규격에 맞게 오브젝트 배치(스냅).

private Grid buildGrid; // 오브젝트가 배치될 그리드
private LayerMask buildLayer; // 건설 가능 지역을 정해줄 Layer
private Vector3 screenPoint;

private float minDistance = 2;
private float maxDistance = 10;

private void Update() { SelectGrid(screenPoint);}

private void SelectGrid(Vector3 screenPoint)
{
    Ray ray = Camera.main.ScreenPointToRay(screenPoint);
    // 카메라에서 스크린 포인트 값으로 발사하는 레이

    if (Physics.Raycast(ray, out RaycastHit hit, maxDistance, buildLayer))
    {	// (레이, out 맞은 대상, 최대 거리, 선별 레이어)
        if (hit.distance < minDistance)
            return;

        Vector3Int pos = buildGrid.WorldToCell(hit.point);
        // WorldToCell : 해당 Cell의 local pos를 World Pos로 리턴해준다.
        currentGridPos = buildGrid.GetCellCenterWorld(pos);
        // GetCellCenterWorld : 해당 Cell의 Center 값을 리턴한다.
        // 셀의 크기는 언제나 1이 아니다.
    }
}

📌 3인칭인 게임이기에 건설 가능한 최소, 최대 거리를 설정해주었다.

📌 Layer로 건설 가능한 지역을 체크하는 이유는 땅의 유무와는 별개로 지을 수 있는 곳과
     아닌 곳으로 나눠야 하기 때문이다.

📌 지금은 물체에 가려져도 건설 가능 지역이다면 없는 것 처럼 작동하는데
     추후에 좀 더 좋은 조작감을 선택해서 변경해야한다.

기만하면 어질어질한 난이도를 가지고 있을 거 같지만 컴퍼넌트스크립트가 상당히 잘 구현되어 있기 때문에 쉽게 만들 수 있다.
우리가 흔히 볼 수 있는 게임 속 기능들은 유니티에서 쉽게 만들 수 있고 비주류 기능이나 동적인 물리 처리 분야로 들어가면 진짜 지옥을 경험할 수도 있다.

🎉 한 장 요약



📄 23.11.02✍ 건설 - 청사진과 생성

오늘은 건물이 만들어지기 전 청사진을 보여주는 것과 그것을 토대로 생성 및 관리를 해보자.

번 기능에서 가장 중요한 부분은 동적인 건물 그리드 관리이다.
모든 건물은 똑같은 사이즈일리가 없으니 건설을 할 때는 그 사이즈를 토대로 청사진과 실제로 건설까지 이어지게 만들어야한다.

🔨 홀수 사이즈에 대한 처리

private void SelectGrid(Vector3 screenPoint)
{
	...
	currentGridCenterPos = buildGrid.GetCellCenterWorld(currentGridPos);
    ----- 추가되는 부분 -----
	if (size.x % 2 == 0)
   		currentGridCenterPos.x -= 0.5f;
	if (size.y % 2 == 0)
   		currentGridCenterPos.z -= 0.5f;
}

사이즈가 짝수일 경우 다른 셀까지 넘어가는 치명적인 결함이 있다.
이 부분은 게임의 방식에 따라서 다른 보정 방식을 사용해줘야 하는데 지금은 '마우스 포인터의 중앙에 오브젝트가 위치'하는 방식이기에 ±0.5f를 해주는 방식으로 해결해 줄 수 있다.

📌 플레이어가 추가되면 마우스 포인터에 z : 0, x : Center로 해주는 방식으로 수정을
     해줘야 할 수도 있다.

🔨 청사진의 셀 주소 찾기

private List<Vector2Int> blueprintArea; // 청사진의 셀 주소값들
private Vector3 current; // currentGridCenterPos : 현재 오브젝트의 포지션값
private Vector2Int size; // 오브젝트의 사이즈


private void CurrentSellsToSize(Vector2Int size)
{
    blueprintArea.Clear();
    
    int wolrdX = (int)(current.x);
    int wolrdZ = (int)(current.z);
    if (current.x < 0 && size.x % 2 == 1)
        wolrdX--;
    if (current.z < 0 && size.y % 2 == 1)
        wolrdZ--;
    // 음수일 때 -1을 해주는 이유
    // 1.5을 형변환하면 1이 되지만 -1.5를 형변환하면 -1이 된다.
    // 하지만 내가 원하는 값은 -2가 나와야한다.

    for (int z = 0; z < size.y; z++)
    {
        for (int x = 0; x < size.x; x++)
        {
            int _x = wolrdX + x - (int)(size.x * 0.5f);
            int _y = wolrdZ + z - (int)(size.y * 0.5f);
            blueprintArea.Add(new Vector2Int(_x, _y));
        }
    }
}

상수값들이 많이 들어가 꼴보기 싫은 것이 리팩토링 1순위인 부분이다.

📌 양수일 때는 작동하고 음수면 안 될 경우 버려지는 값들을 유심히 살펴보자!

🔨 생성과 이미 건설 불가능한 구역 체크

// 건설을 시도하는 함수, out으로 별도의 참조없이 바로 위치 값을 받을 수있다.
// 호출한 쪽에서 리턴값으로 분기점을 만들어줘야한다.
public bool TryPossibleCreation(out Vector3 gridPos)
{
    if (!isCorrect)
    {
        gridPos = Vector3.zero; // 구조체는 null이 없기에 이거라도 줘야한다.
        return false;
    }
	
    // 문제가 없다면 건설한 지역에 청사진의 셀 주소를 넣어준다.
    foreach (var item in blueprintArea)
    {
        builtArea.Add(item);
    }

    gridPos = currentGridCenterPos;
    return true;
}

// 건설한 지역과 청사진 지역을 비교해주고 마테리얼을 변경해준다.
private void CheckPlaceable()
{
	// 비교하는 부분
    foreach (var item in blueprintArea)
    {
        if (!builtArea.Contains(item))
            continue;

        isCorrect = false;
        meshRenderer.material = materials[Convert.ToInt32(isCorrect)];
        return;
    }

    isCorrect = true;
    meshRenderer.material = materials[Convert.ToInt32(isCorrect)];
}

되든 안 되든 out값은 항상 생긴다는 게 마음에 안 드는 방식이지만 그게 내 실력인데 어쩌겠어요...😔

무튼 좀 더 간결하게 만들 수 있을 거 같은데도 최적화를 하는 주차도 따로 있으니 당장은 넘어가야 한다는 것이 마음에 걸리지만 매우 타당한 지침이라는 것에는 이의가 없다.
알고리즘을 연습할 때 썼던 C#기능들, 조금 더 진지하게 사용해 볼 걸이라는 생각이 든다.
그리드 셀을 다루는 알고리즘은 흔해 빠진 문제였는데 기억이 안 나서 버그 잡는 데 한참 걸렸다.

🎉 한 장 요약



📄 23.11.03✍ 건설 - 함수 순서와 고쳐야 할 점

함수의 순서와 최적화 그리고 건설 기능을 만들면서 있었던 일을 정리해보자.

🔨 굳이 꼭 실행해야해??

private void Update()
{
    IsChangeMousePos();
    IsChangeCellPosition();
    CurrentSellsToSize(currentGridCenterPos, size);
    MoveBlueprint(currentGridCenterPos);
    CheckPlaceable();
}

프레임 호출되는 함수 Update, 가장 비호감인데 강력한 친구이다.
정직하게 프레임마다 호출하게 된다면 약 200줄 가까이 되는 코드를 의미도 없이 실행하게 될 것이고 성능에 상당한 악영향이 갈 것이다.
하지만!

private bool IsChangeMousePos()
{
    if (mousePos == Input.mousePosition)
        return false; // 마우스가 움직이지 않았다면 false!!
	...
}

private bool IsChangeCellPosition()
{
    Ray ray = Camera.main.ScreenPointToRay(mousePos);

    if (!Physics.Raycast(ray, out RaycastHit hit, maxDistance, buildLayer)
        || hit.distance < minDistance)
        return false; // Ray가 맞지 않았거나, 너무 가까운걸 맞췄을 때 false!!

    Vector3Int temp = buildGrid.WorldToCell(hit.point);
    if (currentGridPos == temp)
        return false; // 이전 그리드 셀과 레이가 맞은 셀이 똑같으면 false!!
	...
}

// 예외 처리가 이뤄지는 Update
private void Update()
{
    if (IsChangeMousePos() == false)
        return;
    if (IsChangeCellPosition() == false)
        return;
	...
}

리턴 값을 적절히 사용해준다면 꼭 필요할 때만 Update를 온전히 실행시키면서 성능을 최대한 보호할 수 있다!

📌 Update를 써야한다면 꼭 필요한 코드만 실행시킬 수 있게 해라

🔨 프로토 타입은 거의 완성 되었지만 고쳐야 할 점

  1. 탑 뷰에서는 마우스 포인터가 청사진의 가운데 가는 것도 방법이지만, 목표는 3인칭이다.
    그럴 경우 사이즈가 큰 건물일 경우 플레이어가 청사진 안에 들어갈 수도 있다.
    🎲 : 플레이어 기능이 완성된다면 반드시 마우스 포인터에 대한 청사진의 위치를 옮겨주어야 한다

  2. 청사진이 다음 위치로 이동할 때 바로 이동하기 때문에 딱딱한 느낌이다.
    🎲 : 코루틴으로 부드럽게 이동해주는 효과도 좋을 것 같다.

🎉 한 장 요약



📔 주간 결산

1. 본격적인 최종 프로젝트 시작
이번 주부터 코드를 작성하기 시작했다.
한번은 의도치 않게 읽어는 보았던 기능(그리드)이었기에 수월하게 진행할 수 있었지만, 조금만 파고 들어가도 새로운 부분이 나왔다. 예를 들어 건물의 사이즈가 생겼다거나..
가능의 완성이 가장 중요하지만 퀄리티도 무조건 챙겨서 가겠다.

2. 컨디션 관리
분명 부담이 없이 즐기고 있다고 했는데 최면이었는 듯 하다.
숙면할 거 같은데도 새벽 일찍 일어나게 되어서 하루 종일 집중 안 되는 건 물론, 스트레스도 많이 받았다.

하지만 프로토 타입이 나오고 나서부터는 아침 먹을 시간도 부족하게 잔 걸로 보아하니 부담되긴 했나 보다 깔깔

profile
ChuNyan

0개의 댓글