2019년 11월 25일 월요일

8. 스와이프된 블럭 제거하기

이번 장에서는 스와이프된 블럭이 3개이상 연속 배치된 경우 해당 블럭을 제거하는 기능을 구현할 것이다.

실행화면은 다음과 같다.
3개 이상 연속 배치된(이하 3매치 또는 3매칭) 블럭이 있는 경우 대상 블럭이 삭제되고, 3매치 블럭이 없는 경우 스와이프 이전 상태로 블럭이 복귀된다.

소스 코드 다운로드

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

이번 장에서 작업할 내용은 다음과 같다.
  1. Stage 에 게임규칙 적용하기
  2. 3매치 블럭 제거하기
  3. 코루틴의 실행 결과 수신하기

스와이프 시작부터 3매치된 블럭 제거까지 전체 과정을 시퀀스 다이어그램으로 표현하면 아래와 같다.
지난 장에서 진행한 스와이프 이벤트 처리 시퀀스다이어그램에 연속해서 스테이지에 게임규칙을 적용하는 시퀀스를 추가하였다.
즉, 블럭이 위치를 교환한 후에 과정을 구현한다.

Note :이전 장에 이어서, 스와이프를 수행하는 CoDoSwipeAction() 코루틴 안에서 실행되는 로직을 계속해서 추가한다.
  1. 보드에 위치한 전체 블럭을 대상으로 게임규칙을 적용하는 EvaluateBoard() Enumerator를 호출한다.
  2. Stage 객체의 Evaluate Enumerator를 호출한다.
  3. Board 객체에게 Evaluate Enumerator를 위임한다.
  4. 전체 블럭의 매치 상태(매칭개수, 매칭상태, 내구도 등)을 계산한다.
  5. 12 호출결과 계산된 매칭 상태를 기반으로 각 블럭에 필요한 로직을 실행한다.
  6. 3매치된 블럭은 보드에서 제거한다.   
  7. 3매치 블럭이 없는 경우, 스와이프 액션을 다시 실행해서 블럭 위치를 원상 복구 시킨다.

코루틴 vs Enumerator

위의 시퀀스 다이어그램을 자세히 살펴보면 스테레오타입으로 <<코루틴>><<IEnumerator>>을 사용하는 것을 볼 수 있다. 이들 함수를 살펴보면, 코루틴 함수와 Enumerator 함수 모두 IEnumerator를 리턴하는 함수 시그니쳐는 동일하다. IEnumerator CoDoSwipeAction(...) vs IEnumerator EvaluateBoard(...) 시퀀스 다이어그램에서는 위 두 가지를 호출 방법에 따라서 구분해서 표기하였다. <<코루틴>>은 StartCoroutine(CoDoSwipeAction(...))과 같이 코루틴을 시작하는 함수로 사용되는 경우이고, <<IEnumerator>>은 코루틴 안에서 yield return EvauateBoard()와 같이 호출되는 Enumerator의 역할로 사용되는 경우이다.

클래스 다이어그램은 아래와 같다.
이번 장에서 새로 도입되는 클래스는 BoardEnumerator 뿐이고, enum 타입이 여러 개 추가될 것이다.

게임규칙 적용하기

스와이프 액션을 실행하면 스와이프 대상 블럭의 위치가 이동되기 때문에 보드에 배치된 블럭의 순서가 변경된다. 그로 인해서 3 매치 블럭이 발생하게 된다.

ActionManager의 CoDoSwipeAction() 코루틴이 종료되기 전에 보드에 아래 게임규칙을 적용하는 Enumerator를 실행한다.
- 3매치 대상 블럭 추출
- 추출된 3매치 블럭 제거
- 3매칭 미발생시 스와이프된 블럭 원상복구

ActionManager의 CoDoSwipeAction() 코루틴이 종료되기 전에 아래 코드를 추가한다.
    IEnumerator CoDoSwipeAction(int nRow, int nCol, Swipe swipeDir)
    {
        if (!m_bRunning)  //다른 액션이 수행 중이면 PASS
        {
            m_bRunning = true;    //액션 실행 상태 ON

            //1. swipe action 수행
            Returnable bSwipedBlock = new Returnable(false);
            yield return m_Stage.CoDoSwipeAction(nRow, nCol, swipeDir, bSwipedBlock);

            //2. 스와이프 성공한 경우 브드를 평가(매치블럭삭제, 빈블럭 드롭, 새블럭 Spawn 등)한다.
            if (bSwipedBlock.value)
            {
                Returnable bMatchBlock = new Returnable(false);
                yield return EvaluateBoard(bMatchBlock);

                //스와이프한 블럭이 매치되지 않은 경우에 원상태 복귀
                if (!bMatchBlock.value)
                {
                    yield return m_Stage.CoDoSwipeAction(nRow, nCol, swipeDir, bSwipedBlock);
                }
            }

            m_bRunning = false;  //액션 실행 상태 OFF
        }
        yield break;
    }
ActionManager.cs
12 - 22 스와이프 이후 보드에 게임규칙(3매치 블럭삭제, 빈블럭 드롭 등)을 적용하는 코루틴을 실행한다.
14 EvaluateBoard() Enumerator 호출 결과를 수신받을 Returnable<bool> 객체를 생성한다.
실행결과 boolean 값을 리턴받으며, 3매칭 블럭이 발견되는 경우 true, 없는 경우 false 값을 가진다.
15 보드에 게임규칙을 적용하는 Enumerator를 실행한다. (시퀀스 10)
18 - 21 3매칭이 발견되지 않은 경우, 스와이프한 블럭을 원상태로 돌려놓는다.(시퀀스 15)

게임 데이터를 총괄하는 Stage 객체에 게임규칙 적용을 위임하는 IEnumerator를 구현한다.(시퀀스 10)
    /*
     * 현상태에서 보드를 평가한다. 즉 보드를 구성하는 블럭에 게임규칙을 적용시킨다.
     * 매치된 블럭은 제거하고 빈자리에는 새로운 블럭을 생성한다.    
     * matchResult : 실행 결과를 리턴받은 클래스 
     *               true : 매치된 블럭 있는 경우, false : 없는 경우
     */
    IEnumerator EvaluateBoard(Returnable matchResult)
    {
        yield return m_Stage.Evaluate(matchResult);
    }
ActionManager.cs
9 스와이프 이후 보드에 게임규칙(3매치 블럭삭제, 빈블럭 드롭 등)을 적용하는 코루틴을 실행한다.

Stage 에 게임규칙 적용하기

게임규칙 적용을 요청받은 Stage 객체는 Board 객체에 Evaluate Enumerator를 호출함으로써 규칙 적용을 위임한다.(시퀀스 11)
    public IEnumerator Evaluate(Returnable matchResult)
    {
        yield return m_Board.Evaluate(matchResult);
    }
Stage.cs

Board에 게임규칙 적용하기

게임정책을 반영하는 대부분의 작업은 게임 데이터(Block, Cell)를 가지고 있는 Board에서 이루어진다.

Board Enumerator 클래스

보드를 구성하는 Block과 Cell 객체를 조회하기 위한 전담 클래스를 추가한다.(위치 : Board/BoardEnumerator.cs)
특정 조건에 해당되는 Block 또는 Cell 객체들에 대한 접근 기능을 제공해주는 역할을 수행한다. 예를 들어, 현재 위치(row, col)에 인접한 Block 객체 정보를 요청하면 상하좌우 대각선 방향의 Block 8개를 순차적으로 접근할 수 있도록 IEnumerator를 제공한다. 세부적인 용도는 진행하면서 추가로 설명하겠다.
namespace Ninez.Stage
{
    public class BoardEnumerator
    {
        Ninez.Board.Board m_Board;

        public BoardEnumerator(Ninez.Board.Board board)
        {
            this.m_Board = board;
        }

        public bool IsCageTypeCell(int nRow, int nCol)
        {
            return false;
        }
    }
}
Board/BoardEnumerator.cs
5 조회 대상인 Board 객체 참조. 생성자에서 설정된다.
12 - 15 케이지(Cage, 철창) 타입의 Cell인지 검사한다. CAGE 타입 Cell은 나중에 추가될 것이다.
케이지에 갇힌 블럭은 3매치시 블럭이 제거되지 않고 케이지가 먼저 제거된다. 현재는 CAGE 타입이 없음으로 false를 리턴한다.

Board 클래스에 멤버 추가하기

Board 클래스에 아래 멤버를 추가한다.
using Ninez.Quest;
using Ninez.Util;

public class Board
{     
    BoardEnumerator m_Enumerator;

    public Board(int nRow, int nCol)
    {
        m_nRow = nRow;
        m_nCol = nCol;

        m_Cells = new Cell[nRow, nCol];
        m_Blocks = new Block[nRow, nCol];

        m_Enumerator = new BoardEnumerator(this);
    }
}
Board.cs
6 BoardEnumerator 객체를 선언한다.
16 BoardEnumerator 객체를 생성한다.

게임규칙 적용하기 : Evaluate()

보드에 게임규칙을 적용하는 Evaluate()를 다음과 같이 구현한다.
  1. 모든 블럭의 매칭 정보(개수, 상태, 내구도 등)를 계산한다.
  2. 매칭된 모든 블럭이 지정된 임무(Quest)를 수행하도록 한다.
  3. 매칭된 블럭을 상태에 따라 Block 객체와 GameObect를 함께 제거한다.
public IEnumerator Evaluate(Returnable matchResult)
{
    //1. 모든 블럭의 매칭 정보(개수, 상태, 내구도 등)를 계산한 후, 3매치 블럭이 있으면 true 리턴 
    bool bMatchedBlockFound = UpdateAllBlocksMatchedStatus();

    //2. 3매칭 블럭 없는 경우 
    if(bMatchedBlockFound == false)
    {
        matchResult.value = false;
        yield break;
    }

    //3. 3매칭 블럭 있는 경우

    //3.1. 첫번째 phase
    //   매치된 블럭에 지정된 액션을 수행한.
    //   ex) 가로줄의 블럭 전체가 클리어 되는 블럭인 경우에 처리 등
    for (int nRow = 0; nRow < m_nRow; nRow++)
        for (int nCol = 0; nCol < m_nCol; nCol++)
        {
            Block block = m_Blocks[nRow, nCol];

            block?.DoEvaluation(m_Enumerator, nRow, nCol);
        }

    //3.2. 두번째 phase
    //   첫번째 Phase에서 반영된 블럭의 상태값에 따라서 블럭의 최종 상태를 반영한.
    List clearBlocks = new List();

    for (int nRow = 0; nRow < m_nRow; nRow++)
    {
        for (int nCol = 0; nCol < m_nCol; nCol++)
        {
            Block block = m_Blocks[nRow, nCol];

            if (block != null)
            {
                if (block.status == BlockStatus.CLEAR)
                {
                    clearBlocks.Add(block);

                    m_Blocks[nRow, nCol] = null;    //보드에서 블럭 제거 (블럭 객체 제거 X)
                }
            }
        }
    }

    //3.3 매칭된 블럭을 제거한다. 
    clearBlocks.ForEach((block) => block.Destroy());

    //3.4 3매칭 블럭 있는 경우 true 설정   
    matchResult.value = true;

    yield break;
}
Board.cs
4 모든 블럭의 매칭 정보(개수, 상태, 내구도 등)을 계산한다. 3매치 블럭이 있으면 true를 리턴한다.
7 - 11 3매치 블럭이 없는 경우 실행을 종료한다.
18 - 24 모든 블럭에게 부여된 임무(퀘스트)를 수행하도록 요청한다. 수행할 퀘스트가 있는 블럭만 응답할 것이다.
28 - 46 클리어 예정인 블럭을 별도 보관하고, 블럭이 위치한 Board에서 제거한다.
49 클리어 에정인 블럭을 삭제되도록 요청한다.(시퀀스 14)

전체 블럭 매칭 상태 업데이트

모든 블럭의 매칭 정보(개수, 상태, 내구도 등)을 계산하는 함수를 아래와 같이 추가한다.
public bool UpdateAllBlocksMatchedStatus()
{
    List matchedBlockList = new List<Block>();   //for GC
    int nCount = 0;
    for (int nRow = 0; nRow < m_nRow; nRow++)
    {
        for (int nCol = 0; nCol < m_nCol; nCol++)
        {
            if (EvalBlocksIfMatched(nRow, nCol, matchedBlockList)) //개별 블록 매칭정보 계산
            {
                nCount++;
            }
        }
    }

    return nCount > 0;
}
Board.cs
3 블럭의 개수 만큼 생성되는 것을 줄이기 위해 Caller에서 생성한 후에 EvalBlocksIfMatched()의 인자로 전달한다.(GC 최소화)
9 전체 블럭에 대해서 블럭의 매치 정보 계산 함수를 호출한다. 3매치가 있으면 true가 리턴된다.
11 3매치가 발생한 개수를 카운트한다.
16 매치가 있으면 true를 리턴한다.

개별 블럭 매칭 상태 계산하기

주어진 위치의 블럭 상하좌우 방향으로 연속해서 위치한 블럭의 종류(Breed)를 조회하여 3매칭 상태를 계산한다. 3매치 블럭의 상태가 NORAL에서 MATCH로 변경된다.
public bool EvalBlocksIfMatched(int nRow, int nCol, List<Block> matchedBlockList)
{
    bool bFound = false;

    Block baseBlock = m_Blocks[nRow, nCol];
    if (baseBlock == null)
        return false;

    if (baseBlock.match != Ninez.Quest.MatchType.NONE || !baseBlock.IsValidate() || m_Cells[nRow, nCol].IsObstracle())
        return false;

    //검사하는 자신을 매칭 리스트에 우선 보관한다.
    matchedBlockList.Add(baseBlock);

    //1. 가로 블럭 검색
    Block block;

    //1.1 오른쪽 방향
    for (int i = nCol + 1; i < m_nCol; i++)
    {
        block = m_Blocks[nRow, i];
        if (!block.IsSafeEqual(baseBlock))
            break;

        matchedBlockList.Add(block);
    }

    //1.2 왼쪽 방향
    for (int i = nCol - 1; i >= 0; i--)
    {
        block = m_Blocks[nRow, i];
        if (!block.IsSafeEqual(baseBlock))
            break;

        matchedBlockList.Insert(0, block);
    }

    //1.3 매치된 상태인지 판단한다
    //    기준 블럭(baseBlock)을 제외하고 좌우에 2개이상이면 기준블럭 포함해서 3개이상 매치되는 경우로 판단할 수 있다
    if (matchedBlockList.Count >= 3)
    {
        SetBlockStatusMatched(matchedBlockList, true);
        bFound = true;
    }

    matchedBlockList.Clear();

    //2. 세로 블럭 검색
    matchedBlockList.Add(baseBlock);

    //2.1 위쪽 검색
    for (int i = nRow + 1; i < m_nRow; i++)
    {
        block = m_Blocks[i, nCol];
        if (!block.IsSafeEqual(baseBlock))
            break;

        matchedBlockList.Add(block);
    }

    //2.2 아래쪽 검색
    for (int i = nRow - 1; i >= 0; i--)
    {
        block = m_Blocks[i, nCol];
        if (!block.IsSafeEqual(baseBlock))
            break;

        matchedBlockList.Insert(0, block);
    }

    //2.3 매치된 상태인지 판단한다
    //    기준 블럭(baseBlock)을 제외하고 상하에 2개이상이면 기준블럭 포함해서 3개이상 매치되는 경우로 판단할 수 있다
    if (matchedBlockList.Count >= 3)
    {
        SetBlockStatusMatched(matchedBlockList, false);
        bFound = true;
    }

    //계산위해 리스트에 저장한 블럭 제거
    matchedBlockList.Clear();

    return bFound;
}
Board.cs
1 함수를 선언한다.
matchedBlockList : 계산 동안 매칭 블럭을 임시로 보관하는 리스트, GC 최소화를 위해 Caller에서 제공한다.
5 주어진 위치에서 대상 블럭을 구한다.
9, 10 계산이 필요없는 블럭인 경우 리턴한다.
13 검사 대상인 블럭을 매칭 블럭 목록에 추가한다. 초기 매칭 블럭 개수 : 1
19 - 26 대상 블럭 기준 오른쪽 방향에 같은 종류(Breed)의 블럭이 있으면 리스트에 보관한다.
1개 이상 같은 종류 블럭이 있을 수 있다.
29- 36 대상 블럭 기준 왼쪽 방향에 같은 종류(Breed)의 블럭이 있으면 리스트에 보관한다.
1개 이상 같은 종류 블럭이 있을 수 있다.
40 - 44 가로 방향 매칭 검사 결과를 블럭에 반영한다.
3개이상 매칭된 경우 리스트에 보관된 모든 블럭을 매치 상태(BlockStatus.MATCH)로 설정한다.
46 - 49 가로 방향 검사후 계산을 위해 보관한 블럭을 리스트에서 제거하고 세로방향 계산을 위해 기준 블럭을 다시 리스트에 보관한다.
52 - 59 대상 블럭 기준 위쪽 방향에 같은 종류(Breed)의 블럭이 있으면 리스트에 보관한다.
1개 이상 같은 종류 블럭이 있을 수 있다.
대상 블럭 기준 아래쪽 방향에 같은 종류(Breed)의 블럭이 있으면 리스트에 보관한다.
1개 이상 같은 종류 블럭이 있을 수 있다.
73 - 77 세로 방향 매칭 검사 결과를 블럭에 반영한다.
3개이상 매칭된 경우 리스트에 보관된 모든 블럭을 매치 상태(BlockStatus.MATCH)로 설정한다.
82 3매치 블럭이 있으면 ture, 없으면 false를 리턴한다.
긴 코드에 비해 내용은 단순하다. 상하좌우 블럭을 조사해서 같은 블럭이 있으면 상태를 MATCH로 변경한다.

매치 블럭 상태 설정하기

리스트에 보관된 블럭의 상태를 MATCH 상태로 설정한다.
void SetBlockStatusMatched(List<Block> blockList, bool bHorz)
{
    int nMatchCount = blockList.Count;
    blockList.ForEach(block => block.UpdateBlockStatusMatched((MatchType)nMatchCount));
}
Board.cs
1 함수를 선언한다.
bHorz : 리스트에 보관된 블럭의 배치 방향. 가로(Horizontal) 방향이면 true, 세로방향이면 false 값을 갖는다.
4 리스트에 보관에 전체 블럭에 대해서 상태 업데이트를 요청한다.

Cell 클래스 조회 메소드 추가

Cell이 블럭 이동/매치 등에 장애가 되는 종류인지 조회한다. EMPTY가 아니면서 블럭이 올 수 없는 Cell이 이에 해당된다. 예를 들어, 주변 블럭을 2번 제거하면 Cell에 비로소 블럭이 올 수 있는 상태로 변경되는 경우가 여기에 해당된다.(진행하면서 추가될 것이다)
public bool IsObstracle()
{
    return type == CellType.EMPTY;
}
Cell.cs
현재는 EMPTY 블럭인 경우에만 true를 리턴한다.

블럭에 게임규칙 적용하기

먼저, 블럭에 새로 추가할 상태를 정의한다.

블럭 상태 enum 타입 선언

public enum BlockStatus
{
    NORMAL,                 // 기본 상태
    MATCH,                  // 매칭 블럭 있는 상태
    CLEAR                   // 클리어 예정 상태
}

public enum BlockQuestType  //블럭 클리어 발동 효과
{
    NONE = -1,
    CLEAR_SIMPLE = 0,       // 단일 블럭 제거
    CLEAR_HORZ = 1,         // 세로줄 블럭 제거 (내구도 -1)  ->  4 match 가로형
    CLEAR_VERT = 2,         // 가로줄 블럭 제거 -> 4 match 세로형
    CLEAR_CIRCLE = 3,       // 인접한 주변영역 블럭 제거 -> T L 매치 (3 x 3, 4 x 3)
    CLEAR_LAZER = 4,        // 지정된 블럭과 동일한 블럭 전체 제거 --> 5 match
    CLEAR_HORZ_BUFF = 5,    // HORZ + CIRCLE 조합
    CLEAR_VERT_BUFF = 6,    // VERT + CIRCLE 조합    
    CLEAR_CIRCLE_BUFF = 7,  // CIRCLE + CIRCLE 조합
    CLEAR_LAZER_BUFF = 8    // LAZER + LAZER 조합
}
BlockDefine.cs
각 타입의 용도는 소스에 표기된 주석에 자세히 표기하였다.

BlockQuestType은 블럭이 매치된 후에 제거될 때 블럭이 수행해야하는 임무(Quest)이다.
예를 들어, CLEAR_HORZ 타입을 가지는 블럭이 3매치되어 보드에서 제거되는 경우 블럭에 부여된 임무인 전체 행(row)의 모든 블럭을 제거한다. 기본형인 CLEAR_SIMPLE 타입인 경우 해당 블럭 자신만을 보드에서 제거하는 임무(Quest)이므로 블럭 한 개만 제거된다.

블럭의 상태 변화를 상태 다이어그램으로 표기해 보았다, 다음 과정으로 상태가 전이된다.
NORMAL 블럭 생성시 설정, 블럭 평가(DoEvaluation) 결과 내구도가 '0'이 아닌 경우 설정
MATCH Board.UpdateAllBlocksMatchedStatus() 호출 결과 블럭이 3매칭 되는 경우에 설정
CLEAR 블럭 평가(DoEvaluation) 결과 내구도가 '0' 인 경우에 설정


블럭 Match 타입 정의하기

블럭의 매칭 형태를 구분하기 위한 enum 타입을 정의한다.(위치 : Scripts/Quest/QuestDefine.cs)
namespace Ninez.Quest
{
    public enum MatchType
    {
        NONE        = 0,
        THREE       = 3,    // 3 Match
        FOUR        = 4,    // 4 Match     -> CLEAR_HORZ 또는 VERT 퀘스트
        FIVE        = 5,    // 5 Match     -> CLEAR_LAZER 퀘스트
        THREE_THREE = 6,    // 3 + 3 Match -> CLEAR_CIRCLE 퀘스트 
        THREE_FOUR  = 7,    // 3 + 4 Match -> CLEAR_CIRCLE 퀘스트
        THREE_FIVE  = 8,    // 3 + 5 Match -> CLEAR_LAZER 퀘스트
        FOUR_FIVE   = 9,    // 4 + 5 Match -> CLEAR_LAZER 퀘스트
        FOUR_FOUR   = 10,   // 4 + 4 Match -> CLEAR_CIRCLE 퀘스트
    }

    static class MatchTypeMethod
    {
        public static short ToValue(this MatchType matchType)
        {
            return (short)matchType;
        }

        /*
         * 블럭의 매칭 결과를 조합한다
         */
        public static MatchType Add(this MatchType matchTypeSrc, MatchType matchTypeTarget)
        {
            if (matchTypeSrc == MatchType.FOUR && matchTypeTarget == MatchType.FOUR)
                return MatchType.FOUR_FOUR;

            return (MatchType)((int)matchTypeSrc + (int)matchTypeTarget);
        }
    }
}
Quest/QuestDefine.cs
1 QEUST namespace를 선언한다.
3 - 14 매칭 형태를 구분하는 타입을 선언한다. 소스 주석 및 아래 그림 참고.
18 - 21 MatchType enum 타입을 숫자(short)로 변환한 값을 구한다.
26 - 32 매치 타입을 조합하여 새로운 확장된 매치타입을 만든다.
MatchType.THREE(3) + MatchType.THREE(3)을 조합하여 MatchType.THREE_THREE(6)을 만든다.
MatchType에 해당되는 숫자를 더함으로써 조합된 새로운 MatchType이 만들어 진다. 3 + 3 = 6
28, 29 4 + 4 형 MatchType 조합으로 3 + 5 MatchType과 더하기 결과가 중복되기 때문에 예외 처리한다.
아래는 매치 타입별로 가능한 형태를 일부 표현한 그림으로 스와이프 액션으로 나타날 수 없는 형태도 포함되어 있다. 이러한 매치 형태는 블럭이 제거된 후 떨어지는 드롭으로 채워지는 경우에 나타날 수 있다.

Block 매칭상태 멤버 추가하기

using Ninez.Quest;

public class Block
{
    public BlockStatus status;
    public BlockQuestType questType;
    public MatchType match = MatchType.NONE;
    public short matchCount;

    int m_nDurability;                          //내구도, 0이되면 제거
    public virtual int durability
    {
        get { return m_nDurability; }
        set { m_nDurability = value; }
    }
}
Block.cs
1, 2 namespace 사용 추가
5 블럭 상태 (NORMAL, MATCH, CLEAR)
6 블럭 쿼스트 타입
7 MatchType, Evaluation 동안 블럭상태 계산에 사용된다.
8 연속된 블럭 개수, Evaluation 동안 블럭상태 계산에 사용된다.
10 - 15 블럭 내구도.
블럭의 생명(life)로 3매치 마다 내구도가 1씩 감소하며, 내구도가 '0'이 되면 블럭이 제거(Destroy)된다.
일반 블럭의 디폴트 내구도는 '1'이며, 세번 매치되어야 제거되는 블럭이라면 내구도를 '3'으로 설정하면 된다.

계속해서 아래와 같이 생성자에 추가한 멤버에 대한 초기화 코드를 추가한다.
public Block(BlockType blockType)
{
    m_BlockType = blockType;

    status = BlockStatus.NORMAL;
    questType = BlockQuestType.CLEAR_SIMPLE;
    match = MatchType.NONE;
    m_Breed = BlockBreed.NA;

    m_nDurability = 1;
}
Block.cs
위에서 선언한 멤버들을 디폴트 값으로 초기 설정한다.

블럭 평가하기

Board의 Evaluation으로 업데이트된 블럭 자신의 매칭 상태를 평가해서 현재 상태에 부여된 동작을 수행한다.
- BlockQuestType.CLEAR_SIMPLE 인 경우, 내구도(duration)을 1 감소시키킨다.
- 내구도가 '0'이되면 상태를 BlockStatus.CLEAR로 변경한다. (MATCH 상태에서 CLEAR 상태로 전이)
- 매치되지 않은 블럭은 상태 및 매칭 정보를 초기화한다.
public bool DoEvaluation(BoardEnumerator boardEnumerator, int nRow, int nCol)
{
    Debug.Assert(boardEnumerator != null, $"({nRow},{nCol})");

    if (!IsEvaluatable())
        return false;

    //1. 매치 상태(클리어 조건 충족)인 경우
    if (status == BlockStatus.MATCH)
    {
        if (questType == BlockQuestType.CLEAR_SIMPLE || boardEnumerator.IsCageTypeCell(nRow, nCol)) //TODO cagetype cell 조건이 필요한가? 
        {
            Debug.Assert(m_nDurability > 0, $"durability is zero : {m_nDurability}");

            //보드에 블럭 클리어 이벤트를 전달한다.
            //블럭 클리어 후에 보드에 미치는 영향을 반영한다.
            //if (boardEnumerator.SendMessageToBoard(BlockStatus.CLEAR, nRow, nCol))
            durability--;
        }
        else //특수블럭인 경우 true 리턴
        {
            return true;
        }

        if (m_nDurability == 0)
        {
            status = BlockStatus.CLEAR;
            return false;
        }
    }

    //2. 클리어 조건에 아직 도달하지 않는 경우 NORMAL 상태로 복귀
    status = BlockStatus.NORMAL;
    match = MatchType.NONE;
    matchCount = 0;

    return false;
}
Block.cs
1 메소드를 선언한다.
boardEnumerator : 보드 정보에 접근할 수 있는 Enumerator 객체
return : 특수블럭인 경우 true, 그렇지 않는 경우 false
9 - 30 매칭 상태인 블럭인 경우을 처리한다.
11 - 19 단독 처리되는 기본 블럭인 경우에 내구도는 '1' 감소키킨다.
20 - 23 특수블럭인 경우 true를 리턴한다.
25 - 29 내구도가 '0'이 되면 블럭 상태를 CLEAR로 설정한다.
33 - 35 매치된 블럭이 아니거나, 내구도가 '0'이 아닌 경우 다시 플레이에 사용되도록 상태를 초기화한다.


블럭 매칭 상태 업데이트

블럭의 매칭 상태를 업데이트한다. 이미 매치된 블럭인 경우에 기존 상태에 추가되는 상태가 더해진다.
예를 들어, MatchType.THREE 상태에서 파라미터로 MatchType.FOUR 상태를 수신하면 MatchType.THREE_FOUR 상태가 된다.
public void UpdateBlockStatusMatched(MatchType matchType, bool bAccumulate = true)
{
    this.status = BlockStatus.MATCH;

    if (match == MatchType.NONE)
    {
        this.match = matchType;
    }
    else
    {
        this.match = bAccumulate ? match.Add(matchType) : matchType; //match + matchType
    }

    matchCount = (short)matchType;
}
Block.cs
1 메소드르 정의한다.
matchType : 새로 추가될 매치 타입
bAccumulate : true 이면 매치 상태가 누적되고, false 이면 대치된다
5 - 8 매치상태가 아닌 블럭인 경우, MATCH 상태로 설정한다.
9 - 12 기존에 매치 상태인 경우, 누적(Accumulate) 모드이면 Add하고 그렇지 않으면 새로운 매치타입으로 대치한다.
14 매치 개수를 업데이트 한다.


Board에 상태 조회함수 추가

Evaluation 대상이 되는 블럭인지 조회하는 함수를 추가한다.
public bool IsEvaluatable()
{
    //이미 처리완료(CLEAR) 되었거나, 현재 처리중인 블럭인 경우
    if (status == BlockStatus.CLEAR || !IsMatchableBlock())
        return false;

    return true;
}
Block.cs
4, 5 제거 예정인 블럭이거나 매칭될 수 없는 블럭인 경우 false를 리턴한다.

매칭된 블럭 제거하기

블럭을 제거할 때 사용하는 메소드를 추가한다.
public virtual void Destroy()
{
    Debug.Assert(blockObj != null, $"{match}");
    blockBehaviour.DoActionClear();
}
Block.cs
블럭을 제거한다는 것은 씬에 보여지는 Block GameObject를 제거한다는 의미이다.
Block 객체가 참조하는 GameObject에게 Blaock GameObject 제거를 위임한다. Block 객체는 GC에 의해서 제거될 것이다. (나중에 오브젝트풀을 사용해서 제거되지 않고 재사용 되도록 할 것이다)

Block GameObject 제거하기

public void DoActionClear()
{
    Destroy(gameObject);
}
BlockBehaviour.cs
블럭 Game Object를 제거(Destroy)하는 API를 사용해서 Game Object를 제거한다.

실행하기

플레이버튼(▶)을 클릭해서 플레이 결과를 확인해 보자.
3개 이상 연속 배치된(이하 3매치 또는 3매칭) 블럭이 있는 경우 대상 블럭이 삭제되고, 3매치 블럭이 없는 경우 스와이프 이전 상태로 블럭이 복귀되는 것을 볼 수 있다.

삭제되어 비어 있는 위치로 스와이프 액션을 적용하면 ASSERT 가 발생할 것이다. 의도된 것이며 다음 장에서 개선될 것이다.

다음 장에서는 비어있는 블럭을 채우는 과정을 구현해 보겠습니다.
위에 있는 블럭이 떨어지고 새로운 블럭이 빈자리를 채워나가는 과정을 진행하겠습니다.

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

댓글 없음:

댓글 쓰기