프로그래밍(C#) /유니티공부

[유니티 C# 스크립팅 마스터하기]8장_유니티 에디터 사용자화

정체불명의 모모 2023. 5. 30. 18:03

이번 8장에서는 개발 하는 동안 기존 에디터가 

가지지 않은 특정 기능을 만드는 방법에 대해 알아 볼 예정 입니다.

 

책에서는 네가지의 필요한 목적에 맞춰 작업하는 방법에 대해 알려 줍니다.

간략하게 설명하면

 

첫째, 한 번의 처리로 선택된 복수의 오브젝트 이름을 변경할 수 있는 일괄 이름 변경 도구

두 번째, 오브젝트 인스펙터에서 슬라이더를 이용해 두 가지 색상을 혼합할 수 있는 색상 영역 필드 만들기

세 번째, 오브젝트 인스펙터 public C# 프로퍼티를 노출해 값을 설정하고 얻어오는 방법

네 번째,  C# 속성을 사용해 현지화(localization) 도구 키트(toolkit)를 만들어서 버튼 한 번으로 게임 내 문자열들을

선택한 언어(영어, 불어 등)로 자동 변경하는 방법

 

위 내용들에 대해 알아 보도록 하겠습니다.


ㅁ C# 특성과 리플렉션

: 지금부터 다루는 모든 에디터 확장 기능은 특성(attribute) 와 리플렉션(reflection)에 많이 의존 하게 됩니다.

특성
: 특성은 태그처럼 동작하는 메타데이터(metadata)의 형식 입니다.
프로그래머는 클래스나 변수, 메소드에 특성을 선언해 데이터와 연관 지어 컴파일러가 알 수있게 합니다.
 특성 자체는 순전히 기술하기 위한 목적으로 사용하는 것이며, 실제로는 아무 일도 하지 않는 데이터일 뿐 입니다.
리플렉션(반영)
: 닷넷(혹은 모노)에 기반한 모든 코드는 '유체 이탈'해서 프로그램안에 포함된 모든 클래스와 데이터 형식 및 인스턴스를
볼 수 있는 능력을 갖고 있습니다.
프로그램 안의 각각의 오브젝트에 대해 해당 오브젝트의 메타데이터(특성)을 조사할 수도 있습니다. 
이렇게 '스스로를 바깥에서 바라보는' 능력은 마치 거울을 통해 바라보는 것과 같다 하여 리플렉션(반영)이라고 부릅니다.

 

  •  유니티에서 네이티브로 지원하는 Range 특성 예제를 통해 리플렉션에 연관된 개념을 알아 보도록 하겠습니다.

 

   위 그림에서 보이는 것 처럼 inspector 창에 MyNumber 값을 설정 할 수 있습니다.
   숫자 입력란의 숫자를 최솟값과 최댓값 사이로 제한해 특정 범위 안에 들어가는 유효한 값으로 만들기 원할 때가 있습니다.

   그럴때는 Mathf.Clamp 함수를 이용해도 이런 처리가 가능하지만, 특성을 이용해서 해당 항목에 유효한 값만 받도록 만들 수 있습니다.

     다음 코드와 같이 부동소수점 변수(MyNumber)에 Range 특성을 선언하면 에디트 박스 대신 슬라이더가 보이게 됩니다.

public class MyTestClass : MonoBehaviour
{
    [Range(0f, 1f)]
    public float MyNumber;
}

슬라이더로 변경됨

위 그림처럼 0과 1사이의 숫자 범위를 받아들이게 됩니다.

Range 특성에 파라미터로 들어가는 모든 숫자는 컴파일 시점에 명시적으로 알 수 있는 값이어야 하고, 

런타임에 달라질 수 있는 변수를 포함하는 표현식은 허용 되지 않습니다.

모든 특성 값은 반드시 컴파일 타임에 알 수 있는 값이어야 합니다.

 

  • 유니티의 모든 소스 파일을 통틀어 유니티로 만든 애플리케이션에 포함된 직접 만든 모든 클래스들을 순회하는 
    리플렉션을 다루는 예를 봅시다.

- 아래 예제 코드에서는 메소드, 프로퍼티, 변수의 형식을 포함한 모든 형식을 나열 

using System;               // 필수로 포함 되어야 한다.
using System.Reflection;	// 필수로 포함 되어야 한다.
using UnityEngine;

public class MyTestClass : MonoBehaviour
{
    [Range(0f, 1f)]
    public float MyNumber;

    private void Start()
    {
        // 어셈블리 안의 모든 클래스를 나열 합니다.
        foreach (Type t in Assembly.GetExecutingAssembly().GetTypes())
        {
            Debug.Log(t.Name);
        }
    }

   - 특정 형식의 파라미터를 전달받아 여기에 포함된 모든 public 멤버 변수를 나열 하는 예제

   // 클래스 t의 모든 public 변수들을 나열하는 함수
    public void ListAllPublicVariables(Type t)
    {
        //모든 public 변수들을 순회한다
        foreach (FieldInfo FI in t.GetFields(BindingFlags.Public | BindingFlags.Instance))
        {
            // 변수의 이름을 출력한다.
            Debug.Log(FI.Name);
        }
    }

- 메타데이터를 조회하고 프로퍼티를 검사 하는 예제

    public void ListAllAttributes(Type t)
    {
        foreach (Attribute attr in t.GetCustomAttributes(true))
        {
            // 발견한 특성의 형식을 나열한다.
            Debug.Log(attr.GetType());
        }
    }

위의 예제 코드는 런타임에 코드에서 주어진 데이터 형식의 모든 특성 데이터를 얻는 예제 입니다.

데이터 형식과 변수들에서 얻어올 수 있는 연관된 메타데이터가 있는데, 이것들은 어떻게 오브젝트를 처리할지에 대해 큰 영향을 미치곤 합니다. 

데이터 형식이나 멤버 변수에 선언할 수 있게 직접 정의한 특성을 만듦으로써, 유니티 에디터에 우리가 만든 코드를 통합할 때 논리적인

혹은 런타임상의 구조가 올바르지 않은 상태가 되지 않도록 할 수 있어 에디터 플러그인을 만들 때 강력한 면모를 보입니다.


ㅁ 첫째, 한 번의 처리로 선택된 복수의 오브젝트 이름을 변경할 수 있는 일괄 이름 변경 도구

 - 필요한 이유 : 복수의 적, 체력 증가 아이템, 소품, 다른 오브젝트를 복제 기능(Ctrl + D)를 이용해서 복제하게 되면 

    동일한 이름을 가진 오브젝트가 존재하는 결과를 만들어 오브젝트 찾기 'GameObject.Find' 함수를 이용한 오브젝트 검색으로

    원하는 특정 오브젝트를 얻는 것이 불가능 하고, 특정 오브젝트를 분간하기가 사실상 불가능해 지기 때문에 오브젝트에

    고유하면서도 적합한 이름을 지어주기 위해 일괄 이름 변경 도구가 필요 합니다.

 

   1. 'Editor' 폴더 생성

  유니티 에디터에서 사용자화 코드를 만들려면, 우선 프로젝트 안에 'Editor' 라는 이름의 폴더를 만들어야 합니다.
이 부분이 매우 중요한데, 'Editor' 폴더는 유니티가 모든 에디터 사용자화 스크립트를 담는 위치로 인식하는 특별한 폴더 입니다.

editor 폴더 생성

 

   2. 'ScriptableWizard' 클래스를 이용해 일괄 이름 변경 유틸리티(utility)를 만들어 줍니다.

   : 위 클래스는 우리가 파생해서 만들 새로운 클래스의 원형 클래스 입니다. 모든 파생 클래스는 팝업 유틸리티 대화창 처럼

     작동해 유니티의 메인 메뉴에서 실행 할 수 있습니다.

      다시말해, ScriptableWizard에서 파생한 클래스는 하나 혹은 여러 오브젝트에 한 번의 자동화된 처리를 할 때 이상적 입니다.
     (https://docs.unity3d.com/kr/530/ScriptReference/ScriptableWizard.html)

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.PlayerLoop;
using Object = UnityEngine.Object;

public class BatchRename : ScriptableWizard
{
    // 기본 이름
    public string BaseName = "MyObject_";
    
    // 시작 숫자
    public int StartNumber = 0;
    
    // 증가치
    public int Increment = 1;

    [MenuItem("Edit/Batch Rename...")]
    static void CreateWizard()
    {
        ScriptableWizard.DisplayWizard("Batch Rename", typeof(BatchRename), "Rename");
    }
    
    // 창이 처음 나타날 때 호출된다.
    private void OnEnable()
    {
        UpdateSelectionHelper();
    }

    private void OnSelectionChange()
    {
        UpdateSelectionHelper();
    }
    
    // 선택된 개수를 업데이트 한다.
    void UpdateSelectionHelper()
    {
        helpString = "";

        if (Selection.objects != null)
            helpString = "Number of objects selected : " + Selection.objects.Length;
    }
    
    // 이름 변경
    private void OnWizardCreate()
    {
        // 선택된 것이 없으면 종료 한다.
        if (Selection.objects ==  null)
            return;
        
        // 현재 증가치
        int PostFix = StartNumber;
        
        // 순회하며 이름을 변경한다
        foreach (Object O in Selection.objects)
        {
            O.name = BaseName + PostFix;
            PostFix += Increment;
        }
    }
}

위와 같이 코드를 입력 후 Edit > Batch Rename... 을 클릭하면 일괄적으로 오브젝트 네임이 변경 되는걸 확인 할 수 있습니다.

 


ㅁ 두 번째, 오브젝트 인스펙터에서 슬라이더를 이용해 두 가지 색상을 혼합할 수 있는 색상 영역 필드 만들기

: Editor 클래스가 리플렉션을 통해 오브젝트의 특성 데이터를 조회함으로써 오브젝트 인스펙터에 데이터 형식을 어떻게 표현할지

  제어하게 됩니다.

 

  Range 특서은 숫자 형식에 잘 적용 됩니다. 하지만 단지 숫자뿐 아니라 다른 데이터 형식에도 비슷하게 작동하도록 할 수 있다면

  좋을 것 입니다.

 

  예를 들어, 페이드인/ 페이드 아웃 씬 전환 효과를 만들기 위해 검정색에서 투명으로 페이드 되는 경우 처럼 다른 색상 사이에서 

  페이드되는 기능을 흔히 볼 수 있습니다.

  이런 기능을 '색상 선형 보간(color lerp)라고 부릅니다. 

  정규화된 부동소수점 수치를 이용하여 두 극단 사이(0과 1사이)에서 중간 색상이 생성 합니다.

  

 우리는 위 특성을 사용하여 색상 혼합을 위한 데이터를 정의 하도록 합니다.

 

- 색상 혼합에 필요한 변수

  • SourceColor(혼합할 색상)
  • DestColor(혼합할 색상)
  • BlendFactor(중간 색상 부동소수점으로서 선형 보간을 통해 결정 지을 값)
  • BlendColor(출력 색상)   
// Serializable 특성을 사용해 유니티가 오브젝트 인스펙터에 클래스와 클래스의 public을
// 표시 하게 합니다.
[System.Serializable]
public class ColorBlend : System.Object
{
    public Color SourceColor = Color.white;
    public Color DestColor = Color.white;
    public Color BlendedColor = Color.white;
    public float BlendFactor = 0f;
}

이제 유니티가 오브젝트 인스펙터에 이 클래스를 표시하는 방법을 사용자화 해봅 시다.

 

- ColorRangeAttribute 라는 새 특성 클래스를 만들어 봅시다.

// PropertyAttribute에 파생된 클래스이다.
public class ColorRangeAttribute : PropertyAttribute
{
    public Color Min;
    public Color Max;

    // ColorRangeAttribute는 특성이자 메타데이터 구조이며, 일반적인 클래스가 아니다.
    // 그렇기에 인스턴스화 하면 안된다.
    public ColorRangeAttribute(float r1, float g1, float b1, float a1,
        float r2, float g2, float b2, float a2)
    {
        // RGBA 정의 하는 값
        this.Min = new Color(r1, g1, b1, a1);
        this.Max = new Color(r2, g2, b2, a2);
    }
}

 

이번에는 ColorRangeAttribute 특성을 붙인 ColorBlend 인스턴스를 선언하는 클래스를 작성 합니다.

 하지만, 아직 처리할 에디터 클래스를 작성 하지 않았기 때문에 ColorRangeAttribute를 추가하는 것만으로는

아무일이 일어나지 않습니다.

public class ColorAdjuster : MonoBehaviour
{
    [ColorRangeAttribute(1f, 0f, 0f, 0f, 0f, 1f, 0f, 1f)]
    public ColorBlend MyColorBlend;
}

 

ColorRangeAttribute 클래스 처리를 위한 슬라이더 컨트롤을 이용해서 ColorBlend를 오브젝트 인스펙터에 표시하는 에디터 

클래스를 만들어 봅시다.

 

방법 : 유니티에는 확장 기능의 기본 클래스인 PropertyDrawer를 제공하여 변수에 추가하는 특정 특성들이 오브젝트 인스펙터에

표현되는 형식을 재정의 하는 파생 크래스를 새로 만들 수 있게 해줍니다.

 요컨데, PropertyDrawer클래스는 일반적인 특성을 이용해 다양하게 태그된 모든 변수들이 인스펙터에 그려지는 모습을 

사용자화 할 수 있게 해줍니다.

 

* Editor 폴더에 'ColorRangeDrawer' 클래스 생성

using UnityEditor;
using UnityEngine;

// CustomPropertyDrawer 특성은 모든 ColorRangeAttribute 멤버가 표시되는 모습을 재정의 합니다.
[CustomPropertyDrawer(typeof(ColorRangeAttribute))]
public class ColorRangeDrawer : PropertyDrawer
{
     // GUI 업데이트가 일어날 때 유니티가 호출하는 이벤트
     // OnGUI 함수는 ColorRangeAttribute 특성을 가진 모든 필드가 오브젝트 인스펙터에 어떻게 그려져야 하는지를
     // 정의하기 위해 기본 클래스에서 재정의된 것이다.(EditorGUI는 버튼, 텍스트박스, 슬라이더와 같은 GUI 구성 요소를 그리기
     // 위한 유니티 에디터의 내장 유틸리티 클래스 입니다.)
     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
     {
          // ColorRangeAttribute 메타데이터를 얻는다.
          ColorRangeAttribute range = attribute as ColorRangeAttribute;
          
          // 인스펙터에 레이블을 추가합니다.
          position = EditorGUI.PrefixLabel(position, new GUIContent("Color Lerp"));
          
          // 색상 영역과 슬라이더 컨트롤의 크기를 정의 합니다.
          Rect ColorSamplerRect = new Rect(position.x, position.y, 100, position.height);
          Rect SliderRect = new Rect(position.x + 105, position.y, 200, position.height);
          
          // 색상 영역 컨틑롤을 표시 합니다.
          EditorGUI.ColorField(ColorSamplerRect, property.FindPropertyRelative("BlendedColor").colorValue);
          
          // * FindPropertyRelative 함수는 선택된 오브젝트의 SourceColor, DestColor, BlendedColor와 같은
          // public 멤버 변수를 받아와 줍니다.
          
          // 슬라이더 컨트롤을 표시 합니다.
          property.FindPropertyRelative("BlendFactor").floatValue = EditorGUI.Slider(SliderRect,
               property.FindPropertyRelative("BlendFactor").floatValue, 0f, 1f);
          
          // 슬라이더에 의해 혼합된 색상을 업데이트 합니다.
          property.FindPropertyRelative("BlendColor").colorValue = Color.Lerp(range.Min, range.Max,
               property.FindPropertyRelative("BlendFactor").floatValue);

     }
}


ㅁ 세 번째, 오브젝트 인스펙터 public C# 프로퍼티를 노출해 값을 설정하고 얻어오는 방법

: 기본적으로 오브젝트 인스펙터는 클래스의 모든 public 멤버 변수를 표시 합니다.

  디버그 모드인 경우나 SerializeField 특성을 표시한 경우 private 멤버 변수도 표시가 됩니다.

    하지만 C# 프로퍼티는 릴리즈(Release) 모드든 디버그 모드든 전혀 표시 되지 않습니다.

 

 우리는 유니티의 제약에 관계없이 오브젝트 인스펙터에 클래스의 모든 프로퍼티를 보여주는 에디터 확장 기능을 만들어서 

 값을 직접 얻어 올 수 있게 설정 할 수 있습니다.(이번에도 리플렉션에 의존해서 구현 합니다.)

 

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

[System.Serializable]
public class ClassWithProperties : System.Object
{
    //프로퍼티를 가지고 있는 클래스
    public int MyIntProperty
    {
        get { return MyIntProperty; }
        
        // 값의 유효성 검사를 합니다.
        set
        {
            if (value <= 10)
                MyIntProperty = value;
            else
                MyIntProperty = 0;
        }
    }

    public float MyFloatProperty
    {
        get { return MyFloatProperty; }
        set { MyFloatProperty = value; }
    }

    public Color MyColorProperty
    {
        get { return MyColorProperty; }
        set { MyColorProperty = value; }
    }

    private int myIntProperty;
    private float myFloatProperty;
    private Color myColorProperty;
}

위 클래스는 다른 클래스의 내부에서 public 멤버로 사용 됩니다.

using UnityEngine;

public class LargerClass : MonoBehaviour
{
    public ClassWithProperties MyPropClass;
}

기본적으로, public 멤버인 MyPropClass는 오브젝트 인스펙터에 멤버들을 보여주지 않습니다.

C# 프로퍼티는 기본적으로 지원하고 있지 않기 때문 입니다.

이 문제를 해결하기 위해 다시 PropertyDrawer 클래스를 이용합니다.

using System.Reflection;
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(ClassWithProperties))]
public class PropertyLister : PropertyDrawer
{
    //인스펙터 패널의 높이
    private float InspectorHeight = 0;
    
    // 한 줗의 픽셀 단위 높이
    private float RowHeight = 15;
    
    // 줄 간격
    private float RowSpacing = 5;
    
    // 주어진 영역 안에 프로퍼티를 그립니다.
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        
        // 참조된 오브젝트를 얻는다.
        object o = property.serializedObject.targetObject;
        ClassWithProperties CP = o.GetType().GetField(property.name).GetValue(o) as ClassWithProperties;
        ;

        int indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
        
        //  레이아웃
        Rect LayoutRect = new Rect(position.x, position.y, position.width, RowHeight);
        
        // 오브젝트의 모든 프로퍼티를 찾는다.
        foreach (var prop in typeof(ClassWithProperties).GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            // 정수형 프로퍼티일 때
            if (prop.PropertyType.Equals(typeof(int)))
            {
                prop.SetValue(CP,EditorGUI.IntField(LayoutRect, prop.Name,
                    (int)prop.GetValue(CP,null)), null);

                LayoutRect = new Rect(LayoutRect.x, LayoutRect.y + RowHeight + RowSpacing, LayoutRect.width, RowHeight);
            }
            
            // 부동소수점 프로퍼티일 때
            if (prop.IsSpecialName.Equals(typeof(float)))
            {
                prop.SetValue(CP, EditorGUI.FloatField(LayoutRect, prop.Name, (float)prop.GetValue(CP, null)), null);
                LayoutRect = new Rect(LayoutRect.x, LayoutRect.y + RowHeight + RowSpacing, LayoutRect.width, RowHeight);
            }
        }
        
        // 인스펙터 높이를 업데이트 합니다.
        InspectorHeight = LayoutRect.y - position.y;

        EditorGUI.indentLevel = indent;
        EditorGUI.EndProperty();
    }
    
    // 아래의 함수는 필드의 높이(픽셀 단위)가 얼마여야 하는지를 반환합니다.
    // 아래의 GUI 컨트롤과 겹치지 않게 만들기 위한 함수 입니다.
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return InspectorHeight;
    }
}

ㅁ 네 번째,  C# 속성을 사용해 현지화(localization) 도구 키트(toolkit)를 만들어서 버튼 한 번으로 게임 내 문자열들을

선택한 언어(영어, 불어 등)로 자동 변경하는 방법

: 현지화는 개발자가 영어, 프랑스어, 스페인어 등 복수의 자연어를 지원하기 위해 필요한 광범위한 기술적, 경제적, 기호논리하적

  내용을 아우르는 말입니다.

  이런저런 특정 언어를 지원하는 것이 아닌, 언제든지 임의의 언어를 선택할 수 있는 기반을 만드는 것이 현지화의 기술적인 목표 입니다.

   이번 내용에서 유니티 에디터 사용자화를 통한 쉽고 빠른 한 가지 현지화 작업 방법에 대해 알아보도록 하겠습니다.

 

- 먼저 현지화 텍스트를 유니티 프로젝트의 Resources 폴더에 임포트 합니다.(책에 주어진 텍스트)

임포트한 텍스트 파일에는 ID가 부여된 각각의 항목을 통해 게임에 포함될 모든 텍스트 데이터가 포함되어 있습니다.

각각의 문자열 값은 ID와 짝을 이루고 있는데, ID는 언어 간 동일하게 유지되므로 다른 언어 간 전환 시에도 매끄러운 전환이 가능해 집니다.

이러한 ID는 자동화된 현지화를 가능케 하는 하나의 기준이 됩니다.

 

코드에서 현지화 시스템을 구현하기 위해 우선 모든 현지화 문자열에 적용할 수 있는 특성을 하나 만듭니다.

// 문자열 오브젝트에 붙이 특성
public class LocalizationTextAttribute : System.Attribute
{
    // 할당할 ID
    public string LocalizationID = string.Empty;
    
    // 생성자
    public LocalizationTextAttribute(string ID)
    {
        LocalizationID = ID;
    }
}

이렇게 만든 LocalizationTextAttribute 특성을 코드의 문자열 멤버에 적용해 특정 ID 와 연결 할 수 있습니다.

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

public class SampleGameMenu : MonoBehaviour
{
    [LocalizationTextAttribute("text_01")] 
    public string NewGameText = string.Empty;
    
    [LocalizationTextAttribute("text_02")] 
    public string LoadGameText = string.Empty;
    
    [LocalizationTextAttribute("text_03")] 
    public string SaveGameText = string.Empty;
    
    [LocalizationTextAttribute("text_04")] 
    public string ExitGameText = string.Empty;
}

아래 그림에서 볼 수 있듯이 오브젝트 인스펙터에서 SampleGameMenu 클래스는 일반적인 클래스와 동일하게 표시됩니다.

다음으로, Editor 클래스를 통해 자동으로 모든 문자열 멤버를 선택된 언어로 바꿔주는 기능을 만들어 봅시다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using System.Xml;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;

public class LaguageSelector 
{
    [MenuItem("Localization/English")]
    public static void SelectEnglish()
    {
        LaguageSelector.SelectLaguage("english");
    }
    
    [MenuItem ("Localization/French")]
    public static void SelectFrench()
    {
        LaguageSelector.SelectLanguage("french");
    }

    [MenuItem ("Localization/Yoda")]
    public static void SelectYoda()
    {
        LaguageSelector.SelectLanguage("yoda");
    }


    public static void SelectLaguage(string LanguageName)
    {
        // 프로젝트 XML 텍스트 파일에 접근 합니다.
        TextAsset textAsset = Resources.Load("LocalText") as TextAsset;
        
        // XML 리더 오브젝트에 텍스트를 불러온다.
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.LoadXml(textAsset.text);
        
        //Language 노드를 불러온다.
        XmlNode[] LanguageNodes = (from XmlNode Node in xmlDoc.GetElementsByTagName("language")
            where Node.Attributes["id"].Value.ToString().Equals(LanguageName.ToLower())
            select Node).ToArray();
    
        // 일치하는 노드를 불러온다.
        if(LanguageNodes.Length <= 0)
            return;

        // 첫 번째 노드를 얻어온다
        XmlNode LanguageNode = LanguageNodes[0];

        // 텍스트 오브젝트를 얻는다.
        SampleGameMenu GM = Object.FindObjectOfType<SampleGameMenu>() as SampleGameMenu;

        //자식 xml 노드들을 순회한다.
        foreach (XmlNode Child in LanguageNode.ChildNodes)
        {
            //현재 노드의 텍스트 Id를 얻는다.
            string TextID = Child.Attributes["id"].Value;
            string LocalText = Child.InnerText;

            //모든 필드를 순회합니다.
            foreach(var field in GM.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy))
            {
                // 필드 형식이 문자열이면 관련이 있습니다.
                if(field.FieldType == typeof(System.String))
                {
                    //필드의 사용자 특성들을 얻습니다.
                    System.Attribute[] attrs = field.GetCustomAttributes(true) as System.Attribute[];
					
                    foreach (System.Attribute attr in attrs)
                    {
                        if(attr is LocalizationTextAttribute)
                        {
                            //현지화가 필요한 텍스트를 발견하면, ID가 일치하는지 확인 합니다.
                            LocalizationTextAttribute LocalAttr = attr as LocalizationTextAttribute;

                            if(LocalAttr.LocalizationID.Equals(TextID))
                            {
                                //ID가 일치하면 값을 설정 합니다.
                                field.SetValue(GM, LocalText);
                            }
                        }
                    }
                }
            }
        }
   
}
}

메뉴에 'Localization'이 생겨 원하는 언어를 선택해서 골라 주면 됩니다.

영어 선택 후 자동으로 언어 셋팅 됨