유니티 스크립트 작업 중에 Camera.main을 호출하면 해당 씬에서의 주 카메라가 반환된다. Camera.main으로 반환받는 주 카메라를 통해서 스크린의 좌표를 월드의 좌표로 변경하거나 월드 좌표를 스크린의 좌표로 변경하는 증의 작업을 주로 하게 된다.
하지만 가끔 이 Camera.main이 제대로된 주 카메라를 반환하지 않고 null 값을 반환하는 문제가 발생하는 경우가 발생한다.
이런 문제는 새로 생성한 씬에 자동으로 들어있는 기본 카메라를 지우고 새 카메라를 만들었을 때 주로 발생한다.
위의 이미지는 새로운 SampleScene을 만들고, 기본적으로 들어 있던 Main Camera를 지우고 New Camera를 만든 상황이다.
public class MainCameraTest : MonoBehaviour { void Start() { Debug.Log(Camera.main); } }
이 상황에서 위와 같이 Start() 시점에 Camera.main을 호출하는 스크립트를 만들고 게임 오브젝트에 이 스크립트를 컴포넌트로 붙여서 실행해보자.
그렇게 하면 위 이미지처럼 Camera.main이 null 값을 반환하는 것을 확인할 수 있다.
이런 문제가 발생하는 이유는 스크립트에서 Camera.main을 호출해서 메인 카메라를 찾아낼 때, "MainCamera" 태그가 붙어있는 카메라를 찾아내는 방식을 사용하기 때문이다. 그렇기 때문에 기존에 있는 메인 카메라를 지우고 새로운 카메라를 만들었다면 새로 만든 카메라의 Tag를 "MainCamera"로 바꿔줘야 한다.
새로 만들어진 카메라에 MainCamera 태그를 달고 다시 플레이를 해보면 새로 만든 카메라가 Camera.main으로 반환되는 것을 확인할 수 있다.
카메라에 메인 카메라 태그를 붙일 때도 주의해야 하는 점은, 만약 여러 개의 카메라에 메인 카메라 태그를 붙이면 Camera.main으로 호출했을 때 어떤 카메라가 반환될지 확정할 수 없기 때문에 문제가 발생할 수 있다는 점이다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
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의 크기가 늘어나서 스크롤이 가능하게 되는 것을 확인할 수 있다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
게임을 개발하면서 생겨나는 많은 버그들은 개발자들을 고통에 빠뜨리지만, 그 중에서도 엔진의 특성과 맞물려서 발생하는 버그는 깊이가 다른 고통을 느끼게 해준다. 이런 종류의 버그는 개발자가 생각한 로직으로는 완벽하지만 엔진의 특성으로 인해서 그 로직이 완벽하게 동작하지 않는 것이기 때문에 해당 엔진의 동작과 특성을 확실하게 알고 있지 않으면 어디서 버그가 발생했는지, 원인이 무엇인지 알기 어렵다.
이제부터 이러한 버그들에 대해서 알아보도록 하자. 첫 번째 내용은 코루틴과 Instantiate 그리고 Start에 얽힌 문제에 관한 내용이다.
코루틴(Coroutine)의 특성
코루틴은 유니티로 게임을 개발할 때, 자주 사용되는 기능 중에 하나다. 주로 비동기 처리나, 프로그램을 멈추지 않고 다른 동작을 기다리거나, 메인 로직 뒤에서 돌아가는 서브 루틴을 구현할 때 사용된다.
using System.Collections; using UnityEngine;
public classB : MonoBehaviour { public void CallCoroutine() { StartCoroutine(SomeCoroutine()); }
여러 상황의 코루틴에서 과연 Start() 함수는 같은 시점에 호출될까? 아니다. 유니티에서 게임 오브젝트의 Start() 함수는 게임 오브젝트가 Instantiate()로 생성되고 한 프레임 뒤에 호출되기 때문에 코루틴이 돌다가 만난 첫 번째, yield return 시점에 Start() 함수가 호출되게 된다. 만약 Start() 함수가 별다른 역할을 하지 않는다거나, 호출순서에 상관없는 일을 처리한다면 큰 문제는 없겠지만, 만약 코루틴의 동작과 연관된 중요한 작업을 Start()가 처리하고 있다면, 이 yield return의 위치에 따라서 심각하고도 감지하기 어려운 난감한 문제를 만나게 될 확률이 높아진다.
그리고 이것보다 심각한 문제로, Instantiate() 직후에 코루틴을 호출한 경우, 희박한 확률로 코루틴의 앞 부분 코드는 정상 동작을 하다가 첫 번째 yield return을 만나는 순간, 그 이후의 코드를 실행하지 않는 문제도 발생한다. 게다가 이 경우에는 어떠한 에러나 경고 로그도 발생하지 않기 때문에, 이 문제의 원인이나 해결법을 찾아내기가 아주 어렵다.
그렇기 때문에 만약 Instantiate()와 Start() 그리고 코루틴이 긴밀하게 엮여있거나, Instantiate()로 게임 오브젝트를 생성한 직후에 코루틴을 호출해서 어떤 작업을 처리해야 하는 경우라면, 별도로 코루틴을 호출하는 것보다는, 위의 코드처럼 Start() 함수에서 코루틴을 실행하는 것을 권장한다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
게임 오브젝트는 유니티 엔진에서 가장 기본이 되는 개념으로 캐릭터와 바닥에 떨어진 아이템, 배경으로 배치된 건물이나 소품, 폭발하면서 발생하는 이펙트, 빛을 밝히는 광원, 모든 장면을 찍는 카메라까지 씬에 배치되는 모든 것은 게임 오브젝트이다.
기본적인 게임 오브젝트에 나중에 설명할 컴포넌트를 추가로 붙임으로써 게임 오브젝트가 캐릭터가 되느냐, 아이템이 되느냐, 아니면 간단한 배경이 되느냐를 결정하게 된다.
게임 오브젝트는 기본적으로 이름(Name - "GameObject"), 태그(Tag - "Untagged"), 레이어(Layer - "Default")를 가지고 있다. 이를 응용해서 나중에 원하는 오브젝트를 찾거나 할 수 있다.
컴포넌트(Component)
컴포넌트는 게임 오브젝트에 붙일 수 있는 다양한 기능을 가진 구성요소들로, 비어있는 게임 오브젝트에 어떤 컴포넌트를 붙이느냐에 따라서 그 게임 오브젝트의 역할이 달라진다.
트랜스폼 컴포넌트(Transform Component)
모든 컴포넌트 중에서 트랜스폼(Transform) 컴포넌트는 모든 게임 오브젝트에 기본으로 부착되며 제거할 수 없다.
이 트랜스폼 컴포넌트는 해당 게임 오브젝트가 씬에 어느 위치에, 얼마나 회전되어서, 어떤 크기로 배치되어 있는지를 정하는 컴포넌트이다.
기타 컴포넌트
게임 오브젝트에는 여러 컴포넌트를 붙일 수 있고 어떤 컴포넌트를 붙이느냐에 따라서 게임 오브젝트의 역할이 달라진다.
게임 오브젝트에 Light 컴포넌트를 붙이면 광원이 되고 Camera 컴포넌트를 붙이면 게임 내에서 씬을 보여주는 카메라가 된다. 이렇게 유니티에서 기본적으로 제공하는 컴포넌트 이외에도 개발자가 유니티 스크립트 API를 이용해서 직접 컴포넌트를 만들어서 게임 오브젝트에 붙일 수도 있다.
컴포넌트 추가하기
컴포넌트를 추가할 게임 오브젝트를 선택하고 인스펙터 창에서 Add Component 버튼을 누르면 게임 오브젝트에 추가할 수 있는 컴포넌트들이 보여진다. 이 중에서 게임 오브젝트에 추가할 컴포넌트를 선택하면 된다.
커스텀 컴포넌트
유니티에서 기본적으로 제공하는 기본적인 컴포넌트 이외에도 개발자가 직접 컴포넌트를 만들어서 게임 오브젝트에 붙일 수도 있는데 대부분의 실제적인 게임 기능을 하는 시스템들은 커스텀 컴포넌트로 만들어지고 게임 오브젝트에 추가될 것이다.
커스텀 컴포넌트를 만드는 방법은 프로젝트 뷰에서 우클릭한 뒤 Create>C# Script 항목을 선택하면 된다.
그러면 NewBehaviourScript라는 이름으로 C# 스크립트 파일이 생성된다.
이렇게 생성한 컴포넌트는 다른 컴포넌트와 마찬가지로 컴포넌트를 붙일 게임 오브젝트를 선택하고 Add Component 버튼을 누른 뒤 추가한 컴포넌트의 이름을 검색해서 붙일 수 있다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
유니티 엔진에서 게임을 제작할 때, 모든 작업을 일일이 수작업으로 진행하면 개발 시간이 길어진다. 특히 같은 오브젝트를 여러 개 생성하고 각각 다른 수치를 입력하는 반복적인 세팅 작업의 경우에는 꽤나 큰 시간 낭비를 초래한다.
여러 개의 오브젝트를 생성하는데, 그 오브젝트들에 입력되어야 하는 설정이 일정한 규칙을 가지고 있거나, 설정될 값들에 대한 테이블을 미리 가지고 있다면, 오브젝트를 일일이 생성하고 설정 값을 입력하는 것보다, 버튼을 누르면 자동으로 모든 오브젝트들을 생성하고 일정한 규칙에 따라서 설정 값을 세팅하거나 테이블에서 설정 값을 가져와서 세팅하도록 만드는 것이 많은 시간을 절약할 수 있다.
이런 커스텀 버튼을 만드는 데도 작업 시간이 소모되겠지만, 일일이 오브젝트를 생성하고 값을 세팅하는 작업 시간이 누적되면 커스텀 버튼을 만드는 데 드는 누적 시간을 빠르게 추월할 것이다. 그리고 이런 종류의 버튼은 만들어두면 다른 프로젝트에서도 충분히 재활용할 수 있기 때문에 인스펙터 커스텀 버튼을 만드는데 시간을 투자할 가치가 있다.
예제
이번 예제에서는 한 오브젝트를 기준으로 그 오브젝트의 forward 방향으로 distance 거리마다 cubeCount 개의 큐브 오브젝트를 배치하는 인스펙터 커스텀 버튼을 만드는 작업을 해볼 것이다.
위의 작업은 수작업으로 진행할 경우, cubeCount 횟수만큼 반복 작업을 해야하며, 나중에 중심 오브젝트를 추가로 배치할 계획이면 다시 그 추가배치 횟수 * cubeCount 만큼 작업 횟수가 폭발적으로 증가한다.
이런 큐브 생성 작업을 버튼 클릭 한 번에 자동으로 처리해주는 인스펙터 커스텀 버튼을 만들어 보자.
우선 CubeGenerator 클래스를 생성하고 다음과 같은 코드를 작성한다.
public class CubeGenerator : MonoBehaviour { [SerializeField] private GameObject cubePrefab; [SerializeField] private float distance; [SerializeField] private int cubeCount;
public void GenerateCubes() {
if (transform.childCount != 0) { for (int i = transform.childCount - 1; i >= 0; i--) { DestroyImmediate(transform.GetChild(i).gameObject); } }
for (int i = 0; i < cubeCount; i++) { var newCube = Instantiate(cubePrefab); newCube.transform.SetParent(gameObject.transform); newCube.transform.localPosition = new Vector3(0f, 0f, i * distance); newCube.transform.localRotation = Quaternion.identity; } } }
코드를 모두 작성한 뒤에는 CubeGenerator 클래스를 CubeStandard 오브젝트에 컴포넌트로 추가하고 큐브 오브젝트를 프리팹화하여 Cube Prefab 프로퍼티에 추가해준다.
JSON은 웹이나 네트워크에서 서버와 클라이언트 사이에서 데이터를 주고 받을 때 사용하는 개방형 표준 포멧으로, 텍스트를 사용하기 때문에 사람이 이해하기 쉽다는 장점이 있다.
이런 JSON 포멧을 유니티에서도 많이 사용하는 편이다. 네트워크 게임을 개발할 때 게임에 필요한 데이터를 주고 받거나, 게임 진행 상황을 저장하거나, 게임 설정을 저장하는 방식으로도 사용할 수 있다.
유니티에서 XML을 사용하는 것과 사용 범위가 거의 일치하는데, XML은 가독성이 매우 떨어지고 데이터를 넣거나 꺼내기 위해 파싱(Parsing)하는 과정이 까다로운데 반해서, JSON은 XML에 비해서 가독성이 좋고 직렬화(Serialize)와 비직렬화(Deserialize) 함수를 통해서 데이터에서 JSON 데이터로, JSON 데이터에서 데이터로 편하게 변환할 수 있다는 장점을 가지고 있다.
Newtonsoft의 JSON 라이브러리는 다양한 전체 기능을 제공하는 라이브러리로 시리얼라이즈 및 디시리얼라이즈하는 컴팩트한 기능만을 사용하기를 원한다면 유니티 엔진에 내장된 JsonUtility를 사용할 것을 권장한다.
JSON 라이브러리 다운로드 및 프로젝트에 임포트(Download JSON & Import JSON to project)
다운로드 받은 파일을 압축을 해제하고 폴더를 열어보면 위와 같은 폴더와 파일들이 보일텐데 그 중에서 Bin 폴더를 연다.
Bin 폴더 안에는 사용하는 .NET 버전에 따라 라이브러리 파일들이 폴더에 나눠져 담겨있는데, 일반적으로는 net35 폴더 안에 있는 dll을 사용하면 되지만, 유니티에서 .NET 4.x 기능을 사용하거나 최신 버전의 기능이 필요하다면 net45 폴더 안에 있는 dll을 사용해도 된다. 이번 섹션에서는 간단하게 JSON 사용법을 익힐 것이기 때문에 net35 버전을 사용한다.
net35 폴더 안에서 Newtonsoft.Json.dll 파일을 프로젝트 창에 드래그해서 프로젝트에 포함시킨다.
JSON의 기본구조
기본적인 JSON 데이터의 구조는 다음과 같다.
{
"id":"wergia",
"level":10,
"exp":33.3,
"hp":400,
"items":
[
"Sword",
"Armor",
"Hp Potion",
"Hp Potion",
"Hp Potion"
]
}
JSON의 데이터는 키(Key)와 값(Value) 쌍(Pair)로 이루어진 데이터를 저장하는데, items와 같이 배열로 된 데이터 역시 저장이 가능하고 객체 안에 객체를 넣는 것도 가능하며 위의 데이터 내용이 문자열로 이루어져 있기 때문에 사람이 알아보기가 매우 쉽다.
JSON 데이터에서 { } 는 객체를 의미하고, [ ] 는 순서가 있는 배열을 나타낸다. 그리고 JSON은 정수, 실수, 문자열, 불리언, null 타입의 데이터 타입을 지원한다.
JSON은 주석을 지원하지 않기 때문에, JSON 파일을 사람이 읽고 수정할 수 있도록 할 예정이라면, 키의 이름을 명확하게 정해서 이 값이 무엇을 의미하는지 확실히 표현하는게 좋다.
JSON의 단점은 작은 문법 오류에도 매우 민감하다는 점이다. 중간에 중괄호나 대괄호, 콜론, 쉼표가 하나라도 빠지면 JSON 파일이 깨져버리고 파일을 읽어들일 수 없게 된다. 이런 문제 때문에 구글에서 JSON 검사기를 검색하면 JSON 데이터가 유효한지 검사해주는 웹페이지들이 많다. JSON 데이터를 작성하고 난 뒤에는 JSON 데이터 파일의 깨짐으로 인한 버그를 막기 위해서 이런 JSON 검사기로 검사하고 사용하는 것이 좋다.
유니티에서 JSON 사용하기
JSON에 대해서 간단하게 알아보았으니 이제 유니티에서 JSON을 사용하는 방법에 대해서 알아보자.
기본적인 JSON <-> Object 변환하기
우선 Json 예제를 작성할 JsonExample 클래스를 하나 생성한다.
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class JsonExample : MonoBehaviour { // Start is called before the first frame update void Start() {
}
// Update is called once per frame void Update() { } }
JSON과 관련된 기능을 사용하기 위해서 상단의 using 지시문 파트에 다음 using 지시문을 추가한다.
using Newtonsoft.Json;
using 지시문을 추가하지 않아도 기능을 사용할 수는 있지만, 그렇게 하면 Newtonsoft.Json 네임스페이스를 계속해서 타이핑해야하기 때문에 using 지시문을 추가한다.
그리고 JSON 데이터와 오브젝트 간에 시리얼라이즈, 디시리얼라이즈 테스트를 위해 다음과 같은 클래스를 정의한다.
public class JTestClass { public int i; public float f; public bool b; public string str; public int[] iArray; public List<int> iList = new List<int>(); public Dictionary<string, float> fDictionary = new Dictionary<string, float>();
public JTestClass() { }
public JTestClass(bool isSet) {
if (isSet)
{ i = 10; f = 99.9f; b = true; str = "JSON Test String"; iArray = new int[] { 1, 1, 3, 5, 8, 13, 21, 34, 55 };
T JsonToOject<T>(string jsonData) { return JsonConvert.DeserializeObject<T>(jsonData); }
ObjectToJson() 함수는 JsonConvert 클래스의 SerializeObject() 함수를 이용해서 오브젝트를 문자열로 된 JSON 데이터로 변환하여 반환하는 처리를 하고 JsonToObject() 함수는 DeserializeObject() 함수를 이용해서 문자열로 된 JSON 데이터를 받아서 원하는 타입의 객체로 반환하는 처리를 한다.
함수들을 모두 작성했다면 Start() 함수에 우선 ObjectToJson() 함수를 테스트하는 코드를 작성한다.
코드를 저장하고 에디터에서 플레이 해보면 dataPath인 Assets 폴더 안에 JTestClass.json 파일이 생성되고 그 내용이 제대로 쓰여져 있는 것을 확인할 수 있다.
이번에는 방금 저장한 JSON 파일을 읽어들여서 오브젝트로 변환하는 코드를 작성한다. 예시 코드는 아래와 같다.
T LoadJsonFile<T>(string loadPath, string fileName) { FileStream fileStream = new FileStream(string.Format("{0}/{1}.json", loadPath, fileName), FileMode.Open); byte[] data = new byte[fileStream.Length]; fileStream.Read(data, 0, data.Length); fileStream.Close(); string jsonData = Encoding.UTF8.GetString(data); return JsonConvert.DeserializeObject<T>(jsonData); }
LoadJsonFile() 함수를 모두 작성했으면 Start() 함수를 다음과 같이 수정한다.
void Start() { var jtc2 = LoadJsonFile<JTestClass>(Application.dataPath, "JTestClass"); jtc2.Print(); }
코드를 저장하고 에디터에서 플레이해보면 정상적으로 파일의 JSON 데이터가 로드되어서 오브젝트로 변환되어 로그가 출력된 것을 확인할 수 있다.
유니티에서 JSON 사용시 주의점
유니티에서 JSON을 사용할 때, 몇가지 주의점이 있다.
우선 유니티에서 클래스를 만들 때, 일반적으로 대다수의 클래스는 모노비헤이비어(Monobehaviour)를 상속받는다.
public class JsonExample : MonoBehaviour { void Start() { GameObject obj = new GameObject(); obj.AddComponent<TestMono>(); Debug.Log(JsonConvert.SerializeObject(obj.GetComponent<TestMono>())); }
}
위의 예시 코드에 TestMono 클래스는 int 타입 변수 하나를 가지고 모노비헤이비어를 상속받는 클래스이다. 빈 게임 오브젝트에 TestMono 클래스를 컴포넌트로 붙여서 JSON데이터로 시리얼라이즈해서 로그로 출력하는 테스트인데 이를 플레이해서 테스트해보면 아래와 같이 에러가 발생한다.
이 예외는 gameObject에서 gameObject를 호출할 수 있는 순환구조 때문에 생기는 것인데 이것을 해결할 수는 있지만, 이후에도 다른 예외를 많이 발생시키기고 몇몇 문제는 해결책이 없기 때문에 Newtonsoft의 JSON 라이브러리로는 모노비헤이비어를 상속받는 클래스의 오브젝트를 JSON 데이터로 시리얼라이즈할 수는 없다. 그렇기 때문에 모노비헤이비어를 상속받는 클래스의 오브젝트를 시리얼라이즈하는 대신에 스크립트가 가지고 있는 프로퍼티를 클래스로 묶어서 해당 클래스만 시리얼라이즈하거나 유니티가 제공하는 JsonUntility 기능을 사용해서 시리얼라이즈하는 것을 추천한다.
다음은 Vector3를 시리얼라이즈하는 문제인데, Vector3를 그냥 시리얼라이즈 하려고 하면 모노비헤이비어를 시리얼라이즈하려고 할 때처럼 Self referencing loop 문제가 발생한다. 이것은 Vector3의 프로퍼티인 normalized에서 다시 normalized를 호출할 수 있기 때문에 발생하는 문제이다.
public class UJsonTester { public Vector3 v3;
public UJsonTester() { }
public UJsonTester(float f) { v3 = new Vector3(f, f, f); } }
public class JsonExample : MonoBehaviour { void Start() { JsonSerializerSettings setting = new JsonSerializerSettings(); ; setting.Formatting = Formatting.Indented; setting.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
UJsonTester jt = new UJsonTester(3f); Debug.Log(JsonConvert.SerializeObject(jt, setting)); }
}
이를 해결하기 위해서는 위의 코드처럼 JsonSerializerSetting을 만들어서 ReferenceLoopHandling을 Ignore로 설정하고 시리얼라이즈를 해야한다.
하지만 이런 방식으로 레퍼런스 반복을 무시하게 만들어도 normalized 벡터나 벡터의 길이 등의 불필요한 값들이 시리얼라이즈되기 때문에, 불필요하게 JSON 데이터의 길이가 늘어나는 문제가 발생한다.
public class JVector3 { [JsonProperty("x")] public float x; [JsonProperty("y")] public float y; [JsonProperty("z")] public float z;
public JVector3() { x = y = z = 0f; }
public JVector3(Vector3 v) { x = v.x; y = v.y; z = v.z; }
public JVector3(float f) { x = y = z = f; } }
public class UJsonTester { public JVector3 v3;
public UJsonTester() { }
public UJsonTester(float f) { v3 = new JVector3(f); }
public UJsonTester(Vector3 v) { v3 = new JVector3(v); } }
public class JsonExample : MonoBehaviour { void Start() { UJsonTester jt = new UJsonTester(transform.position); Debug.Log(JsonConvert.SerializeObject(jt)); }
}
외부 라이브러리를 이용해서 Vector3 중에서 x, y, z 좌표값만을 JSON 데이터로 시리얼라이즈하기를 원한다면 위의 예시 코드와 같이 별도의 시리얼라이즈용 Vector 클래스를 만들어서 시리얼라이즈를 해야한다.
이러한 번거로운 과정이 불편하다면, 유니티가 기본 제공하는 JsonUtility를 혼용해서 사용하는 방법도 있다. 유니티가 제공하는 JsonUtility로 Vector3를 시리얼라이즈하면 x, y, z 좌표값만을 JSON 데이터로 변환한다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
JSON은 웹이나 네트워크에서 서버와 클라이언트 사이에서 데이터를 주고 받을 때 사용하는 개방형 표준 포멧으로, 텍스트를 사용하기 때문에 사람이 이해하기 쉽다는 장점이 있다.
이런 JSON 포멧을 유니티에서도 많이 사용하는 편이다. 네트워크 게임을 개발할 때 게임에 필요한 데이터를 주고 받거나, 게임 진행 상황을 저장하거나, 게임 설정을 저장하는 방식으로도 사용할 수 있다.
유니티에서 XML을 사용하는 것과 사용 범위가 거의 일치하는데, XML은 가독성이 매우 떨어지고 데이터를 넣거나 꺼내기 위해 파싱(Parsing)하는 과정이 까다로운데 반해서, JSON은 XML에 비해서 가독성이 좋고 직렬화(Serialize)와 비직렬화(Deserialize) 함수를 통해서 데이터에서 JSON 데이터로, JSON 데이터에서 데이터로 편하게 변환할 수 있다는 장점을 가지고 있다.
유니티에서 기본 제공하는 JsonUtility는 컴팩트한 최소한의 기능만을 제공하기 때문에 JSON 라이브러리의 모든 기능을 사용하고 싶다면 다른 JSON 라이브러리를 사용할 것을 권장한다.
JSON의 기본구조
기본적인 JSON 데이터의 구조는 다음과 같다.
{
"id":"wergia",
"level":10,
"exp":33.3,
"hp":400,
"items":
[
"Sword",
"Armor",
"Hp Potion",
"Hp Potion",
"Hp Potion"
]
}
JSON의 데이터는 키(Key)와 값(Value) 쌍(Pair)로 이루어진 데이터를 저장하는데, items와 같이 배열로 된 데이터 역시 저장이 가능하고 객체 안에 객체를 넣는 것도 가능하며 위의 데이터 내용이 문자열로 이루어져 있기 때문에 사람이 알아보기가 매우 쉽다.
JSON 데이터에서 { } 는 객체를 의미하고, [ ] 는 순서가 있는 배열을 나타낸다. 그리고 JSON은 정수, 실수, 문자열, 불리언, null 타입의 데이터 타입을 지원한다.
JSON은 주석을 지원하지 않기 때문에, JSON 파일을 사람이 읽고 수정할 수 있도록 할 예정이라면, 키의 이름을 명확하게 정해서 이 값이 무엇을 의미하는 지 하는게 좋다.
JSON의 단점은 작은 문법 오류에도 매우 민감하다는 점이다. 중간에 중괄호나 대괄호, 콜론, 쉼표가 하나라도 빠지면 JSON 파일이 깨져버리고 파일을 읽어들일 수 없게 된다. 이런 문제 때문에 구글에서 JSON 검사기를 검색하면 JSON 데이터가 유효한지 검사해주는 웹페이지들이 많다. JSON 데이터를 작성하고 난 뒤에는 JSON 데이터 파일의 깨짐으로 인한 버그를 막기 위해서 이런 JSON 검사기로 검사하고 사용하는 것이 좋다.
유니티에서 JSON 사용하기
JSON에 대해서 간단하게 알아보았으니 이제 유니티에서 JSON을 사용하는 방법에 대해서 알아보자.
기본적인 JSON <-> Object 변환하기
우선 Json 예제를 작성할 JsonExample 클래스를 하나 생성한다.
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class JsonExample : MonoBehaviour { // Start is called before the first frame update void Start() {
}
// Update is called once per frame void Update() { } }
유니티가 제공하는 JsonUtility는 UnityEngine 네임스페이스에 포함되어 있기 때문에, 다른 using 지시문을 추가할 필요가 없다.
그리고 JSON 데이터와 오브젝트 간에 시리얼라이즈, 디시리얼라이즈 테스트를 위해 다음과 같은 클래스를 정의한다.
[System.Serializable]
public class JTestClass { public int i; public float f; public bool b;
public Vector3 v; public string str; public int[] iArray; public List<int> iList = new List<int>();
public JTestClass() { }
public JTestClass(bool isSet) {
if (isSet)
{ i = 10; f = 99.9f; b = true;
v = new Vector3(39.56f, 21.2f, 6.4f); str = "JSON Test String"; iArray = new int[] { 1, 1, 3, 5, 8, 13, 21, 34, 55 };
T JsonToOject<T>(string jsonData) { return JsonUtility.FromJson<T>(jsonData); }
ObjectToJson() 함수는 JsonUtility 클래스의 ToJson() 함수를 이용해서 오브젝트를 문자열로 된 JSON 데이터로 변환하여 반환하는 처리를 하고 JsonToObject() 함수는 FromJson() 함수를 이용해서 문자열로 된 JSON 데이터를 받아서 원하는 타입의 객체로 반환하는 처리를 한다.
함수들을 모두 작성했다면 Start() 함수에 우선 ObjectToJson() 함수를 테스트하는 코드를 작성한다.
코드를 저장하고 에디터에서 플레이 해보면 dataPath인 Assets 폴더 안에 JTestClass.json 파일이 생성되고 그 내용이 제대로 쓰여져 있는 것을 확인할 수 있다.
이번에는 방금 저장한 JSON 파일을 읽어들여서 오브젝트로 변환하는 코드를 작성한다. 예시 코드는 아래와 같다.
T LoadJsonFile<T>(string loadPath, string fileName) { FileStream fileStream = new FileStream(string.Format("{0}/{1}.json", loadPath, fileName), FileMode.Open); byte[] data = new byte[fileStream.Length]; fileStream.Read(data, 0, data.Length); fileStream.Close(); string jsonData = Encoding.UTF8.GetString(data); return JsonUtility.FromJson<T>(jsonData); }
LoadJsonFile() 함수를 모두 작성했으면 Start() 함수를 다음과 같이 수정한다.
void Start() { var jtc2 = LoadJsonFile<JTestClass>(Application.dataPath, "JTestClass"); jtc2.Print(); }
코드를 저장하고 에디터에서 플레이해보면 정상적으로 파일의 JSON 데이터가 로드되어서 오브젝트로 변환되어 로그가 출력된 것을 확인할 수 있다.
유니티의 JsonUtility가 제공하는 특수한 기능들
유니티의 JsonUtility는 유니티 엔진을 위한 특수한 기능들을 몇 가지 제공한다.
Vector3 시리얼라이즈
Vector3는 유니티에 내장된 클래스로써 위치나 방향을 표시하는데 자주 사용되는 클래스이다. 그렇기 때문에 이전에 접속했을 때의 캐릭터의 마지막 위치같은 데이터로 저장될 수 있다. 하지만 Vector3는 유니티의 JsonUtility가 아닌 다른 JSON 라이브러리를 사용해서 시리얼라이즈를 하면 normalized 프로퍼티로 인해서 Self reference loop 문제를 발생시키고 이 문제를 해결해도 아래의 이미지와 같이 x, y, z 좌표값 이외에 정규화된 벡터와 그 길이와 길이의 제곱등 불필요한 정보들을 많이 포함하게 된다.
이런 문제는 Vector3를 시리얼라이즈할 때 JsonUtility를 사용하면 간단하게 해결된다.
[System.Serializable] public class UJsonTester { public Vector3 v3;
public UJsonTester() { }
public UJsonTester(float f) { v3 = new Vector3(f, f, f); }
public UJsonTester(Vector3 v) { v3 = v; } }
public class JsonExample : MonoBehaviour { void Start() { UJsonTester jt = new UJsonTester(transform.position); Debug.Log(JsonUtility.ToJson(jt)); }
}
테스트 코드를 저장하고 테스트해보면 불필요한 값 없이 x, y, z 좌표값만 저장된 것을 확인할 수 있다.
모노비헤이비어를 상속받는 클래스의 오브젝트 시리얼라이즈
다른 JSON 라이브러리를 사용해서 모노비헤이비어를 상속받는 클래스의 오브젝트를 시리얼라이즈하려고 하면 여러 문제가 발생하며 시리얼라이즈가 되지 않는다.
[System.Serializable] public class TestMono : MonoBehaviour { public int i = 10; public Vector3 pos = new Vector3(1f, 2f, 3f); }
모노비헤이비어를 상속받는 TestMono 클래스를 선언했으면 JsonUtility로 시리얼라이즈하는 코드를 작성한다. 모노비헤이비어를 상속받는 클래스의 오브젝트를 시리얼라이즈할 때, 주의할 점은 반드시 클래스가 컴포넌트로 붙어있는 게임 오브젝트가 아니라 GetComponent() 등으로 직접 가져온 클래스로 시리얼라이즈를 해야한다.
GameObject obj = new GameObject(); obj.name = "TestMono 01"; var jd = JsonUtility.ToJson(obj.GetComponent<TestMono>()); Debug.Log(jd);
JsonUtility로 모노비헤이비어를 상속받는 클래스의 오브젝트를 시리얼라이즈하면 문제없이 깔끔하게 오브젝트가 JSON 데이터로 변환되는 것을 확인할 수 있다.
이렇게 JSON 데이터로 변환한 모노비헤이비어를 상속받는 클래스의 오브젝트는 디시리얼라이즈를 할 때 FromJson() 함수를 사용하면 아래와 같이 새로운 인스턴스를 생성하지 못했다고 에러가 발생하고 시리얼라이즈에 실패한다.
이런 문제를 해결하기 위해서는 FromJson() 함수 대신에 FromJsonOverwrite() 함수를 사용해야 한다. 이 함수는 JSON 데이터를 오브젝트로 변환할 때, 새로운 오브젝트를 만들지 않고 기존에 있는 오브젝트에 클래스의 변수 값을 덮어씌우는 처리를 한다.
FromJsonOverwrite() 함수의 테스트를 위해서 Start() 함수의 내용을 아래와 같이 수정한다.
GameObject obj = new GameObject(); obj.name = "TestMono 01"; var t = obj.AddComponent<TestMono>(); t.i = 333; t.pos = new Vector3(-939, -33, -22); var jd = JsonUtility.ToJson(obj.GetComponent<TestMono>()); Debug.Log(jd);
GameObject obj2 = new GameObject(); obj2.name = "TestMono 02"; var t2 = obj2.AddComponent<TestMono>(); JsonUtility.FromJsonOverwrite(jd, t2);
에디터로 가서 테스트를 진행해보면 TestMono 02 오브젝트가 생성되고, 이 오브젝트가 가진 TestMono 컴포넌트의 프로퍼티의 값이 TestMono 01 오브젝트를 JSON 데이터로 변환한 값이 덮어씌워져 있는 것을 확인할 수 있다.
JsonUtility와 딕셔너리(Dictionary)
유니티에 내장된 JsonUtility를 통해서 JSON을 다룰 때 알아두어야 할 점은 JsonUtility는 딕셔너리에 대한 시리얼라이즈 및 디시리얼라이즈를 지원하지 않는다는 것이다. JsonUtility는 JSON을 다루기 위한 가장 최소한의 기능만 제공하기 때문에 딕셔너리를 JSON으로 다루려면 Newtonsoft나 다른 JSON 라이브러리를 사용하거나 이에 대한 기능을 직접 구현해야 할 것이다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
어떤 공간에서 위치를 찾고자 하는 것인지 기준을 잡기 위해서 축이라는 것을 사용하는데, X라는 하나의 축을 사용하면 수직선 상에서의 점의 위치를 찾아낼 수 있게 된다.
X축과 Y축, 2개의 축을 이용하면 평면 상의 중심으로부터의 점의 위치를 알 수 있다.
X축과 Y축 그리고 Z축까지 3개의 축을 사용하면 3차원 공간 상의 점의 위치를 알아낼 수 있게 된다.
유니티 엔진에서는 씬이라는 공간 안에서 오브젝트의 위치를 표현하기 위해서 좌표계를 이용한다.
수직선을 이용한 1차원 상의 공간을 사용하는 게임은 별로 없고 대부분은 2D 좌표계나 3D 좌표계를 사용한다.
2D 좌표계를 사용하는 게임으로는 슈퍼 마리오 브라더스를 예로 들 수 있고, 3D 좌표계를 사용하는 게임으로는 하프라이프를 예로 들 수 있다. 2D 좌표계를 사용하는 게임은 움직임이 상하좌우 또는 전후좌우로 움직임이 제한되지만 3D 좌표계를 사용하는 게임은 전후좌우 뿐만 아니라 상하의 움직임까지 가능하다.
왼손 좌표계와 오른손 좌표계
좌표의 축을 정하는 방법은 여러 가지가 있는데 그 중 대표적인게 바로 왼손 좌표계와 오른손 좌표계이다.
우선 오른손 좌표계는 엄지 손가락이 X축, 검지 손가락이 Y축, 중지 손가락이 Z축이라고 가정하고 엄지를 종이 위에 수직선을 그었을 때 양수의 방향, 즉 오른쪽을 향하게 하고 검지를 X축과 직교하는 위 방향으로 향하게 했을 때, 중지가 나를 바라보는 방향이 되게 XYZ축을 정의하는 방식이다. 일반적인 수학에서는 이 오른손 좌표계를 표준으로 사용한다.
그 다음 왼손 좌표계는 엄지와 검지의 방향을 오른손 좌표계와 같이 맞췄을 때 중지는 내가 바라보는 방향을 가리키게 되도록 XYZ축을 정의한다. 유니티에서는 이 왼손 좌표계를 기준으로 사용한다.
한마디로 왼손 좌표계와 오른손 좌표계의 차이는 Z축이 가리키는 방향이 달라진다는 것이다. 오른손 좌표계에서는 화면에서 바라보는 사람에게로 다가오는 방식으로 Z축이 가리키게 되지만, 왼손 좌표계는 화면을 바라보는 사람에게서 화면 방향으로 Z축이 가리키게 된다.
Y-Up과 Z-Up
좌표계를 정의할 때, X축은 기본적으로 첫 번째 수평 방향의 수직선을 기준으로 하기 때문에 대부분 같은 방향으로 고정되어 있다. 여기서 발생하는 문제는 두 번째 축인 Y축의 방향을 어떻게 정의하느냐이다.
여기에는 두 가지 관점이 있는데 위에서 내려다보는 시점으로 Y축을 앞으로 나가는 방향으로 정의하는 방식이 하나로, 이렇게하면 새로 추가되는 세 번째 축인 Z축이 높이 축이 되는 Z-Up 방식이 된다. 언리얼 엔진과 3D 모델링 툴인 3ds Max가 이 방식을 채택한다.
다른 방식으로는 옆에서 바라보는 시점에서 Y축을 위로 향하는 방향으로 정의하는 것이다. Y축이 높이 축이 되기 때문에 Y-Up이라고 하고 유니티 엔진은 이 방식을 채택한다.
월드 좌표와 로컬 좌표
바로 전 파트까지 좌표계란 무엇인지와 유니티 엔진에서는 어떤 방식의 좌표계를 채택했는지를 설명했다. 이번 파트에서 이야기할 내용은 월드 좌표와 로컬 좌표에 대한 이야기이다.
월드 좌표란 세상을 중심으로 어느 위치에 있느냐를 의미하는 것이고, 로컬 좌표는 나 혹은 어느 한 오브젝트를 중심으로 어느 위치에 있느냐 하는 것이다.
사실 실제 세상에서 세상을 중심으로 어떠한 객체가 어느 위치에 있느냐 하는 것은 그 세상의 중심이 어디인지는 사람마다 생각이 다르고 절대적이라고 할 수 있는 중심이 없기 때문에 세상의 중심을 기준으로 한 위치라는 것은 구할 수 없겠지만, 게임이나 유니티 엔진에서는 가능하다.
바로 씬 안의 의 위치가 바로 게임 안에서의 세상의 중심이 된다.
월드 좌표를 대상으로 봤을 때, 선택된 큐브는 {-6, 0, -4}의 위치에 존재한다.
그렇다면 로컬 좌표란 무엇인가? 왜 월드의 중심이 아닌 어느 한 오브젝트를 중심으로 위치를 측정해야하는 걸까?
위의 이미지를 보자. 스피어 오브젝트 하나가 월드 좌표를 설명할 때 사용했던 큐브 오브젝트보다 XZ좌표가 각각 1씩 월드의 중심에 가깝게 존재하고 있다. 큐브 오브젝트의 위치가 {-6, 0, -4}였으니, 스피어 오브젝트는 {-5, 0, -3}의 위치에 있다. 만약에 추가된 이 스피어 오브젝트를 큐브 스피어를 중심으로 공전하게 만들고 싶다면 어떻게 해야할까?
만약 월드 좌표만으로 처리하려고 한다면 위의 이미지와 같이 좌표가 복잡하게 바뀌는 것을 알 수 있다.
하지만 스피어 오브젝트를 큐브 오브젝트의 자식 오브젝트로 만들면 포지션이 월드의 중심 좌표를 기준으로한 월드 좌표인 {-5, 0, -3}이 아니라 큐브 오브젝트를 중심으로한 로컬 좌표 로 표시되는 것을 확인할 수 있다.
이렇게 하고 나면 간단하게 큐브 오브젝트를 회전시키는 것만으로도 궤도를 따라서 스피어 오브젝트가 간단하게 공전하는 것을 볼 수 있다. 물론 큐브도 함께 자전한다는 문제가 있기는 하지만 이런 문제는 간단하게 해결하고 스피어 오브젝트만 궤도를 따라서 공전하게도 만들 수 있다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.