2019년 11월 28일 목요일

12. 비어있는 보드에 블럭 생성하기

이번 장에서는 블럭이 드롭된 후 생기는 빈자리에 새로운 블럭으로 채우는 작업을 진행한다.

실행 화면은 다음과 같다.


소스 코드 다운로드

https://github.com/ninez-entertain/BingleStarter 에서 소스코드를 다운받을 수 있다. >git clone https://github.com/ninez-entertain/BingleStarter.git 또한, 각 진행 과정은 개별 branch로 제공되며 소스코드 다운로드 후에 각 스텝 별로 브랜치를 이용할 수 있다. 이번 장의 브랜치는 "step-12"으로 아래 git 명령어로 이번 장의 브랜치로 바로 이동할 수 있다. git checkout step-12

블럭이 드롭된 후에 새로운 블럭을 생성(Spawn)하는 과정을 추가한다.(시퀀스 21 ~ 24)

  1. 제거된 자리에 새로운 블럭 생성(Spawn)을 요청한다.
  2. 비어있는 모든 블럭 위치에 블럭을 생성하는 함수를 호출한다.
  3. 블럭 생성(Spawn)을 담당하는 StageBuilder에게 새로운 블럭을 요청한다.
  4. 3에서 생성된 블럭을 보드의 빈자리로 드롭 시킨다. 즉, 새로 생성되는 블럭이 보드의 지정된 위치로 드롭된다.

Respawn 요청하기

제거된 블럭으로 인해 상위 블럭이 드롭된 후에 남게되는 빈자리에 새로운 블럭을 생성하도록 요청하는 코드를 추가한다.(시퀀스 21)
실행된 후 movingBlocks에 새로 생성된 블럭이 추가된다.
public IEnumerator PostprocessAfterEvaluate()
{
    List<KeyValuePair<int, int>> unfilledBlocks = new List<KeyValuePair<int, int>>();
    List<Block> movingBlocks = new List<Block>();

    //1. 제거된 블럭에 따라, 블럭 재배치(상위 -> 하위 이동/애니메이션)
    yield return m_Board.ArrangeBlocksAfterClean(unfilledBlocks, movingBlocks);

    //2. 재배치 완료(이동 애니메이션 완료)후, 비어있는 블럭 다시 생성
    yield return m_Board.SpawnBlocksAfterClean(movingBlocks);

    //3. 블럭 재생성 후, 매치블럭 제거하기 위한 루프를 돌때
    //   유저에게 생성된 블럭이 잠시동안 보이도록 다른 블럭이 드롭되는 동안 대기한다.
    yield return WaitForDropping(movingBlocks);
}
Stage.cs

빈자리 블럭 생성하기

보드에서 비어있는 블럭이 있는 위치를 찾아서 블럭 생성을 요청한다.
/*
 * 비어있는 블럭을 다시 생성해서 전체 보드를 다시 구성한다
 */
public IEnumerator SpawnBlocksAfterClean(List<Block> movingBlocks)
{
    for (int nCol = 0; nCol < m_nCol; nCol++)
    {
        for (int nRow = 0; nRow < m_nRow; nRow++)
        {
            //비어있는 블럭이 있는 경우, 상위 열은 모두 비어있거나, 장애물로 인해서 남아있음.
            if (m_Blocks[nRow, nCol] == null)
            {
                int nTopRow = nRow;

                int nSpawnBaseY = 0;
                for (int y = nTopRow; y < m_nRow; y++)
                {
                    if (m_Blocks[y, nCol] != null || !CanBlockBeAllocatable(y, nCol))
                        continue;

                    Block block = SpawnBlockWithDrop(y, nCol, nSpawnBaseY, nCol);
                    if (block != null)
                        movingBlocks.Add(block);

                    nSpawnBaseY++;
                }

                break;
            }
        }
    }
    
    yield return null;
}
Block.cs
4 Enumerator 메소드를 정의한다.
movingBlocks : 새로 생성되어 드롭 애니메이션이 진행되는 블럭 객체가 저장되는 리스트
6, 8 보드 전체를 탐색해서 처리한다.
11 보드에서 해당 위치에 블럭이 비어있는 경우에만 처리한다.
15 블럭이 생성되는 원점. '0'은 보드 상단 기준으로 첫번째 블럭이 생성(Spawn)되는 위치를 나타낸다.
새로운 블럭이 생성되면 +1 씩 증가해서 다음 블럭이 이전 블럭의 위쪽에서 드롭이 시작되도록 한다.
16 - 26 현재 행(row) 위쪽에 빈블럭을 모두 찾아서 블럭을 생성한다.
18, 19 빈 블럭이 아니거나, 새로운 블럭을 추가할 수 없는 위치이면 다음 행으로 패스한다.
21 블럭 생성이 필요한 경우, 새로운 블럭 생성을 요청한다.(시퀀스 22)
23 생성된 블럭이 드롭 액션을 수행하기 때문에 movingBlocks 리스트에 저장한다.
25 블럭이 생성되는 기준 위치를 '1' 증가 시킨다. 다음으로 생성되는 블럭이 현재 블럭 바로 위에 위치할 것이다.

새 블럭 생성하기

/*
 * 블럭을 생성하고 목적지(nRow, nCol) 까지 드롭한다
 * @param nRow, nCol : 생성후 보드에 저장되는 위치
 * @param nSpawnedRow, nSpawnedCol : 화면에 생성되는 위치, nRow, nCol 위치까지 드롭 액션이 연출된다
 */
Block SpawnBlockWithDrop(int nRow, int nCol, int nSpawnedRow, int nSpawnedCol)
{
    float fInitX = CalcInitX(Core.Constants.BLOCK_ORG);
    float fInitY = CalcInitY(Core.Constants.BLOCK_ORG) + m_nRow;

    Block block = m_StageBuilder.SpawnBlock().InstantiateBlockObj(m_BlockPrefab, m_Container);
    if (block != null)
    {
        m_Blocks[nRow, nCol] = block;
        block.Move(fInitX + (float)nSpawnedCol, fInitY + (float)(nSpawnedRow));
        block.dropDistance = new Vector2(nSpawnedCol - nCol, m_nRow + (nSpawnedRow - nRow));
    }

    return block;
}
Block.cs
6 지정된 위치에 블럭을 생성하기 위한 함수를 선언한다.
nRow, nCol : 블럭이 생성된 후에 저장되는 보드에서의 위치
nSpawnedRow, nSpawnedCol : 블럭이 생성되는 위치, 보드 영역 밖에서 생성되어 지정된 위치(nRow, nCol)까지 드롭된다.
8 블럭이 Spawn되는 col 기준 원점
9 블럭이 Spawn되는 row 기준 원점. 보드 영역 밖에서 생성되기 때문에 보드의 가장 Top(m_nRow)를 기준점으로 설정한다.
11 SpawnBlock()으로 Block 객체가 생성되고, InstantiateBlockObj() 메소드를 호출해서 Block GameObject를 생성한다.(시퀀스 23)
14 새로 생성된 블럭을 보드의 지정된 위치(nRow, nCol)에 배치한다.
15 블럭이 생성되면 드롭 시작 위치로 이동시킨다.
16 떨어지는 거리를 설정한다(시퀀스 24). 내부에서 드롭 애니메이션이 시작된다

StageBuilder 참조 추가

Board 객체에서 블럭의 생성을 담당하는 StageBuilder를 참조할 수 있도록 멤버를 추가한다.
public class Board
{
    // -- 중 략 --
    GameObject m_BlockPrefab;
    StageBuilder m_StageBuilder;

    internal void ComposeStage(GameObject cellPrefab, GameObject blockPrefab, Transform container, StageBuilder stageBuilder)
    {
        //1. 스테이지 구성에 필요한 Cell,Block, Container(Board) 정보를 저장한다. 
        m_CellPrefab = cellPrefab;
        m_BlockPrefab = blockPrefab;
        m_Container = container;
        m_StageBuilder = stageBuilder;

        // -- 중 략 --
    }
}
Board.cs
5 StageBuilder 객체 참조를 선언한다.
7 StageBuilder 객체에 대한 참조를 전달받을 파라미터를 추가한다.
13 StageBuilder 객체에 대한 참조를 저장한다.

Stage 객체에서 Board 객체의 ComposeStage() 메소드 호출시 StageBuilder 레퍼런스를 전달하는 코드를 추가한다.
internal void ComposeStage(GameObject cellPrefab, GameObject blockPrefab, Transform container)
{
    m_Board.ComposeStage(cellPrefab, blockPrefab, container, m_StageBuilder);
}
Stage.cs
3 StageBuilder 객체를 인자로 전달한다.

실행하기

플레이버튼(▶)을 클릭해서 플레이 결과를 확인해보자.
블럭이 생성과 동시에 드롭되면서 지정된 위치로 이동하는 것을 볼 수 있다.

클래스 다이어그램 리뷰

이번 장에서 추가한 코드를 클래스 다이어그램에 반영하면 아래와 같다.
Board 객체에서 StageBuilder를 참조하는 코드가 추가됨으로써 위의 붉은색의 관계(Aggregation)가 추가되었다.
Board와 StageBuilder 객체사이에는 이전에 아무런 관계가 있지 않았고, Stage 객체에서 Board와 StageBuilder를 관리하고 있었으나 새롭게 관계가 추가되었다.

Stage - StageBuilder - Board 클래스의 관계만을 표현하면 아래와 같다.
[변경 전]의 구조는 Stage 객체가 Board와 StageBuilder를 저장하고 있기 때문에 바라보는 방향이 일방적이다. StageBuilder에서 Stage에 Dependency를 가지고 있으나 연결고리가 약하기 때문에 일단 무시한다.
그러나 [변경 후]의 구조는 Circle 형태처럼 보이는 관계가 만들어져 버렸다. A ➔ B ➔ C ➔ A와 같은 순환 구조는 아니지만 가능하면 권장하고 싶지 않는 구조이다. 이런 구조에서 만약 StageBuilder가 Board를 바라보는 관계가 만들어 진다면 가장 최악이 될 것이다(다행히 그러한 사태는 만들지 않을 것이다).

조금 더 나은 개선안을 생각해 보면, 아래의 [변경 1안]이 대안이 될 수 있다. Stage 객체가 StageBuilder를 참조하고 있기 때문에 Board 객체는 Stage 객체를 통해서 StageBuilder에서 제공하는 기능을 대행받는 구조이다. 그러나 Board와 Stage가 상호 참조를 하는 구조이기 때문에 좀 더 나은 구조를 고민볼 수 있다. 상호 참조가 나쁜 구조는 아니다. Child-Parent 관계에서 Child가 Parent를 참조하는 구조는 우리에게 익숙하다. 유니티에서도 즐겨 사용하는 것 같다^^.
[변경 2안]은 Stage 객체와 Board 객체가 StageBuilder에 Dependency 만을 가지는 구조로 상호참조 하지 않기 때문에 현재 구조에서는 가장 좋은 대안이 될 수 있을 것 같다.
클래스간의 구조에 대한 개인적인 호불호가 있기때문에 정답이 있지는 않지만, 개인적인 취향으로는 가능하면 한쪽에서 일방적으로 바라보는 구조를 지향하는 관점에서 위의 구조에 대한 지극히 개인적인 생각을 기술하였기 때문에 이 글을 읽는 분들은 다른 의견이 많이 있을 것입니다.
그러나, 현 시점에서 [변경 1안] 이나 [변경 2안]으로 구조를 바꾸지는 않을 것이다. 모양이 Circle 형태이지만 Board에서 StageBuilder로 의 요청이 StageBuilder의 상태에 영향을 주지 않기 때문에 현재 구조를 유지할 것이다.

다음 장에서는 이동하지 않는 장애물을 추가하고, 장애물로 인하여 블럭의 드롭 방식이 달라지는 과정을 진행하겠습니다.

문의사항 및 잘못된 부분은 댓글 및 메일(ninez.entertain@gmail.com)으로 부탁드립니다.
감사합니다.

댓글 6개:

  1. 이후 내용 업데이트 되나요?

    답글삭제
  2. 안녕하세요.
    개발중인 게임 마무리 단계라서 글을 못쓰고 있습니다. ㅜㅜ

    3월 중 런칭 계획이라서 런칭후에 업로드 하겠습니다.

    도움은 조금이라도 되셨는지요?

    답글삭제
  3. 기다리고 있습니다. 현기증 날거같아요 ㅠ

    답글삭제
  4. 작성자님. 너무 잘 보고 열심히 공부하고 있습니다.
    너무너무 큰 도움 진심으로 감사합니다.
    작성자님 같은 분이 계시기에 오늘도 희망을 가지고 살아봅니다.
    바쁘시고 힘드시겠지만 꼭 이후 내용 업데이트 부탁드립니다. ^^

    답글삭제
  5. 진짜 구조 잘 짜시네요 존경합니다..

    답글삭제
  6. 안녕하세요 혹시 실례지만... 질문하나 해도 되겠습니니까?

    답글삭제