2019년 11월 11일 월요일

3. 블럭(Block) 추가하기

이번 장에서는 보드에 Block을 추가하는 과정을 진행하겠습니다.
구현될 화면은 아래와 같습니다.


소스 코드 다운로드

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

Block 스프라이트 출력하기

블럭 이미지 준비하기

Block에 사용할 스프라이트 이미지를 아래에서 다운받을 수 있다.
https://github.com/ninez-entertain/BingleStarter

다운받은 이미지들을 블럭 Sprite로 사용해서 아래와 같이 렌더링되는 씬을 구성할 것이다.

Block 이미지는 opengameart에서 제공하는 공개 2D Art를 사용한다. https://opengameart.org/content/animal-pack-redux 프로젝트에서 사용하는 이미지 외에도 많은 이미지가 있으며, 필요하면 원하는 이미지로 변경해보자. 이때, 이미지 크기는 가로/세로 동일하게 설정하고 이미지 전체가 1미터에 랜더링 될 수 있도록 PixelPerUnit를 이미지 크기로 설정해야 한다.

아래 과정으로 스프라이트를 추가한다.
  1. github 소스에서 스프라이트 이미지를 다운로드한다. (6개)
  2. Assets/Sprites/Board/Blocks 폴더를 생성한다.
  3. Blocks 폴더 하위에 Animals 폴더를 생성한다(.../Blocks/Animals).
  4. 1에서 다운로드한 이미지를 Animals 폴더에 복사한다.
  5. 6개 이미지를 모두 선택한 후 Inspector | PixelsPerUnit 을 128로 입력한다.
    이미지 크기가 128 x 128로 블럭 1개를 1미터로 렌더링할 때 이미지 전체(128 pixel)이 1미터 영역 안에 출력되도록 PixelsPerUnit을 이미지 크기(128)로 설정했다.
  6. Inspector 하단의 [Apply] 버튼을 클릭해서 변경 내용을 적용한다.

Block Prefab 만들기

Block에 사용할 GameObect를 Prefab으로 등록하고, 플레이 동안 동적으로 생성해 보자.

Block GameObject 만들기

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

Block Prefab 등록하기

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

Block GameObject를 씬에 추가하기

Block Prefab을 이용해서 씬이 플레이될 때 화면에 Block GameObject를 추가하고, 씬에 등록된 Board GameObject의 차일드로 등록해 보자.

Block GameObject가 Prefab으로 부터 생성되면 아래와 같은 구조를 갖게 된다.
(노란색 객체와 붉은색으로 표기된 관계를 추가했다.)
GameObject를 인접한 곳에 모으기 위해서 레이아웃을 일부 변경했다. enum 타입 관계은 객체가 아니므로 제거하고 개별 표기했으며, 그동안 누락시킨 카메라(MainCamera, CameraAgent)를 추가했다.


Block GameObject가 Board GameObject의 차일드로 구성되며, Block Prefab으로 부터 Instantiate되는 것을 알 수 있다. Board 객체에 저장된 Block 객체는 생성된 Block GameObject의 BlockBehaviour 컴포넌트를 참조(Agrregation 관계로 표기 : 붉은색 ♢)하고 있다.

Board 구성시 Block 생성하기

Block GameObject가 생성되는 과정은 Cell GameObject가 생성되는 과정과 동일하다.
Board 객체의 ComposeStage()에서 Cell GameObject와 Block GameObject를 모두 생성한다. 이전 장에서는 Cell을 생성했음으로 Block GameObject을 생성하는 과정을 아래와 같이 추가한다.
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);

            Block block = m_Blocks[nRow, nCol]?.InstantiateBlockObj(blockPrefab, container);
            block?.Move(initX + nCol, initY + nRow);
        }
}
Board.cs
블럭을 생성하는 코드 두 줄을 추가한다.
17 row, col에 위치한 Block 객체에게 Block GameObject를 생성(Instanticate)하도록 요청한다.
18 Block 객체가 생성된 경우 지정된 위치로 이동하도록 요청한다.
Elvis 연산자(?.)를 사용했다. 즉 null이 리턴될 수 있다는 것을 의미하며, BlockType.EMPTY인 경우에 null을 리턴한다(뒤에서 다시 설명하겠다). Block 객체에게 위치를 이동하도록 요청한다. 실제 구현은 Block이 참조하는 Block GameObject의 위치가 변경될 것이다.

Block GameObject 생성하기

Block Prefab을 이용해서 Block GameObject를 생성하고, Board GameObject의 차일드로 등록한다.
public class Block
{
       -- 중략 --
    protected BlockBehaviour m_BlockBehaviour;
    public BlockBehaviour blockBehaviour
    {
        get { return m_BlockBehaviour; }
        set
        {
            m_BlockBehaviour = value;
            m_BlockBehaviour.SetBlock(this);
        }
    }

    internal Block InstantiateBlockObj(GameObject blockPrefab, Transform containerObj)
    {
        //1. Block 오브젝트를 생성한다.
        GameObject newObj = Object.Instantiate(blockPrefab, new Vector3(0, 0, 0), Quaternion.identity);

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

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

        return this;
    }

    internal void Move(float x, float y)
    {
        blockBehaviour.transform.position = new Vector3(x, y);
    }
}
Block.cs
4 - 13 BlockBehaviour를 참조하기 위해서 멤버 변수를 선언하고 프로퍼티를 정의한다.
11 BlockBehaviour가 참조로 설정될 때,  BlockBehaviour의 SetBlock() 메소드를 호출해서 Block 객체를 참조하도록 한다.
15 - 27 파라미터로 전달된 리소스(block prefab, 컨테이너)를 이용해서 Block GameObject를 생성하고 컨테이너(Board)를 부모로 지정한다(아래 관계 설정됨)
29 - 33 지정된 위치로 Block이 참조하는 GameObject의 위치를 설정한다.

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

BlockBehaviour는 Block GameObject의 행동을 처리하는 컴포넌트로서 Block 객체와 1:1 매핑되어 있다.즉, Cell과 마찬가지로 Block 개수 만큼 Block GameObject가 생성되고, Block GameObject 각각은 BlockBehaviour 컴포넌트를 가지고 있다.
namespace Ninez.Board
{
    public class BlockBehaviour : MonoBehaviour
    {
        Block m_Block;
        SpriteRenderer m_SpriteRenderer;

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

            UpdateView(false);
        }

        internal void SetBlock(Block block)
        {
            m_Block = block;
        }

        public void UpdateView(bool bValueChanged)
        {
            if (m_Block.type == BlockType.EMPTY)
            {
                m_SpriteRenderer.sprite = null;
            }
        }

    }
}
BlockBehaviour.cs
1 namespace를 선언한다
5 참조하는 Block 객체 레퍼런스를 선언한다.
6 Block이 화면에 출력되는 Sprite Renderer, Block 종류(BlockType)에 따라서 출력될 Sprite가 결정된다.
10 시작될 때 Sprite Renderer 컴포넌트 참조를 저장한다.
12 Block 종류에 따라서 정해진 Sprite가 출력되도록 UpdateView(false)를 호출한다. false는 초기 생성되는 것을 알릴 때 인자로 전달한다
15 - 17 Block 객체 참조를 저장한다.
20 - 26 블럭종류에 해당되는 Sprite를 SpriteRenderer에 설정한다.
22 Block 종류(BlockType)가 EMPTY인 경우에 Sprite가 보이지 않도록 한다.

실행하기

모든 소스를 저장하고, 유니티 에디터에서 StageController 컴포넌트의 Block Prefab 속성에 Prefab을 등록한다.
  1. 씬에서 Board GameObject를 선택한다.
  2. Inspector가 Board로 변경되고, Stage Controller 컴포넌트가 보인다.
  3. Assets/Prefabs/Block을 드래그해서 Inspector | Stage Controller | Block Prefab 속성에 드롭한다.

플레이버튼(▶)을 클릭해서 플레이 결과를 확인해 보자.
Cell 이 비어있는 곳에서만 Block Sprite가 출력되는 것을 볼 수 있다.
원인은 Cell Sprite가 Block Sprite 위에 렌더링 되기 때문에 실제로 Block Sprite가 출력이 되고 있지만 화면에 보이지 않게 된 것이다. 플레이 동안 씬 계층구조를 살펴보면 Block GameObject가 81개 생성되어있는 것을 알 수 있다(5페이지 정도의 스크롤 바를 볼 수 있다).

Sorting Layer 추가하기

유니티에서 제공하는 Sortint Layer 기능을 이용해서 Block Sprite가 Cell Sprite 보다 위에 렌더링 되도록 수정해보자.

Sorting Layer란

유니터 2D Sprite 렌더링을 위해 제공하는 기능으로 Sprite의 렌더링 순서를 지정하는데 사용된다. Sorting Layer의 이름은 원하는 이름으로 추가할 수 있으며, 아래쪽에 위치 할수록 상위에 렌더링된다. 직접 추가해보면서 알아보자.

Cell 과 Block에 사용할 Sorting Layer를 다음의 순서로 추가한다.
  1. Inspector 상단 [Layers]를 클릭한다.
  2. 팝업된 Layers의 하단에서 [Edit Layers...]를 선택한다.
  3. [Tags & Layers] Inspector에서 [Sorting Layers]를 선택한다.
  4. 하단 [+] 를 클릭해서 [New Layer]가 추가한다.
  5. [New Layer]의 이름을 'Background'로 변경한다. (Cell SpriteRenderer에 사용)
  6. 하단 [+] 를 클릭하면, [New Layer]가 추가된다.
  7. [New Layer]의 이름을 'Block'으로 변경한다. (Block SpriteRenderer에 사용)
'Block' Sorting Layer가 아래쪽에 있기 때문에 Background Sorting Layer보다 상위에 렌더링된다.

Cell과 Block Prefab에 설정된 Sprtie Renderer의 Sorting Layer를 각각 아래와 같이 지정한다.
  1. Assets/Prefabs/Cell을 선택한다
  2. Inspector에서 [Open Prefab] 버튼을 선택하면 Cell Prefab의 구성 요소가 보여진다.
  3. Sprite Renderer의 Additional Settings | Sorting Layer를 'Background'로 선택한다
  4. Assets/Prefabs/Block 선택한다
  5. Inspector에서 [Open Prefab] 버튼을 선택하면 Block Prefab의 구성 내용이 보여진다.
  6. Sprite Renderer의 Additional Settings | Sorting Layer를 'Block'로 선택한다

Sorting Layer 적용 결과 확인하기

플레이버튼(▶)을 클릭해서 플레이 결과를 확인해보자.
아래와 같이 Block에 지정된 Sprite가 Cell Sprite 보다 상위에 렌더링되는 것을 볼 수 있다.

Block 종류에 적합한 스프라이트 출력하기

이번에는 Block의 세부종류마다 다른 스프라이트가 출력되도록 해보자.

Block에 어떤 스프라이트가 보여질지는 BlockBehaviour의 UpdateView()에서 설정한다는 것을 보았다.
public class BlockBehaviour : MonoBehaviour
{
    public void UpdateView(bool bValueChanged)
    {
        if (m_Block.type == BlockType.EMPTY)
        {
            m_SpriteRenderer.sprite = null;
        }
    }
}
BlockBehaviour.cs
현재 적용된 로직은 블럭이 지정되지 않은(BlockType.EMPTY) 경우에 스프라이트가 출력되지 않도록 하고 있으며, 그외 스프라이트를 별도로 설정하는 코드는 없는 것을 알 수 있다. (Block Prefab 등록시에 BlockBehaviour의 SpriteRenderer.sprite에 bear 스프라이트를 설정했기 때문에 UpdateView()에서 별도의 스프라이트 설정하지 않아도 디폴트 지정된 곰돌이(bear)가 출력된다.)

블럭의 세부종류(동물들) 별로 서로 다른 스프라이트가 출력되도록 하기 위해서는 아래와 같은 코드가 필요할 것으로 예상해 볼 수 있다.
public class BlockBehaviour : MonoBehaviour
{
    AnimalType m_AnimalType;  //출력할 동물 종류 enum type
    Sprite m_AnimalSprites[];

    public void UpdateView(bool bValueChanged)
    {
        if (m_Block.type == BlockType.EMPTY)
        {
            m_SpriteRenderer.sprite = null;
        }
        else if(m_Block.type == BlockType.BASIC)
        {
            switch (m_AnimalType)
            {
                case AnimalType.BEAR: m_SpriteRenderer.sprite = m_AnimalSprites[0]; break;
                case AnimalType.DUCK: m_SpriteRenderer.sprite = m_AnimalSprites[1]; break;
                case AnimalType.PANDA: m_SpriteRenderer.sprite = m_AnimalSprites[2]; break;
                    ...
            }
        }
    }
}
3 블럭의 세부 종류. 동물 캐릭터를 사용함으로 AnimalType enum을 정의한 것으로 가정하자.
4 각 AnimalType에 따라 사용될 스프라이트
12 - 21 블럭이 기본형(BASIC)인 경우에 지정된 동물 종류에 따라 출력할 스프라이트를 설정한다.
아래와 같은 구조를 가지게 될 것이다.

위의 구조는 BlockBehaviour에서 사용할 6개의 스프라이트를 직접 레퍼런스하고 있는 형태를 가진다. 그러나 위의 구조는 몇 가지 단점이 있다.
  • 각 BlockBehaviour가 6개의 스프라이트에 대해서 레퍼런스를 가지기 때문에 메모리를 많이 사용하다. (81개 블럭이 경우 6x81 = 486개 레퍼런스가 발생하고 레퍼런스 당 4byte인 경우 1936byte 약 2K 가까운 메모리가 필요)
  • 스프라이트처럼 모든 BlockBehaviour가 공유(Shared)하는 요소들(떨어지는 속도, 사라질때 효과 등)이 늘어날수록 메모리 낭비는 기하급수적으로 증가하게 된다.

아마도 자연스럽게 누구나 아래와 같은 구조를 생각하게 될 것입니다.

BlockBehaviour는 단지 BlockConfig 객체에 대한 한 개의 레퍼런스 만을 가지고 있기 때문에 스프라이트 종류가 늘어나거나 그외 다른 공유 속성이 필요한 경우에도 메모리 낭비가 추가로 발생하지 않으며, 속성이 추가될 때 경우에 따라서 Block Prefab을 수정해야하는 경우도 사라진다.

BlockType

빙글 프로젝트에서는 3가지 종류의 BlockType을 구현할 것이다.(EMPTY, BASIC 그리고 COLOR) EMPTY : 빈 블럭, 블럭이 존재하지 않는 위치에 설정 BASIC : 기본형, 세부종류로 6가지의 동물 캐릭터 스프라이트가 출력된다. COLOR : 컬러형, 세부종류로 6가지 색상을 가진 스프라이트가 출력된다. (나중에 추가 예정)

ScriptableObject로 공통 리소스 관리하기

위의 클래스 다이어그램에서 표현된 BlockConfig와 같이 공유(Shared) 데이터로서 사용되는 경우 일반적인 프로그래밍 모델에서는 싱글톤(Singleton)으로 BlockConfig를 작성해서 레퍼런스하면 된다. BlockConfig의 경우에는 유니티 리소스인 Sprite에 대한 레퍼런스를 담고 있는 객체가 필요하다. 유니티에서 Sprite를 사용하는 경우 Inspector에서 사용할 Sprite를 드래그해서 설정 한 후에 사용하는 것이 일반적인 개발 방법이다. C# 싱글턴 객체를 사용하는 경우에는 Sprite를 일반(plain) C# 객체에 드래그해서 등록하는 방법은 제공하지 않는다. 이 경우에 동적으로 로딩해서 설정해야하는 별도의 코딩이 필요하다.

이러한 불편함을 해결해주기 위해 유니티에서는 공용으로 사용하는 리소스를 에셋으로 보관해서 사용할 수 있는 유용한 기능으로 ScriptableObject를 제공한다.

ScriptableObject

객체의 인스턴스들이 참조하는 공유 데이터로서 데이터 복사를 방지하여 메모리 사용을 줄이고, 특히 MonoBehaviour 객체에 변경되지 않는 데이터가 있는 경우에 유용하다. ScriptableObject는 유니티 Asset으로 디스크에 저장되어 런타임에 사용되며, 개발시에는 유니티 에디터에서 값을 등록/편집할 수 있어 관리가 용이하다. 작성 방법은 1. ScriptableObject에서 상속받은 클래스를 추가한다. 2. 클래스 Attribute로 CreatAssetMenu(fileName, menuName)를 설정한다. 3. 저장한 다음 Asset | Create 메뉴를 확인한다. 4. 2에서 입력한 이름으로 새로운 메뉴가 등록되어 있는 것을 볼 수 있다. 5. 메뉴를 선택하면 현재 선택된 폴더 위치에 ScriptableObject Asset이 2에서 지정한 파일이름으로 추가된다. 6. 사용하고자하는 GameObject의 MonoBehaviour에서 1에서 ScriptableObject 클래스를 멤버를 public으로 추가한다. 7. 소스 저장 후, 유니티 에디터에서 GameObject를 선택하면 6에서 추가한 ScriptableObject 멤버가 보여진다. 7. Inspector에 5에서 추가한 ScriptableObject Asset을 드래그해서 추가한다. 자세한 내용은 유니티 메뉴얼 및 아래 코드를 참고하기 바란다. https://docs.unity3d.com/Manual/class-ScriptableObject.html

ScriptableObject 작성하기

다음 순서로 ScriptableObject를 추가한다.
  1. Assets/Scripts/Scriptable 폴더를 생성한다.
  2. Scriptable 폴더 하위에 BlockConfig C# 스크립트 파일을 생성한다.
  3. ScriptableObject를 상속하는 클래스를 선언하고, CreateAssetMenu Attribute를 추가한다.
namespace Ninez.Scriptable
{
    [CreateAssetMenu(menuName = "Bingle/Block Config", fileName = "BlockConfig.asset")]
    public class BlockConfig : ScriptableObject
    {
        public Sprite[] basicBlockSprites;
    }
}
BlockConfig.cs
1 namespace Ninez.Scriptable를 선언한다.
3 CreateAssetMenu Attribute를 설정한다.
menuName : 유니티 에디터의 메뉴에 출력되는 위치 (Assets | Create | Bingle | Block Config 메뉴 생성)
fileName : 저장시 사용되는 디폴트 파일이름(BlockConfig.asset)
4 ScriptableObject를 상속받도록 클래스를 선언한다.
6 스프라이트를 저장할 배열을 선언한다.

아래와 같이 유니티 에디터의 메뉴에 추가되는 것을 볼 수 있다.

ScriptableObject Asset 추가하기

다음 순서로 ScriptableObject Asset을 프로젝트에 추가한다.
  1. Assets/Config 폴더를 생성한다.
  2. Config 폴더를 더블클릭해서 현재 폴더로 선택한다.
  3. 메뉴에서 Assets | Create | Bingle | Block Config 또는 Project 탭에서 Create | Bingle | Block Config 를 선택한다.
  4. Assets/Config 폴더 하위에 BlockConfig Asset이 생성되고, 이름을 편집할 수 있는 상태가 된다. 디폴트 이름(BlockConfig)을 사용한다.
  5. 생성된 BlockConfig Asset을 선택하고, Inspector를 살펴보자.
    "Basic Block Sprites" 항목이 있는 것을 확인할 수 있다. BlockConfig 클래스에서 선언한 Sprite 배열의 이름이다.

 BlockConfig Asset에 스프라이트 등록하기

다음과 같이 추가한 BlockConfig에 블럭에 사용할 스프라이트를 등록한다.
  1. Assets/Config/BlockConfig를 선택한다.
  2. Inspector에서 "Basic Block Sprites"를 선택하여 펼친다.
  3. size에 '6'을 입력한 후, [Enter]를 입력한다.
    Sprite를 등록할 수 있는 스프라이트 요소 6개가 하위에 나타난다(Element 0 ~ Element 5)
  4. Assets/Sprites/Blocks/Animals 하위의 스프라이트 6개를 원하는 순서로 Element 0에서 5까지 등록한다.
    (스프라이트에서 마우스를 UP하지 않고 즉시 드래그해서 Inspector에 드롭한다.)

BlockBehaviour에 BlockConfig 참조 추가하기

BlockConfig 에셋을 BlockBehaviour에 적용해보자
다음 코드를 BlockBehaviour에 추가한다.
using Ninez.Scriptable;

public class BlockBehaviour : MonoBehaviour
{
    [SerializeField] BlockConfig m_BlockConfig;

    public void UpdateView(bool bValueChanged)
    {
        if (m_Block.type == BlockType.EMPTY)
        {
            m_SpriteRenderer.sprite = null;
        }
        else if(m_Block.type == BlockType.BASIC)
        {
            m_SpriteRenderer.sprite = m_BlockConfig.basicBlockSprites[1];
        }
    }
}
BlockBehaviour.cs
1 Scriptable 네임스페이스를 추가한다.
5 BlockConfig 멤버를 추가한다. Inspector에서 설정할 수 있도록 [SerializeField] Attribute를 추가한다.
13 - 16 BlockType.BASIC 인 경우에 Block Config 애셋의 두번째 스프라이트가 출력되도록 설정한다.

BlockBehaviour가 BlockConfig를 참조하도록 설정하기

이제 Block에 사용할 Asset과 코드가 준비가 되었다. 모든 소스를 저장하고 유니티 IDE로 돌아온다.
Block Prefab에서 사용하는 BlockBehaviour에 BlockConfig 애셋을 public으로 등록했으므로 Prefab에 BlockConfig 애셋을 등록해야 한다.
  1. BlockPrefab를 더블클릭(또는 선택 후에 Inspector에서 [Open Prefab] 클릭)해서 Prefab를 오픈한다.
  2. Inpector | Block Behaviour (Script) 컴포넌트에 'Block Config' 속성이 있는지 확인한다.
  3. Assets/Config/BlockConfig 에셋을 드래그해서 2에서 확인한 Block Config로 드롭한다.

지금까지의 구조를 UML 다이어그램으로 표현하면 아래와 같다.
스프라이트 6개를 담을 수 있는 BlockConfig 클래스가 추가되었고, BlockBehaviour에서 레퍼런스하고 있는 것을 볼 수 있다.

실행하기

플레이버튼(▶)을 클릭해서 플레이 결과를 확인해보자.

아래와 같이 블럭 스프라이트가 변경된 것을 확인할 수 있다. 위에서 BlockBehaviour.UpdateView() 코드를 살펴보면 항상 두번째 스프라이트가 출력되도록 설정해서 같은 종류의 스프라이트가 출력된다.
(BlockConfig 애셋에 등록한 스프라이트 순서에 따라서 다른 Sprite가 출력될 수 있다.)


블럭 세부종류 추가하기

이번에는 블럭의 세부종류를 설정해서 아래 화면과 같이 스프라이트가 출력되도록 해보자.
또한, 비어있는 Cell을 제외하고 유효한 Cell 위에 Block 스프라이트가 출력되도록 할 것이다.

Block 세부종류 enum 타입 추가하기

    public enum BlockBreed
    {
        NA      = -1,   //Not Assigned
        BREED_0 = 0,
        BREED_1 = 1,
        BREED_2 = 2,
        BREED_3 = 3,
        BREED_4 = 4,
        BREED_5 = 5,
    }
BlockDefine.cs
블럭 세부 종류를 enum 타입 BlockBreed를 선언한다. breed는 품종을 의미하는 단어이며, 여기에서는 블럭의 세부종류를 정의하는 용도로 사용할 것이다. 이후 같은 블럭인지 체크하는 단계에서는 breed를 비교해서 3개 연속 배치된 경우에 Match 상태를 판단하는데 사용할 것이다.

3 미지정. BlockType.EMPTY 인 경우 즉, Breed를 가지지 않는 경우에 해당된다.
4 - 9 Breed 0 ~ 5까지 정의한다.
BlockConfig 에셋에 등록된 Sprite에 접근하는 인덱스 0 ~ 5와 일치하도록 enum 값이 0 ~ 5를 가지도록 설정한다.

public class Block
{
    protected BlockBreed m_Breed;   //렌더링되는 블럭 캐린터(즉, 이미지 종류)
    public BlockBreed breed
    {
        get { return m_Breed; }
        set
        {
            m_Breed = value;
            m_BlockBehaviour?.UpdateView(true);
        }
    }

    internal Block InstantiateBlockObj(GameObject blockPrefab, Transform containerObj)
    {
        //유효하지 않은 블럭인 경우, Block GameObject를 생성하지 않는다.
        if (IsValidate() == false)
            return null;

        //  -- 중략 -- 
    }

    public bool IsValidate()
    {
        return type != BlockType.EMPTY;
    }
}
Block.cs
3 Block의 세부종류(Breed)를 선언한다.
4 - 12 breed 프로퍼티 get, set을 정의한다.
10 breed 값이 변경(set)되는 경우에 Block이 참조하고 있는 BlockBehaviour에게 변경되었음을 알려준다.
(BlockBehaviour가 변경된 breed 값을 참조하여 스프라이트를 변경할 것으로 예상된다.)
17 - 18 Block이 유효하지 않는 경우 BlockBehaviour GameObject를 생성하지 않고 리턴한다.
GameObject 사용을 최소화한다.
23 - 26 Block이 유효한지 체크하는 메소드.

Block 종류별로 생성하기

StageBuilder에서 Block을 생성하는 코드를 아래와 같이 수정한다.(추가 또는 변경된 메소드만 나타내었다.)
public class StageBuilder
{
    Block SpawnBlockForStage(int nRow, int nCol)
    {
        return new Block(BlockType.BASIC);
        return nRow == nCol ? SpawnEmptyBlock() :SpawnBlock();
    }

    public Block SpawnBlock()
    {
        return BlockFactory.SpawnBlock(BlockType.BASIC);
    }

    public Block SpawnEmptyBlock()
    {
        Block newBlock = BlockFactory.SpawnBlock(BlockType.EMPTY);

        return newBlock;
    }
}
StageBuilder.cs
5 블럭을 직접 생성하는 코드는 삭제한다.
6 직접 Block 객체를 생성하는 코드에서 별도의 메소드를 호출하는 방식으로 수정하였다.
행(row)과 열(column)이 일치하는 경우는 빈 블럭 생성을 요청하고, 그외에는 기본 블럭을 생성하도록 요청한다.
9 - 12 기본 블럭 생성을 요청하는 메소드. BlockFactory는 아래에서 설명한다.
14 - 18 빈 블럭 생성을 요청한 메소드.

BlockFactory 도입하기

BlockFactory는 Block 객체 생성을 담당하는 클래스로서 요청에 따라서 Block 객체를 생성한 후 리턴한다.
BlockFactory를 도입함으로써 Block을 제공받는 기능을 좀 더 유연하게 만들었다. 요청에 따라서 Block을 새로 생성해서 전달할 수도 있고, 이후 오브젝트 풀(Object Pool)을 도입하는 경우 Object Pool에서 남아있는 Block을 전달할 수도 있을 것이다. Block 객체를 어디에서 가져오는 요청하는 클래스의 코드는 변경되지 않을 것을 것이다.
namespace Ninez.Board
{
    public static class BlockFactory
    {
        public static Block SpawnBlock(BlockType blockType)
        {
            Block block = new Block(blockType);

            //Set Breed
            if(blockType == BlockType.BASIC)
                block.breed = (BlockBreed)UnityEngine.Random.Range(0, 6);
            else if(blockType == BlockType.EMPTY)
                block.breed = BlockBreed.NA;

            return block;
        }
    }
}
BlockFactory.cs
3 BlockFactory 클래스를 static으로 선언한다.
7 파라미터로 전달된 BlockType으로 Block 객체를 생성한다.
10 - 11 BlockType.BASIC 인 경우, 세부종류(breed)를 랜덤하게 설정한다.
12 - 13 BlockType.EMPTY 인 경우, breed를 NA로 설정한다.

Breed에 해당되는 스프라이트 적용하기

public void UpdateView(bool bValueChanged)
{
    if (m_Block.type == BlockType.EMPTY)
    {
        m_SpriteRenderer.sprite = null;
    }
    else if(m_Block.type == BlockType.BASIC)
    {
        m_SpriteRenderer.sprite = m_BlockConfig.basicBlockSprites[(int)m_Block.breed];
    }
}
BlockBehaviour.cs
7 - 10 BASIC 타입인 경우를 처리한다.
9 Block에 설정된 세부종류(breed)에 해당되는 Sprite가 출력되도록 설정한다.

BlockFactory가 추가된 구조는 다음과 같다. (일부 클래스는 제외하고 표기하였다.)

실행하기

플레이버튼(▶)을 클릭해서 플레이 결과를 확인해보자.
아래와 같이 유효한 Cell 위에서 Block에 설정된 Breed의 Sprite가 출력되는 것을 볼 수 있다.

다음 장에서는 스테이지 파일에서 데이터를 읽어들여서 스테이지를 구성하는 과정을 구현해 보겠습니다.
그후에 터치/마우스 이벤트를 받아서 블럭을 매칭하는 과정을 진행하겠습니다.

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

댓글 1개: