Machineboy空

BubbleShooter 03 : 인접한 같은 색 공을 떨어뜨리는 로직을 짜보자! (Hexgrid와 DFS) 본문

Game/개발 일지

BubbleShooter 03 : 인접한 같은 색 공을 떨어뜨리는 로직을 짜보자! (Hexgrid와 DFS)

안녕도라 2024. 5. 11. 14:26

알고리즘 문제에서나 써보던 DFS를 드디어 프로젝트에 활용해본다 감격

인접한 공들을 돌아가며 같은 색인지 체크하고 액션을 취해야하니 DFS가 적절할 것이란 판단.

 

사각형 모양의 2차원 배열과 벌집 모양의 2차원 배열을 어떻게 매칭해야할지 모르겠어 막막했다.

우선은 사각형 모양 2차원 배열에서 dfs로 인접한 같은 색의 공을 없애는 DFS 테스트를 해봄


00. 일반 사각형 grid에서 DFS 테스트

 

일반 사각형 모양의 그리드에선, 검사 방향이 상하좌우, 대각선 4방향이라 헷갈릴 것이 없어 무리없이 성공!

사라지는 순서가 정교하지 못한 건 눈감아주기..

 

public class GridTest : MonoBehaviour
{
    public Tilemap tilemap;

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
        	// 스크린 좌표 <-> 월드 좌표
            Vector3 clickPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            Vector3Int cellPos = tilemap.WorldToCell(clickPos);
            TileBase clickedTile = tilemap.GetTile(cellPos);

            if (clickedTile != null)
            {
                StartCoroutine(DestroySameColorTiles(cellPos, clickedTile));
            }
        }
    }

	// 타일맵 그리드 상 좌표 범위
    int maxX = 3, minX = -3, maxY = 4, minY = -4;
    
    // 8방향(대각선까지 포함)으로 DFS 
    int[] dx = {-1, 1, 0, 0, 1, -1, -1, 1}, dy = {0, 0, 1, -1,1,-1, 1,-1};
    
    // 방문배열은 넉넉하게 100칸짜리로!
    int[,] visited = new int[100, 100];

	// 타일 삭제 간 시간 딜레이를 주고 싶어서 코루틴 사용!
    IEnumerator DestroySameColorTiles(Vector3Int cellPos, TileBase clickedTile)
    {
    	// -좌표들 양수 좌표로 옮겨줌.
        visited[cellPos.x + 3, cellPos.y + 4] = 1;

        for (int i = 0; i < 8; i++)
        {
            int nx = cellPos.x + dx[i];
            int ny = cellPos.y + dy[i];
			
            // 오버플로우 체크 및 방문 체크
            if (nx < minX || nx > maxX || ny < minY || ny > maxY || visited[nx + 3, ny + 4] == 1) continue;

            TileBase currentTile = tilemap.GetTile(new Vector3Int(nx, ny, 0));

            // 현재 검사하는 타일이 null이거나 클릭한 타일과 색이 다르면 스킵
            if (currentTile == null || currentTile.name != clickedTile.name) continue;

            // 앞선 조건들에 걸러지지 않았다면 조건 만족했으니 타일 파괴!
            tilemap.SetTile(new Vector3Int(nx, ny, 0), null);
            
            // 딜레이 추가
            yield return new WaitForSeconds(0.2f); 
            
            // 재귀
            StartCoroutine(DestroySameColorTiles(new Vector3Int(nx, ny, 0), clickedTile));
        }

        // 마지막으로, 클릭한 타일 자기 자신까지 파괴
        tilemap.SetTile(cellPos, null);
    }
}

01. Hexagrid 에서 DFS 적용  (★★★)

 

예전 코테 문제 중에 벌집 모양 배열을 다뤘던 것이 기억나서

hexagrid좌표를 사각형 2차원 배열 좌표로 표시해 봤다.

 

요런 방향으로 검사하면 된다는 것을 깨닫긴 했으나, 좀 더 파고들어보고 싶긴 했다.

시간 되면 나중에 자세히 정리해보겠다.

 

어떤 블로그에서 소개해준 사이트인데 아래 사이트에 hexagrid관련 이론들 너무 잘 정리되어 있음!

https://www.redblobgames.com/grids/hexagons/

 

Red Blob Games: Hexagonal Grids

Amit's guide to math, algorithms, and code for hexagonal grids in games

www.redblobgames.com

 

이상적으로 검사 방향이 모두 통일되면 좋겠지만, Unity의 Hexagrid flat-top의 좌표에 적용되지 않는다.

그려가며 규칙을 찾아보니, y좌표의 홀짝에 따라 검사패턴을 따로 짜줘야 함.

hexagrid의 좌표 설정 방식에 대해 좀 더 파고들어 공부해보면 좋겠다만 우선은 발견한 패턴에 따라 코드를 수정해주겠다.

 


02. DFS로 공 떨어뜨리기

 

우선 같은 색 공을 맞췄을 때 인접한 같은 색의 공들까지 파괴하기까지 구현해보자!

다른 색 공을 맞췄을 때, 아래에 공이 붙는 로직은 4장에서!

 


public class Ball : MonoBehaviour
{
    public Tilemap tilemap;
    string currentBallColor;


    // dfs용 
    int maxX = 15, minX = -15, maxY = 15, minY = -15;
    int[] dxEven = { 0, 1, 0, -1, -1, -1 };
    int[] dyEven = { -1, 0, 1, 1, 0, 1 };

    int[] dxOdd = { 1, 1, 1, 0, -1, 0 };
    int[] dyOdd = { -1, 0, 1, 1, 0, -1 };

    int[,] visited = new int[30, 30];

    void Start()
    {
        // 현재 공의 sprite 이름 = 색
        currentBallColor = gameObject.GetComponent<SpriteRenderer>().sprite.name;
    }

    IEnumerator IDestroyTile(Vector3Int cellPos, TileBase HitTile)
    {
        print("맞은 곳은" + cellPos);
        visited[cellPos.x, cellPos.y + 4] = 1;


        if (cellPos.y % 2 == 0)
        {
            for (int i = 0; i < 6; i++)
            {
                int nx = cellPos.x + dxEven[i];
                int ny = cellPos.y + dyEven[i];
                print("nx,ny는 " + nx + " " + ny);

                if (nx < minX || nx > maxX || ny < minY || ny > maxY || visited[nx, ny + 4] == 1)
                {
                    print("오버플로우 혹은 방문");
                    continue;
                }
                TileBase nearTile = tilemap.GetTile(new Vector3Int(nx, ny, 0));

                if (nearTile == null || nearTile.name != HitTile.name)
                {
                    print("null이거나 다른 색");
                    continue;
                }

                tilemap.SetTile(cellPos, null);
                print("파괴");

                yield return new WaitForSeconds(0.2f);
                StartCoroutine(IDestroyTile(new Vector3Int(nx, ny, 0), HitTile));
            }
        }
        else
        {
            for (int i = 0; i < 6; i++)
            {
                int nx = cellPos.x + dxOdd[i];
                int ny = cellPos.y + dyOdd[i];
                print("nx,ny는 " + nx + " " + ny);

                if (nx < minX || nx > maxX || ny < minY || ny > maxY || visited[nx, ny + 4] == 1)
                {
                    print("오버플로우 혹은 방문");
                    continue;
                }
                TileBase nearTile = tilemap.GetTile(new Vector3Int(nx, ny, 0));

                if (nearTile == null || nearTile.name != HitTile.name)
                {
                    print("null이거나 다른 색");
                    continue;
                }

                tilemap.SetTile(cellPos, null);
                print("파괴");

                yield return new WaitForSeconds(0.2f);
                StartCoroutine(IDestroyTile(new Vector3Int(nx, ny, 0), HitTile));
            }
        }


        tilemap.SetTile(cellPos, null);
    }

    private void OnCollisionEnter2D(Collision2D other)
    {
        if (other.collider.CompareTag("TileMap"))
        {
            //셀 좌표: Vector3Int <-> 월드 좌표: Vector2

            Vector3Int cellPos = tilemap.WorldToCell(other.contacts[0].point);
            TileBase hitTile = tilemap.GetTile(cellPos);

            if (hitTile != null)
            {
                StartCoroutine(IDestroyTile(cellPos, hitTile));
            }
        }
    }
}

 

이 로직을 기본으로 이제 다른 색에 맞으면 그곳에 붙도록,

인접한 타일이 모두 떨어지면 던져진 공 자기 자신도 파괴되도록 등 로직을 좀 더 정교화해 나가보겠다.

투비컨티뉴