반응형

 

언리얼 개발자라면 놓쳐서는 안될 마켓플레이스 이 달의 무료 콘텐츠에 대해서 알아봅시다!

 

타임라인

0:00 언리얼 마켓플레이스 개요

0:52 마켓플레이스 무료 콘텐츠

1:15 언리얼 마켓플레이스 무표 콘텐츠 검색

2:23 무료 콘텐츠 받기

반응형
반응형

Prefab 

게임 오브젝트를 에셋화 하기

 

작성 기준 버전 :: 2019.1.4f1

 

[본 포스트의 내용은 유튜브 영상을 통해서 시청하실 수도 있습니다]

 

유니티에서 게임 오브젝트는 씬에 배치될 수 있는 오브젝트를 의미한다. 이 게임 오브젝트에 어떤 컴포넌트가 붙는가에 따라서 그 게임 오브젝트의 역할이 결정되는데, 씬에 하나만 배치되는 오브젝트는 컴포넌트를 직접 부착해서 배치할 수는 있지만 똑같은 오브젝트를 많이 배치해야 되는 경우에 매번 배치할 때마다 필요한 컴포넌트를 부착하는 작업을 해야한다면 이것은 매우 비효율적인 작업이 된다. 

 

일일이 게임 오브젝트를 생성한 다음 컴포넌트를 붙이는 비효율에서 벗어나기 위해서 제일 처음 만들어진 게임 오브젝트를 복사해서 배치할 수도 있는데, 이것은 또 다른 비효율적인 작업에 봉착하게 된다. 만약 이렇게 붙여넣은 오브젝트들의 크기를 전부 2배로 키워야 한다면? 그럼 붙여넣은 게임 오브젝트들을 일일이 찾아서 스케일 값을 바꿔주어야 한다. 이것 역시 심각하게 비효율적인 작업이다.

 

이러한 예시 이외에 어떤 게임 오브젝트를 게임이 진행하는 도중에 생성해서 배치해야 된다면, 코드 상에서 빈 게임 오브젝트를 생성하고, 거기에 필요한 컴포넌트를 붙여서 일일이 초기화해서 배치를 하는 것 역시 비효율적이다.

 

 

비효율적인 작업들 1 : 새 오브젝트마다 손수 컴포넌트 붙이고 설정하기

 

앞서 제시한 예시들을 하나씩 따라가보자.

 

 

우리는 이제 씬에 이른바 "Elegance Black Box"라고 명명된 검은색의 Black Box 컴포넌트가 부착된 상자를 여러 개 배치하려고 한다. 이 멋진 검은 상자를 만들기 위해서 우리는 다음과 같은 작업을 해야한다.

 

 

먼저 새 상자를 만든다.

 

 

새 상자의 이름을 "Elegance Black Box"로 변경한다.

 

 

그 다음 머티리얼에 검은 색 머티리얼을 넣고 Black Box 컴포넌트를 붙여준다(사실 따로 순서를 진행할 수도 있지만 그만큼 번거롭고 지루해지기 때문에 그냥 합쳤다).

 

자 총 4단계의 과정을 거쳤다. 이 작업을 만들고자 하는 "Elegance Black Box"의 갯수만큼 반복하면 된다. 고작 4단계인데 이렇게 번거롭다. 만약 더 복잡한 구조의 게임 오브젝트라면 어떻겠는가?

 

 

비효율적인 작업들 2 : 배치된 게임 오브젝트 복제하기

 

 

유니티 엔진에서는 복제하고자 하는 게임 오브젝트를 선택하고 우클릭하여 [Duplicate] 항목을 선택하거나 [Ctrl + D] 단축키를 눌러서 복제할 수 있다.

 

 

와! 일일이 새 게임 오브젝트를 만들고 컴포넌트를 붙이는 작업을 하지 않아도 된다! 혁명적인가? 분명 여기까지는 혁명적이다. 하지만 원수같은 기획자들이 "Elegance Black Box"를 좀 더 우아하게 강조하기 위해서 크기를 25% 키우자고 주장했다.

 

 

그나마 예시에서는 갯수가 적고 하이어라키 뷰에서 오브젝트가 흩어져 있지 않아서 모두 선택해서 빠르게 해결했다. 하지만 하이어라키 뷰에서 다른 게임 오브젝트 밑에 숨어있다거나 흩어져있다면 일일이 찾아서 수정해야 한다. 물론 하이어라키 뷰의 검색 기능을 이용하면 훌륭하게 해결할 수 있을 지도 모른다. 그러나 이런 검색 작업 역시 비효율적인것은 사실이다.

 

 

비효율적인 작업들 3 : 코드에서 동적으로 생성하기

 

이번에는 동료 디자이너가 "멋진 검은 상자가 게임 중에 동적으로 생성되면 좋겠는데!"라고 말했다. 

 

public static BlackBox CreateNewBlackBox()

{

    var newBox = GameObject.CreatePrimitive(PrimitiveType.Cube).AddComponent<BlackBox>();

    newBox.name = "Elegance Black Box";

    newBox.GetComponent<Renderer>().material = Resources.Load<Material>("M_Black");

    return newBox;

}

 

거기에 당신은 위와 같이 블랙 박스를 생성하는 코드를 만들어 냈다. 이러면 끝난 것일까? 아니다. 디자이너가 블랙 박스에 대해서 수정사항을 가지고 올 때마다 당신은 코드를 수정해야 한다. 거기에 컴파일 시간은 덤이다! 그리고 지금은 간단한 오브젝트라 코드가 몇 줄 되지 않지만 복잡한 오브젝트면 코드의 양이 늘어나고 거기에 더불어 버그의 확률도 함께 상승한다.

 

 

 

우리의 구세주 프리팹

 

이러한 모든 문제를 해결하기 위해서 있는 것이 바로 프리팹이다. 프리팹은 게임 오브젝트와 거기에 붙여진 컴포넌트와 그 프로퍼티들을 에셋의 형태로 저장하는 것이다.

 

프리팹 만들기

 

 

 

프리팹을 만드는 방법은 아주 간단하다. 하이어라키 뷰에서 프리팹으로 만들고자 하는 게임 오브젝트를 선택해서 프로젝트 뷰로 끌어다 놓기만 하면 된다. 프리팹이 된 게임 오브젝트는 앞의 아이콘이 무채색 육면체에서 파란 육면체로 바뀐다.

 

배치된 게임 오브젝트 한꺼번에 변경하기

 

이번에도 아까 전처럼 씬에 배치된 모든 블랙 박스의 크기를 변경하고 싶을 수 있다. 프리팹으로는 이런 작업이 아주 간단하다.

 

 

프로젝트 뷰에서 원본 프리팹을 선택하고 프리팹의 크기를 변경해주면 씬에 배치된 모든 프리팹 인스턴스의 크기가 함께 변경된다. 하지만 이 방법은 씬에 배치된 각각의 인스턴스의 프로퍼티가 수정된 상태라면 적용되지 않으니 주의해야 한다.

 

프리팹 인스턴스에서 편집

 

 

위 예시에서는 프리팹 원본에서 수정된 것을 프리팹 인스턴스로 적용되는 내용이었다. 반대로 씬에 배치된 프리팹 인스턴스를 수정하고 이것을 원본 프리팹에 적용할 수도 있다. 씬에 배치된 프리팹 인스턴스 게임 오브젝트를 선택하면 일반 게임 오브젝트와는 다르게 인스펙터 뷰의 게임 오브젝트의 이름 아래에 Prefab : Open, Select, Override 버튼을 볼 수 있다.

 

 

여기서 Open 버튼을 선택하면 선택된 프리팹의 원본만을 수정할 수 있는 전용 씬으로 이동된다. 여기서는 프로젝트 뷰에서는 보이지 않는 프리팹 원본의 깊은 자식 오브젝트까지 열어서 수정할 수 있게 된다. 또한 수정된 프리팹의 내용은 자동으로 저장되며 하이어라키 뷰의 프리팹 아이콘 옆의 < 버튼을 클릭하면 다시 원래 씬으로 돌아올 수 있다.

 

원본 프리팹을 더블 클릭하거나 씬에 배치된 프리팹 인스턴스 옆의 > 버튼을 클릭해도 프리팹 수정 씬으로 들어올 수 있다.

 

 

Select 버튼을 클릭하면 프로젝트 뷰의 원본 프리팹이 바로 선택된다.

 

 

Override 버튼은 만약 프리팹 인스턴스에 원본 인스턴스와 달라진 점이 있다면 내용이 나타난다. 여기서 Revert All을 선택하면 프리팹 인스턴스의 변경 사항이 초기화되고 프리팹 원본 값으로 돌아간다. Apply All을 선택하면 프리팹 인스턴스의 수정 사항이 반대로 프리팹 원본에 덮어 씌워진다.

 

프리팹 인스턴스화

 

프로젝트 뷰에 존재하는 프리팹 원본은 에셋 상태로 이 상태 그대로는 게임 씬에서 보거나 사용할 수 없다. 이것을 게임 씬에 배치하고 사용할 수 있게 생성하는 과정을 인스턴스화라고 한다. 프리팹 인스턴스화는 게임 오브젝트의 Instantiate() 함수를 이용해서 할 수 있다.

 

프로젝트 뷰에 있는 게임 오브젝트는 크게 두 가지 방법으로 가져올 수 있다.

 

Resources 폴더에서 가져오기

 

 

첫 번째 방법은 Resources 폴더에서 가져오는 것이다. 이 방법은 어느 경로이든 무관하게 가져오고자 하는 프리팹이 프로젝트 뷰에서 Resources 폴더 안에 들어있기만 하면 된다. 단, Resources 폴더에 들어있는 파일들은 게임이 실행되면 무조건 메모리에 적재되기 때문에 메모리 이슈를 일으키고 싶지 않다면, 필요한 에셋만을 Resources 폴더에 넣어둘 것을 권장한다.

 

public static BlackBox CreateNewBlackBox()

{

    var boxPrefab = Resources.Load<BlackBox>("Elegance Black Box");

    return Instantiate(boxPrefab);

}

 

그 다음 Resources.Load() 함수로 Resources 폴더 안의 프리팹을 가져와서 Instantiate() 함수로 씬에 생성할 수 있다.

 

씬에 배치된 게임 오브젝트의 컴포넌트의 프로퍼티로 참조하기

 

public class BoxSpawn : MonoBehaviour

{

    [SerializeField]

    private GameObject boxPrefab;

 

    private void Start()

    {

        Instantiate(boxPrefab);

    }

}

 

두 번째 방법은 씬에 배치된 게임 오브젝트에 부착된 컴포넌트의 프로퍼티로 프리팹 원본을 참조하고 있다가 생성하는 방법이다.

 

 

씬에 Box Spawner 게임 오브젝트를 만들고 위에서 작성한 Box Spawn 컴포넌트를 부착하고 Box Prefab 프로퍼티에 프리팹을 할당해주면 된다. 그러면 게임이 시작되면 박스 스포너가 블랙 박스를 생성하는 것을 확인할 수 있다.

 

기타

 

위에서 제시한 방법 이외에도 에셋 번들에서 가져와서 생성하는 방법 등 다른 기능과 연계된 심화 방법들이 존재한다.

 

 

프리팹의 장점

 

게임 오브젝트가 프리팹화됨으로써 얻을 수 있는 장점은 굉장히 많다. 첫 번째는 재사용이 굉장히 편하다는 점이고, 씬에 흩어져서 배치된 프리팹의 인스턴스들을 한꺼번에 수정하기도 쉽다. 그리고 프로그래머가 컴포넌트만 제대로 만들어준다면, 게임 디자이너들이 프로그래머에게 요청하지 않고도 손쉽게 게임 요소들을 수정할 수 있다는 점이 제일 큰 장점이다.

반응형
  1. 코딩캣츠 2021.01.20 11:26 신고

    항상 잘 보고 있어요!

  2. 라이프리 2021.09.14 22:31 신고

    프리팹 강의 대박이네요.. 왜쓰는지 이해안됐었는데 쏙쏙 잘됐어요

반응형

RPG :: 마우스 입력 이동 구현하기

 

영상 기준 버전 : 4.27

 

작성 기준 버전 :: 4.21

 

이전 섹션에서는 C++ 내려보기 템플릿을 참고해서 일반적인 RPG처럼 마우스 클릭을 통해 캐릭터를 이동시키는 방법을 구현해보고 코드를 분석해본다.

 

프로젝트 세팅

 

RpgProject 라는 이름으로 새 프로젝트를 하나 만든다.

 

 

 

RpgProject를 생성한 뒤에는 내려보기 템플릿으로도 새 프로젝트를 하나 만드는데, 이것은 캐릭터의 메시와 애니메이션을 가져오기 위함이다.

 

제일 먼저 할 일은 내려보기 프로젝트에서 캐릭터의 메시와 애니메이션을 가져올 것이다. 방금 만든 내려보기 프로젝트의 콘텐츠 패널의 콘텐츠 폴더 하위에 Mannequin 폴더가 보일 것이다. 이 안에 캐릭터의 메시와 애니메이션이 들어있다. 이 폴더의 내용물들을 RpgProject로 옮겨야 한다. 이렇게 프로젝트에 포함된 애셋들을 다른 프로젝트로 옮기는 작업을 이주(Migrate)라고 한다. 직접 파일을 옮기지 않아도 언리얼 엔진에서는 이것을 도와주는 기능을 제공한다.

 

 

Mannequin 폴더에 우클릭을 하고, 이주... 를 선택한다. 애셋 리포트 창이 뜨면 리스트를 체크하고 확인 버튼을 누른다.

 

 

그 다음 대상 콘텐츠 폴더 선택 대화상자가 열리면 RpgProject 프로젝트의 Content 폴더를 찾아서 폴더 선택을 한다.

 

 

이주 작업이 끝난 뒤에 RpgProject로 가서 콘텐츠 브라우저 패널을 확인하면 내려보기 프로젝트에 있던 Mannequin 폴더와 그 안의 애셋들이 RpgProject에 성공적으로 옮겨진 것을 확인할 수 있다.

 

 

캐릭터의 메시와 애니메이션을 모두 이주시켰으면 그 다음은, 비주얼 스튜티오를 열고 솔루션 탐색기에서 RpgProject.Build.cs를 찾아서 소스파일을 연다.

 

 

Build.cs에서는 게임을 개발하면서 사용할 모듈을 추가하거나 뺄 수 있는데, 여기서는 두 가지 모듈을 추가할 것이다. 아래의 예시 코드와 같이 PublicDependencyModuleNames에 "NavigationSystem"과 "AIModule"을 추가해주자. NavigationSystem 모듈은 네비게이션 메시와 관련된 기능에 도움을 주는 모듈이고 AIModule 은 이름 그대로 AI 기능에 관련된 모듈이다. 클릭된 위치로 캐릭터를 이동시킬 때, 처리하는 코드를 일일이 만드는 대신, 이 두 모듈의 기능의 도움으로 내비게이션된 경로를 따라서 움직이도록 만들 예정이다.

 

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "NavigationSystem", "AIModule" });

 

 

맵 세팅하기

 

프로젝트에 대한 세팅이 끝났다면, 캐릭터가 움직일 맵을 구성해보자.

 

맵을 세팅하기 이전에 맵에 배치할 기둥이나 벽을 대신할 메시 파일을 다운받는다.

 

Box.zip
다운로드

 

그리고 콘텐츠 브라우저 패널에서 Props 폴더를 생성하고 그 폴더에 방금 다운받은 Box.fbx를 임포트(Import)한다.

 

 

그 다음 RpgProject의 레벨 에디터를 보면 빈 평면만 있는 것을 볼 수 있다. 여기에 방금 전에 받은 박스를 이용해서 유닛의 이동을 방해할 수 있게 적절하게 배치해주도록 하자. 아래의 예시와 같이 배치하여도 되고 원하는 대로 편하게 배치해도 된다.

 

 

맵에 장애물들을 모두 배치했다면, 모드 패널의 볼륨에서 내비 메시 바운드 볼륨을 선택해서 레벨의 정중앙에 배치한다. 이 내비 매시 바운드 볼륨은 볼륨의 영역 내에 있는 오브젝트들을 찾아서 계산한 뒤 이동 경로를 찾아줄 내비 메시를 만들어내는 역할을 한다.

 

 

내비 메시 바운드 볼륨을 맵 중앙에 배치한 뒤, 이것의 스케일을 맵을 충분히 덮을 만큼 키워준 다음, P[각주:1]를 눌러보면 캐릭터가 이동할 수 있는 범위가 초록색으로 표시되는 것을 볼 수 있다.

 

 

모든 맵 세팅을 마쳤다면 콘텐츠 브라우저에서 Maps 폴더를 만들고 Ctrl + S를 눌러서 Maps 폴더에 지금 만든 맵을 RpgTestMap 이라는 이름으로 저장한다.

 

 

맵을 저장한 뒤에는 프로젝트 세팅 창을 열고 맵 & 모드에서 Editor Startup Map을 방금 저장한 RpgTestMap으로 설정해준다. 이렇게 하면 다음에 프로젝트를 열었을 때, 지정한 멥이 제일 먼저 열릴 것이다.

 

 

 

Player Controller로 마우스 입력 받기

 

이 다음 작업은 플레이어 컨트롤러로 클릭 입력을 받아서 컨트롤러가 소유한 폰을 클릭한 위치로 이동시키는 코드를 작성한다.

 

코드 작성 이전에 프로젝트 세팅 창의 입력에서 다음과 같이 입력 매핑을 세팅해준다. 이번 섹션에서는 클릭 지점으로 캐릭터를 이동시키는 것만을 목표로 할 것이기 때문에, 입력 매핑은 InputClick을 왼쪽 마우스 버튼으로 하는 것으로 충분하다.

 

 

입력 환경설정을 마쳤다면, 콘텐츠 브라우저 패널에서 신규 추가 버튼을 눌러서 새 C++ 클래스를 추가한다. 부모 클래스로는 Player Controller를 선택하고 다음 버튼을 누른다.

 

 

클래스의 이름은 RpgPlayerController로 하고 클래스 생성 버튼을 누른다.

 

 

클래스가 생성되고 비주얼 스튜디오가 열리면, 우선 다음 코드를 추가해서 생성자를 선언한다.

 

public:
    ARpgPlayerController();

 

RpgPlayerController.cpp에 생성자를 구현한다.

 

ARpgPlayerController::ARpgPlayerController()
{
    bShowMouseCursor = true;
}

 

bShowMouseCursor 프로퍼티를 true로 설정하면 게임 내에서 마우스 커서가 보이도록 만들어준다. 우리는 마우스로 캐릭터를 이동시킬 계획이기 때문에 반드시 필요한 코드이다.

 

RpgPlayerController.h에 다음 변수를 추가한다.

 

protected:
    bool bClickMouse;

 

이 변수는 왼쪽 마우스 버튼을 누를 때 true가 되고, 뗄 때 false가 될 것이다.

 

그 아래에 다음 함수들을 선언을 추가한다.

 

void InputClickPressed();

void InputClickReleased();

 

이 함수들은 입력 매핑과 바인딩되어서 입력을 받으면 bClickMouse 변수의 값을 바꿔주는 역할을 한다. RpgPlayerController.cpp에 위 두 함수를 구현한다.

 

void ARpgPlayerController::InputClickPressed()
{
    bClickMouse = true;
}

void ARpgPlayerController::InputClickReleased()
{
    bClickMouse = false;
}

 

SetupInputComponent() 함수를 덮어씌워서 구현할 차례다. 헤더에 다음 선언을 추가한다.

 

virtual void SetupInputComponent() override;

 

다시 RpgPlayerController.cpp로 가서 SetupInputComponent() 함수를 구현한다.

 

void ARpgPlayerController::SetupInputComponent()
{
    Super::SetupInputComponent();

    InputComponent->BindAction("InputClick", IE_Pressed, this, &ARpgPlayerController::InputClickPressed);
    InputComponent->BindAction("InputClick", IE_Released, this, &ARpgPlayerController::InputClickReleased);
}

 

왼쪽 마우스 버튼을 누르면 InputClickPressed() 함수가 호출되서 bClickMouse 변수가 true가 되고, 왼쪽 마우스 버튼을 떼면 InputClickReleased() 함수가 호출되서 bClickMouse 변수가 false가 되도록 세팅되었다.

 

이 다음 작업은 마우스를 클릭하면 클릭한 위치로 캐릭터를 이동시키는 코드를 작성하는 작업이다.

 

먼저 RpgPlayerController.h에 다음 함수를 정의한다.

 

void SetNewDestination(const FVector DestLocation);

 

이 함수의 역할은 새로운 목표 위치를 받아서 컨트롤러가 소유한 폰을 그 위치로 이동시키는 역할을 할 것이다.

 

SetNewDestination() 함수에서 내비 메시 위에서 움직이기 위한 작업을 처리하기 위해 다음 전처리기를 추가한다.

 

#include "Blueprint/AIBlueprintHelperLibrary.h"

 

이제 RpgPlayerController.cpp에 SetNewDestination() 함수를 구현해보자.

 

void ARpgPlayerController::SetNewDestination(const FVector DestLocation)
{
    APawn* const MyPawn = GetPawn();
    if (MyPawn)
    {
        float const Distance = FVector::Dist(DestLocation, MyPawn->GetActorLocation());

        if (Distance > 120.0f)
        {
            UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, DestLocation);
        }
    }
}

 

이 함수에서는 우선 컨트롤러가 소유하고 있는 폰을 가져와서 폰과 목적지 사이의 거리를 측정해서, 그 거리가 120 언리얼 유닛보다 크면 폰을 목적지로 이동시킨다. UAIBlueprintHelperLibrary클래스의 SimpleMoveToLocation() 함수는 프로그래머가 목적지로 폰을 이동시키기 위한 처리를 하는 모든 코드를 일일이 작성하는 대신에 간단한 함수 호출로 그 모든 일을 할 수 있도록 도와준다. 아까 전 프로젝트 세팅 단계에 모듈을 추가한 것은 이 기능을 사용하기 위해서 였다.

 

헤더 파일로 돌아가서 다음 함수를 정의한다.

 

void MoveToMouseCursor();

 

그리고 cpp파일에 MoveToMouseCursor() 함수를 구현한다.

 

void ARpgPlayerController::MoveToMouseCursor()
{
    FHitResult Hit;
    GetHitResultUnderCursor(ECC_Visibility, false, Hit);

    if (Hit.bBlockingHit)
    {
        SetNewDestination(Hit.ImpactPoint);
    }
}

 

MoveToMouseCursor() 함수는 GetHitResultUnderCursor() 함수를 통해 마우스 커서 아래에 레이 트레이스를 쏴서 그 위치를 SetNewDestination() 함수에 전달하는 역할을 한다.

 

PlayerTick() 함수를 덮어쓸 차례이다. 헤더에 다음 함수 선언을 추가한다.

 

virtual void PlayerTick(float DeltaTime) override;

 

Cpp 파일에 함수 구현 코드를 추가한다.

 

void ARpgPlayerController::PlayerTick(float DeltaTime)
{
    Super::PlayerTick(DeltaTime);

    if (bClickMouse)
    {
        MoveToMouseCursor();
    }
}

 

코드 추가가 끝났다면 솔루션 탐색기에서 RpgProject를 우클릭 해서 빌드하고 에디터로 돌아간다.

 

 

캐릭터 구현

 

캐릭터가 움직일 맵과 캐릭터를 컨트롤할 플레이어 컨트롤러를 모두 만들었으니 이제 맵 위에서 움직일 캐릭터를 만들 차례이다.

 

콘텐츠 브라우저 패널에서 신규 추가 버튼을 누르고 새 C++ 클래스를 추가한다. 부모 클래스로는 Character 클래스를 선택한다.

 

 

클래스의 이름은 RpgCharacter로 한다.

 

 

RpgCharacter 클래스가 생성되면 RpgCharacter.h로 가서 다음 변수들을 추가한다.

 

private:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
        class UCameraComponent* RpgCameraComponent;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
        class USpringArmComponent* RpgCameraSpringArmComponent;

 

이 변수들은 카메라의 위치를 Rpg 게임에 알맞은 위치로 맞춰주는 역할을 할 것이다. 카메라 컴포너트와 스프링 암 컴포넌트를 초기화 시켜주기 전에, 필요한 컴포넌트들을 사용하기 위한 헤더들을 포함시키는 전처리기들을 RpgCharacter.cpp에 추가해주자.

 

#include "Engine/Classes/Components/CapsuleComponent.h"
#include "Engine/Classes/Camera/CameraComponent.h"

#include "Engine/Classes/GameFramework/CharacterMovementComponent.h"
#include "Engine/Classes/GameFramework/SpringArmComponent.h"

 

그리고 ARpgCharacter::ARpgCharacter() 생성자 함수로 가서 다음 코드들을 차례로 추가한다.

 

GetCapsuleComponent()->InitCapsuleSize(42.0f, 96.0f);

 

이 코드는 캐릭터 클래스가 기본적으로 가지고 있는 캡슐 콜라이더의 크기를 초기화한다.

 

bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;

 

이 코드는 캐릭터가 카메라의 회전을 따라서 회전하지 않도록 한다.

 

GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 640.0f, 0.0f);
GetCharacterMovement()->bConstrainToPlane = true;
GetCharacterMovement()->bSnapToPlaneAtStart = true;

 

이 코드는 캐릭터의 무브먼트를 규정하는 코드로, 캐릭터를 이동시키기 전에 이동 방향과 현재 캐릭터의 방향이 다르면 캐릭터를 이동 방향으로 초당 640도의 회전 속도로 회전시킨다음 이동시킨다. 그리고 캐릭터의 이동을 평면으로 제한하고, 시작할 때 캐릭터의 위치가 평면을 벗어난 상태라면 가까운 평면으로 붙여서 시작되도록 한다. 여기서 평면이란 내비게이션 메시를 의미한다.

 

RpgCameraSpringArmComponent = CreateDefaultSubobject<USpringArmComponent>(TEXT("RpgCameraSpringArm"));
RpgCameraSpringArmComponent->SetupAttachment(RootComponent);
RpgCameraSpringArmComponent->bAbsoluteRotation = true;
RpgCameraSpringArmComponent->TargetArmLength = 800.0f;
RpgCameraSpringArmComponent->RelativeRotation = FRotator(-60.0f, 45.0f, 0.0f);
RpgCameraSpringArmComponent->bDoCollisionTest = false;

 

이 코드는 카메라를 캐릭터에게서 적절한 위지를 잡도록 도와주는 스프링 암 컴포넌트를 생성하고 설정한다.

 

bAbsoluteRotation은 스프링 암의 회전이 루트 컴포넌트와 상위 컴포넌트를 따르지 않고 월드 좌표계의 회전을 따르도록 한다.

 

TargetArmLength는 카메라와 캐릭터의 거리를 800으로 설정하고 ReleativeRotation은 스프링 암을 회전시켜 위에서 캐릭터를 내려다보도록 설정한다.

 

bDoCollisionTest는 카메라가 벽에 닿으면 충돌 계산을 통해 카메라와 캐릭터의 거리를 좁혀 카메라가 벽을 뚫지 않게 만들어주는 프로퍼티이지만, Rpg게임에서는 사용되지 않는 옵션이기 때문에 false로 설정한다.

 

RpgCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("RpgCamera"));
RpgCameraComponent->SetupAttachment(RpgCameraSpringArmComponent, USpringArmComponent::SocketName);
RpgCameraComponent->bUsePawnControlRotation = false;

 

이 코드는 카메라 컴포넌트를 생성하고 스프링 암 컴포넌트에 붙이는 작업을 한다.

 

PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;

 

그리고 마지막으로 틱 함수가 동작하도록 설정한다.

 

모든 코드 작업이 끝났다면 솔루션 탐색기에서 RpgProject를 우클릭해서 프로젝트를 빌드하고 에디터로 돌아간다.

 

에디터로 돌아왔다면, 콘텐츠 브라우저 패널에서 Bluprints 폴더를 생성한 뒤, RpgCharacter 클래스를 찾아서 우클릭하고 RpgCharacter 기반 블루프린트 클래스 생성을 선택한다.

 

 

 

 

그리고 Blueprints 폴더에 BP_RpgCharacter라는 이름으로 블루프린트 클래스를 생성한다.

 

 

블루프린트가 생성되면, 생성된 블루프린트 클래스를 더블클릭해서 블루프린트 에디터를 열고 컴포넌트 패널에서 Mesh 컴포넌트를 선택한다.

 

 

그리고 디테일 패널에서 Mesh 카테고리를 찾아 Skeletal Mesh 프로퍼티를 내려보기 프로젝트에서 이주시킨 SK_Mannequin으로 설정한다.

 

 

애니메이션 역시 내려보기 프로젝트에서 가져온 ThirdPerson_AnimBP로 설정한다.

 

 

위 작업을 하고 나서 블루프린트 에디터의 뷰포트 패널을 보면 캐릭터의 메시가 캡슐 콜라이더를 벗어나고 방향 역시 다르게 되어 있을 것이다.

 

 

이를 일치시키기 위해서 메시 컴포넌트의 위치를 {0.0, 0.0, -90.0}으로 회전을 {0.0, 0.0, -90.0}으로 수정해주자.

 

 

세팅이 모두 끝났다면 블루프린트 클래스를 컴파일하고 저장한다.

 

 

게임 모드 설정

 

플레이어 컨트롤러와 캐릭터의 설정이 모두 끝났으니, 이제 게임이 우리가 만든 플레이어 컨트롤러와 캐릭터를 사용하도록 할 차례이다.

 

콘텐츠 브라우저 패널에서 RpgProjectGameModeBase 클래스를 찾아서 우클릭하여 RpgProjectGameModeBase 기반 블루프린트를 생성한다.

 

 

BP_RpgProjectGameModeBase라는 이름으로 Blueprints 폴더에 블루프린트 클래스를 생성한다.

 

 

게임 모드 블루프린트가 생성되면 더블클릭하여 블루프린트 에디터를 열고, 디테일 패널에서 Player Controller Class를 RpgPlayerController로, Default Pawn Class를 BP_RpgCharacter로 설정한다. 그리고 블루프린트를 컴파일하고 저장한 뒤 블루프린트 에디터를 닫는다.

 

 

레벨 에디터 상단 메뉴바에서 세팅>월드 세팅을 선택하면 월드 세팅 패널이 열린다.

 

 

월드 세팅 패널에서 Game Mode Override를 방금 만든 BP_RpgProjectGameMode로 설정한다.

 

 

모든 과정을 마친 뒤 레벨 에디터에서 플레이 버튼을 눌러서 PIE 모드로 들어가면 캐릭터가 마우스 클릭 지점으로 이동하고, 그 과정에서 적절하게 장애물을 회피하는 것을 볼 수 있다.

 

 

  1. 언리얼 엔진에서 생성된 내비 메시를 보여주는 단축키이다. [본문으로]
반응형
반응형

에셋 번들 중 원하는 번들만 빌드하기


유니티 엔진을 이용한 게임 개발 과정에서 패치 시스템 등을 위한 도구로서 에셋 번들이 자주 사용되는데 번들의 효율적인 관리와 사용을 위해서 모든 에셋들을 한 번들에 묶지 않고, 필요한 분류에 따라서 여러 번들에 나누어서 묶어서 사용하게 된다. 예를 들자면 캐릭터에 사용되는 리소스와 에셋들만 모아서 "character"라는 이름의 번들로 묶거나, UI에 사용되는 이미지들만 모아서 "ui_texture"라는 이름의 번들로 묶는 것이다.


이렇게 분류별로 나누어둔 번들은 다른 포스트에서 언급(http://wergia.tistory.com/29)했듯이 다음과 같이 BuildAssetBundles() 라는 함수를 통해서 빌드할 수 있다. 다음은 그 예시이다.


BuildPipeline.BuildAssetBundles(Application.dataPath + "/AssetBundles", buildAssetBundleOption, buildTarget);


위의 예시를 통해서 묶어둔 에셋들을 번들로 빌드할 수 있는데, 이 코드를 사용할 경우, 당신이 지정한 모든 번들들을 한꺼번에 빌드한다. 이것은 에셋 번들을 테스트로 빌드하거나, 적은 수의 혹은 작은 용량의 번들을 빌드할 때는 인식하지 못했던 문제를 발생시킨다. 이 문제는 개발자가 게임을 개발하는데 투자하는 가장 중요한 자원을 소모시킨다. 그 자원은 바로 "시간"이다.




작은 용량의 에셋을 번들로 빌드할 때는 고작 몇 초의 시간이 걸릴 뿐이지만, 수 GB의 에셋들을 번들로 빌드할 때는 5-10분, 혹은 그보다 많은 시간을 소모하게 된다. 만약에 가장 작은 크기의 번들에 들어가는 에셋을 수정했는데 새로이 번들을 빌드하기 위해 모든 에셋번들을 빌드해야 한다면 얼마나 많은 시간을 무의미하게 소모하게 될 것인가? 필요한 번들만을 빌드했다면 고작 수십 초에서 2-3분의 시간만을 소모했을 작업을 수 분, 수십 분을 소모해야 한다면 이 얼마나 불합리한 일인가?


그렇기 때문에, 필요한 번들만을 빌드하는 방법을 알아두어야만 한다.


AssetBundleManifest BuildPipeline.BuildAssetBundles(

string outputpath, // 빌드된 에셋 번들이 생성될 경로

AssetBundleBuild[] builds, // 빌드할 에셋 번들들의 정보

BuildAssetBundleOption assetBundleOptions, // 에셋 번들 빌드 옵션

BuildTarget targetPlatform // 에셋 번들의 타겟 플랫폼

);


필요한 번들만을 빌드하는 방법으로 유니티에서는 위의 코드와 같은 BuildAssetBundles() 함수의 오버로드를 제공한다. 원래 에셋 번들 빌드에 사용하던 함수에서 AssetBundleBuild라는 구조체 형식의 매개변수가 추가되었는데, 빌드하고자 하는 번들의 정보를 담는 구조체이다. 원하는 에셋 번들을 빌드하기 위해 제공해야할 정보들은 다음과 같다.


public struct AssetBundleBuild
{
    public string assetBundleName;   // 빌드할 에셋 번들의 이름
    public string assetBundleVariant; // 빌드할 에셋 번들의 Variant
    public string[] assetNames;        // 에셋 번들에 포함될 에셋들의 경로와 이름
}


빌드하고자 하는 에셋 번들에 대한 AssetBundleBuild 구조체 혹은 구조체 배열을 만든 뒤, 빌드할 에셋 번들의 이름, Variant(variant가 없다면 넣지 않아도 된다.), 에셋번드레 포함될 에셋들의 경로와 이름을 넣어주고 BuildAssetBundles() 함수의 2번째 매개변수로 전달하고 함수를 실행하게 되면 한 번에 모든 에셋 번들들을 빌드할 필요없이 원하는 에셋 번들만을 빌드할 수 있게 된다.





빌드하고자 하는 에셋 번들에 포함될 에셋들 찾기


원하는 에셋 번들만을 빌드하고자 할 때는 위에서 봤듯이 AssetBundleBuild 구조체를 만들어 필요한 정보들을 채워넣어 주어야 하는데, 그 중에서 에셋 번들에 포함될 에셋들의 경로와 이름인 assetNames의 경우에는 수동으로 입력하는 것은 매우 불편할 뿐더러 프로젝트가 커지면 커질 수록 관리하기도 힘들고 어떤 에셋이 어느 번들에 포함되기로 되어있는지 기억하기도 힘들어질 것이다.


이런 경우를 위해서 유니티에서 제공하는 함수가 있는데 바로 다음의 함수이다.


string[] AssetDatabase.GetAssetPathsFromAssetBundle(

string assetBundleName  // 찾고자 하는 에셋들이 포함된 에셋 번들 이름

);


위의 함수를 사용하면 유니티 에디터의 Inspector 창에서 해당 번들 이름으로 지정해둔 모든 에셋들의 경로와 이름을 string의 배열 형태로 반환한다. 이것을 AssetBundleBuild의 assetNames에 넣어주면 간편하게 번들에 포함될 에셋들의 정보를 AssetBundleBuild 구조체에 넣을 수 있다.


빌드하고자 하는 에셋 번들만을 빌드하는 간단한 예제 함수의 전체는 다음과 같다.


public void BuildNeedAssetBundle(string bundleName)
{
    if (!Directory.Exists(Application.dataPath + "/AssetBundles"))
    {
        Directory.CreateDirectory(Application.dataPath + "/AssetBundles");
    }

    AssetBundleBuild[] buildBundles = new AssetBundleBuild[1];

    buildBundles[0].assetBundleName = bundleName;
    buildBundles[0].assetNames = AssetDatabase.GetAssetPathsFromAssetBundle(bundleName);

    BuildPipeline.BuildAssetBundles(Application.dataPath + "/AssetBundles", buildBundles, buildAssetBundleOption, buildTarget);
}


반응형
  1. ㅇㅇ 2018.05.11 02:50

    좋은 글들 남기어 주셔서, 많은 도움이 되고 있습니다.
    감사합니다.

반응형

AssetBundleManifest 제대로 불러오는 방법

AssetBundleManifest 오브젝트 불러오기 실패와 유니티 문서의 문제점

요 며칠간 패치 시스템을 만들면서 아주 많이 고민해야 했던 문제가 있었는데 그것이 바로 AssetBundleManifest 오브젝트를 불러오는 방법이었다. 여러가지 방법을 이용해서 에셋 번들의 매니페스트 파일을 불러오는 것을 시도했으나 번번히 AssetBundleManifest 객체는 Null 값을 뱉으며 실패하는 경험을 했다. 이를 해결하기 위해서 구글링해서 발견한 코드도 적용해보고, AssetBundleManager의 코드도 참고해 봤지만 문제는 해결될 기미가 보이지 않았다.


제일 처음 기본적으로 참고한 유니티 5.6 문서에서는 AssetBundleManifest 객체를 불러오기 위해서 다음과 같은 코드를 사용하라고 섹션 6에서 언급하고 있다 :


AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]


경험해본 바에 의하면 저 코드를 이용해서 매니페스트 파일을 불러올 수 있는 것은 사실이다. 하지만 매니페스트 파일을 사용하기 위해 처음으로 유니티 5.6 문서를 본 개발자는 저 manifsetFilePath에 대해서 정확하게 어떤 경로를 의미하는지에 대한 난감함을 느끼게 될 것이다. 작성자 역시 같은 난감함을 느꼈고 이 manifestFilePath를 알아내기 위해 여러가지 시도를 해야봐야 했다.



시도 1 : 매니페스트 파일을 불러오고자 하는 에셋 번들


AssetBundle assetBundle = AssetBundle.LoadFromFile(assetBundleDirectory + "/character.unity3d");
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");


이 방법은 에셋 번들을 불러오는데는 성공하였지만 이후의 매니페스트 파일을 불러오는데는 실패해서 manifest가 null 값을 가지고 있었다.



시도 2 : 매니페스트 파일을 불러오고자 하는 에셋 번들의 매니페스트 파일


AssetBundle assetBundle = AssetBundle.LoadFromFile(assetBundleDirectory + "/character.manifest");
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");



이 방법은 파일 불러오기 자체에서부터 실패했다.



시도 3 : 매니페스트 파일이 포함되어 있는 폴더의 경로


AssetBundle assetBundle = AssetBundle.LoadFromFile(assetBundleDirectory);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");


이 방법 역시 시도 2와 마찬가지로 불러오기 자체를 실패했다.



위의 세 가지 방법 이 외에도 LoadAsset을 하는 방법을 바꾸어 보기도 했고 LoadFromFile 대신 LoadFromMemory나 비동기 함수를 사용하는 방법도 사용해 봤었고, 에셋 번들을 빌드한 이후에 나온 매니페스트 파일을 각 에셋 번들에 포함시킨 후에 재빌드하는 방법도 사용해 봤다. 간단히 결론만 말하면 그 모든 방법은 실패했다.


그리고 마지막 방법으로 시도해본 것이 AssetBundles.unity3d라는 파일을 호출하는 것이었다. 유니티에서 에셋 번들을 빌드하면 에디터에서 지정한 에셋 번들 이름을 가진 에셋 번들들과 그 매니페스트 파일이 생성되고, 그 외에 직접 지정한 이름이 아닌 AssetBundles라는 파일과 AssetBundles.manifest라는 파일이 생성되는데, 유니티 5.6 문서에는 이 파일의 역할이 무엇인지 명시되어 있지 않았다.


그렇게 마지막으로 시도한 코드는 다음과 같았다 :


AssetBundle assetBundle = AssetBundle.LoadFromFile(assetBundleDirectory + "/AssetBundles.unity3d");
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");



이 마지막 코드를 사용한 이후에야 매니페스트 파일을 불러올 수 있게 되었다.


유니티 문서에서는 AssetBundleManifest를 호출하는 방법은 정확히 알려주었으나, 어떤 파일을 불러와야 되는지는 제대로 알려주지 않은 것이다. 구글링한 내용들에서도 "AssetBundles" 파일에 대한 언급은 없었기 때문에 정확한 해결법을 찾는데 더 어려웠던 것 같다.




1. ".unity3d"라는 확장자는 웹 서버를 통해 다운받은 에셋 번들을 로컬 저장소에 저장할 때 직접 지정한 확장자이다.

2. 에셋 번들을 빌드하면 확장자 없이 에셋 번들 이름만 적힌 파일이 나온다.


반응형
  1. 파란파랑별 2018.01.11 12:59

    안녕하세요. 에셋번들 관련한 글 잘 보고 있습니다.

    하다가 해결이 안되는 문제가 있어서 그러는데
    AssetBundle assetBundle = AssetBundle.LoadFromFile(assetBundleDirectory + "/AssetBundles.unity3d");
    여기서 assetBundleDirectory는 어디를 말하는건지 알 수 있을까요..?


    • wergia 2018.05.22 13:38 신고

      assetBundleDirectory는 애셋 번들이 저장되어 있는 경로를 말합니다. 사용자가 직접 경로를 지정해서 불러오면 됩니다.

  2. 라임 2018.05.19 10:45

    안녕하세요
    혹시 말씀좀 물어봐두댈까여 ㅠ
    AssetBundleExtractor 프로그램으로 assets.lst 파일을 열어서 속의 사운드 파일 추출하려고하는데
    오픈누르고 해당파일을 눌러도 unable to read bundle file(invaild file or unknown vision) 이라고나옵니다 ㅠㅠ
    혹시 이부분에대해서 도와주실수있나요?

    • wergia 2018.05.22 13:37 신고

      음 애셋번들 추출은 해본적이 없어서 만족하실만한 답변은 못드릴것 같아요 ㅎㅎ

  3. 감사... 2018.10.01 20:26

    압도적 감사...!

반응형

로컬 저장소의 에셋 번들로부터 씬을 불러오는 방법

유니티(Unity)에서 씬(Scene) 역시 에셋 번들(Asset Bundle)에 포함 될 수 있다. 그 기본적인 방법은 유니티 5.6 문서의 섹션 7에 나와있다. 하지만 그 방법은 에셋 번들 매니저(Asset Bundle Manager)를 이용해야 하는 방법이고 너무 대략적인 내용이라 따라 실행하는 데에만 많은 시행 착오를 겪게 된다. 이 문서에서는 그에 비해 비교적 간단하고 즉시 활용할 수 있는 코드를 보여주고자 한다.


다음의 내용이 알려주고자 하는 코드의 전체이다 :

using System.IO;
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class AssetLoader : MonoBehaviour

{

[SerializeField]

string[] assetBundleNames;

[SerializeField]

string assetBundleDirectory = "Assets/AssetBundles";


void Start()

{

LoadSceneFromAssetBundle("불러오고자 하는 씬의 이름", false);

}


public void LoadSceneFromAssetBundle(string sceneName, bool isAdditive)

{

// 저장한 에셋 번들로부터 에셋 불러오기

// gamescene.unity는 작성자가 로컬 저장소에 저장한 게임 씬들이 들어있는 에셋 번들이다.

AssetBundle assetBundle = AssetBundle.LoadFromFile(Path.Combine(assetBundleDirectory + "/", "gamescene.unity3d"));

// 에셋 번들 내에 존재하는 씬의 경로를 모두 가져오기

string[] scenes = assetBundle.GetAllScenePaths();

string loadScenePath = null;

foreach (string sname in scenes)

{

if (sname.Contains(sceneName))

{

loadScenePath = sname;

}

}


if (loadScenePath == null) return;


LoadSceneMode loadMode;


if (isAdditive) loadMode = LoadSceneMode.Additive;

else loadMode = LoadSceneMode.Single;


SceneManager.LoadScene(loadScenePath, loadMode);

}

}


위의 코드를 활용하면 에셋 번들에 포함된 씬을 불러오는 것이 가능해진다. 다만, 게임 씬 에셋 번들이 다른 에셋 번들에 대한 종속성(Dependencies)를 가지고 있다면 씬을 불러오기 이전에 종속성을 가지고 있는 에셋 번들들을 먼저 불러와야만 한다. 만약 그렇게 하지 않으면, 불러오지 못한 오브젝트들은 게임 씬에서 missing으로 처리되어 빈 오브젝트로 나타나게 될 것이다.


예시로 보여준 코드에서는 게임 씬 에셋 번들을 불러오고 곧바로 에셋 번들 내에 존재하는 씬의 경로를 모두 탐색해서 씬을 불러왔지만 다른 방법으로는 게임이 시작되었을 때 게임 씬 에셋 번들을 불러와서 게임 씬들의 목록을 구성한 뒤에 필요할 때마다 활용하는 방식으로 사용할 수도 있다.

반응형
반응형

UnityWebRequest를 이용해서 원격 서버에서 받아온 에셋 번들을 로컬 저장소에 저장하는 방법

유니티 5.6 문서에서는 알려주지 않는 로컬 저장소 저장 방법

유니티 5.6 문서에서는 웹 서버에서 에셋 번들을 받아올 때 WWW.LoadFromCacheOrDownload 대신에 UnityWebRequest와 DownloadHandlerAssetBundle을 사용할 것을 권장하고 있다. 아래의 코드가 유니티 5.6 문서에서 보여주는 웹 서버에서 에셋 번들을 받아와서 메모리에 로드하는 예제이다.

using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class UnityWebRequestExample : MonoBehaviour
{
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();

AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);

GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");

Instantiate(cube);
Instantiate(sprite);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class UnityWebRequestExample : MonoBehaviour
{
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();

AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);

GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");

Instantiate(cube);
Instantiate(sprite);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class UnityWebRequestExample : MonoBehaviour
{
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();

AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);

GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");

Instantiate(cube);
Instantiate(sprite);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class UnityWebRequestExample : MonoBehaviour
{
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();

AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);

GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");

Instantiate(cube);
Instantiate(sprite);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]
using UnityEngine;
using UnityEngine.Networking;

public class UnityWebRequestExample : MonoBehaviour
{
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityWebRequest request = UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();

AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);

GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");

Instantiate(cube);
Instantiate(sprite);
}
}
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class UnityWebRequestExample : MonoBehaviour
{
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();

AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);

GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");

Instantiate(cube);
Instantiate(sprite);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]

서버에서 받아온 에셋 번들을 메모리에 넣어두고 사용하고자 하는 목적이 아니라 받아온 에셋 번들을 클라이언트의 로컬 저장소에 저장하고 패치하는 시스템을 만들고자할 때에는 부적절한 예제이며, 로컬 저장소에 저장하는 방법은 유니티 5.6의 문서에서는 알려주지 않는다.



에셋 번들을 로컬 저장소에 저장하는 방법

서버에서 받아온 에셋 번들을 로컬 저장소에 저장하려면 받아온 데이터를 파일 입출력을 해야했는데, 그러기 위해서는 우선 받아온 에셋 번들의 데이터, byte[] data를 찾아내고 거기에 엑세스할 수 있어야 했다. 그래서 첫 번째로 시도한 방법이 위의 예제에서 UnitWebRequest.GetAssetBundle()로 받아온  UnityWebRequest request에서 request.downloadHandler.data를 통해서 접근하는 것이었다.

using System.IO;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class AssetLoader : MonoBehaviour
{
public string[] assetBundleNames;

IEnumerator AccessFailMethod()
{

string uri = "http://127.0.0.1/character";


UnityWebRequest request = UnityWebRequest.GetAssetBundle(uri);

yield return request.Send();
string assetBundleDirectory = "Assets/AssetBundles";

if (!Directory.Exists(assetBundleDirectory))
{
Directory.CreateDirectory(assetBundleDirectory);
}

FileStream fs = new FileStream(assetBundleDirectory + "/" + "character.unity3d", System.IO.FileMode.Create);
fs.Write(request.downloadHandler.data, 0, (int)request.downloadedBytes);
fs.Close();
}
}

하지만 위의 방법은 에셋 번들에 대한 원시 데이터 접근은 지원하지 않는다는 예외를 발생시키고 실패한다. 즉, GetAssetBundle로 웹 서버에서 불러온 데이터는 데이터에 대한 직접 접근을 허용하지 않으니 파일 입출력을 통해서 로컬 저장소에 저장할 수 없다는 것이다.


그렇기 때문에 로컬 저장소에 저장하기 위해서는 웹 서버에서 에셋 번들을 받아올 때 다른 방법을 취해야 했다. 아래의 예제가 다른 방식으로 웹 서버에서 에셋 번들을 받아와서 로컬 저장소에 저장하는 예제이다 :

using System.IO;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class AssetLoader : MonoBehaviour
{

// 서버에서 받아오고자 하는 에셋 번들의 이름 목록

// 지금은 간단한 배열 형태를 사용하고 있지만 이후에는

// xml이나 json을 사용하여 현재 가지고 있는 에셋 번들의 버전을 함께 넣어주고

// 서버의 에셋 번들 버전 정보를 비교해서 받아오는 것이 좋다.
public string[] assetBundleNames;

IEnumerator SaveAssetBundleOnDisk()
{

// 에셋 번들을 받아오고자하는 서버의 주소

// 지금은 주소와 에셋 번들 이름을 함께 묶어 두었지만

// 주소 + 에셋 번들 이름 형태를 띄는 것이 좋다.
string uri = "http://127.0.0.1/character";


// 웹 서버에 요청을 생성한다.
UnityWebRequest request = UnityWebRequest.Get(uri);
yield return request.Send();


// 에셋 번들을 저장할 경로

string assetBundleDirectory = "Assets/AssetBundles";
// 에셋 번들을 저장할 경로의 폴더가 존재하지 않는다면 생성시킨다.
if (!Directory.Exists(assetBundleDirectory))
{
Directory.CreateDirectory(assetBundleDirectory);
}


// 파일 입출력을 통해 받아온 에셋을 저장하는 과정
FileStream fs = new FileStream(assetBundleDirectory + "/" + "character.unity3d", System.IO.FileMode.Create);
fs.Write(request.downloadHandler.data, 0, (int)request.downloadedBytes);
fs.Close();
}
}

위 예제에서 보다시피 UnityWebRequest.Get() API를 사용했을 경우 받아온 에셋 번들의 원시 데이터에 대한 접근이 가능해진다. 이로 인해서 파일 입출력을 통한 원격 서버에서 받아온 에셋 번들의 로컬 저장소 저장에 성공하게 되었다.



파일 입출력을 통한 저장 방법

받아온 에셋 번들을 파일 입출력을 통해 로컬 저장소에 저장하는 방법은 여러가지가 있다. 적당하다고 생각되는 방법을 골라서 사용하면 될 것이다.

// 파일 저장 방법 1
FileStream fs = new FileStream(assetBundleDirectory + "/" + "character.unity3d", System.IO.FileMode.Create);
fs.Write(request.downloadHandler.data, 0, (int)request.downloadedBytes);
fs.Close();

// 파일 저장 방법 2
File.WriteAllBytes(assetBundleDirectory + "/" + "character.unity3d", request.downloadHandler.data);
 

// 파일 저장 방법 3
for (ulong i = 0; i < request.downloadedBytes; i++)
{
fs.WriteByte(request.downloadHandler.data[i]);
// 저장 진척도 표시

}



로컬 저장소에 저장한 에셋 번들을 불러와서 사용하는 방법

위의 과정을 통해 원격 서버에서 받아온 에셋 번들을 로컬 저장소에 저장하는데 성공했다. 이 다음에 해야할 작업은 저장한 에셋 번들을 불러와서 사용하는 것이다. 그 작업은 유니티 5.6 문서에 나오는 기본 예제를 구현하는 것으로 충분히 가능하다.

using System.IO;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class AssetLoader : MonoBehaviour
{
IEnumerator LoadAssetFromLocalDisk()
{
string assetBundleDirectory = "Assets/AssetBundles";
// 저장한 에셋 번들로부터 에셋 불러오기
var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(assetBundleDirectory + "/", "character.unity3d"));
if (myLoadedAssetBundle == null)
{
Debug.Log("Failed to load AssetBundle!");
yield break;
}
else
Debug.Log("Successed to load AssetBundle!");

var prefab = myLoadedAssetBundle.LoadAsset<GameObject>("P_C0001");
Instantiate(prefab, Vector3.zero, Quaternion.identity);
}
}


반응형
  1. blueasa 2017.05.10 20:15 신고

    좋은 정보 감사합니다. :)

    • wergia 2017.05.10 21:48 신고

      방문 감사드립니다. 올린 글이 도움이 되셨으면 좋겠네요 ^^

  2. Vector Space 2017.05.18 14:49 신고

    UnityWebRequest에서도 버전 번호나 Hash128을 입력하면 LoadFromCacheOrDownload와 동일하게 캐싱 영역에 저장이 되어
    한번 다운로드되면 다음에는 캐싱된 것을 읽습니다. 굳이 로컬저장소에 별도로 저장하시는 이유가 있나요?

    • wergia 2017.05.18 17:07 신고

      받아온 애셋번들을 일정한 경로에 저장해두고 서버와 클라이언트의 애셋번들이 버전이 일치하는지 확인하고 일치한다면 해당 애셋번들을 로드하고 일치하지 않는다면 새롭게 애셋번들을 받아서 로드하기 위해서 로컬저장소에 저장했습니다.

      제 짧은 지식으로는 UnityWebRequest를 통해서 웹 서버에서 에셋 번들을 받아오면 메모리에만 캐싱되고 실제 로컬 저장소에는 저장되지 않는 것으로 알고 있어서 매번 게임을 실행하게 될때마다 에셋번들들을 서버에서 받아와야하는 것으로 알고 있습니다. 소규모의 작은 애셋번들만 받아오면 되는 게임의 경우에는 문제가 없겠으나 대용량의 애셋번들을 사용하는 게임에 경우에는 애셋번들들을 로컬저장소에 저장해두고 버전이 바뀌는 번들들만 패치형식으로 바꾸어서 사용하기 위해서 이러한 방식을 사용했습니다.

  3. prhymery 2018.05.31 15:39

    올려주신 글 덕분에 큰 도움이 되었습니다.
    질문이 하나 있습니다.
    위에 설명해주신 방법으로
    에셋번들 빌드, 에셋번들 다운까지는 잘 됩니다.
    에셋번들 업로드는 구글 드라이브에 올렸고
    다운로드는 파일 공유주소를 이용해서 받았습니다.
    여기까지는 문제없이 되는데
    에셋번들을 로드하는 과정에서 에러가 자꾸 발생합니다.
    var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(assetBundleDirectory + "/", "character.unity3d"));
    위의 라인에서
    Unable to read header from archive file:
    위의 에러가 발생합니다.
    혹시 원인을 아신다면 알려주시면 감사하겠습니다.

    ps. 이게 원인인지는 모르겠으나 구글드라이브 웹사이트를 이용하여 받은 파일과 유니티 에디터를 이용해서 받은 파일의 용량 차이가 납니다.
    유니티 에디터에서 다운받아서 로드하면 에러가 나지만, 웹사이트를 이용하여 해당 폴더에 넣어서 로드하게되면 또 잘 됩니다.

    • wergia 2018.06.01 14:22 신고

      음 파일 크기가 바뀌어서 발생한 문제가 맞는것 같습니다. 에디터에서 구글 드라이브에서 다운받을때 어떤 방식을 사용하나요?

      구글에서 제공하는 api를 사용했다면 파일 받으면서 구글 api 쪽에서 덧붙인 헤더 같은게 파일 데이터에 들어가서 그렇게 된 것으로 추정됩니다.

    • prhymery 2018.06.01 15:30

      친절한 답변 감사합니다.
      구글 드라이브가 문제였습니다.
      다른 웹 호스팅을 이용해서 하니까 잘 되네요
      감사합니다.

  4. 코드농사 2018.06.21 16:07

    안녕하세요~

    덕분에 많은 걸 배우고 갑니다.

    마지막에 궁금한게 생겼습니다.

    에셋번들목록을 json, xml로 만든다고 하셨는데

    manifest파일로 번들목록을 대신 할 수 있지않을까요?(manifest.GetAllAssetBundles())

    만약 패치부분이 문제라면 내부에 hash정보도 있으므로 서버manifest파일과 클라캐쉬된manifest파일의 해쉬값비교로 패치하는 시스템을 구현 할 수 있을 것같은데

    혹시 다른 이유라도 있을까요??

    감사합니다.

    • wergia 2018.07.18 10:34 신고

      네 충분히 매니페스트 파일에 들어있는 CRC나 해시 값을 비교해서도 충분히 패치 시스템을 구현할 수 있습니다.

  5. 데브 2018.07.23 22:23

    안녕하세요

    좋은 글 감사합니다.

    혹시 저장 단계에서 프리팹이나 게임오브젝트로도 가능한지 여쭙고자 이렇게 글 남겨봅니다.

    혹시 가능하다면 방법이나 키워드를 부탁드려도 될까요?

    더운 여름 건강 조심하시고 즐거운 하루하루 되세요 ^^

    • wergia 2018.07.24 13:25 신고

      저장단계에서 프리팹니아 게임 오브젝트로 저장한다는게 애셋번들에서 프리팹이나 게임오브젝트로 추출해서 로컬에 저장한다는 뜻인가요?

      그런 방법은 생각해본적이 없어서 그런지, 잘 모르겠네요.

      도움을 못드려서 죄송합니다.

    • 데브 2018.07.25 16:49

      말씀 해주신 부분이 맞습니다 ^^

      제가 유별나게 생각을해서 고민스럽게 만들어 드렸네요 ㅠㅠ

      남겨주신 글들로 어셋번들 잘 구현하고 있습니다.

      감사합니다 ^^

  6. redblacktree 2018.07.26 15:02

    혹시 에셋번들을 로컬에 저장한뒤에 삭제하는 방법이 따로 존재하나요?

    • wergia 2018.08.20 16:23 신고

      일반 파일 삭제하듯이 삭제하는 방법도 있고 덮어쓰기도 가능합니다.

  7. get이 없음 2020.04.12 17:37

    1년 가까이 된거라서 그런가용 ㅠ..

    UnityWebRequest request = UnityWebRequest.Get(uri);
    yield return request.Send();

    위 구문에서 .Get() 쪽을 읽어오질 못하네요.. unity 에서 unet를 지원하지않는다고 해서 그런건가요?

    • wergia 2020.04.13 11:12 신고

      2019 버전에서도 UnityWebRequest.Get() 함수는 지원되고 있습니다.
      WebRequest부분은 UNET 지원과 상관없는 부분이라 영향이 없을 겁니다.
      아마 Get을 요청하고자 하는 URL에 해당 API가 지원되고 있는지를 알아봐야 할 것 같습니다.
      이 부분은 웹 서버랑 연관된 부분이라 변수가 많네요.

반응형

섹션 9 : 일반적인 함정

이번 섹션에서는 프로젝트에서 에셋 번들을 사용할 때 일반적으로 발생하는 몇 가지 문제에 대해서 설명할 것이다.


에셋 복제(Asset Duplication) - 유니티 5의 에셋 번들 시스템은 오브젝트가 에셋 번들에 내장 되었을 때 객체의 모든 종속성을 발견하게 된다. 이것은 에셋 데이터베이스를 통해서 수행된다. 이 종속성 정보는 에셋 번들에 포함된 오브젝트 집합을 결정하는데 사용된다.


에셋 번들에 명시적으로 등록된 객체는 해당 에셋 번들에만 등록된다. 오브젝트의 에셋 임포터의 assetBundleName 속성이 비어있지 않은 문자열로 설정된 경우에만 객체가 "명시적으로 등록"되는 것이다.


에셋 번들에 명시적으로 등록되지 않은 오브젝트는 이 것을 참조하는 오브젝트가 포함된 모든 에셋 번들에 등록된다.


두 개의 서로 다른 오브젝트가 서로 다른 에셋 번들에 등록되어 있는데, 둘 다 하나의 객체를 참조하고 있다면 그 객체는 두 에셋 번들에 모두 복사된다. 이렇게 중복된 종속성 역시 인스턴스화 되는데, 이러한 인스턴스는 하나의 객체가 아니라 달라진 식별자로 인해 서로 다른 객체로 간주된다. 이로 인해서 응용 프로그램의 에셋 번들의 전체 크기가 증가하게 된다. 또한 응용 프로그램에서 부모를 모두 불러오는 경우 두 개의 오브젝트 사본이 메모리에 불러와진다.


이 문제를 해결할 수 있는 몇 가지 방법이 있는데 그것은 다음과 같다 :

1. 서로 다른 에셋 번들에 등록된 오브젝트들이 종속성을 공유하지 않도록해야 한다. 종속성을 공유하는 모든 오브젝트는 동일한 에셋 번들에 배치함으로써 종속성의 복제를 배제할 수 있다.

- 이 방법은 일반적으로 공유 종속성이 많은 프로젝트에서는 사용하기 부적절하다. 편리하고 효율적으로 만들기 위해서 과도하게 자주 다시 빌드하고 다시 다운로드해야 하는 한덩어리의 에셋 번들이 생성될 것이다.

2. 종속성을 공유하는 2개의 에셋 번들이 동시에 불러와지지 않도록 에셋 번들을 세그먼트화해야 한다.

- 이 방법은 레벨 기반 게임과 같은 특정 유형의 프로젝트에 적용할 수 있다. 하지만 여전히 프로젝트의 에셋 번들의 크기가 불필요하게 증가하고 빌드 시간과 로딩 시간이 증가하는 문제가 있다.

3. 모든 종속성 에셋이 자체 에셋 번들에 빌드되었는지 확인해야 한다. 이 방식은 에셋의 복제 위험을 완전히 제거하지만 복잡성 문제를 발생시킨다. 이것을 위해서 응용 프로그램은 에셋 번들 간의 종속성을 추적하고 AssetBundle.LoadAsset API를 호출하기 전에 올바른 에셋 번들이 불러와졌는지 확인해야 한다.


유니티 5에서, 오브젝트 의존성은 UnityEditor 네임 스페이스의 AssetDatabase API를 통해서 추적된다. 네임 스페이스의 이름이 의미하듯이 이 API는 유니티 에디터 상에서만 사용할 수 있으며 런타임 시에는 사용할 수 없다. AssetDatabase.GetDependencies는 특정 오브젝트나 에셋의 모든 즉각적인 종속성을 찾는데 사용할 수 있다. 참고로 이러한 종속성에는 자체 종속성이 있을 수 있다. 추가적으로 AssetImporter API를 사용하여 특정 오브젝트가 등록되어 있는 에셋 번들을 쿼리할 수 있다.


AssetDatabase와 AssetImporter API를 결합하여 모든 에셋 번들의 직접 또는 간접 종속성이 에셋 번들에 등록되도록 하거나, 두 개의 에셋 번들이 에셋 번들에 등록되지 않은 종속성을 공유하지 않도록하는 에디터 스크립트를 작성할 수 있다. 에셋을 복제하는데 소모되는 메모리 비용을 생각하면 모든 프로젝트에 이러한 스크립트가 있는 것이 좋다.


스프라이트 아틀라스 복제(Sprite Atlas Duplication) - 이 섹션에서는 자동으로 생성된 스프라이트 아틀라스와 함께 사용될 때 발생하는 유니티 5의 에셋 종속성 계산 코드의 단점에 대해서 설명한다.


자동으로 생성된 스프라이트 아틀라스느느 스프라이트 아틀라스가 생성된 스프라이트 오브젝트를 포함하는 에셋 번들에 등록된다. 만약 스프라이트 오브젝트가 여러 에셋 번들에 등록되어 있다면 스프라이트 아틀라스는 하나의 에셋 번들에 등록되지 않고 복제된다. 스프라이트 오브젝트가 에셋 번들에 등록되지 않은 경우라면 스프라이트 아틀라스는 에셋 번들에도 등록되지 않는다.


스프라이트 아틀라스가 중복되지 않도록 하려면 동일한 스프라이트 아틀라스에 태그가 지정된 모든 스프라이트가 하나의 에셋 번들에 등록되어 있는지 확인해야 한다.


유니티 5.2.2p3 이전 버전

자동으로 생성된 스프라이트 아틀라스는 에셋 번들에 등록되지 않는다. 그렇기 때문에 구성 스프라이트를 포함하는 모든 에셋 번들 및 해당 구성 스프라이트를 참조하는 모든 에셋 번들에 포함된다. 이 문제 때문에 유니티 스프라이트 패커를 사용하는 모든 유니티 5 프로젝트에 대해서 5.2.2p4, 5.3 또는 최신 버전의 유니티로 업드레이드하는 것을 강력히 권장한다.


만약 업그레이드를 할 수 없는 상황에 놓인 프로젝트의 경우에 이 문제에 대한 두 가지 해결 방법이 있다.

1. 쉬운 방법 : 유니티의 내장 스프라이트 패커의 사용을 피할 것. 외부 도구에 의해 생성된 스프라이트 아틀라스는 정상적인 에셋이 될 것이고, 에셋 번들에 적절하게 등록될 수 있다.

2. 어려운 방법 : 자동으로 아틀라스화된 스프라이트를 사용하는 모든 오브젝트를 스프라이트와 동일한 에셋 번들에 등록하라.

- 이렇게 하면 생성도니 스프라이트 아틀라스가 다른 에셋 번들의 간접적인 종속성으로 보이지 않으며 복제되지 않게 된다.

- 이 해결책은 유니티의 스프라이트 패커를 사용하는 워크 플로우를 보존하지만 에셋을 다른 에셋 번들로 분리하는 개발자의 능력을 저하시키며, 아틀라스를 참조하는 컴포넌트에서 데이터가 변경될 때마다 전체 스프라이트 어트리뷰트의 재 다운로드를 강제한다. 아틀라스 자체는 변경되지 않는다.


안드로이드 텍스처(Android Texture) - 안드로이드 환경은 기기 별로 매우 세분화되어 있기 때문에, 텍스처를 여러 가지 형식으로 압축해야하는 경우가 있다. 모든 안드로이드 기기가 ETC1을 지원하지만 ETC1은 알파 채널이 있는 텍스처를 지원하지 않는다. 만약 응용 프로그램이 OpenGL ES2 지원이 필요하지 않다면, 이 문제를 해결하는 가장 깔끔한 방법은 모든 안드로이드 OpenGL ES3 장치에서 지원되는 ETC2를 사용하는 것이다.


대부분의 응용 프로그램은 ETC2 지원을 사용할 수 없는 구형 기기에서 제공되어야 한다. 이 문제를 해결하는 한 가지 방법은 유니티 5의 에셋 번들 Variants를 사용하는 것이다(다른 옵션에 대한 자세한 내용은 유니티 안드로이드 최적화 가이드를 참조).


에셋 번들 Variants를 사용하려면 ETC1을 사용하여 깨끗하게 압축할 수 없는 모든 텍스처를 텍스처 전용 에셋 번들로 분리해야 한다. 그 다음 DXT5, PVRTC, ATITC와 같은 공급 업체별 텍스처 압축 형식을 사용하여 안드로이드 환경 별 비-ETC-가능 슬라이스를 지원하기 위해 이러한 에셋 번들의 충분한 variants를 만들어야 한다. 각 에셋 번들 Variants에 대해 포함된 텍스처의 텍스처 임포터 설정을 Variants에 적합한 압축 포맷으로 변경한다.


런타임 시에, SystemInfo.SupportsTextureFormat API를 사용하여 다양한 텍스처 압축 형식에 대한 지원을 감지할 수 있다. 이 정보는 지우너되는 형식으로 압축된 텍스처가 포함된 에셋 번들 Variants를 선택하고 불러오는데 사용해야 한다.


안드로이드 텍스처 압축 형식에 대한 자세한 내용은 이곳(here)을 참조하라.


iOS 파일 처리 과용(iOS File Handle Overuse) - 이 섹션에서 설명하는 문제는 유니티 5.3.2p2에서 수정되었다. 현재 버전의 유니티는 이 문제의 영향을 받지 않는다.


유니티 5.3.2p2 이전 버전에서는 유니티가 에셋 번들이 불러와지는 전체 시간동안 에셋 번들에 대한 열린 파일 핸들을 보유한다. 이것은 사실 대부분의 플랫폼에서는 문제가 되지 않지만, iOS는 프로세스가 동시에 열 수 있는 파일의 핸들 수를 255개까지만 제한하기 때문에 이 한도를 초과하여 에셋 번들을 불러오면 로딩 호출이 "Too Many Open File Handles" 오류와 함께 실패한다.


이것은 수백 또는 수천개의 에셋 번들에서 콘텐츠를 나누려는 프로젝트에서 발생하는 일반적인 문제였다.


새로운 버전의 유니티로 업그레이드할 수 없는 프로젝트에 대한 임시 해결책은 다음과 같다.

1. 관련된 에셋 번들을 합쳐서 사용중인 에셋 번들의 수를 줄인다.

2. AssetBundle.Unload(false)를 사용하여 에셋 번들의 파일 핸들을 닫고 로드된 오브젝트의 라이프 사이클을 수동으로 관리한다.


반응형

+ Recent posts