2019년 11월 23일 토요일

2. 플레이 보드(Board) 만들기

이번에는 9 x 9 보드 구성에 필요한 객체들의 관계를 구체화하고, 보드를 구성하는 Cell 81개(9 x 9)를 생성한 후에 화면에 출력해 보겠습니다. 그리고, StageController - Stage - Cell/Block 사이의 관계를 명확하게 구성하도록 하겠습니다.
구현될 화면은 아래와 같습니다.


소스 코드 다운로드

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

Stage 구성하기

Stage는 Board를 가지고 있고, Board는 주어진 크기(row * column)의 개수 만큼의 Cell과 Block을 각각 가지고 있는 구조를 만들 것이다.

이전 장에서 만든 객체들의 관계를 아래와 같이 구성하고, 보드를 구성하는 데이터(Cell, Block)를 저장하도록 코드를 추가할 것이다.

StageBuilder 객체를 추가하고, StageController - Stage - Cell/Block 사이의 관계를 구성한 것을 볼 수 있다. StageBuilder는 스테이지 파일에서 데이터(미션, 사용가능 아이템, Cell, Block 구성정보 등)을 읽어서 Stage를 구성하는 역할을 담당할 예정이다.

이번 장에서는 스테이지 파일을 사용하지 않고 Cell과 Block 정보만을 임의로 생성한 데이터를 사용한다.(파일은 다음장에서 지원한다)

Cell 생성자

public Cell(CellType cellType)
{
    m_CellType = cellType;
}
Cell.cs
Cell 클래스에 CellType을 파라미터로 가지는 생성자를 추가한다.
파라미터로 전달된 타입 정보를 m_CellType에 저장한다.

Block 생성자

public Block(BlockType blockType)
{
    m_BlockType = blockType;
}
Block.cs
Block 클래스에 BlockType을 파라미터로 가지는 생성자를 추가한다.
인자로 전달된 타입 정보를 m_BlockType에 저장한다.

StageController 초기화

씬이 시작될 때 플레이에 필요한 Stage 정보를 구성하기 위해서 InitStage()와 BuildStage()를 추가한다.
void InitStage()
{
    if (m_bInit)
        return;

    m_bInit = true;

    BuildStage();

    m_Stage.PrintAll();
}

void BuildStage()
{
    //1. Stage를 구성한다.
    m_Stage = StageBuilder.BuildStage(nStage : 0, row : 9, col : 9);
}
StageController.cs
8 초기화 과정에서 BuildStage() 메소드를 호출한다.
10 구성정보를 확인하기 위한 디버깅 코드. Board를 구성하는 Cell과 Block 정보를 출력한다.
13 - 17 Stage를 생성/구성하는 역할을 하는 StageBuilder에 스테이지 구성을 요청한다.


StageBuilder

StageBuilder 클래스는 Stage 객체를 생성하고, Board를 구성하고 있는 Cell과 Block GameObect를 생성(Instantiate)해서 Board GameObject에 제공하는 역할을 담당한다.
(다음 장에서 스테이지 파일에서 정보를 읽어 Board를 구성하도록 할 것이다)
using Ninez.Board;

namespace Ninez.Stage
{
    public class StageBuilder
    {
        int m_nStage;

        public StageBuilder(int nStage)
        {
            m_nStage = nStage;
        }

        public Stage ComposeStage(int row, int col)
        {
            //1. Stage 객체를 생성한다.
            Stage stage = new Stage(this, row, col);

            //2. Cell,Block 초기 값을 생성한다.
            for (int nRow = 0; nRow < row; nRow++)
            {
                for (int nCol = 0; nCol < col; nCol++)
                {
                    stage.blocks[nRow, nCol] = SpawnBlockForStage(nRow, nCol);
                    stage.cells[nRow, nCol] = SpawnCellForStage(nRow, nCol);
                }
            }

            return stage;
        }

        Block SpawnBlockForStage(int nRow, int nCol)
        {
            return new Block(BlockType.BASIC);
        }

        Cell SpawnCellForStage(int nRow, int nCol)
        {
            return new Cell(CellType.BASIC);
        }

        public static Stage BuildStage(int nStage, int row, int col)
        {
            StageBuilder stageBuilder = new StageBuilder(0);
            Stage stage = stageBuilder.ComposeStage(row, col);

            return stage;
        }
    }
}
StageBuilder.cs
43 - 49 static 메소드로서 StageBuilder 객체를 생성하고, Stage를 구성하는 Cell, Block을 생성하는 ComposeStage()를 호출해서 Stage 객체를 생성한다.
StageController 초기화 코드에서 스테이지를 구성하기 위해서 호출한다.
7 플레이중인 스테이지 번호를 저장하는 멤버를 선언한다.(파일에서 스테이지 정보 로딩시 사용한다)
9 - 12 생성자. 플레이하는 스테이지 번호를 저장한다.
14 - 30 입력받은 크기(행:row, 열:col)의 Board를 가지는 Stage 객체를 생성한 후, Board를 구성하는 Cell과 Block 객체를 생성한다.
예를들어 9 x 9 보드에 대해서 Cell과 Block은 각각 81개 객체가 생성된다.
32 - 40 지정된 위치(row, col)에 Cell과 Block 객체를 생성한 후 리턴한다.
초기값으로 모두 CellType.BASIC과 BlockType.BASIC을 갖도록 한다.
(스테이지 파일로 부터 동적으로 생성하도록 변경할 것이다)

지금까지의 과정을 시퀀스 다이어그램으로 표현하면 아래와 같다.

시퀀스 다이어그램은 전체 흐름을 한눈에 볼 수 있는 장점이 있다.
세부적으로 모든 메소드를 표현하지 않고 주요 흐름 위주로 표현하면 전체 로직 및 객체간의 관계를 이해하는데 도움이 된다.
  1. StageBuilder에게 스테이지 정보 구성을 요청하는 static 메소드를 호출한다. 호출결과 Stage 객체를 리턴 받는다.
  2. StageBuilder 객체 인스턴스를 생성(new)한다.
  3. Stage 정보를 구성하는 ComposeStage() 함수를 호출한다.
  4. Stage 객체 인스턴스를 생성(new)한다.
  5. Stage에서 Board 객체를 생성(new)한다.
    이때, Board를 구성하는 Cell 과 Block을 저장할 수 있는 배열을 각각 생성(new)한다.
  6. 보드를 구성하는 모든 행과 열에 대해서 Block 객체를 요청한다.
  7. 6의 요청에 따라 Block 객체를 생성한다.
  8. 보드를 구성하는 모든 행과 열에 대해서 Cell 객체를 요청한다.
  9. 8의 요청에 따라 Cell 객체를 생성한다.
간단히 요약하면, StageBuiler에게 Stage 구성을 요청하면 Stage 객체를 생성 한후, Stage를 구성하는 Board를 구성하는 흐름이다.

UML

UML(Unified Modeling Lanaguage)도 C#, Java와 같은 언어(Language)이다. 다른 점은 프로그래밍을 위한 언어가 아닌 모델링을 위한 언어이다. 설계도를 그리기 위한 언어이며 의사소통을 위한 도구로 사용된다. 소프트웨어 설계에 주로 많이 사용하지만, 다양한 용도로 사용될 수 있다. 예를 들어, 드라마의 인물 관계 및 갈등 구조등을 UML로 표현할 수 있으며, 근무하고 있는 조직의 팀 간의 관계 및 역할 등을 UML로 표현할 수도 있다. 활용하는 방법은 무궁무진하며 표현하는 주제의 목적에 맞게 활용하면 된다. UML 또한 언어이기때문에 기본적인 Syntax가 주어지고 그에 따르면 되지만 방대한 표준을 이해하고 사용하기는 쉽지 않다. BingleStarter 프로젝트에서는 클래스 다이어그램과 시퀀스 다이어그램을 이용해서 설계 및 커뮤케이션을 할 것이다. 유니티 프로젝트에서 사용되는 요소들 간의 관계를 UML로 표현하기 위한 나름의 규칙을 세워서 표현할 것이다.

Stage 데이터 디버깅 출력하기

아직까지 화면에 출력되는 결과가 없기 때문에 Board를 구성하는 Cell과 Block 정보를 확인할 수 있는 간단한 디버깅 코드를 추가해서 생성된 정보를 로그로 출력해보자.
public void PrintAll()
{
    System.Text.StringBuilder strCells = new System.Text.StringBuilder();
    System.Text.StringBuilder strBlocks = new System.Text.StringBuilder();

    for (int nRow = maxRow -1; nRow >=0; nRow--)
    {
        for (int nCol = 0; nCol < maxCol; nCol++)
        {
            strCells.Append($"{cells[nRow, nCol].type}, ");
            strBlocks.Append($"{blocks[nRow, nCol].type}, ");
        }

        strCells.Append("\n");
        strBlocks.Append("\n");
    }

    Debug.Log(strCells.ToString());
    Debug.Log(strBlocks.ToString());
}
Stage.cs

실행 후 결과 확인하기

플레이버튼(▶)을 클릭해서 실행 한 후, [Consol] 탭에서 로그를 확인해 보자.
9 x 9 배열의 Cell과 Block 구성정보를 출력한 결과이다.

Cell 구성정보

BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC,

Block 구성정보

BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC,

Board를 구성하는 각 Cell 과 Block의 종류를 모두 BASIC으로 지정했기 때문에 모든 Cell/Block이 BASIC 타입으로 출력되었다.
아래와 같이 Cell의 초기 종류를 row와 col이 동일한 경우에 EMPTY 타입으로 설정한 후 결과를 확인해보자.
Cell SpawnCellForStage(int nRow, int nCol)
{
    return new Cell(nRow == nCol ? CellType.EMPTY : CellType.BASIC);
}

플레이버튼(▶)을 클릭해서 실행 한 후, [Consol] 탭에서 로그를 확인해보자.
EMPTY Cell이 행과 열이 일치하는 대각선 방향으로 위치한 것(노란색 표기)을 확인할 수 있다.

Cell 구성정보

BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, EMPTY, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, EMPTY, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, EMPTY, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, EMPTY, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, EMPTY, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, EMPTY, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, EMPTY, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, EMPTY, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, EMPTY, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC, BASIC,

계속해서 Cell을 표현하는 이미지를 출력해서 Board 구성 정보를 화면에 출력해보자.

Cell 스프라이트 출력하기

보드를 구성하는 각 Cell의 CellType에 어울리는 Sprite를 출력할 것이다.
실행된 화면은 아래와 같다.
위의 로그에 출력된 Cell에 해당되는 이미지가 렌더링된 결과를 볼 수 있다. 행과 열의 번호가 같은 경우에는 비어있는 것을 확인할 수 있다.

작업을 시작하기에 앞서 씬에서 GameObject의 속성을 아래와 같이 설정한다.
  • MainCamera | Camera
    Background : R:228, G:250, B:173, A:255
    Size : 8.1777  
  • Board |  Tranform
    Position : X:0, Y:0, Z:0     보드가 화면 중앙(원점, 0, 0)에 놓이도록 지정한다.
게임에 사용할 배경색을 설정하고, 카메라 size를 8.1777 로 입력한다. (보드 크기를 9 x 9로 고정해서 개발하기 때문에 개발편의상 8.1777로 설정했으며, 다른 값으로 입력해도 실행에 영향을 주지는 않는다.)
Board GameObject는 앞으로 추가될 Cell GameObject와 Block GameObject의 부모가 되는 컨테이너로 사용될 것이다(아래 그림 참고).
화면 중앙에 위치하도록 원점(0,0)으로 설정합니다.
Cell GameObject를 차일드로 가지고 있는 Board GameObject

Cell 이미지 준비하기

  1. 여기에서 이미지를 다운로드한다. (cell_bg_basic.png)
  2. Assets/Sprites/Board/Cells 폴더에 복사한다. 
  3. 이미지 선택 후에 Inspector에서 PixelsPerUnit을 '72'로 설정한다.

Note: PixelsPerUnit

스프라이트가 화면에 출력될 때 기본단위(Unit) 즉, 1미터에 렌더링 되는 픽셀수를 의미한다. 유니티에서 기본값은 100으로 설정되어 있다. 예를 들어, 200 x 200 스프라이트가 화면에 렌더링되면 2미터 x 2미터 영역을 차지한다.

Cell 배경(cell_bg_basic.png)에 사용되는 스프라이트는 74 x 74 크기를 가지고 있다. 그러나 스프라이트 Import Settings에서 PixelsPerUnit을 '72'로 설정했다.

이유는 Cell 배경 스프라이트는 2pixel 크기의 테두리를 가지고 있다. PixelsPerUnit을 '72'로 설정 함으로서 테두리 일부와 주변 Cell과 겹치는 부분이 발생하도록 했다. 아래 그림은 PixelsPerUnit을 '74'로 설정한 경우와 차이점을 보여주고 있다.

'72'로 설정한 경우, 주변 Cell과 겹치는 부분으로 인해서 좀 더 자연스럽게 보여지게 된다.

Cell Prefab 만들기

Cell 에 사용할 GameObject를 Prefab으로 만들어보자.

Cell GameObject 만들기

다음 과정으로 Cell GameObject를 생성한다.
  1. Board GameObject의 차일드로 Empty GameObject를 생성 한 후 이름을 'Cell'로 변경한다.
  2. 생성한 Cell GameObject를 선택한 후, Inspector에서 [Add Component] 버튼을 클릭한 다음 SpriteRenderer 컴포넌트를 추가한다.
  3. Cell 배경 스프라이트(Sprites/Cells/cell_bg_basic)을 드래그해서 2에서 추가한 SpriteRenderer의 Sprite 속성에 드래그한다.
  4. Scripts/Board/Cells 폴더에 스크립트 파일(CellBehaviour.cs)을 추가한다.
  5. 4에서 추가한 CellBehaviour를 Cell GameObject의 컴포넌트로 추가한다.

화면 중앙 (0,0,0)에 Cell 스프라이트가 렌더링 된 것을 확인할 수 있다.
Cell을 3개 복제(Duplicate)해서 아래와 같이 Transform의 Position을 설정 한 후에 테두리가 자연스럽게 보이는지 확인해 보자.
  • Cell(1) : Position X,Y : (1, 0)
  • Cell(2) : Position X,Y : (0, 1)
  • Cell(3) : Position X,Y : (1, 1)
[Game] 탭에서 자연스럽게 경계가 겹치는 렌더링 결과를 확인해 볼 수 있다. (Sprite의 PixelsPerUnit를 '74'변경하면 겹치는 부분이 두껍게 렌더링되는 것을 볼 수 있다)


씬에서 Cell(1), Cell(2), Cell(3) GameObject를 삭제하고 Cell GameObject 한개만 남기고 Prefab을 등록해보자.

Cell Prefab 등록하기

Cell GameObject를 플레이 시에 동적으로 생성할 수 있도록 Prefab으로 등록한다.
  1. 씬에서 Cell GameObject를 선택한다.
  2. Cell GameObject를 드래그해서 Assets/Prefabs 폴더로 드롭한다.
  3. Assets/Prefabs 폴더에 Prefab이 생성된다( 드래그한 GameObject와 동일한 이름 Cell로 등록된다.)
  4. 씬의 Cell GameObject 텍스트 색상이 파란색으로 변경되는 것을 볼 수 있다.
    (Prefab으로부터 생성된 GameObject는 파란색으로 구분해서 표시된다.)


Cell GameObject를 Board에 추가하기

이제부터 Cell Prefab을 씬이 플레이될 때 화면에 출력되도록 해보자. Cell Prefab으로 부터 생성(Instantiate)된 Cell GameObject를 Board GameObject의 차일드로 등록하는 과정을 보여줄 것이다.

Cell GameObject가 Prefab으로 부터 생성되면 아래와 같은 구조를 갖게 된다.


Cell GameObject가 Board GameObject의 차일드로 구성되며, Prefab으로 부터 Instantiate되는 것을 알 수 있다. Board 객체에 저장된 Cell 객체는 Cell Prefab으로 부터 생성된 Cell GameObject의 CellBehaviour 컴포넌트를 참조하고 있으며, 이러한 참조 관계를 표기하기 위해서 Aggregation()으로 표기했다.

UML : Composition vs Aggregation

UML 문서를 읽어 보면 Compositon과 Aggregation이 이해하기 쉽지않은 설명으로 되어있다. UML이 프로그래밍 아키텍쳐의 용도에 한정해서 개발된 것이 아니기 때문에 여러가지 예를 들어서 설명하는 내용이 잘 와 닿지 않는다.
여기에서는 이 두가지 용도를 단순하게 명확히 구분해서 사용할 것이다.
Composition 관계 A객체와 B객체가 Life Cycle을 같이 하는 경우, 즉 A객체가 없어질 때 B객체가 더이상 필요하지 않는 경우에 사용한다.
주로 m_Cell = new Cell()와 같이 객체 생성자 또는 메소스에서 생성해서 멤버에 보관하는 경우에 해당된다.
Aggregation 관계 A객체가 B객체를 참조하고 있지만 A객체가 없어지더라고 B객체를 다른 C객체가 여전히 참조하고 있는 경우에 사용한다. 예를 들어, SetCell(Cell cell) 과 같이 메소드를 통해 이미 존재하는 객체를 참조로 받아서 멤버에서 레퍼런스하는 경우가 해당된다.
구체적인 사용예는 진행하면서 자연스럽게 보게 될 것이다.

Cell 오브젝트를 생성하는 과정은 아래와 같다.

  1. 씬이 로드될 때 Board GameObject에 등록된 StageController가 Stage 객체에게 스테이지 구성을 요청한다.
  2. 스테이지 구성을 요청받은 Stage 객체는 소유하고 있는 Board 객체에게 스테이지 구성을 위임한다.
  3. Board 객체는 Cell 객체에게 Cell GameObject 생성을 요청한다. (Prefab, Container 정보제공)
  4. Cell 객체는 전달된 정보(Prefab, Container)를 이용해서 Cell GameObject를 생성한다.
  5. 생성된 Cell GameObject가 Cell 객체를 참조하도록한다.

Stage 객체에 구성 요청하기

    public class StageController : MonoBehaviour
    {
        [SerializeField] Transform m_Container;
        [SerializeField] GameObject m_CellPrefab;
        [SerializeField] GameObject m_BlockPrefab;

        void BuildStage()
        {
            //1. Stage를 구성한다.
            m_Stage = StageBuilder.BuildStage(nStage : 0, row : 9, col : 9);

            //2. 생성한 stage 정보를 이용하여 씬을 구성한다.
            m_Stage.ComposeStage(m_CellPrefab, m_BlockPrefab, m_Container);
        }
    }
StageController.cs
3 Cell GameObject가 씬에 추가될 때 부모(컨테이너) 역할을 담당할 Game Object
4 Cell Prefab, Cell GameObject를 생성(Instanticate)하는데 사용한다.
5 Block Prefab, Block GameObject를 생성(Instanticate)하는데 사용한다
13 Stage 객체에게 주어진 리소스(cell/block prefab, 컨테이너) 를 이용해서 스테이지를 구성하도록 요청한다.

[SerializedField] Attribute

MonoBehaviour를 상속받은 클래스가 GameObject의 컴포넌트로 등록되면, public 멤버들이 Inspector에 보여지고 유니티 에디터에서 값을 설정할 수 있다. 그러나 public으로 지정하면 해당 객체를 참조하는 다른 객체에서 멤버에 접근이 가능하다는 단점이 있다. 이러한 단점을 보완하기 위해서 유니티는 SerializedField Attribute를 제공한다.
[SerializedField]로 선언하면 private임에도 불구하고 Inspector에서 값을 설정할 수 있도록 노출된다.

Board 객체에 구성 요청하기


internal void ComposeStage(GameObject cellPrefab, GameObject blockPrefab, Transform container)
{
     m_Board.ComposeStage(cellPrefab, blockPrefab, container);
}
Stage.cs
2 - 4 Board 객체에게 보드를 구성하도록 요청한다.
보드 구성에 사용할 cell/block prefab, 컨테이너를 전달한다.

Board 구성하기

인자로 전달된 Prefab과 컨테이너 GameObject를 이용해서 Cell GameObject를 생성하고, 지정된 위치로 Cell GameObject의 위치를 이동한다.
public class Board
{
    //   -- 중략 --
    Transform m_Container;
    GameObject m_CellPrefab;
    GameObject m_BlockPrefab;

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

        //2. Cell, Block Prefab을 이용해서 Board에 Cell/Block GameObject를 추가한다.
        float initX = CalcInitX(0.5f);
        float initY = CalcInitY(0.5f);
        for (int nRow = 0; nRow < m_nRow; nRow++)
            for (int nCol = 0; nCol < m_nCol; nCol++)
            {
                Cell cell = m_Cells[nRow, nCol]?.InstantiateCellObj(cellPrefab, container);
                cell?.Move(initX + nCol, initY + nRow);
            }
    }

    public float CalcInitX(float offset = 0)
    {
        return -m_nCol / 2.0f + offset;
    }

    public float CalcInitY(float offset = 0)
    {
        return -m_nRow / 2.0f + offset;
    }
}
Board.cs
4 - 6 Cell/Block Prefab, 컨테이너 GameObject를 참조하기 위한 멤버를 선언한다.
8 - 24 주어진 리소스(cell/block prefab, 컨테이너)를 이용해서 보드를 구성한다.
11 - 13 Cell/Block Prefab, 컨테이너 GameObject 참조를 보관한다
16 - 17 row = 0, col = 0 에 해당되는 화면 position를 구한다.
18 - 19 모든 cell/block을 처리하도록 for loop 사용한다.
21 해당 row, col에 위치한 Cell 객체에게 Cell GameObject를 생성(Instantiate)하도록 요청한다.
22 생성된 Cell 객체에게 Cell GameObject의 초기 위치 설정하도록 알려준다.
26 - 29 row = 0, col = 0에 해당되는 화면 위치 X position을 구한다.
9 x 9 보드일 때 '-4'가 리턴된다.
31 - 34 row = 0, col = 0에 해당되는 화면 위치 Y position을 구한다.
9 x 9 보드일 때 '-4'가 리턴된다.


9 x 9 보드에서 씬에 렌더링되는 첫번째 Cell/Block의 위치는 왼쪽하단 (row = 0, col = 0) 위치이다. 부모인 Board GameObject의 원점(0,0)을 기준으로 row = 0, col = 0 인덱스에서 씬에서 위치는 (-4, -4)이다(붉은색 텍스트).
CalcInitX(), CalcInitY()는 원점 기준으로 (row = 0, col = 0)번째 블럭이 씬에서 렌더링되는 X, Y위치를 나타내며, 계산식은 아래와 같다.

X 위치 : -열(column)개수 / 2 + 0.5 ,  Y 위치 : -행(row)개수 / 2 + 0.5

파라미터로 offset(0.5)를 전달받아 계산식에 사용한 이유는 나중에 출력위치를 변경하여 다른 GameObject를 출력할 때 사용하기 위해서 인자로 전달받도록 한 것으로 추후 진행하면서 사용할 것이다. 

Cell GameObject 생성하기

인자로 전달된 Prefab과 컨테이너 GameObject를 이용해서 Cell GameObject를 생성하고, 컨테이너(Board)를 부모로 설정하고, CellBehaviour에게 Cell 객체 정보를 전달하여 참조하도록 설정한다.

public class Cell
{
    //     -- 중략 --
    protected CellBehaviour m_CellBehaviour;
    public CellBehaviour cellBehaviour
    {
        get { return m_CellBehaviour; }
        set
        {
            m_CellBehaviour = value;
            m_CellBehaviour.SetCell(this);
        }
    }

    public Cell InstantiateCellObj(GameObject cellPrefab, Transform containerObj)
    {
        //1. Cell 오브젝트를 생성한다.
        GameObject newObj = Object.Instantiate(cellPrefab, new Vector3(0, 0, 0), Quaternion.identity);

        //2. 컨테이너(Board)의 차일드로 Cell을 포함시킨다.
        newObj.transform.parent = containerObj;

        //3. Cell 오브젝트에 적용된 CellBehaviour 컴포너트를 보관한다.
        this.cellBehaviour = newObj.transform.GetComponent<CellBehaviour>();

        return this;
    }

    public void Move(float x, float y)
    {
        cellBehaviour.transform.position = new Vector3(x, y);
    }
}
Cell.cs
4 - 13 CellBehaviour를 참조하기 위해서 멤버 변수를 선언하고 프로퍼티를 작성한다.
11 CellBehaviour가 참조로 설정될 때, CellBehaviour의 SetCel() 메소드에 Cell 객체를 인자로 전달한다. (아래 관계 설정됨)
15 - 27 파라미터로 전달된 리소스(cell/block prefab, 컨테이너)를 이용해서 Cell GameObject를 생성하고, 컨테이너를 Cell GameObject의 부모로 지정한다.(아래 관계 설정됨)
29 - 30 지정된 위치로 Cell이 참조하는 GameObject의 위치를 변경한다.

Cell GameObject의 Behaviour에게 이벤트 전달하기

namespace Ninez.Board
{
    public class CellBehaviour : MonoBehaviour
    {
        Cell m_Cell;
        SpriteRenderer m_SpriteRenderer;

        void Start()
        {
            m_SpriteRenderer = GetComponent<SpriteRenderer>();

            UpdateView(false);
        }

        public void SetCell(Cell cell)
        {
            m_Cell = cell;
        }

        public void UpdateView(bool bValueChanged)
        {
            if (m_Cell.type == CellType.EMPTY)
            {
                m_SpriteRenderer.sprite = null;
            }
        }
    }
}
CellBehaviour.cs
CellBehaviour는 Cell GameObject의 시각효과 및 행동을 처리하는 컴포넌트로서 Cell 객체와 1:1 매핑되어 있다. 즉, Cell 개수 만큼 Cell GameObject가 생성되고 각 Cell GameObject에는 CellBehaviour 컴포넌트가 포함되어 있다.
1 namespace를 Ninez.Board로 정의한다
4 참조하는 Cell 객체 선언
6 Cell이 화면에 보이도록 해주는 Sprite Renderer 컴포넌트, Cell 종류에 따라서 출력될 Sprite가 결정된다.
10 Start()에서 Sprite Renderer 컴포넌트에 대한 참조를 보관한다.
12 Cell 종류에 해당되는 Sprite가 출력되도록 UpdateView(false)를 호출한다.
15 - 18 Cell 객체 참조를 저장한다.
20 - 26 Cell 종류에 해당되는 Sprite를 SpriteRenderer에 설정한다.
22 - 25 Cell 종류(Type)가 EMPTY인 경우에 Sprite가 보이지 않도록 한다.

실행하기

이제 필요한 코딩은 모두 마쳤다. 모든 소스를 저장하고 Unity 에디터로 돌아온다.
씬을 구성하는 Board GameObject를 선택 한 후, StageController 컴포넌트의 속성을 다음과 같이 지정한다.
  • Container : Scene 의 Board GameObject
  • Cell Prefab : Assets/Prefab/Cell 
  • Block Prefab : None (보드 Prefab을 만든 후에 사용 예정)


플레이버튼(▶)을 클릭해서 플레이 결과를 확인한다.
row와 column이 같은 경우에 CellType.EMPTY 이기 때문에 대각선 방향으로 비어있는 화면이 렌더링 되는 것을 볼 수 있다.

StageController에서 보드 크기를 5 x 5 로 지정하면 아래과 같이 실행된 화면을 볼 수 있다.


다음 장에서는 Block이 화면에 출력되는 과정을 진행하겠습니다.

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

댓글 5개:

  1. 감사합니다. 많이 배우고 있습니다.^^

    답글삭제
  2. 잘 보았습니다! 최고의 강좌!!

    답글삭제
  3. stage builder에 Stage stage = new Stage(This, row, col); 이부분에에서 error CS1729: 'Stage' does not contain a constructor that takes 3 arguments이 에러가 나오는데 어떻게 해야하나요

    답글삭제
    답글
    1. 저도 같은 문제가 나서 그러는데 혹시 네글자야 님은 해결하셨나요?

      삭제
  4. Stage.cs에 생성자 함수가 예제에 빠져있네요.
    예제 따라하시는 분은 오류 시
    github 파일 내용 확인하시면 될 듯해요
    public Stage(StageBuilder stageBuilder, int nRow, int nCol)

    답글삭제