본문 바로가기
다이어리/내일배움 개발일지

게임개발캠프 - 팀 과제(E) 5일차

by E.Clone 2024. 3. 4.

과정명 : 내일배움캠프 Unity 게임개발 3기

전체진행도 : 47일차

부분진행도 : Chapter5.2 - 5일차

작성일자 : 2024.03.04(월)

개발일지 목록 : 클릭


1. 진행중인 과정에 대해

4일차에 필수 구현 목표를 대부분 완료하고 마무리 단계.

금일 예정은, Monster 코드의 상속 구조 리팩토링 후, 몬스터 로밍 자동화 실패했던 내용에 대해 해결 해 보기.

2. 오늘 학습에 대해

오늘 오전에는 몬스터 종류에 관한 코드를 리팩토링 하고, 오후에는 아래와 같은 사장되었던 기능을 살려보았다.

몬스터의 자동 배치 및 로밍에 대해 Fix

문제

구현 초기에, 몬스터를 소환 할 때 몬스터 바로 아래의 바닥 콜라이더( Layer 중 'Ground', 'Passthrough' 에 대해)를 찾아 그 콜라이더의 표면으로 몬스터의 y값을 조정하고, 콜라이더의 좌우 끝값과 몬스터의 콜라이더 폭에 따라, 몬스터가 좌우로 이동할 수 있는 범위를 동적으로 지정해 주었다.

슬라임을 스폰하면, 스폰한 위치의 하단의 콜라이더를 찾아 Y위치를 조정하고, 콜라이더 폭 만큼 X범위를 갖는다

이걸 파기 한 이유는, 실제 MainScene에 팀원이 구현해 놓은 것을 보면 Ground 그리고 Passthrough가 각각 타일맵으로 한 개의 덩어리로 만들어져 있어 슬라임을 이 스테이지 안에 대충 배치해 놓는다고 해도 실제 게임을 실행 해 보면 스테이지의 y최상단에 슬라임이 올라가 있고 스테이지 전체의 좌우폭을 슬라임의 이동거리로 갖게 되는 문제가 있었기 때문이다.

MainScene에서 적용할 경우, 스테이지가 하나의 콜라이더이기였기 때문에, 최상단에 슬라임이 생겼다. X이동범위도 스테이지 폭과 같음.

아래 스크립트는, 위 알고리즘을 파기하여 주석으로 둔 후, 임시로 x의 범위를 직접 SO에서 설정하도록 하였다. y위치도 역시 SO에서 정확한 수치를 지정해주어야 한다.

private void SpawnMonstersFromList(List<SpawnInfo> spawnData)
{
    foreach (var spawnInfo in spawnData)
    {
        string prefabPath = $"Prefabs/Monsters/{spawnInfo.monsterType}";
        GameObject monsterPrefab = Resources.Load<GameObject>(prefabPath);
        if (monsterPrefab != null)
        {
            GameObject monsterObj = Instantiate(monsterPrefab, spawnInfo.spawnPosition, Quaternion.identity);
            SpawnedMonsters.Add(monsterObj);
            // "Ground"와 "Passthrough" 레이어에만 있는 물체를 감지하기 위한 LayerMask 생성
            int layerMask = LayerMask.GetMask("Ground", "Passthrough");
            // ray 쏘기
            RaycastHit2D hit = Physics2D.Raycast(spawnInfo.spawnPosition, Vector2.down, 10f, layerMask);
            if (hit.collider != null)
            {
                // 하단 콜라이더의 경계를 기반으로 MinX와 MaxX 계산
                BoxCollider2D monsterCollider = monsterObj.GetComponent<BoxCollider2D>();
                float colliderHalfWidth = monsterCollider != null ? monsterCollider.size.x * monsterObj.transform.localScale.x / 2f : 0;
                float minX = hit.collider.bounds.min.x + colliderHalfWidth;
                float maxX = hit.collider.bounds.max.x - colliderHalfWidth;
                float genY = hit.collider.bounds.max.y;
                // 해결 될 때 까지 임시조치
                //monsterObj.transform.position = new Vector3(monsterObj.transform.position.x, genY, monsterObj.transform.position.z);
                // MonsterStat MinX, MaxX 설정, bool 관련 설정
                MonsterStat monsterStat = monsterObj.GetComponent<MonsterStat>();
                if (monsterStat != null)
                {
                    //monsterStat.MinX = minX; // 해결 될 때 까지 임시조치
                    //monsterStat.MaxX = maxX;
                    // 임시조치
                    monsterStat.MinX = spawnInfo.spawnPosition.x - spawnInfo.tempMinX;
                    monsterStat.MaxX = spawnInfo.spawnPosition.x + spawnInfo.tempMaxX;
                    monsterStat.IsStopOnIdle = spawnInfo.isStopOnIdle;
                    monsterStat.IsStopOnTrack = spawnInfo.isStopOnTrack;
                }
            }
        }
        else
        {
            Debug.LogError($"NotFound : MonsterPrefab {spawnInfo.monsterType} | path {prefabPath}");
        }
    }
}

또한, 위와 같은 몬스터 배치 스크립트는 하단의 바닥 콜라이더가 가로로 평평한 직사각형이 아닐 경우 몬스터가 엉뚱한 위치에 배치되거나 엉뚱한 이동범위를 가질 수 있다. 예로 아래와 같은 느낌의 계단 모양의 콜라이더에 슬라임을 배치 할 경우 아래와 같은 모양을 하게 된다.

그림. 기대한 슬라임의 위치와 범위

■■■
□□■ ← slime →
□□■■■■■■■
□□□□□□□□□ ㅇ

그림. 실제 스크립트를 적용한 슬라임의 y위치와 이동범위

← slime →
■■■
□□■
□□■■■■■■■
□□□□□□□□□

이러한 계단이나 언덕 모양에서도 잘 작동하도록 하기 위해, 스크립트를 다시 구성할 필요가 있다.

해결

위 두 가지의 문제를 동시에 해결하기 위한 솔루션으로 아래와 같은 방법을 사용하였다.

  1. 몬스터의 하단에 Raycast를 사용하는 방법은 동일하다.
  2. 단, Raycast가 인식되는 지점을 y위치로 지정한다. 이전 스크립트에서는, 콜라이더의 최상단 지점이 몬스터의 y좌표가 되었었다.
  3. 몬스터를 좌우로 이동시키며, 상단 및 하단으로 Ray를 쏴 동일한 플랫폼인지 확인한다.

2번 과정으로 몬스터가 스폰되는 y위치의 조정을 해결하고, 3번 과정을 통해 계단 및 언덕 모양 등 높이가 다른 바닥으로는 몬스터가 이동하지 않도록 할 수 있게 된다.

3번 과정의 구현에서 꽤 시간이 걸렸는데, 자세히 작성하면 아래와 같다.

좌측 범위에 대한 탐색을 먼저 시작한다. 상단(몬스터의 하단을 기준으로 레이를 발사하여, 몬스터의 상단 지점까지)은 플랫폼 Collider가 탐색되지 않아야 하고, 하단은 아주 짧은 거리에 플랫폼 Collider가 탐색되어야 하며, 이 두 조건을 만족할 경우, 이동 가능한 X 범위에 포함시키며 반복문을 통해 계속 확장해 나간다. 두 조건 중 하나라도 만족하지 않게 된다면 확장을 멈추고 좌측으로의 X범위를 확정한다. 이를 우측 범위에 대해서도 똑같이 탐색한다.

자세한 스크립트는 아래와 같으며, 주석으로 각 구문에 대한 설명을 하였다.

FindBoundary 메서드에서 Ray를 통한 X범위 탐색을 구현하였다.

    private void SpawnMonstersFromList(List<SpawnInfo> spawnData)
    {
        foreach (var spawnInfo in spawnData)
        {
            string prefabPath = $"Prefabs/Monsters/{spawnInfo.monsterType}";
            GameObject monsterPrefab = Resources.Load<GameObject>(prefabPath);
            if (monsterPrefab != null)
            {
                GameObject monsterObj = Instantiate(monsterPrefab, spawnInfo.spawnPosition, Quaternion.identity);
                SpawnedMonsters.Add(monsterObj);

                // 현재 몬스터 Stat의 MinX, MaxX 등을 재설정하기 위해 준비
                MonsterStat monsterStat = monsterObj.GetComponent<MonsterStat>();

                // 스폰 시 Y값 및, X범위를 바로 아래 플랫폼의 모양에 따라 재할당
                if (spawnInfo.autoAdjustPosition)
                {
                    // "Ground"와 "Passthrough" 레이어에만 있는 물체를 감지하기 위한 LayerMask 생성
                    int layerMask = LayerMask.GetMask("Ground", "Passthrough");

                    // ray 쏘기
                    RaycastHit2D hit = Physics2D.Raycast(spawnInfo.spawnPosition, Vector2.down, Mathf.Infinity, layerMask);

                    if (hit.collider != null)
                    {
                        Vector3 monsterPosition = spawnInfo.spawnPosition;

                        // 몬스터의 Y 위치 설정
                        monsterPosition.y = hit.point.y;
                        monsterObj.transform.position = monsterPosition;

                        // 이동 범위 설정
                        float leftBoundary = FindBoundary(monsterObj, Vector2.left, layerMask);
                        float rightBoundary = FindBoundary(monsterObj, Vector2.right, layerMask);

                        BoxCollider2D monsterCollider = monsterObj.GetComponent<BoxCollider2D>();
                        float colliderHalfWidth = monsterCollider != null ? monsterCollider.size.x * monsterObj.transform.localScale.x / 2f : 0;
                        monsterStat.MinX = Mathf.Min(spawnInfo.spawnPosition.x, leftBoundary + colliderHalfWidth);
                        monsterStat.MaxX = Mathf.Max(rightBoundary - colliderHalfWidth, spawnInfo.spawnPosition.x);

                    }
                }
                // 스폰 시 위치 및 X범위를 수동으로 할당
                else
                {
                    // x 이동 범위 수동 지정
                    monsterStat.MinX = spawnInfo.spawnPosition.x - spawnInfo.tempMinX;
                    monsterStat.MaxX = spawnInfo.spawnPosition.x + spawnInfo.tempMaxX;
                }

                monsterStat.IsStopOnIdle = spawnInfo.isStopOnIdle;
                monsterStat.IsStopOnTrack = spawnInfo.isStopOnTrack;

            }
            else
            {
                Debug.LogError($"프리팹({spawnInfo.monsterType})을 경로({prefabPath})에서 찾을 수 없음");
            }
        }
    }

    // 몬스터의 X이동범위 자동으로 할당할 경우 탐색
    float FindBoundary(GameObject monster, Vector2 direction, int layerMask)
    {
        float step = 0.05f; // Raycast를 발사할 간격
        float maxDistance = 20f; // 최대 탐색 거리
        float rayLength = monster.GetComponent<BoxCollider2D>().size.y; // 몬스터 콜라이더의 높이

        // 몬스터 하단에서 시작
        Vector2 basePosition = new Vector2(monster.transform.position.x, monster.transform.position.y);
        float goalPositionX = basePosition.x;

        for (float distance = 0; distance <= maxDistance; distance += step)
        {
            Vector2 origin = basePosition + (direction * distance);
            Vector2 rayStartBelow = origin + new Vector2(0, 0.01f); // 아래쪽 Ray 시작점
            Vector2 rayStartAbove = origin + new Vector2(0, 0.1f); // 위쪽 Ray 시작점
            // 시각화 FOR DEBUG
            Debug.DrawRay(rayStartBelow, Vector2.down * 0.05f, Color.red, 60f);
            Debug.DrawRay(rayStartAbove, Vector2.up * rayLength, Color.blue, 60f);

            // 아랫방향으로 짧은 Raycast 발사
            RaycastHit2D hitBelow = Physics2D.Raycast(rayStartBelow, Vector2.down, 0.05f, layerMask);

            // 몬스터 위치보다 약간 위에서 위쪽으로 Raycast 발사
            RaycastHit2D hitAbove = Physics2D.Raycast(rayStartAbove + new Vector2(0, 0.05f), Vector2.up, rayLength, layerMask);

            // 아래에는 플랫폼이 있고, 위에는 플랫폼이 없는 경우
            if (hitBelow.collider != null && !hitAbove.collider)
            {
                // 이동 가능한 경계를 찾음
                goalPositionX = basePosition.x + direction.x * distance;
            }
            else break;
        }

        return goalPositionX;
    }

상단(파랑)과 하단(빨강)의 탐색 시각화

X탐색에 관한 Ray를 시각화하면 위와 같다.

3. 과제에 대해

  • 이번 프로젝트에 대한 과제는 일단 더 없고, 생각했던 리팩토링도 끝냈기 때문에, 발표에 쓰일 트러블슈팅(개발 중 문제 해결 경험)에 대한 영상을 찍어 발표자에게 전해주는 것, 기타 프로젝트에 관한 정리를 하면 되겠다.
  • (추가)오후 중 영상까지 완료하여 일단 종료

 

반응형