이 버전의 글은 약간의 오류를 포함하고 있으며 이를 수정한 최신 버전의 글을 링크를 통해서 보실 수 있습니다.
개요(Overview)
게임을 제작할 때, 애니메이션이 실행되는 도중에 적절한 타이밍에 맞춰서 무언가가 실행되어야 하는 경우가 종종 발생한다. 예를 들자면 대표적으로는 캐릭터가 걷거나 뛸 때 발소리가 나거나 먼지가 일어나야 한다던지 캐릭터가 무기를 휘두를 때 맞는 타이밍에 맞춰서 데미지가 들어가야 한다던지 하는 경우가 있을 수 있다. 이러한 문제를 해결하는 하드한 방법으로는 계산해낸 타이밍에 맞춰서 해당 함수를 실행시킨다던지, 콜라이더를 원하는 위치에 붙여서 그 콜라이더가 충돌했을때 처리하는 방법이 있지만, 이러한 방법들은 매우 비효율적이다.
계산해낸 타이밍에 맞춰서 원하는 함수를 실행하는 방법의 경우에는 공격 속도나 이동 속도에 맞춰서 모션의 속도가 변하는 유동적인 상황에 대처하기가 매우 어렵고, 특히 콜라이더를 사용하는 방법은 겨우 발소리나 발움직임에 맞춰 먼지 이펙트를 만드는데 쓰기에는 매우 비용이 비싸다. 공격 애니메이션에 맞춰서 데미지를 주는 경우에는 무기나 데미지를 주는 부위에 콜라이더를 붙여서 사용하는 것을 고려해볼만 하지만, 이러한 방법은 주로 공격의 명중이 확정되지 않은 게임, 즉 논타겟(None-Target) 방식의 게임에 적합한 방법이며, 공격의 명중이 확정되어있는 게임의 경우에는 역시 사용하기가 곤란하다.
애니메이션 이벤트(Animation event)
그래서 존재하는 것이 바로 유니티의 애니메이션 이벤트(Animation event)라는 기능이다. 이 기능은 애니메이션의 실행 도중, 원하는 시점에 원하는 함수를 호출할 수 있게 해준다. 이 문장 하나만으로도 개요에서 설명한 상황에 전부 대처가 가능할 것임을 알 수 있다.
이 애니메이션 이벤트를 사용하는 방법은 2가지가 있는데 FBX 모델과 함께 임포트된 애니메이션에서 사용하는 방법과 애니메이션 클립(Animation clip)에서 사용하는 방법이다.
방법 1 : FBX 모델과 함께 임포트된 애니메이션에서 애니메이션 이벤트 사용하기
프로젝트 창(Project view)에서 임포트한 FBX파일을 선택하면 Inspector 창에 그 FBX파일의 정보들이 보일 것이다. 그 중에서 상단의 Animations라는 탭을 선택하면 그 FBX에 포함된 애니메이션의 정보를 볼 수 있다. 우리가 필요로 하는 애니메이션 이벤트 기능은 그 중에서 Events라는 곳 안에서 사용할 수 있다. 그 항목을 열면 다음과 같은 내용을 볼 수 있다 :
노란색으로 표시된 버튼을 누르면 다음과 같이 애니메이션이 재생되는 도중에 실행될 이벤트가 새로 생성된다 :
위의 이미지에서 파란색으로 표시된 것을 드래그하면 이 이벤트가 실행될 시점을 조절할 수 있으며 그 아래의 입력 필드의 내용들은 다음과 같다 :
필드 이름
내용
Function
이벤트 시점에 실행될 함수의 이름
Float
함수에 float형 매개변수가 있을 때 들어갈 값
Int
함수에 int형 매개변수가 있을 때 들어갈 값
String
함수에 string형 매개변수가 있을 때 들어갈 값
Object
실행할 함수가 들어있는 스크립트
이번 예시로는 공격 애니메이션의 타이밍에 맞춰 데미지를 주는 것으로 진행 해보겠다. 먼저 적에게 데미지를 주는 작업을 할 스크립트를 생성한다.
예제이기 때문에 자세한 과정에 대한 코드를 생략하고 AttackComponent 스크립트는 다음과 같은 함수를 가지고 있다.
using UnityEngine;
public class AttackComponent : MonoBehaviour
{
public void AttackDamage()
{
// 적에게 데미지를 주는 처리
Debug.Log("데미지 60!");
}
}
공격 애니메이션에서 적에게 무기가 맞는 시점이 위의 Events 이미지에서 0:050라고 가정하자. 다음과 같이 이전에 생성한 이벤트를 0:050 지점으로 옮기고 Object에 생성한 AttackComponent 스크립트를 넣어주고 Function에는 AttackDamage 함수 이름을 입력해주면 된다.
다음과 같은 과정을 끝내고 나면 애니메이션이 실행되고 0:050 지점을 지날 때마다 유니티 에디터에서 "데미지 60!"이라는 로그가 출력될 것이다.
방법 2 : 애니메이션 클립에서 애니메이션 이벤트 사용하기
애니메이션 클립은 3D 모델 없이 애니메이션에 대한 정보만을 가지고 있는 것을 이야기한다. 물론 임포트한 FBX 모델 파일에서 애니메이션만 꺼내와서 사용하는 것도 가능하다. 이러한 애니메이션 클립에서 애니메이션 이벤트를 사용하는 방법은 이전에 소개한 FBX 모델에 포함된 애니메이션에서 사용하는 방법과 상당히 비슷하다.
프로젝트 창에서 애니메이션클립을 선택하고 애니메이션 탭을 열면 다음과 같은 화면을 볼 수 있다.(만약 애니메이션 탭이 보이지 않는다면 상단의 Windows 메뉴에서 Animation을 찾아서 열거나 Ctrl+6 단축키를 이용해서 열 수 있다)
애니메이션 탭에서도 FBX 파일 애니메이션의 이벤트에서 봤던 것과 같은 모양의 버튼을 볼 수 있는데 이것을 통해서 이벤트를 생성할 수 있고 드래그를 통해서 이벤트의 시점을 이동시킬 수도 있다.
그리고 이 이벤트를 선택하면 Inspector 창에서 다음과 같은 내용을 볼 수 있다 :
이 이후에는 방법 1에서 진행한 것과 같이 진행하면 원하는 결과를 얻을 수 있게 된다.
마무리...
이 애니메이션 이벤트를 사용할 때, 프레임이 씹힐 경우에 애니메이션 이벤트가 같이 스킵되는 경우가 발생한다는 이야기도 있다. 다른 이야기로는 이 문제는 레거시(Legacy) 애니메이션을 사용할 때 주로 발생하며, 메카님에서는 개발자의 실수로 트랜지션 구간을 잘못 설정했을 때 발생하는 문제이며 일반적으로는 발생하지 않는 문제라고도 한다. 이런 문제가 발생하면 애니메이션의 트랜지션에서 블랜드되는 과정을 잘 확인해보는 것이 우선일 것 같다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
게임을 제작할 때 개발자는 애니메이션의 속도에 관여해야하는 경우가 자주 발생한다. 대표적인 예시를 들자면 캐릭터의 공격 속도 수치가 변할 수 있는 게임에서 캐릭터의 공격 속도와 공격 애니메이션의 속도를 맞추기 위해 관여하는 것이다. 이러한 두 속도를 세심하게 맞추는데 실패한다면 캐릭터의 공격 애니메이션이 끝나지 않았는데 캐릭터가 이동을 한다던지, 공격 애니메이션과 데미지가 들어가는 타이밍이 다르다던지 하는 상당히 부자연스러운 상황이 발생하게 될 것이다. 이러한 상황은 게임의 타격감이라는 요소를 떨어뜨리는 심각한 문제가 될 수도 있다.
그렇게 때문에 적절한 순간에 애니메이션의 속도를 조절하는 방법을 알아둘 필요가 있다. 유니티에서는 애니메이션의 속도를 조절하는 방법을 여러가지 지원하는데 하나씩 알아보도록 하자.
방법 1 : Animator에서 선택한 State의 Speed를 Inspector 창에서 애니메이션 속도 직접 변경하기
위의 그림에서 표시된 Speed 값을 직접 변경하는 것으로 애니메이션의 속도를 변경할 수 있다. 하지만 이 방법은 게임 내에서 애니메이션의 속도가 동적으로 변경되지 않을 것이라는 보장이 있을 때에만 사용이 가능하다.
방법 2 : Animator를 소유한 스크립트에서 코드를 통해 애니메이션 속도 변경하기
다음은 스크립트에서 코드로 애니메이션의 속도를 변경하는 예제이다.
using UnityEngine;
public class AnimationSpeedExample : MonoBehaviour
{
public float animSpeed = 1.0f;
public Animator animator;
// Update is called once per frame
void Update ()
{
animator.speed = animSpeed;
}
}
위의 코드를 이용하면 animSpeed라는 변수의 값이 변경될 때마다 Animator 내에 있는 애니메이션들의 속도가 자동으로 변하게 될 것이다. 이 방법은 자유롭게 게임이 실행되는 와중에 애니메이션의 속도를 변경할 수 있지만, animator.speed를 통해서 애니메이션의 속도를 조절하면 해당 animator에 속하는 모든 애니메이션의 속도가 변한다는 단점이 있다. 예를 들자면, 공격 모션의 속도를 빠르게 하기 위해 speed를 1에서 2로 바꾸었는데, 공격 모션 뿐만 아니라, 이동 모션, 대기 모션 등 모든 모션의 속도가 2배 빨라지는 현상이 나타날 수 있다는 것이다. 그렇기 때문에 이 방법은 애니메이션이 블랜드되는 상황에서는 사용하기 부적합하며, 속도를 빠르게 한 애니메이션의 출력이 끝난 이후에는 다음 애니메이션의 정상적인 출력을 위해서 속도 값은 원상복귀시켜 주어야 한다는 단점이 존재한다.
방법 3 : Animator의 Parameter와 Multiplier를 이용한 애니메이션 속도 변경하기
공격 속도가 증가했는데 공격 애니메이션 이외의 애니메이션의 속도가 빨라지는 것은 부적절하며, 뛰어가면서 총을 쏜다던가 하는 방법의 애니메이션 블랜드에서는 모든 애니메이션의 속도가 변경되면 좋지 않다는 것을 앞선 방법에서 이야기했다. 그렇다면 우리가 알아야 할 방법은 속도의 변경이 필요하지 않은 애니메이션은 놔두고, 필요한 몇몇의 애니메이션의 속도만 조절하는 것이다.
이 방법을 위해서 필요한 것은 Animator의 Parameter와 Multiplier라는 것이다.
Animator의 parameter
Animator에서 State를 선택했을 때, Inspector 창에서 찾을 수 있는 Multiplier
이 방법을 적용하는 순서는 다음과 같다.
1. Animator의 Parameter에서 +를 눌러서 float형의 "AttackSpeed"라는 parameter를 만들어 준다.
2. 속도를 조절하고자 하는 State를 선택하고 Multiplier의 Parameter를 체크하고 추가한 AttackSpeed를 선택한다.
3. 속도 변경을 위해 animator의 "AttackSpeed" parameter를 변경하는 코드를 작성한다.
using UnityEngine;
public class AnimationSpeedExample : MonoBehaviour
{
public float animSpeed = 1.0f;
public Animator animator;
// Update is called once per frame
void Update ()
{
animator.SetFloat("AttackSpeed", animSpeed);
}
}
위의 과정을 끝내고나면 Animator.SetFloat()을 통해 원하는 parameter의 값을 조정해 주는 것만으로 원하는 애니메이션의 속도를 조절할 수 있게 된다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
과거에 프로그래밍 관련 번역에 대한 불만들 토로하는 글을 쓴 적이 있다(프로그래밍 관련 번역에 대한 사설). 번역된 단어에 제대로 된 뜻이 담기지 않았다는 불만이었다.
나는 최근 며칠 간 유니티 5.6의 AssetBundle 초안 문서를 번역하는 작업을 했었다. 거기에 등장하는 용어 중에 배리언트(Variants, 변형)이라는 단어가 있는데, 유니티의 에셋 번들에서는 에셋 번들 배리언트로 하나의 오브젝트에 대해서 에셋 번들을 만들 때 사용하고자 하는 방식이나 버전에 따라서 에셋 번들을 다르게 만드는 것을 의미하는 것이었다.
예를 들어 하나의 오브젝트가 있는데 이것이 작동하는 방식이 iOS와 안드로이드가 달라서 에셋 번들을 다르게 빌드해야한다던지, 두 장의 텍스쳐를 하나는 고해상도 배리언트로 묶고 하나는 저해상도 배리언트로 묶는다던지 하는 방식으로 사용하는 것이다.
하지만 나는 이 단어의 의미나 용도에 대해서 깊게 고민하지 않았고 그 뜻이 명확하게 드러나 보이지 않는, 단순한 사전의 뜻 그대로 "변형"이라는 단어를 사용해서 번역하는 실수를 저지르고 말았다.
Direct3D에서 사용되는 서피스라는 단어의 표면적인 뜻에서 진짜 뜻을 찾아 헤매던 것을 고생을 잊은 것이다.
번역 작업을 할 때는 기계적으로 번역하지 말자.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
나는 과거에 같은 학교를 다니는 후배들에게 프로그래밍 멘토링을 진행한 적이 있다. 물론 지금도 그렇게는 뛰어나지 않고 그 당시에도 뛰어나지 않은게 내 프로그래밍 실력이었지만 적어도 막 입문한 친구들에게 가르쳐줄 정도는 되었던 것 같다.
뭐, 중요한 점은 그 부분은 아니고, 내가 그때 가르쳤던 친구들이나 혹은 내 주변 사람들이 가르치던 사람들이 공통적으로 하던 이야기가 있는데, 그것은 바로 "머리 속으로는 어떻게 만들어야 할지 알겠는데, 막상 코드로 옮기려고 하니 너무 어렵다." 라는 것이었다. 물론 이런 이야기 없이 그냥 쭉쭉 잘해 나가던 친구들도 있었다. 자랑은 아니지만 나 역시 저런 생각을 해본 적이 없는 편에 속해서, "아니 이게 무슨 말인가?" 싶었다. 어떻게 만들어야 할지 알았으면, 그냥 코드로 만들면 되는거지, 왜 코드로 옮기려고 하면 그게 안된다는 것인지 그 당시에는 도무지 이해가 되지 않았다.
하지만 내가 프로그래밍을 공부한 과정을 곰곰히 되새겨본 결과, 프로그래밍을 배우기 시작한 지 얼마 되지 않았을 때의 나 역시 같은 문제에 부딪힌 적이 있다는 것을 깨달을 수 있었다. 그것을 깨닫고 난 이후에 내가 다시 생각하게 된 것은 "머리 속으로는 어떻게 만들어야 할지 알겠는데, 막상 코드로 옮기려고 하니 너무 어렵다."라는 문제에는 누구나 부딪히게 된다는 것이고, 진짜로 그 시점에서 중요한 문제는 "코드로 옮기려고 하니 어렵다."가 얼마나 어려우냐보다 "그 상태에서 얼마나 빨리 벗어나느냐"라는 것이다.
누군가는 "머리 속으로는 어떻게 만들어야 할지 알겠는데, 막상 코드로 옮기려고 하니 너무 어렵다." 라는 말을 일주일 정도 하더니 더 이상 그런 말을 하지 않고 프로그래밍을 하게 되고, 누군가는 그 말을 1년도 더 넘게 하고 있다면, 그 차이는 무엇에 있는 것일까?
난 그 차이가 숙달과 경험, 그리고 연습량에 달려있다고 본다.
간단한 예를 들자면, 한 번도 요리를 해본 적도 없는 사람에게 마파두부를 만들고 시켰다고 해보자. 그 사람의 머리 속에는 자신이 평소에 먹던 마파두부가 떠오를 것이다. 하지만 막상 요리를 진행하면 그 사람은 무엇부터 해야하는지 모르고 우왕좌왕하게 될 것이다.
그러면 그 다음엔 요리를 해본적 없는 사람에게 마파두부를 만드는 법을 알려주고 만들라고 시켜보자. 과연 그 사람은 완벽하게 마파두부를 만들어낼 확률이 높을까? 아니다. 분명 상당한 시간이 걸리고도 제대로 된 결과물을 만들어 내지 못할 확률이 높을 것이다. 칼질은 서투르고, 재료도 볶다가 태울지도 모른다. 그리고 레시피를 이미 알고 있지만, 그것을 제대로 따라하지 못하고 버벅대는 경우가 많을 것이다. 그렇다면 이 사람이 마파두부를 잘 만들기 위해서는 어떻게 해야 할까? 답은 하나 뿐이다. 마파두부를 계속 만들어 보는것.
나는 프로그래밍도 이것과 마찬가지라고 생각한다. 머리 속에 만들어진 논리를 코드로 바꾸는 능력을 많은 프로그래밍을 통해 숙달시키는것과 머리 속에 들어있는 마파두부 만드는 법을 많은 연습을 통해 익히는 것. 무엇이 다른가?
앞서서 나는 이 과정을 상당히 빨리 벗어났다고 이야기 했었다. 나는 한때 내 꿈이 무엇인지를 놓고 방황하다가 24의 나이가 되어서야 프로그래밍에 입문했다. 게임을 만들기 위해서. 그리고 내가 게임 프로그래밍을 배울 수 있는 학교에 왔을 때, 내 주변의 친구들은 아직 20살이었고, 군대를 다녀온 시간 2년을 그 친구들보다 앞서있다고 생각해도 2년은 뒤쳐져서 시작하는 것이었다. 그러니 당연히 이렇게 생각할 수 밖에 없었다. 남들보다 더 많이 코딩을 하자. 분명 이 프로그래밍 책의 뒤에 예제의 해답이 있는 건 알지만, 나만의 답, 내가 해결하기 전에는 보지 말자. 책의 해답이 더 나은 것이라면 익히고, 내 답이 더 나은 것이라면 뿌듯함을 느끼면 된다. 그렇게 공부를 하니 남들이 "코드로 옮기는 것이 어렵다"는 말에 공감하기 어려워질 정도로 그 단계를 빨리 지나가 버렸다.
연습량이 적은 데에는 3가지 이유가 있을 수 있다.
1. 정말 게을러서.
2. 너무 완벽을 추구해서.
3. 겁이 많아서.
1번은 절실함을 느끼지 않는 한 답이 없다.
2번은 완벽한 코드를 만들어 내야한다는 압박감에 시달려서, 혹은 코드가 작동하는 원리의 깊은 곳까지 알고싶어서, 코드를 이리 뒤지고 저리 뒤지다 한 세월이 걸려서 연습하는 속도도 느려지고 연습량이 적어진다. 물론 이것은 좋은 자세임에는 틀림이 없다. 하지만 지금의 문제를 느끼는 단계라면 너무 과한 태도라고 생각된다. 지금의 단계는 분명 빨리 지나가도 되는 단계일 것이다. 지금 이 단계를 넘어가면 깊이를 알 수 없고 드넓은 바다가 있는데 작은 물웅덩이 속에서 웅덩이 속을 떠다니는 모래 알갱이를 세고 있는 꼴이다. 지금 단계가 그렇게까지 완벽하게 파야되는 단계인지 그렇게까지 완전해야 하는 단계인지 잘 생각해보라.
3번이 가장 일반적으로 많은 케이스라고 생각된다. 코드를 치기 전에도 버그가 나면 어떻게 하지? 하고 시작조차 못하는 혹은 코드 한줄한줄마다 이게 틀렸으면 어떻게 하지? 하는 부류이다. 내 친구의 프로그래밍 과외를 받는 어떤 사람은 그랬다. "이런 것을 만들어보라"고 시키면 마우스를 이리저리 움직여보다 코드 한 줄을 겨우 치고는 묻는 것이다. "이렇게 하는게 맞나요?"하고. 그리고 그 질문은 한줄한줄 계속 된다.
나는 이런 겁 많은 사람들에게 일단 버그가 있든 없든 알 수 없지만 신텍스 에러(Syntax error, 문법 오류)만 없게 주어진 문제를 완성하라고 말하고 싶다. 그리고 컴파일해보면 알게 될 것이다. 버그가 있는지 없는지. 만약 버그가 있다면 버그를 수정하면 되는 것이고 없다면 좋은 것이다. 물론 처음부터 버그가 없게 설계를 한다면 좋겠지만 아무리 좋은 설계라고 해도 버그가 아예 없을 확률은 낮으니, 완성해서 컴파일 해보는 것이 중요한 것이다. 그래야 당신은 프로그래밍 연습을 한 것이고, 공부를 한 것이다.
그러니 연습을 해라. 코딩을 해라. 프로그래밍을 해라.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
요 며칠간 패치 시스템을 만들면서 아주 많이 고민해야 했던 문제가 있었는데 그것이 바로 AssetBundleManifest 오브젝트를 불러오는 방법이었다. 여러가지 방법을 이용해서 에셋 번들의 매니페스트 파일을 불러오는 것을 시도했으나 번번히 AssetBundleManifest 객체는 Null 값을 뱉으며 실패하는 경험을 했다. 이를 해결하기 위해서 구글링해서 발견한 코드도 적용해보고, AssetBundleManager의 코드도 참고해 봤지만 문제는 해결될 기미가 보이지 않았다.
제일 처음 기본적으로 참고한 유니티 5.6 문서에서는 AssetBundleManifest 객체를 불러오기 위해서 다음과 같은 코드를 사용하라고 섹션 6에서 언급하고 있다 :
경험해본 바에 의하면 저 코드를 이용해서 매니페스트 파일을 불러올 수 있는 것은 사실이다. 하지만 매니페스트 파일을 사용하기 위해 처음으로 유니티 5.6 문서를 본 개발자는 저 manifsetFilePath에 대해서 정확하게 어떤 경로를 의미하는지에 대한 난감함을 느끼게 될 것이다. 작성자 역시 같은 난감함을 느꼈고 이 manifestFilePath를 알아내기 위해 여러가지 시도를 해야봐야 했다.
위의 세 가지 방법 이 외에도 LoadAsset을 하는 방법을 바꾸어 보기도 했고 LoadFromFile 대신 LoadFromMemory나 비동기 함수를 사용하는 방법도 사용해 봤었고, 에셋 번들을 빌드한 이후에 나온 매니페스트 파일을 각 에셋 번들에 포함시킨 후에 재빌드하는 방법도 사용해 봤다. 간단히 결론만 말하면 그 모든 방법은 실패했다.
그리고 마지막 방법으로 시도해본 것이 AssetBundles.unity3d라는 파일을 호출하는 것이었다. 유니티에서 에셋 번들을 빌드하면 에디터에서 지정한 에셋 번들 이름을 가진 에셋 번들들과 그 매니페스트 파일이 생성되고, 그 외에 직접 지정한 이름이 아닌 AssetBundles라는 파일과 AssetBundles.manifest라는 파일이 생성되는데, 유니티 5.6 문서에는 이 파일의 역할이 무엇인지 명시되어 있지 않았다.
유니티 문서에서는 AssetBundleManifest를 호출하는 방법은 정확히 알려주었으나, 어떤 파일을 불러와야 되는지는 제대로 알려주지 않은 것이다. 구글링한 내용들에서도 "AssetBundles" 파일에 대한 언급은 없었기 때문에 정확한 해결법을 찾는데 더 어려웠던 것 같다.
1. ".unity3d"라는 확장자는 웹 서버를 통해 다운받은 에셋 번들을 로컬 저장소에 저장할 때 직접 지정한 확장자이다.
2. 에셋 번들을 빌드하면 확장자 없이 에셋 번들 이름만 적힌 파일이 나온다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
유니티(Unity)에서 씬(Scene) 역시 에셋 번들(Asset Bundle)에 포함 될 수 있다. 그 기본적인 방법은 유니티 5.6 문서의 섹션 7에 나와있다. 하지만 그 방법은 에셋 번들 매니저(Asset Bundle Manager)를 이용해야 하는 방법이고 너무 대략적인 내용이라 따라 실행하는 데에만 많은 시행 착오를 겪게 된다. 이 문서에서는 그에 비해 비교적 간단하고 즉시 활용할 수 있는 코드를 보여주고자 한다.
다음의 내용이 알려주고자 하는 코드의 전체이다 :
using System.IO; using System.Collections; using UnityEngine; using UnityEngine.SceneManagement;
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);
}
}
서버에서 받아온 에셋 번들을 메모리에 넣어두고 사용하고자 하는 목적이 아니라 받아온 에셋 번들을 클라이언트의 로컬 저장소에 저장하고 패치하는 시스템을 만들고자할 때에는 부적절한 예제이며, 로컬 저장소에 저장하는 방법은 유니티 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;
하지만 위의 방법은 에셋 번들에 대한 원시 데이터 접근은 지원하지 않는다는 예외를 발생시키고 실패한다. 즉, 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); } }
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.