다른 cs 파일의 클래스 호출과 static 로직에 대하여


로딩 씬(Loading Scene) 구현하기 글에 달아주신 ㅇㅇ님의 질문 댓글에 대한 답변입니다. 질문의 내용은 아래와 같습니다.


 

로딩 씬 구현하기 글을 보면 LoadingSceneManager 클래스는 아래와 같이 구현되어 있습니다.


LoadingSceneManager.cs

using System.Collections;

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class LoadingSceneManager : MonoBehaviour
{
    public static string nextScene;

    [SerializeField]
    Image progressBar;

    private void Start()
    {
        StartCoroutine(LoadScene());
    }

    public static void LoadScene(string sceneName)
    {
        nextScene = sceneName;
        SceneManager.LoadScene("LoadingScene");
    }

    IEnumerator LoadScene()
    {
        yield return null;

        AsyncOperation op = SceneManager.LoadSceneAsync(nextScene);
        op.allowSceneActivation = false;

        float timer = 0.0f;
        while (!op.isDone)
        {
            yield return null;

            timer += Time.deltaTime;

            if (op.progress >= 0.9f)
            {
                progressBar.fillAmount = Mathf.Lerp(progressBar.fillAmount, 1f, timer);

                if (progressBar.fillAmount == 1.0f)
                    op.allowSceneActivation = true;
            }
            else
            {
                progressBar.fillAmount = Mathf.Lerp(progressBar.fillAmount, op.progress, timer);
                if (progressBar.fillAmount >= op.progress)
                {
                    timer = 0f;
                }
            }
        }
    }
}


첫 번째 질문에서의 LoadingSceneManager.LoadScene("Scene2");를 호출하는 클래스는 아래와 같이 구현되어 있었습니다.


TestCode.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestCode : MonoBehaviour
{

    // Use this for initialization
    void Start ()
    {
        LoadingSceneManager.LoadScene("Scene2");
    }
}



다른 cs 파일의 클래스 호출


우선 첫 번째로 LoadingSceneManager.cs 파일에 구현된 LoadingSceneManager 클래스를 어떻게 TestCode.cs 파일로 다른 선언 없이 바로 호출할 수 있었느냐라는 의도로 질문하신 것 같습니다. 어떻게 다른 cs 파일에 정의된 클래스를 바로 호출할 수 있었냐는 질문을 하신 걸로 보아, ㅇㅇ님께서는 아마 유니티를 배우시기 이전에 C/C++과 같이 다른 소스 파일의 코드를 불러오기 위해서는 별도의 include 등의 전처리가 필요한 언어를 배우신게 아닌가 싶습니다.


그런 C/C++에서는 다른 소스 파일의 클래스를 불러와서 사용하기 위해서는 아래의 예시와 같이 전처리기에서 사용하고자 하는 클래스가 담긴 헤더를 포함시켜주어야 합니다.


LoadingSceneManager.h

#include <string>


#pragma once


class LoadingSceneManager

{

public:

static void LoadingScene(std::string sceneName)

{

// 씬 로딩 처리 ...

}

};


TestCode.h

#include "LoadingSceneManager.h" // C/C++에서는 이렇게 호출하고자 하는 클래스가 담긴 헤더를 호출해주어야 한다.


#pragma once


class TestCode

{

public:

void Start()

{

LoadingSceneManager::LoadingScene("Scene2");

}

};


하지만 C#에서는 헤더 파일이 따로 존재하지 않고 cs파일만 존재하며, 같은 프로젝트 안에 있는 cs파일이라면, 다른 cs 파일 안에 정의된 클래스를 가져와서 사용할 수 있다는 뜻입니다.



정적 함수(Static Function)


두 번째 질문으로는 static으로 선언된 LoadScene() 함수가 LoadSceneManager 오브젝트가 없는 다른 씬에서 어떻게 호출될 수 있는가 입니다.



 

로딩 씬 구현하기 글에서 구현한 방식을 그림으로 표현하면 위의 이미지와 같습니다. 씬은 Scene1, LoadingScene, Scene2가 존재하고 TestCode 클래스의 인스턴스는 Scene1에서만 존재하고 LoadSceneManager 클래스의 인스턴스는 LoadingScene에서만 존재합니다. 그런데 Scene1에서 LoadScene에만 존재하는 LoadSceneManager의 함수를 호출할 수 있느냐가 메인인데, 이 부분은 정적 함수(static function)에 대한 기본적인 이해가 필요합니다.


    public static void LoadScene(string sceneName)
    {
        nextScene = sceneName;
        SceneManager.LoadScene("LoadingScene");

    }


코드를 다시 보면 아시겠지만, LoadSceneManager 클래스의 LoadScene(string) 함수는 static으로 선언되어 있습니다. 이렇게 static으로 선언된 함수를 정적 함수라고 하며, 이러한 정적 함수는 클래스의 인스턴스가 생성되지 않았더라도 컴파일 시점에 전역에 할당되어 있습니다. 그렇기 때문에 TestCode.cs에서와 같이 해당 클래스의 이름을 통해서 곧바로 접근이 가능해집니다.


다시 한번 풀어서 이야기하자면, TestCode.cs가 실행되는 때에는 LoadSceneManager 클래스의 인스턴스가 생성되어 있지 않지만, 전역에 할당되어 있는 LoadSceneManager 클래스의 전역 함수에 접근하여 정적 변수(이것 역시 전역에 할당되어 모든 LoadSceneManager 클래스의 인스턴스들이 공유한다.)인 nextSceneName에 다음으로 넘어갈 씬 이름을 전달하고 LoadingScene을 불러옵니다. 


LoadingScene이 전부 불러와지면 LoadSceneManager 클래스의 인스턴스가 생성되고, LoadSceneManager의 Start() 함수가 실행되면서 LoadScene 코루틴을 호출하는 순서로 동작합니다.




PS.

로딩 씬 구현하기 글은 2017년에 작성한 오래 전 글이라 내용이 많이 불친절한 편입니다. 기본적인 내용은 많이 바뀌지 않겠지만, 유니티 버전이 많이 바뀌면서 좀 더 알아보기 쉽게 새로 작성할 계획은 오래 전부터 세워둔 상태였는데, 이래저래 시간을 보내면서 아직까지 작성하지 못했습니다. 최대한 이른 시일 내로 새로운 버전으로 작성해보도록 하겠습니다.

반응형
  1. 료용 2019.12.26 02:02

    닉네임도 ㅇㅇ이라는 싸가지없는 닉으로 해놨는데 답변해주셔서감사합니다.

Tutorial (7)

 

애니메이션

 

작성 기준 버전 :: 2018.3.2f1

 

[튜토리얼의 내용을 유튜브 영상을 통해서도 확인하실 수 있습니다.]

 

게임에서 캐릭터가 어떠한 동작도 하지 않고 가만히 멈춘 채로 플레이어가 입력하는 대로 움직이가만 한다면 그 게임은 과연 어떤 느낌일까? 아마 그것은 굉장히 기괴한 모습이거나, 게임이 완성되지 못한 느낌일 것이다. 이렇듯이 애니메이션은 게임에 생동감을 불어넣는 중요한 요소이다.

 

메카님(Mecanim)

 

유니티에서 지원하는 애니메이션 시스템을 유니티 측에서는 메카님이라고 이름을 붙였다. 이 메카님 시스템은 기본적인 애니메이션 기능은 물론 애니메이션 레이어, 애니메이션 블렌드, 애니메이션 리타게팅 등의 다양한 기능을 제공한다. 애니메이션과 관련된 고급 기능들은 이후에 다른 섹션을 통해서 알아보도록 하고 이번 섹션에는 메카님 시스템의 기초적인 애니메이션 기능을 알아보도록 하자.

 

 

애니메이션 클립과 애니메이터 컨트롤러(Animation Clip & Animator Controller)

 

유니티의 애니메이션 시스템은 해당 오브젝트가 어떻게 움직여야 하는지에 대한 정보들이 포함된 애니메이션 클립과 플로우 차트와 같은 방식으로 애니메이션 클립들을 구조화하여 현재 어떤 클립이 재생되어야 하고 언제 애니메이션이 변경되어야 하는지 등을 추적하는 상태머신 형태의 애니메이터 컨트롤러로 이루어진다.

 

애니메이터 컨트롤러와 애니메이션 클립의 아이콘 형태는 다음과 같다.

 

 

애니메이션 클립을 만드는 방법은 크게 두 가지가 있다. 첫 번째는 3ds Max나 Maya 같은 외부의 프로그램으로 애니메이션을 만들어서 임포트 하는 것이고, 다른 하나는 유니티에서 직접 애니메이션 키를 잡아서 클립을 만드는 것이다.

 

3ds Max 같은 외부 프로그램에서 애니메이션을 만들어서 임포트하는 방식은 3D 모델링의 애니메이션을 만들고자 할 때 주로 사용하며 유니티 엔진에서 직접 키를 잡아서 애니메이션 클립을 만드는 것은 비교적 간단한 애니메이션이나 UI 애니메이션을 만들고자 할 때 사용하는 빈도가 높다.

 

 

외부에서 만든 애니메이션 임포트하기

 

Practice Animation.zip
다운로드

 

우선은 외부에서 만들어진 애니메이션을 임포트하는 방법을 배워보자. 연습용 애니메이션은 위의 첨부파일을 다운로드 받으면 된다. 다운 받은 압축파일의 압축을 해제하면 BoxMan@Run.FBX, BoxMan@Stand.FBX, BoxMan@Walk.FBX, BoxMan@Attack.FBX 네 개의 FBX 파일이 나올 것이다. 일반적으로 외부의 3D 모델링 프로그램을 통해 만들어진 애니메이션들은 FBX 확장자를 가진 것을 사용한다.

 

 

이렇게 받은 네 개의 파일을 유니티 엔진의 프로젝트 뷰로 드래그&드롭 한다. 그렇게 하면 네 개의 파일이 우리의 프로젝트에 포함되는 것을 확인할 수 있다.

 

 

임포트된 FBX 파일은 프로젝트 뷰에서 파란 직육면체에 종이가 붙은 아이콘으로 표현되며, 그 앞의 작은 삼각형을 클릭해서 접힌 부분을 열면 그 아래에 애니메이션 클립이 포함되어 있는 것을 확인할 수 있다.

 

 

이 FBX 파일을 선택한 상태로 인스펙터 창에서 애니메이션의 이름이나 길이, 반복 여부 등을 수정할 수 있다.

 

 

 

 

 

유니티 엔진에서 직접 애니메이션 클립 만들기

 

 

유니티 엔진에서 직접 애니메이션 클립을 만드는 방법을 배우기 위해서는 우선 씬에 애니메이션 클립을 만들 게임오브젝트 하나를 생성한다.

 

 

그리고 생성된 오브젝트를 선택한다. 

 

 

그 다음에 상단 메뉴바에서 Window>Animation>Animation을 선택하거나 Ctrl+6 단축키를 누르면 애니메이션을 수정할 수 있는 애니메이션 패널이 열린다.

 

 

아직 큐브에는 다른 애니메이션이 없기 때문에 애니메이션 클립을 생성하게 도와주는 Create 버튼만 보인다. 유니티 엔진에서 애니메이션 클립을 직접 만들기 위해서는 이 버튼을 사용하면 된다. 이 버튼을 클릭한다.

 

 

 

 

그러면 애니메이션 클립 생성 및 저장을 위한 대화상자가 뜨는데 CubeRotating.anim 이라는 이름으로 애니메이션 클립을 하나 생성하자.

 

 

애니메이션 클립이 생성되면 애니메이션 창에 타임라인이 표시된다.

 

  

이제 큐브가 회전하는 애니메이션을 추가하기 위해서 Add Property 버튼을 누르고 Transform 항목의 Rotation의 + 버튼을 눌러준다.

 

 

로테이션 프로퍼티가 추가되면 타임라인의 1초 지점을 클릭하고 Rotation.y 값을 360으로 설정한다.

 

 

그 다음 애니메이션 창의 재생 버튼을 눌러보면 화면에 배치된 큐브가 회전하는 것을 확인할 수 있다.

 

유니티 엔진에서 직접 만들어지는 애니메이션 클립은 대부분 이런 과정을 통해서 만들어지며 일반적으로 간단한 애니메이션이나 UI 애니메이션을 만들 때 사용되는 경우가 많다.

 

 

 

 

 

애니메이터 컨트롤러

 

애니메이션 클립에 대해서 설명했으니 이제 애니메이터 컨트롤러에 대해서 이야기할 차례이다. 앞서 이야기 했듯이 애니메이션 클립이 한 동작에 대한 애니메이션이라면 애니메이터 컨트롤러는 여러 애니메이션 클립을 모아서 오브젝트가 어느 시점에 어떤 애니메이션을 어떻게 재생할지 결정하는 역할을 한다.

 

  

애니메이터의 기본적인 구성요소는 스테이트(State), 트랜지션(Transition), 파라미터(Parameter) 이렇게 세 가지이다.

 

 

스테이트(State)

 

 

스테이트는 일반적으로 애니메이터에서 애니메이션 클립을 담고 있는 하나의 상태로, 지금 어느 애니메이션 클립이 재생되어야 하는가를 표현한다.

 

 

스테이트 중 하나를 클릭하여 선택하면 인스펙터 창에서 현재 선택된 스테이트의 정보를 확인하고 수정할 수 있다. 스테이트의 이름을 바꿀 수 있는 것은 물론이고 Motion 프로퍼티에는 현재 스테이트가 재생할 애니메이션 클립을 설정할 수 있고 Speed 프로퍼티를 통해서 애니메이션이 재생될 속도 역시 설정할 수 있다.

 

또한 위 예시 이미지에서 배치된 스테이트들은 애니메이터의 가장 기본적인 스테이트들로 스테이트당 하나의 애니메이션 클립을 담는다. 애니메이션 클립 하나를 담는 스테이트 이외에도 파라미터 값에 따라서 여러 애니메이션을 블랜딩해주는 블랜드 트리나, 여러 스테이트들을 담을 수 있는 서브-스테이트 머신이 있다. 추가적인 내용은 다른 파트에서 다루도록 하겠다.

 

특수한 스테이트

 

예시로 보여진 애니메이터 컨트롤러의 그래프를 보면 일반적인 노드는 회색의 사각형으로 표시되고 있는데, 그 외에 특별한 형태의 노드를 볼 수 있다. 

 

 

첫 번째는 엔트리(Entry)다. 엔트리는 애니메이션이 처음 시작될 때의 진입점을 의미한다.

 

 

이 엔트리에서 제일 처음으로 연결된 노드는 주황색으로 표시되며, 게임오브젝트가 활성화되어 애니메이션이 시작되면 이 주황색으로 표시된 애니메이션부터 재생이 시작된다.

 

 

노드를 기본 스테이트로 만들기 위해서는 기본 스테이트로 만들고자 하는 노드를 우클릭하고 [Set as Layer Default State] 항목을 선택하면 된다.

 

 

두 번째는 모든 스테이트(Any State) 노드이다. 이 노드는 애니메이터가 어떤 애니메이션을 재생하고 있는 상태이던 간에 트랜지션의 조건이 만족되면 무조건 다음 스테이트로 넘어가서 애니메이션을 재생하게 만든다.

 

예를 들어 캐릭터가 걷는 중이든, 가만히 서있는 중이든, 아니면 포션을 마시는 중이었든, 큰 데미지를 입어서 죽으면, 바로 Die 스테이트로 넘어가도록 하는 것이다.

 

 

마지막으로 엑시트(Exit) 노드이다. 엑시트 노드는 애니메이터의 흐름이 한 번 끝났음을 의미하고, 엑시트 노드를 통과하면 엔트리 노드로부터 다시 애니메이터의 흐름이 다시 시작된다.

 

 

파라미터(Parameter)

 

 

애니메이터의 파라미터는 한 애니메이션에서 다른 애니메이션으로 전환되는 조건이 되는 변수의 역할을 한다. Parameters에서 + 버튼을 누르면 추가할 파라미터의 종류를 선택할 수 있다. 파라미터의 종류로는 Float, Int, Bool, Trigger가 있으며 Float는 소수점을 나타낼 수 있는 실수, Int는 정수, Bool는 참/거짓을 표현하는 논리 변수이며, Trigger는 Set되면 True가 되고 해당 Trigger가 걸린 트랜지션을 통과하면 자동으로 False로 바뀌는 타입이다.

 

 

트랜지션(Transition)

 

 

트랜지션은 애니메이터에서 스테이트와 스테이트 사이를 이어주는 것이다. 스테이트 사이를 이어줄 때, 애니메이션이 어느 방향으로 흘러갈지 방향을 정할 수 있다. 그 외에도 트랜지션을 선택하면 인스펙터 창을 통해서 선택된 트랜지션이 실행될 조건이나, 한 스테이트에서 다른 스테이트로 넘어갈 때 애니메이션을 어떻게 블랜딩해줄 것인지 등을 설정할 수 있다.

 


 

이 외의 애니메이션과 관련된 포스트는 애니메이션 카테고리에서 확인할 수 있다.

 

 

반응형

Q&A ::  일반 클래스 내부에


MonoBehaviour 상속 클래스


멤버 변수 저장하기


유니티에서 JSON 사용하기(Unity JSON Utility) 글에 달아주신 psj님의 질문 댓글에 대한 답변입니다. 질문의 내용은 다음과 같습니다.


안녕하세요 올려주신 글 보고 잘 공부하고 갑니다.
궁금한 게 하나 있는데요
public Class Aclass : MonoBehaviour {}
public Class Bclass
{
Aclass a;
}
이런 식으로 MonoBahaviour를 상속 받지 않은 순수 클래스 안에 멤버 변수로 MonoBehaviour를 상속 받은 클래스가 있을 경우에는
Bclass안에 Aclass를 어떤 식으로 저장해야하나요??

출처: https://wergia.tistory.com/164#comment15327425 [베르의 프로그래밍 노트]
안녕하세요 올려주신 글 보고 잘 공부하고 갑니다.
궁금한 게 하나 있는데요
public Class Aclass : MonoBehaviour {}
public Class Bclass
{
Aclass a;
}
이런 식으로 MonoBahaviour를 상속 받지 않은 순수 클래스 안에 멤버 변수로 MonoBehaviour를 상속 받은 클래스가 있을 경우에는
Bclass안에 Aclass를 어떤 식으로 저장해야하나요??

출처: https://wergia.tistory.com/164#comment15327425 [베르의 프로그래밍 노트]

안녕하세요 올려주신 글 보고 잘 공부하고 갑니다.
궁금한 게 하나 있는데요


public class Aclass : MonoBehaviour {}


public class Bclass
{
    Aclass a;
}


이런 식으로 MonoBahaviour를 상속 받지 않은 순수 클래스 안에 멤버 변수로 MonoBehaviour를 상속 받은 클래스가 있을 경우에는
Bclass안에 Aclass를 어떤 식으로 저장해야하나요??

안녕하세요 올려주신 글 보고 잘 공부하고 갑니다.
궁금한 게 하나 있는데요
public Class Aclass : MonoBehaviour {}
public Class Bclass
{
Aclass a;
}
이런 식으로 MonoBahaviour를 상속 받지 않은 순수 클래스 안에 멤버 변수로 MonoBehaviour를 상속 받은 클래스가 있을 경우에는
Bclass안에 Aclass를 어떤 식으로 저장해야하나요??

출처: https://wergia.tistory.com/164#comment15327425 [베르의 프로그래밍 노트]


모노비헤이비어 클래스를 상속받지 않은 일반 클래스인 Bclass가 모노비헤이비어 클래스를 상속받은 Aclass를 멤버 변수로서 가질 때, 이 Bclass의 멤버 변수인 a에 어떻게 Aclass의 객체를 대입/할당 할 수 있는지에 대한 질문입니다.


우선 여기서 알아두어야 할 점은, Bclass는 앞서 말했듯이 모노비헤이비어 클래스를 상속받지 않았기 때문에 Awake(), Start(), Update() 등의 함수를 가지지 못했고, 유니티 엔진에 의해서 코드가 실행되지 않는다는 것입니다. 이 클래스에 속한 함수 등의 코드를 실행시키기 위해서는 유니티 엔진에 의해서 작동하는 모노비헤이비어 클래스를 상속받는 클래스의 동작을 통해서 간접적으로 실행되어야 할 것입니다.


이렇게 Bclass를 가지고 함수를 실행하기 위한 클래스를 다음과 같이 Cclass라고 정의하겠습니다.


public class Cclass : MonoBehaviour

{

    private Bclass b;


    private void Awake()

    {

        b = new Bclass();

    }

}


그리고 다시 Bclass를 살펴보면 Bclass의 멤버 변수인 a는 private로 선언되어 있어서 접근 방법이 없음을 알 수 있습니다. 외부에서 이 a에 값을 저장하거나 볼 수 있는 경로를 제공해주어야 합니다. 이를 위한 방법은 여러가지가 있습니다.


첫 번째로는 멤버변수의 접근지정자를 public으로 선언하는 것입니다.


public class Bclass
{
    public Aclass a;
}


접근지정자를 public으로 선언하면 아래의 예시와 같이 b아래의 a에 곧바로 접근하여 값을 대입할 수 있게 됩니다.


public class Cclass : MonoBehaviour

{

    private Bclass b;


    private void Awake()

    {

        b = new Bclass();

    }


    private void Start()

    {

        b.a = FindOfObject<Aclass>();

    }

}


두 번째 방법은 private인 멤버 변수에 접근해서 값을 넣도록 해주는 기능을 가진 함수를 만드는 것입니다.


public class Bclass
{
    private Aclass a;


    public void SetA(Aclass a)

    {

        this.a = a;

    }


    public Aclass GetA()

    {

        return a;

    }

}


private인 멤버 변수에 접근해서 값을 넣도록 해주는 함수를 만듬으로써 함수를 통해서 b안에 있는 a의 값을 수정할 수 있게 됩니다.


public class Cclass : MonoBehaviour

{

    private Bclass b;


    private void Awake()

    {

        b = new Bclass();

    }


    private void Start()

    {

        b.SetA(FindOfObject<Aclass>());

    }

}


반응형

Camera.main에서 Null Reference가 발생하는 문제


작성 기준 버전 :: 2018.3.2f1


유니티 스크립트 작업 중에 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으로 호출했을 때 어떤 카메라가 반환될지 확정할 수 없기 때문에 문제가 발생할 수 있다는 점이다.

반응형

인스펙터 커스텀 버튼 만들기


작성 기준 버전 :: 2018.3.2f1


유니티 엔진에서 게임을 제작할 때, 모든 작업을 일일이 수작업으로 진행하면 개발 시간이 길어진다. 특히 같은 오브젝트를 여러 개 생성하고 각각 다른 수치를 입력하는 반복적인 세팅 작업의 경우에는 꽤나 큰 시간 낭비를 초래한다.


여러 개의 오브젝트를 생성하는데, 그 오브젝트들에 입력되어야 하는 설정이 일정한 규칙을 가지고 있거나, 설정될 값들에 대한 테이블을 미리 가지고 있다면, 오브젝트를 일일이 생성하고 설정 값을 입력하는 것보다, 버튼을 누르면 자동으로 모든 오브젝트들을 생성하고 일정한 규칙에 따라서 설정 값을 세팅하거나 테이블에서 설정 값을 가져와서 세팅하도록 만드는 것이 많은 시간을 절약할 수 있다.


이런 커스텀 버튼을 만드는 데도 작업 시간이 소모되겠지만, 일일이 오브젝트를 생성하고 값을 세팅하는 작업 시간이 누적되면 커스텀 버튼을 만드는 데 드는 누적 시간을 빠르게 추월할 것이다. 그리고 이런 종류의 버튼은 만들어두면 다른 프로젝트에서도 충분히 재활용할 수 있기 때문에 인스펙터 커스텀 버튼을 만드는데 시간을 투자할 가치가 있다.



예제


이번 예제에서는 한 오브젝트를 기준으로 그 오브젝트의 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 프로퍼티에 추가해준다.



이 다음에는 커스텀 버튼을 추가하기 위해 CubeGenerteButton 클래스를 새로 추가한다. 인스펙터 창에 커스텀 버튼을 추가하는 기능은 유니티 에디터를 수정하는 것이기 때문에 UnityEditor 네임스페이스에 들어가는 기능을 사용해야한다. UnityEditor 네임스페이스의 기능을 사용하는 클래스는 반드시 Editor 폴더 아래에 들어가야 되기 때문에 Editor 폴더를 만들어서 그 안에 넣어준다.


 

그리고 아래와 같이 CubeGenerateButton의 코드를 작성한다.


using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(CubeGenerator))]
public class CubeGenerateButton : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        CubeGenerator generator = (CubeGenerator)target;
        if (GUILayout.Button("Generate Cubes"))
        {
            generator.GenerateCubes();
        }
    }
}


코드를 저장하고 에디터로 돌아가면 CubeGenerator 컴포넌트 하단에 "Generate Cubes" 버튼이 생겨난 것을 확인할 수 있다.


 

distanse와 cubeCount 값을 수정하고 Generate Cubes 버튼을 누르면 설정된 값에 맞춰서 큐브가 생겨나는 것을 확인할 수 있다.



반응형

유니티에서 JSON 사용하기(Newtonsoft JSON)


작성 기준 버전 :: 2018.3.1f1


JSON은 웹이나 네트워크에서 서버와 클라이언트 사이에서 데이터를 주고 받을 때 사용하는 개방형 표준 포멧으로, 텍스트를 사용하기 때문에 사람이 이해하기 쉽다는 장점이 있다.


이런 JSON 포멧을 유니티에서도 많이 사용하는 편이다. 네트워크 게임을 개발할 때 게임에 필요한 데이터를 주고 받거나, 게임 진행 상황을 저장하거나, 게임 설정을 저장하는 방식으로도 사용할 수 있다.


유니티에서 XML을 사용하는 것과 사용 범위가 거의 일치하는데, XML은 가독성이 매우 떨어지고 데이터를 넣거나 꺼내기 위해 파싱(Parsing)하는 과정이 까다로운데 반해서, JSON은 XML에 비해서 가독성이 좋고 직렬화(Serialize)와 비직렬화(Deserialize) 함수를 통해서 데이터에서 JSON 데이터로, JSON 데이터에서 데이터로 편하게 변환할 수 있다는 장점을 가지고 있다.


Newtonsoft의 JSON 라이브러리는 다양한 전체 기능을 제공하는 라이브러리로 시리얼라이즈 및 디시리얼라이즈하는 컴팩트한 기능만을 사용하기를 원한다면 유니티 엔진에 내장된 JsonUtility를 사용할 것을 권장한다.



JSON 라이브러리 다운로드 및 프로젝트에 임포트(Download JSON & Import JSON to project)


우선 JSON 라이브러리를 다운로드받기 위해 아래 링크에 접속한다.


Newtonsoft JSON Library


 

그리고 릴리즈된 애셋 중에 원하는 버전의 Json(버전).zip 파일을 다운로드받는다.


 

다운로드 받은 파일을 압축을 해제하고 폴더를 열어보면 위와 같은 폴더와 파일들이 보일텐데 그 중에서 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 };

            for (int idx = 0; idx < 5; idx++)
            {
                iList.Add(2 * idx);
            }


            fDictionary.Add("PIE", Mathf.PI);
            fDictionary.Add("Epsilon", Mathf.Epsilon);
            fDictionary.Add("Sqrt(2)", Mathf.Sqrt(2));

        }
    }


    public void Print()
    {
        Debug.Log("i = " + i);
        Debug.Log("f = " + f);
        Debug.Log("b = " + b);
        Debug.Log("str = " + str);

        for (int idx = 0; idx < iArray.Length; idx++)
        {
            Debug.Log(string.Format("iArray[{0}] = {1}", idx, iArray[idx]));
        }

        for (int idx = 0; idx < iList.Count; idx++)
        {
            Debug.Log(string.Format("iList[{0}] = {1}", idx, iList[idx]));
        }

        foreach(var data in fDictionary)
        {
            Debug.Log(string.Format("iDictionary[{0}] = {1}", data.Key, data.Value));
        }
    }
}

 

여러 가지 타입과 배열, 리스트, 딕셔너리를 가지고 있는 클래스이기 때문에 오브젝트를 JSON 데이터로 변환하기에 좋은 클래스이다.


JSON 테스트용 클래스를 모두 작성했으면 JsonExample 클래스에 다음 함수 두 개를 구현한다.


string ObjectToJson(object obj)
{
    return JsonConvert.SerializeObject(obj);
}

T JsonToOject<T>(string jsonData)
{
    return JsonConvert.DeserializeObject<T>(jsonData);
}


ObjectToJson() 함수는 JsonConvert 클래스의 SerializeObject() 함수를 이용해서 오브젝트를 문자열로 된 JSON 데이터로 변환하여 반환하는 처리를 하고 JsonToObject() 함수는 DeserializeObject() 함수를 이용해서 문자열로 된 JSON 데이터를 받아서 원하는 타입의 객체로 반환하는 처리를 한다.


함수들을 모두 작성했다면 Start() 함수에 우선 ObjectToJson() 함수를 테스트하는 코드를 작성한다.


void Start()
{
    JTestClass jtc = new JTestClass(true);
    string jsonData = ObjectToJson(jtc);
    Debug.Log(jsonData);
}


코드를 저장한 뒤 에디터로 돌아가서 JsonExample을 게임 오브젝트에 붙이고 플레이 버튼을 눌러보면 JTestClass 객체가 JSON 데이터로 변환되어 로그로 출력되는 것을 확인할 수 있다.



그 다음에는 Start() 함수 아래에 JsonToObject() 함수를 테스트하는 다음 코드를 작성한다.


var jtc2 = JsonToOject<JTestClass>(jsonData);
jtc2.Print();


그리고 코드를 저장하고 에디터로 가서 플레이 버튼을 눌러보면 문자열인 JSON 데이터가 JTestClass 객체로 변환되어 정상적으로 작동하는 것을 확인할 수 있다.




JSON 데이터 파일로 저장하고 불러오기


JSON 데이터를 파일로 저장하거나 파일에 저장된 JSON 데이터 파일을 불러올 일이 있을 수 있다. 이번에는 이것에 대해서 배워보자.


우선은 문자열로 만든 JSON 데이터를 파일로 저장하는 코드의 예시는 다음과 같다.


void CreateJsonFile(string createPath, string fileName, string jsonData)
{
    FileStream fileStream = new FileStream(string.Format("{0}/{1}.json", createPath, fileName), FileMode.Create);
    byte[] data = Encoding.UTF8.GetBytes(jsonData);
    fileStream.Write(data, 0, data.Length);
    fileStream.Close();
}


CreateJsonFile() 함수를 작성한 뒤 Start() 함수를 아래와 같이 CreateJsonFile() 함수를 호출하도록 수정한다.


void Start()
{
    JTestClass jtc = new JTestClass(true);
    string jsonData = ObjectToJson(jtc);
    CreateJsonFile(Application.dataPath, "JTestClass", jsonData);
}


코드를 저장하고 에디터에서 플레이 해보면 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 데이터로 변환한다.

반응형
  1. 인디 2020.10.19 11:26

    고맙습니다 도움 많이 됐어요.

유니티에서 JSON 사용하기(Unity JSON Utility)


작성 기준 버전 :: 2018.3.1f1


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 };


            for (int idx = 0; idx < 5; idx++)
            {
                iList.Add(2 * idx);
            }
        }
    }


    public void Print()
    {
        Debug.Log("i = " + i);
        Debug.Log("f = " + f);
        Debug.Log("b = " + b);

        Debug.Log("v = " + v);
        Debug.Log("str = " + str);

        for (int idx = 0; idx < iArray.Length; idx++)
        {
            Debug.Log(string.Format("iArray[{0}] = {1}", idx, iArray[idx]));
        }

        for (int idx = 0; idx < iList.Count; idx++)
        {
            Debug.Log(string.Format("iList[{0}] = {1}", idx, iList[idx]));
        }
    }
}

 

여러 가지 타입과 배열, 리스트를 가지고 있는 클래스이기 때문에 오브젝트를 JSON 데이터로 변환하기에 좋은 클래스이다.


JSON 테스트용 클래스를 모두 작성했으면 JsonExample 클래스에 다음 함수 두 개를 구현한다.


string ObjectToJson(object obj)
{
    return JsonUtility.ToJson(obj);
}

T JsonToOject<T>(string jsonData)
{
    return JsonUtility.FromJson<T>(jsonData);
}


ObjectToJson() 함수는 JsonUtility 클래스의 ToJson() 함수를 이용해서 오브젝트를 문자열로 된 JSON 데이터로 변환하여 반환하는 처리를 하고 JsonToObject() 함수는 FromJson() 함수를 이용해서 문자열로 된 JSON 데이터를 받아서 원하는 타입의 객체로 반환하는 처리를 한다.


함수들을 모두 작성했다면 Start() 함수에 우선 ObjectToJson() 함수를 테스트하는 코드를 작성한다.


void Start()
{
    JTestClass jtc = new JTestClass(true);
    string jsonData = ObjectToJson(jtc);
    Debug.Log(jsonData);
}


코드를 저장한 뒤 에디터로 돌아가서 JsonExample을 게임 오브젝트에 붙이고 플레이 버튼을 눌러보면 JTestClass 객체가 JSON 데이터로 변환되어 로그로 출력되는 것을 확인할 수 있다.


 

그 다음에는 Start() 함수 아래에 JsonToObject() 함수를 테스트하는 다음 코드를 작성한다.


var jtc2 = JsonToOject<JTestClass>(jsonData);
jtc2.Print();


그리고 코드를 저장하고 에디터로 가서 플레이 버튼을 눌러보면 문자열인 JSON 데이터가 JTestClass 객체로 변환되어 정상적으로 작동하는 것을 확인할 수 있다.


 




JSON 데이터 파일로 저장하고 불러오기


JSON 데이터를 파일로 저장하거나 파일에 저장된 JSON 데이터 파일을 불러올 일이 있을 수 있다. 이번에는 이것에 대해서 배워보자.


우선은 문자열로 만든 JSON 데이터를 파일로 저장하는 코드의 예시는 다음과 같다.


void CreateJsonFile(string createPath, string fileName, string jsonData)
{
    FileStream fileStream = new FileStream(string.Format("{0}/{1}.json", createPath, fileName), FileMode.Create);
    byte[] data = Encoding.UTF8.GetBytes(jsonData);
    fileStream.Write(data, 0, data.Length);
    fileStream.Close();
}


CreateJsonFile() 함수를 작성한 뒤 Start() 함수를 아래와 같이 CreateJsonFile() 함수를 호출하도록 수정한다.


void Start()
{
    JTestClass jtc = new JTestClass(true);
    string jsonData = ObjectToJson(jtc);
    CreateJsonFile(Application.dataPath, "JTestClass", jsonData);
}


코드를 저장하고 에디터에서 플레이 해보면 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 라이브러리를 사용하거나 이에 대한 기능을 직접 구현해야 할 것이다.

반응형
  1. 2019.06.16 18:43

    비밀댓글입니다

    • wergia 2019.07.22 11:37 신고

      답변은 좀 더 보시기 편하게 글로 올렸습니다.

      확인해주세요.

      https://wergia.tistory.com/174

  2. 오션스8 2020.05.15 16:12

    포스팅 잘 보고 갑니다.

    • wergia 2020.05.29 19:43 신고

      네! ㅎㅎ 다음에도 유용한 포스트로 찾아뵙겠습니다.

  3. hsan 2020.11.17 16:42

    좋은 포스팅 감사합니다.
    유니티와 json을 처음 접할때에 좋은 자료인 것 같습니다!

  4. 2021.06.01 22:50

    비밀댓글입니다

    • 2021.06.03 10:01

      비밀댓글입니다

  5. 클라요 2021.06.21 16:07

    큰 도움이 되었습니다. 좋은글 감사합니다.

  6. 2021.07.09 19:14

    비밀댓글입니다

Tutorial (5)

 

유니티의 좌표계

 

작성 기준 버전 :: 2018.3.1f1

 

[본 튜토리얼의 내용은 유튜브 영상으로도 시청할 수 있습니다]

 

이번 섹션에서는 유니티의 좌표계에 대해서 알아보자.

 

 

좌표계란?

 

좌표계란 공간 내에서 특정한 위치를 나타내기 위한 방식이다.

 

어떤 공간에서 위치를 찾고자 하는 것인지 기준을 잡기 위해서 축이라는 것을 사용하는데, 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}이 아니라 큐브 오브젝트를 중심으로한 로컬 좌표 로 표시되는 것을 확인할 수 있다.

 

 

이렇게 하고 나면 간단하게 큐브 오브젝트를 회전시키는 것만으로도 궤도를 따라서 스피어 오브젝트가 간단하게 공전하는 것을 볼 수 있다. 물론 큐브도 함께 자전한다는 문제가 있기는 하지만 이런 문제는 간단하게 해결하고 스피어 오브젝트만 궤도를 따라서 공전하게도 만들 수 있다.

 

 

반응형

+ Recent posts