Unity에서 .NET 4.x 사용

Unity 스크립팅의 기반이 되는 기술인 C# 및 .NET 2002년에 처음 릴리스된 Microsoft 이후 업데이트를 계속 받고 있습니다. 그러나 Unity 2017.1 이전에는 Unity가 수년간의 업데이트가 반영되지 않은 .NET 3.5 상당의 스크립팅 런타임을 사용해 왔기 때문에, Unity 개발자들은 C# 언어와 .NET Framework에 지속적으로 추가되어 온 새 기능들을 잘 모를 수 있습니다.

Unity 2017.1 릴리스와 함께 Unity는 .NET 4.6, C# 6.0 호환 버전으로 업그레이드된 실험적 버전의 스크립팅 런타임을 도입했습니다. Unity 2018.1에서 .NET 4.x 동등한 런타임은 더 이상 실험적인 것으로 간주되지 않지만 이전 .NET 3.5에 해당하는 런타임은 이제 레거시 버전으로 간주됩니다. Unity 2018.3 릴리스를 통해 Unity는 업그레이드된 스크립팅 런타임을 기본 선택으로 만들고 C# 7로 더 업데이트할 것으로 예상합니다. 이 로드맵에 대한 자세한 내용과 최신 업데이트는 Unity의 블로그 게시물을 참조하거나 실험적 스크립팅 미리 보기 포럼을 방문하세요. 그 동안 .NET 4.x 스크립팅 런타임에서 사용할 수 있는 새로운 기능에 대해 자세히 알아보려면 아래 섹션을 확인하세요.

사전 요구 사항

Unity에서 .NET 4.x 스크립팅 런타임 사용

.NET 4.x 스크립팅 런타임을 사용하도록 설정하려면 다음 단계를 수행합니다.

  1. 편집 > 프로젝트 설정 > 플레이어 > 기타 설정을 선택하여 Unity 인스펙터에서 PlayerSettings를 엽니다.

  2. 구성 제목 아래에서 Api 호환성 수준 드롭다운을 클릭하고 .NET Framework를 선택합니다. Unity를 다시 시작하라는 메시지가 표시됩니다.

.NET 4.x에 해당하는 항목 선택을 보여 주는 스크린샷

.NET 4.x 및 .NET Standard 2.1 프로필 중에서 선택

.NET 4.x 해당 스크립팅 런타임으로 전환한 후에는 PlayerSettings(>)의 드롭다운 메뉴를 사용하여 Api 호환성 수준을 지정할 수 있습니다. 두 옵션이 있습니다.

  • .NET Standard 2.1. 이 프로필은 .NET Foundation에서 게시한 .NET Standard 2.1 프로필과 일치합니다. Unity는 새 프로젝트에 .NET Standard 2.1을 권장합니다. 크기가 제한된 플랫폼에 유리한 .NET 4.x보다 작습니다. 또한 Unity는 Unity에서 지원하는 모든 플랫폼에서 이 프로필을 지원하기 위해 최선을 다하고 있습니다.

  • .NET Framework. 이 프로필은 최신 .NET 4 API에 대한 액세스를 제공합니다. 여기에는 .NET Framework 클래스 라이브러리에서 사용할 수 있는 모든 코드가 포함되며 .NET Standard 2.1 프로필도 지원합니다. 프로젝트에 .NET Standard 2.0 프로필에 포함되지 않은 API의 일부가 필요한 경우 .NET 4.x 프로필을 사용합니다. 그러나 이 API의 일부 부분은 Unity의 모든 플랫폼에서 지원되지 않을 수 있습니다.

이러한 옵션에 대한 자세한 내용은 Unity의 블로그 게시물을 참조하세요.

.NET 4.x Api 호환성 수준을 사용할 때 어셈블리 참조 추가

Api 호환성 수준 드롭다운에서 .NET Standard 2.1 설정을 사용하는 경우 API 프로필의 모든 어셈블리가 참조되고 사용할 수 있습니다. 그러나 더 큰 .NET 4.x 프로필을 사용하는 경우 Unity와 함께 제공되는 일부 어셈블리는 기본적으로 참조되지 않습니다. 이러한 API를 사용하려면 어셈블리 참조를 수동으로 추가해야 합니다. Unity 편집기 설치의 MonoBleedingEdge/lib/mono 디렉터리에서 Unity가 함께 제공되는 어셈블리를 볼 수 있습니다.

MonoBleedingEdge 디렉터리를 보여 주는 스크린샷

예를 들어, .NET 4.x 프로필을 사용 중이며 HttpClient를 사용하려는 경우 System.Net.Http.dll에 대한 어셈블리 참조를 추가해야 합니다. 이 항목이 없으면 컴파일러에서 어셈블리 참조가 누락되었다고 불평합니다.

누락된 어셈블리 참조를 보여 주는 스크린샷

Visual Studio 열려 있을 때마다 Unity 프로젝트에 대한 .csproj.sln 파일을 다시 생성합니다. 따라서 프로젝트를 다시 열면 어셈블리 참조가 손실되므로 Visual Studio 직접 어셈블리 참조를 추가할 수 없습니다. 대신 csc.rsp 라는 특수 텍스트 파일을 사용해야 합니다.

  1. Unity 프로젝트의 루트 Assets 디렉터리에 csc.rsp라는 새 텍스트 파일을 만듭니다.

  2. 빈 텍스트 파일의 첫 번째 줄에서 다음 -r:System.Net.Http.dll 을 입력한 다음, 파일을 저장합니다. "System.Net.Http.dll"을 참조가 누락될 수 있는 포함된 어셈블리로 바꿀 수 있습니다.

  3. Unity 편집기를 다시 시작합니다.

.NET 호환성 활용

새로운 C# 구문 및 언어 기능 외에도 .NET 4.x 스크립팅 런타임을 통해 Unity 사용자는 레거시 .NET 3.5 스크립팅 런타임과 호환되지 않는 거대한 .NET 패키지 라이브러리에 액세스할 수 있습니다.

NuGet에서 Unity 프로젝트에 패키지 추가

NuGet은 .NET 패키지 관리자입니다. NuGet은 Visual Studio 통합됩니다. 그러나 Unity 프로젝트에서 NuGet 패키지를 추가하려면 특별한 프로세스가 필요합니다. Unity에서 프로젝트를 열면 Visual Studio 프로젝트 파일이 다시 생성되어 필요한 구성을 실행 취소하기 때문입니다. NuGet에서 패키지를 추가하려면 Unity 프로젝트에 다음을 수행합니다.

  1. NuGet을 찾아 추가하려는 호환 패키지를 찾습니다(.NET Standard 2.0 또는 .NET 4.x). 이 예제에서는 JSON 작업을 위한 인기 있는 패키지인 Json.NET을 .NET Standard 2.0 프로젝트에 추가하는 방법을 보여 줍니다.

  2. 다운로드 단추를 클릭합니다.

    다운로드 단추를 보여 주는 스크린샷.

  3. 다운로드한 파일을 찾아 확장 프로그램을 .nupkg 에서 .zip변경합니다.

  4. zip 파일 내에서 lib/netstandard2.0 디렉터리로 이동하여 Newtonsoft.Json.dll 파일을 복사합니다.

  5. Unity 프로젝트의 루트 자산 폴더에서 플러그 인이라는 새 폴더를 만듭니다. 플러그 인은 Unity의 특수 폴더 이름입니다. 자세한 내용은 Unity 설명서를 참조하세요.

  6. Newtonsoft.Json.dll 파일을 Unity 프로젝트의 플러그 인 디렉터리에 붙여넣습니다.

  7. Unity 프로젝트의 Assets 디렉터리에 link.xml 파일을 만들고 다음 XML을 추가하여 Unity의 바이트 코드 제거 프로세스가 IL2CPP 플랫폼으로 내보낼 때 필요한 데이터를 제거하지 않도록 합니다. 이 단계는 이 라이브러리와 관련이 있지만 리플렉션을 비슷한 방식으로 사용하는 다른 라이브러리에 문제가 발생할 수 있습니다. 자세한 내용은 이 문서에서 Unity의 문서를 참조하세요.

    <linker>
      <assembly fullname="System.Core">
        <type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" />
      </assembly>
    </linker>
    

모든 항목이 준비되면 이제 Json.NET 패키지를 사용할 수 있습니다.

using Newtonsoft.Json;
using UnityEngine;

public class JSONTest : MonoBehaviour
{
    class Enemy
    {
        public string Name { get; set; }
        public int AttackDamage { get; set; }
        public int MaxHealth { get; set; }
    }
    private void Start()
    {
        string json = @"{
            'Name': 'Ninja',
            'AttackDamage': '40'
            }";

        var enemy = JsonConvert.DeserializeObject<Enemy>(json);

        Debug.Log($"{enemy.Name} deals {enemy.AttackDamage} damage.");
        // Output:
        // Ninja deals 40 damage.
    }
}

종속성이 없는 라이브러리를 사용하는 간단한 예제입니다. NuGet 패키지가 다른 NuGet 패키지를 사용하는 경우 이러한 종속성을 수동으로 다운로드하고 동일한 방식으로 프로젝트에 추가해야 합니다.

새 구문 및 언어 기능

업데이트된 스크립팅 런타임을 사용하면 Unity 개발자가 C# 8 및 다양한 새 언어 기능 및 구문에 액세스할 수 있습니다.

자동 속성 이니셜라이저

Unity의 .NET 3.5 스크립팅 런타임에서 자동 속성 구문을 사용하면 초기화되지 않은 속성을 쉽게 정의할 수 있지만 초기화는 스크립트의 다른 위치에서 발생해야 합니다. 이제 .NET 4.x 런타임을 사용하여 동일한 줄에서 자동 속성을 초기화할 수 있습니다.

// .NET 3.5
public int Health { get; set; } // Health has to be initialized somewhere else, like Start()

// .NET 4.x
public int Health { get; set; } = 100;

문자열 보간

이전 .NET 3.5 런타임에서는 문자열 연결에 어색한 구문이 필요했습니다. 이제 .NET 4.x 런타임 $ 을 사용하면 문자열 보간 기능을 통해 식을 보다 직접적이고 읽기 쉬운 구문으로 문자열에 삽입할 수 있습니다.

// .NET 3.5
Debug.Log(String.Format("Player health: {0}", Health)); // or
Debug.Log("Player health: " + Health);

// .NET 4.x
Debug.Log($"Player health: {Health}");

표현식 멤버

.NET 4.x 런타임에서 사용할 수 있는 최신 C# 구문을 사용하여 람다 식은 함수 본문을 대체하여 보다 간결하게 만들 수 있습니다.

// .NET 3.5
private int TakeDamage(int amount)
{
    return Health -= amount;
}

// .NET 4.x
private int TakeDamage(int amount) => Health -= amount;

읽기 전용 속성에도 식 본문 멤버를 사용할 수 있습니다:

// .NET 4.x
public string PlayerHealthUiText => $"Player health: {Health}";

TAP(작업 기반 비동기 패턴)

비동기 프로그래밍 을 사용하면 애플리케이션이 응답하지 않게 하지 않고도 시간이 많이 걸리는 작업을 수행할 수 있습니다. 또한 이 기능을 사용하면 코드가 이러한 작업의 결과에 따라 코드를 계속 실행하기 전에 시간이 많이 걸리는 작업이 완료되기를 기다릴 수 있습니다. 예를 들어 파일이 로드되거나 네트워크 작업이 완료되기를 기다릴 수 있습니다.

Unity에서 비동기 프로그래밍은 일반적으로 코루틴을 사용하여 수행됩니다. 그러나 C# 5 이후 .NET 개발에서 선호되는 비동기 프로그래밍 방식은 System.Threading.Task와 함께 asyncawait 키워드를 사용하는 작업 기반 비동기 패턴(TAP)이었습니다. 요약하면 async 함수에서 애플리케이션의 나머지 부분 업데이트를 막지 않고 작업이 완료되기를 await 기다릴 수 있습니다:

// Unity coroutine
using UnityEngine;
public class UnityCoroutineExample : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(WaitOneSecond());
        DoMoreStuff(); // This executes without waiting for WaitOneSecond
    }
    private IEnumerator WaitOneSecond()
    {
        yield return new WaitForSeconds(1.0f);
        Debug.Log("Finished waiting.");
    }
}
// .NET 4.x async-await
using UnityEngine;
using System.Threading.Tasks;
public class AsyncAwaitExample : MonoBehaviour
{
    private async void Start()
    {
        Debug.Log("Wait.");
        await WaitOneSecondAsync();
        DoMoreStuff(); // Will not execute until WaitOneSecond has completed
    }
    private async Task WaitOneSecondAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Debug.Log("Finished waiting.");
    }
}

TAP은 복잡한 주제이며, 개발자가 고려해야 하는 Unity 관련 뉘앙스가 있습니다. 따라서 TAP은 Unity에서 코루틴을 보편적으로 대체하는 것이 아닙니다. 그러나 사용할 또 다른 도구입니다. 이 기능의 범위는 이 문서를 벗어나지만 몇 가지 일반적인 모범 사례 및 팁이 아래에 제공됩니다.

Unity를 사용하여 TAP에 대한 참조 시작

이러한 팁은 Unity에서 TAP을 시작하는 데 도움이 될 수 있습니다.

  • 대기하려는 비동기 함수에는 반환 형식 Task 또는 Task<TResult>.
  • 작업을 반환하는 비동기 함수는 이름에 접미사 "Async" 를 추가해야 합니다. "Async" 접미사는 함수가 항상 대기되어야 함을 나타내는 데 도움이 됩니다.
  • 기존 동기 코드에서 비동기 함수를 발생시키는 함수에만 반환 형식을 사용합니다 async void . 이러한 함수 자체는 대기할 수 없으며 이름에 "Async" 접미사가 없어야 합니다.
  • Unity는 UnitySynchronizationContext를 사용하여 기본적으로 기본 스레드에서 비동기 함수가 실행되도록 합니다. Unity API는 주 스레드 외부에서 액세스할 수 없습니다.
  • 다음과 같은 Task.Run 메서드를 사용하여 백그라운드 스레드에서 작업을 실행할 수 있습니다 Task.ConfigureAwait(false). 이 기술은 성능을 향상시키기 위해 주 스레드에서 비용이 많이 드는 작업을 오프로드하는 데 유용합니다. 그러나 백그라운드 스레드를 사용하면 경합 조건과 같이 디버그하기 어려운 문제가 발생할 수 있습니다.
  • Unity API는 주 스레드 외부에서 액세스할 수 없습니다.
  • 스레드를 사용하는 작업은 Unity WebGL 빌드에서 지원되지 않습니다.

코루틴과 TAP 간의 차이점

코루틴과 TAP /async-await 사이에는 몇 가지 중요한 차이점이 있습니다.

  • 코루틴은 값을 반환할 수 없지만 Task<TResult> 값을 반환할 수 있습니다.
  • try-catch 문에는 yield을 넣을 수 없어서 코루틴으로 오류를 처리하기가 어렵습니다. 하지만 try-catch는 TAP과 함께 사용할 수 있습니다.
  • Unity의 코루틴 기능은 MonoBehaviour에서 파생되지 않는 클래스에서 사용할 수 없습니다. TAP은 이러한 클래스의 비동기 프로그래밍에 적합합니다.
  • 이 시점에서 Unity는 TAP을 코루틴의 도매 대체로 제안하지 않습니다. 프로파일링은 지정된 프로젝트에 대해 한 방법의 특정 결과와 다른 방법의 결과를 알 수 있는 유일한 방법입니다.

nameof 연산자

연산자는 nameof 변수, 형식 또는 멤버의 문자열 이름을 가져옵니다. nameof가 유용한 경우로는 오류를 로깅하는 것과 열거형의 문자열 이름을 가져오는 것이 있습니다.

// Get the string name of an enum:
enum Difficulty {Easy, Medium, Hard};
private void Start()
{
    Debug.Log(nameof(Difficulty.Easy));
    RecordHighScore("John");
    // Output:
    // Easy
    // playerName
}
// Validate parameter:
private void RecordHighScore(string playerName)
{
    Debug.Log(nameof(playerName));
    if (playerName == null) throw new ArgumentNullException(nameof(playerName));
}

호출자 정보 특성

호출자 정보 특성 은 메서드의 호출자에 대한 정보를 제공합니다. 호출자 정보 특성과 함께 사용하려는 각 매개 변수에 대한 기본값을 제공해야 합니다.

private void Start ()
{
    ShowCallerInfo("Something happened.");
}
public void ShowCallerInfo(string message,
        [System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
        [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
        [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
    Debug.Log($"message: {message}");
    Debug.Log($"member name: {memberName}");
    Debug.Log($"source file path: {sourceFilePath}");
    Debug.Log($"source line number: {sourceLineNumber}");
}
// Output:
// Something happened
// member name: Start
// source file path: D:\Documents\unity-scripting-upgrade\Unity Project\Assets\CallerInfoTest.cs
// source line number: 10

정적 사용하기

정적을 사용하면 클래스 이름을 입력하지 않고 정적 함수를 사용할 수 있습니다. 정적을 사용하면 동일한 클래스의 여러 정적 함수를 사용해야 하는 경우 공간과 시간을 절약할 수 있습니다.

// .NET 3.5
using UnityEngine;
public class Example : MonoBehaviour
{
    private void Start ()
    {
        Debug.Log(Mathf.RoundToInt(Mathf.PI));
        // Output:
        // 3
    }
}
// .NET 4.x
using UnityEngine;
using static UnityEngine.Mathf;
public class UsingStaticExample: MonoBehaviour
{
    private void Start ()
    {
        Debug.Log(RoundToInt(PI));
        // Output:
        // 3
    }
}

IL2CPP 고려 사항

iOS와 같은 플랫폼으로 게임을 내보낼 때 Unity는 IL2CPP 엔진을 사용하여 IL을 C++ 코드로 "변환"한 다음, 대상 플랫폼의 네이티브 컴파일러를 사용하여 컴파일합니다. 이 시나리오에는 리플렉션 부분 및 키워드 사용 dynamic 과 같이 지원되지 않는 몇 가지 .NET 기능이 있습니다. 사용자 고유의 코드에서 이러한 기능을 사용하여 제어할 수 있지만 Unity 및 IL2CPP를 염두에 두고 작성되지 않은 타사 DLL 및 SDK를 사용하는 데 문제가 발생할 수 있습니다. 이 문서에 대한 자세한 내용은 Unity 사이트의 스크립팅 제한 문서를 참조하세요.

또한 위의 Json.NET 예제에서 설명한 대로 Unity는 IL2CPP 내보내기 프로세스 중에 사용되지 않는 코드를 제거하려고 시도합니다. 이 프로세스는 일반적으로 문제가 되지 않지만 리플렉션을 사용하는 라이브러리에서는 런타임에 호출될 속성이나 메서드를 실수로 제거하여 내보내기 시 확인할 수 없습니다. 이러한 문제를 해결하려면 제거 프로세스를 실행하지 않을 어셈블리 및 네임스페이스 목록이 포함된 link.xml 파일을 프로젝트에 추가합니다. 자세한 내용은 바이트 코드 제거에 대한 Unity의 문서를 참조하세요.

.NET 4.x 샘플 Unity Project

샘플에는 여러 .NET 4.x 기능의 예가 포함되어 있습니다. 프로젝트를 다운로드하거나 GitHub 소스 코드를 볼 수 있습니다.

추가 리소스