UI 비법서 (1)


당신의 레이아웃에 딱맞는 Content Size Fitter


작성 기준 버전 :: 2018.3.2f1


UI를 제작할 때, 보기 좋은 레이아웃을 만들어 내는 일은 매우 어렵다. 어렵사리 보기 좋은 레이아웃을 만들어 냈다고 해도 UI에 들어가는 내용의 크기가 바뀔 수 있는 상황이라면 정해진 텍스트나 이미지 등의 콘텐츠가 정해진 레이아웃을 벗어나는 경우가 쉽게 발생한다.

이러한 유동적인 상황에 대처하는 UI를 만드는 것은 이전까지는 스크립트에서 일일이 처리해야하는 번거로움이 있었다. 그런 번거로움을 해소하기 위한 기능으로 유니티는 콘텐츠 사이즈 피터(Content Size Fitter)라는 컴포넌트를 지원한다. 이 컴포넌트를 이용해서 콘텐츠의 크기에 따라 자유롭게 변화하는 UI를 제작하는 방법에 대해서 알아보도록 하자.



콘텐츠 사이즈 피터(Content Size Fitter)


콘텐츠 사이즈 피터는 단어 그대로 컴포넌트가 붙어있는 오브젝트의 크기를 내용물, 콘텐츠의 크기에 맞게 만들어주는 역할을 한다.



콘텐츠 사이즈 피터 컴포넌트를 게임 오브젝트에 추가하는 방법은 위의 이미지처럼 씬에서 Add Component 버튼을 누르고 "Content"를 검색해서 나오는 Content Size Fitter 컴포넌트를 선택하는 것이다.



콘텐츠 사이즈 피터를 사용하지 않으면 위의 이미지처럼 내용이 UI의 사이즈보다 커지면 옵션에 따라 뒷 부분이 잘리거나 영역을 넘어가서 표시되는 바람에 다른 UI를 침범하는 문제가 발생한다.

 

 

콘텐츠 사이즈 피터를 사용하면 전과 다르게 UI의 사이즈가 텍스트의 내용에 따라 자동으로 변하는 것을 확인할 수 있다.

 

 

추가된 콘텐츠 사이즈 피터는 인스펙터 뷰에서 위의 이미지와 같은 모습으로 보이며, Horizental Fit과 Vertical Fit라는 두 가지 프로퍼티를 설정할 수 있다.


선택 가능한 옵션으로는 Unconstrained, Min Size, Preferred Size 이렇게 세 가지가 있다. Unconstrained는 UI의 크기를 콘텐츠에 맞추지 않는 옵션으로 UI의 크기를 직접 맞추거나 고정된 크기를 사용해야 하는 경우 사용하면 되는 옵션이다. Min Size는 레이아웃 요소의 최소 크기를 기준으로 UI의 크기를 맞추는 옵션인데, 이 옵션의 경우에는 단독으로 사용되는 경우는 거의 없고 Layout group 계열의 컴포넌트와 함께 주로 사용된다. 마지막으로 Preferred Size 옵션은 기본적인 콘텐츠의 크기에 따라 UI의 크기를 맞추는 옵션으로 위의 예시 이미지처럼 텍스트나 이미지에 단독으로 콘텐츠 사이즈 피터를 사용하는 경우에 주로 사용된다.





콘텐츠 사이즈 피터와 피봇


콘텐츠 사이즈 피터로 인해 Rect Transform의 크기가 변경될 때는 UI의 피벗을 중심으로 조정되기 때문에, 피벗을 사용해서 UI의 크기가 조정되는 방향을 조절할 수 있다. 위의 이미지 중에서 콘텐트 사이즈 피터를 붙인 gif를 보면 피벗이 UI의 중심에 있어서 피벗을 중심으로 양 쪽 방향을 향해서 UI가 커지는 것을 볼 수 있다.



위의 이미지를 보면 X Pivot을 0으로 변경해서 UI의 피봇을 가장 좌측으로 옮긴 결과, UI의 트랜스폼이 왼쪽에서 오른쪽으로 확장되는 것을 볼 수 있다.



콘텐츠 사이즈 피터와 스크롤 뷰(Content Size Fitter & Scroll View)


콘텐츠 사이즈 피터를 가장 잘 응용할 수 있는 부분은 바로 스크롤 뷰이다. 이전에 스크롤 뷰에 대해서 설명했던 글에서는 스크롤 뷰에 들어가는 내용에 따라서 Content 오브젝트의 크기를 맞춰주는 방법을 스크립트로 설명했지만 콘텐츠 사이즈 피터를 사용하면 스크립트를 작성하지 않고도 훨씬 편하게 Content 오브젝트의 크기를 맞출 수 있다.


이제 콘텐츠 사이즈 피터를 사용해서 스크롤 뷰의 Content의 크기를 조절하는 방법을 알아보자.


 

우선 (200, 200) 사이즈의 스크롤 뷰를 생성한다.



그리고 스크롤 뷰의 Content 오브젝트 아래에 (150, 50) 사이즈의 이미지를 하나 만들어주자.



콘텐츠 사이즈 피터가 없는 상태에서 플레이 버튼을 눌러서 실행해보면 스크롤 뷰의 크기는 이미지의 크기나 갯수에 상관없이 동작하는 것을 확인할 수 있다.


다음에는 이 스크롤 뷰에 콘텐츠 사이즈 피터를 적용해보자.



우선 아이템들이 들어갈 Content 오브젝트를 선택하고 여기에 오브젝트들을 자동으로 정렬해주는 Vertical Layout Group 컴포넌트를 추가한다.


원하는 정렬 방법에 따라 Grid Layout Group이나 Horizontal Layout Group을 사용해도 된다. 각 프로퍼티의 값은 보기 좋아보이는 값을 찾아서 입력하도록 하자. 이 예시에서는 위 아래의 패딩을 10씩 주고 각 아이템 간의 간격을 5로 설정했다. 그리고 아이템의 정렬은 상단 중앙으로 설정했다.


그 다음에 내용물에 따라 오브젝트의 크기를 조정해주는 콘텐츠 사이즈 피터 컴포넌트를 추가한 뒤에 사용한 Layout Group에 맞게 Fit 옵션을 Min Size로 변경한다.


 

스크롤 뷰에 콘텐츠 사이즈 피터와 레이아웃 그룹을 적용하고 플레이를 해보면 아이템이 추가/제거됨에 따라서 자연스럽게 내부 Content의 크기가 늘어나서 스크롤이 가능하게 되는 것을 확인할 수 있다.

당신을 고통에 빠뜨릴 문제 (1)


코루틴과 Instantiate 그리고 Start


게임을 개발하면서 생겨나는 많은 버그들은 개발자들을 고통에 빠뜨리지만, 그 중에서도 엔진의 특성과 맞물려서 발생하는 버그는 깊이가 다른 고통을 느끼게 해준다. 이런 종류의 버그는 개발자가 생각한 로직으로는 완벽하지만 엔진의 특성으로 인해서 그 로직이 완벽하게 동작하지 않는 것이기 때문에 해당 엔진의 동작과 특성을 확실하게 알고 있지 않으면 어디서 버그가 발생했는지, 원인이 무엇인지 알기 어렵다.


이제부터 이러한 버그들에 대해서 알아보도록 하자. 첫 번째 내용은 코루틴과 Instantiate 그리고 Start에 얽힌 문제에 관한 내용이다.



코루틴(Coroutine)의 특성


코루틴은 유니티로 게임을 개발할 때, 자주 사용되는 기능 중에 하나다. 주로 비동기 처리나, 프로그램을 멈추지 않고 다른 동작을 기다리거나, 메인 로직 뒤에서 돌아가는 서브 루틴을 구현할 때 사용된다.


using System.Collections;
using UnityEngine;

public class B : MonoBehaviour
{
    public void CallCoroutine()
    {
        StartCoroutine(SomeCoroutine());
    }

    private IEnumerator SomeCoroutine()
    {
        Debug.Log("Start Coroutine");
        yield return null;
        Debug.Log("End Coroutine");
    }
}


using UnityEngine;

public class A : MonoBehaviour
{
    public B b;

    void Start()
    {
        b.CallCoroutine();
    }
}


위의 코드를 보면 A라는 클래스가 B라는 클래스의 CallCoroutine() 함수를 호출하여 B 클래스의 SomeCoroutine()을 동작하게 만들고 있다.


 

이 코드는 만약 B 오브젝트가 활성화(enabled)되어 있다면 정상적으로 동작하겠지만, 비활성화(disabled)된 상태라면 위의 이미지처럼 오브젝트가 활성화되지 않은 상태에서 코루틴을 호출했다는 에러 로그가 발생하고 해당 코루틴의 코드가 하나도 실행되지 않는다.


이것은 코루틴을 사용할 때 기본적인 주의 사항으로 해당 오브젝트가 활성화(Enabled)된 상태에서 코루틴을 호출해야 한다는 것을 의미한다.


이런 문제는 에러 로그가 명확하기 때문에 코루틴을 호출하기 전에 게임 오브젝트의 액티브를 확인해주거나 게임 오브젝트가 활성화된 이후에만 코루틴을 호출하도록 수정하면 간단하게 해결된다.





진짜 문제


이전의 예시처럼 이미 생성되어 있는 게임 오브젝트의 코루틴을 호출하는 문제는 활성화 상태만 잘 체크하면 된다. 진짜 문제는 이 코루틴과 Start, Instantiate가 엮였을 때부터 발생한다.


using UnityEngine;

public class A : MonoBehaviour
{
    public GameObject bPrefab;

    void Start()
    {
        var b = Instantiate(bPrefab).GetComponent<B>();
        b.CallCoroutine();

    }
}


이미 생성되어 있는 B 오브젝트를 참조하는 이전의 예시와 다르게 이번에는 B 오브젝트의 프리팹을 소유하고 있다가 A 오브젝트가 Start() 되면 B 오브젝트를 새로 생성해서 B 오브젝트의 코루틴을 호출하는 상황을 가정해보자.


using System.Collections;
using UnityEngine;

public class B : MonoBehaviour
{
    private void Start()
    {
        Debug.Log("Start");
    }

    public void CallCoroutine()
    {
        StartCoroutine(SomeCoroutine());
    }

    private IEnumerator SomeCoroutine()
    {
        // yield return null;
        Debug.Log("Start Coroutine");
      
        Debug.Log("Ing Coroutine");
       
        Debug.Log("End Coroutine");
    }
}


그리고 B 클래스는 Start() 함수가 호출되면 "Start"라는 로그를 띄우고 SomeCoroutine()에서 각 과정에 맞는 로그를 띄울 것이다.


자, 그러면 아래와 같이 여러 상황의 코루틴들을 가정해보자.


private IEnumerator SomeCoroutine()
{
    yield return null;
    Debug.Log("Start Coroutine");
      
    Debug.Log("Ing Coroutine");
       
    Debug.Log("End Coroutine");


}



private IEnumerator SomeCoroutine()
{
   
    Debug.Log("Start Coroutine");
   
yield return null;
    Debug.Log("Ing Coroutine");
       
    Debug.Log("End Coroutine");


}



private IEnumerator SomeCoroutine()
{

    Debug.Log("Start Coroutine");
      
    Debug.Log("Ing Coroutine");
   
yield return null;
    Debug.Log("End Coroutine");


}



private IEnumerator SomeCoroutine()
{

    Debug.Log("Start Coroutine");
      
    Debug.Log("Ing Coroutine");
       
    Debug.Log("End Coroutine");

    yield return null;
}


여러 상황의 코루틴에서 과연 Start() 함수는 같은 시점에 호출될까? 아니다. 유니티에서 게임 오브젝트의 Start() 함수는 게임 오브젝트가 Instantiate()로 생성되고 한 프레임 뒤에 호출되기 때문에 코루틴이 돌다가 만난 첫 번째, yield return 시점에 Start() 함수가 호출되게 된다. 만약 Start() 함수가 별다른 역할을 하지 않는다거나, 호출순서에 상관없는 일을 처리한다면 큰 문제는 없겠지만, 만약 코루틴의 동작과 연관된 중요한 작업을 Start()가 처리하고 있다면, 이 yield return의 위치에 따라서 심각하고도 감지하기 어려운 난감한 문제를 만나게 될 확률이 높아진다.


그리고 이것보다 심각한 문제로, Instantiate() 직후에 코루틴을 호출한 경우, 희박한 확률로 코루틴의 앞 부분 코드는 정상 동작을 하다가 첫 번째 yield return을 만나는 순간, 그 이후의 코드를 실행하지 않는 문제도 발생한다. 게다가 이 경우에는 어떠한 에러나 경고 로그도 발생하지 않기 때문에, 이 문제의 원인이나 해결법을 찾아내기가 아주 어렵다.


using System.Collections;
using UnityEngine;

public class B : MonoBehaviour
{
    private void Start()
    {
        Debug.Log("Start");
        StartCoroutine(SomeCoroutine());
    }

    private IEnumerator SomeCoroutine()
    {
        Debug.Log("Start Coroutine");
        yield return null;
        Debug.Log("Ing Coroutine");
       
        Debug.Log("End Coroutine");
    }
}


그렇기 때문에 만약 Instantiate()와 Start() 그리고 코루틴이 긴밀하게 엮여있거나, Instantiate()로 게임 오브젝트를 생성한 직후에 코루틴을 호출해서 어떤 작업을 처리해야 하는 경우라면, 별도로 코루틴을 호출하는 것보다는, 위의 코드처럼 Start() 함수에서 코루틴을 실행하는 것을 권장한다.

Tutorial (6)


게임 오브젝트와 컴포넌트


작성 기준 버전 :: 2018.3.2f1


이번 섹션에서는 게임 오브젝트와 컴포넌트에 대해서 알아보자.


게임 오브젝트(Game Object)


게임 오브젝트는 유니티 엔진에서 가장 기본이 되는 개념으로 캐릭터와 바닥에 떨어진 아이템, 배경으로 배치된 건물이나 소품, 폭발하면서 발생하는 이펙트, 빛을 밝히는 광원, 모든 장면을 찍는 카메라까지 씬에 배치되는 모든 것은 게임 오브젝트이다.


기본적인 게임 오브젝트에 나중에 설명할 컴포넌트를 추가로 붙임으로써 게임 오브젝트가 캐릭터가 되느냐, 아이템이 되느냐, 아니면 간단한 배경이 되느냐를 결정하게 된다.



게임 오브젝트는 기본적으로 이름(Name - "GameObject"), 태그(Tag - "Untagged"), 레이어(Layer - "Default")를 가지고 있다. 이를 응용해서 나중에 원하는 오브젝트를 찾거나 할 수 있다.



컴포넌트(Component)


컴포넌트는 게임 오브젝트에 붙일 수 있는 다양한 기능을 가진 구성요소들로, 비어있는 게임 오브젝트에 어떤 컴포넌트를 붙이느냐에 따라서 그 게임 오브젝트의 역할이 달라진다.


트랜스폼 컴포넌트(Transform Component)


모든 컴포넌트 중에서 트랜스폼(Transform) 컴포넌트는 모든 게임 오브젝트에 기본으로 부착되며 제거할 수 없다.



이 트랜스폼 컴포넌트는 해당 게임 오브젝트가 씬에 어느 위치에, 얼마나 회전되어서, 어떤 크기로 배치되어 있는지를 정하는 컴포넌트이다.


기타 컴포넌트


게임 오브젝트에는 여러 컴포넌트를 붙일 수 있고 어떤 컴포넌트를 붙이느냐에 따라서 게임 오브젝트의 역할이 달라진다.


게임 오브젝트에 Light 컴포넌트를 붙이면 광원이 되고 Camera 컴포넌트를 붙이면 게임 내에서 씬을 보여주는 카메라가 된다. 이렇게 유니티에서 기본적으로 제공하는 컴포넌트 이외에도 개발자가 유니티 스크립트 API를 이용해서 직접 컴포넌트를 만들어서 게임 오브젝트에 붙일 수도 있다.


컴포넌트 추가하기


컴포넌트를 추가할 게임 오브젝트를 선택하고 인스펙터 창에서 Add Component 버튼을 누르면 게임 오브젝트에 추가할 수 있는 컴포넌트들이 보여진다. 이 중에서 게임 오브젝트에 추가할 컴포넌트를 선택하면 된다.


 

커스텀 컴포넌트


유니티에서 기본적으로 제공하는 기본적인 컴포넌트 이외에도 개발자가 직접 컴포넌트를 만들어서 게임 오브젝트에 붙일 수도 있는데 대부분의 실제적인 게임 기능을 하는 시스템들은 커스텀 컴포넌트로 만들어지고 게임 오브젝트에 추가될 것이다.


 

커스텀 컴포넌트를 만드는 방법은 프로젝트 뷰에서 우클릭한 뒤 Create>C# Script 항목을 선택하면 된다.



그러면 NewBehaviourScript라는 이름으로 C# 스크립트 파일이 생성된다.


 

이렇게 생성한 컴포넌트는 다른 컴포넌트와 마찬가지로 컴포넌트를 붙일 게임 오브젝트를 선택하고 Add Component 버튼을 누른 뒤 추가한 컴포넌트의 이름을 검색해서 붙일 수 있다.

+ Recent posts