Triangle Factory는 빠르게 성장하는 벨기에의 게임 회사로, Hyper Dash 및 최신 게임인 Breachers와 같은 고품질 멀티플레이어 VR 타이틀의 제작에 Unity를 사용하고 있습니다. Triangle Factory는 Cinemachine, Unity Profiler, 게임 서버 호스팅, Matchmaker, 음성 채팅(Vivox), Friends 와 같은 툴을 활용하여 플레이어에게 몰입도 높은 경험을 선사합니다.
이 블로그에서는 리드 레벨 디자이너/테크 아티스트인 젤 사도네스와 리드 디벨로퍼인 피터 방토르가 Blender-Unity 파이프라인과 VR 전술 FPS 타이틀인 Breachers를 구현한 방법을 소개합니다.
저희는 10년 넘게 Unity를 대표 엔진이자 개발 환경으로 사용해 왔으며, 환경 모델링과 디자인을 위해 수년 동안 많은 워크플로를 거쳤습니다. 여기에는 신속한 프로토타이핑을 위해 자주 사용하고 있는 ProBuilder와 같은 엔진 내 모델링 툴을 활용하고 다른 모델링 패키지에서 만든 프리팹으로 씬을 구성하는 것도 포함됩니다. 하지만 현재 프로젝트의 경우 Blender에서 레벨을 모델링하고 구성한 다음 Unity의 AssetPostprocessor를 사용하여 Unity 프로젝트에 통합하는 워크플로를 채택했습니다.
이번에는 이러한 워크플로를 개발하게 된 과정을 설명하고, 이 워크플로가 게임에 필요한 신속한 디자인 반복 작업을 어떻게 지원하는지 소개합니다.
2021년에는 빠르게 진행되는 5대5 아레나 슈팅 게임인 Hyper Dash라는 첫 번째 대형 VR 타이틀을 출시했습니다. 2019년에 게임 개발을 시작했을 때는 아마 많은 분들이 알고 계실 기본 Blender-Unity 워크플로를 사용했습니다. Blender에서 지오메트리를 모델링하고 에셋을 FBX 파일로 익스포트한 다음 수동으로 Unity에 통합하는 것이었습니다. 그런데 수동 통합에는 다음과 같이 여러 단계가 필요했습니다.
이 프로세스는 소규모 프로젝트에서는 잘 작동할 수 있지만, 프로젝트가 확장되고 발전함에 따라 금세 번거로워집니다. 그래서 새로운 타이틀 개발을 계획하기 시작했을 때 워크플로를 대폭 개선해야 한다는 것을 깨달았습니다.
Breachers는 복잡한 레벨 레이아웃, 섬세한 게임플레이 메카닉, 더 풍부한 기술 시스템, 최신 세대의 스탠드얼론 VR 하드웨어를 타게팅하는 높은 수준의 그래픽 완성도를 갖춘 경쟁식 슈팅 게임입니다. 복잡성 측면에서도 Hyper Dash보다 훨씬 개선되어, 워크플로에 미치는 영향을 금방 느낄 수 있었습니다.
프로토타이핑 단계에서는 창문 바리케이드와 같은 동적 오브젝트의 경우 여전히 프리팹에 크게 의존했습니다. 창문 바리케이드는 창틀 안쪽에 설치하여 실내와 실외 사이의 시야를 차단해서 경기 워밍업 중에 양 팀이 서로를 보지 못하도록 하는 오브제입니다.
프로토타입을 테스트하는 동안 게임플레이를 개선하기 위해 계속 여러 창을 사용해야 했는데, Blender에서 지오메트리를 변경하고 Unity로 다시 익스포트한 다음 바리케이드 오브젝트를 변경 사항에 맞게 수동으로 옮기는 식이었습니다. Unity 씬(Scene) 뷰를 돌아다니면서 이런 종류의 문제를 수동으로 확인하고 수정하는 데 많은 시간을 보냈습니다. 그럼에도 불구하고 플레이 테스트를 하는 동안 게임 플레이 중에 간과한 부분을 여러 번 발견했습니다.
물론 이 워크플로로는 내부적으로 플레이 테스트를 진행하면서 맵 디자인 반복 작업을 빠르게 진행할 수 없었습니다. 커뮤니티의 피드백을 받기 위해 하나의 맵을 무료로 공개할 계획이었던 오픈 알파 버전에서도 마찬가지였습니다. 저희는 이러한 피드백을 기대했지만, 수정 사항을 맵에 적용하는 데 필요한 수작업은 예상하지 못했습니다.
프리팹 기반 디자인 워크플로에서 발생할 수 있는 또 다른 단점은 성능입니다. 저희는 게임에 주로 모바일 스탠드얼론 VR 헤드셋을 타게팅합니다. 비주얼을 최대한 끌어올려야 하기 때문에 워크플로에서 마지막 한 방울까지 성능을 짜내야 하죠.
프리팹으로 레벨을 구성하는 것은 모델링 프로그램에서 방수 메시를 만드는 것보다 효율성이 떨어질 수 있습니다. 두 개의 모듈식 벽 조각을 함께 스냅하면 항상 그 사이에 병합되지 않은 지오메트리 루프가 있습니다. 프리팹을 사용하면 오브젝트 아래쪽에 있거나 벽에 배치되어 보이지 않지만 중요한 라이트맵 공간을 차지하는 지오메트리를 씬에 많이 배치하기 쉽습니다. 전체 레벨에서 봤을 때 이처럼 작은 비효율성이라도 성능 낭비와 비주얼 저하로 이어질 수 있습니다.
마지막으로 언급하고 싶은 프리팹의 문제점은 오브젝트 이름 변경과 같이 Blender의 소스 모델에 별 문제가 없어 보이는 변경 사항을 적용했을 때 문제가 발생하기 쉽다는 점입니다. 게임이나 레벨이 발전함에 따라 에셋을 재구성하고 개선되거나 더 일관성 있는 이름을 부여해야 하는 경우가 많습니다. 그러나 Blender에서 오브젝트의 이름을 변경하고 다시 익스포트하면 Unity에서 오브젝트에 대한 오버라이드 및 추가가 경고 없이 쉽게 중단되어 회귀(regression)가 발생할 수 있습니다.
아래의 간단한 예제에서는 창살이 달린 환기구 프리팹이 있고 여기에서 연기가 나오게 하려고 합니다. 메시를 Unity로 임포트한 후 아티스트는 연기 파티클 시스템을 자식 오브젝트로 추가하고 프리팹에 표면 유형 컴포넌트를 추가하여 금속 오브젝트라는 것을 표시했습니다.
Blender에서 메시의 이름을 변경하면 어떻게 되는지 확인해 보세요.
업데이트된 이름으로 메시를 다시 임포트할 때 Unity는 더 이상 이전 메시의 이름을 찾을 수 없으므로 모델 프리팹에서 오브젝트를 제거합니다. 이렇게 제거된 오브젝트의 자식은 프리팹의 루트로 옮겨지고 기존 스크립트는 제거되므로 다시 수동으로 정리하는 작업을 방지할 수 있습니다.
Breachers의 프로토타이핑 단계가 마무리되고 2022년 초에 본격적인 프로덕션 모드로 전환할 준비를 하면서, 아트 팀과 개발 팀이 함께 모여 이러한 문제를 어떻게 해결할 수 있을지 조사했습니다. 저희는 Breachers에 필요한 신속하고 유연한 반복 작업을 지원할 수 있는 이상적인 에셋 파이프라인에 대해 명확한 목표를 정의했습니다.
위에서 언급했듯이, 저희의 주요 목표는 Blender에서 게임을 정확하게 시각화하여 최종 결과물이 Unity에서 어떻게 보일지뿐만 아니라 게임플레이 메카닉이 어떻게 설정되는지를 적절히 반영하는 것이었습니다. Breachers의 게임플레이는 레벨의 레이아웃뿐만 아니라 동적 오브젝트(예: 뚫을 수 있는 벽)와 보이지 않는 요소(예: 사운드 볼륨 및 콜라이더)에 따라서도 달라집니다. 저희는 이 모든 정보를 디자인 단계에서 확인하고 Unity로 정확하게 전달할 수 있기를 원합니다.
커스텀 프로퍼티는 워크플로에 매우 중요하며, 저희는 Blender에서 이러한 프로퍼티를 오브젝트에 할당합니다. 그런 다음 에셋을 Unity로 임포트할 때 이를 읽고 커스텀 로직을 실행할 수 있도록 FBX 포맷으로 Unity에 전달합니다.
이를 통해 안정성은 물론 뛰어난 유연성을 확보할 수 있습니다. 이러한 프로퍼티는 파이프라인 전체에 걸쳐 오브젝트에 연결된 상태로 유지되므로 레벨의 오브젝트를 원하는 만큼 재구성하고 이름을 변경할 수 있습니다. 오브젝트가 깨지거나 동기화되지 않을까 걱정하지 않아도 됩니다.
Unity에는 에셋을 임포트하는 동안 에셋을 수정할 수 있는 강력한 클래스인 AssetPostprocessor가 있습니다. 저희는 임포트할 때 AssetPostprocessor를 사용해서 커스텀 프로퍼티를 파싱하고 작업을 수행합니다.
PrefabLink라는 커스텀 프로퍼티가 있는데, 이 프로퍼티는 임포트한 모델의 트랜스폼을 유지하면서 Blender에서 임포트한 오브젝트를 Unity 프로젝트에 이미 있는 프리팹으로 대체해야 한다고 알립니다. 이를 통해 이러한 동적 오브젝트를 Unity로 임포트한 후에도 프리팹의 장점을 유지하면서 Blender에 배치할 수 있습니다. 위 Blender 씬의 창문 바리케이드가 좋은 예시입니다.
표면 정의는 Breachers에서 매우 중요합니다. 금속 계단을 걷는 것은 콘크리트 바닥을 걷는 것과 다른 느낌이며, 총알이 나무를 관통하는 것은 강철을 관통하는 것과 많은 차이가 있습니다. 그리고 각 표면 유형에는 고유한 효과가 있습니다. Unity에서 각 프랍을 검토하고 올바른 표면 유형으로 태그를 지정하는 작업은 시간이 많이 걸리기 때문에, 저희는 디자인 시 Blender에서 지오메트리 콜라이더에 커스텀 프로퍼티를 설정하여 이 문제를 해결합니다.
최적화를 위한 또 다른 중요한 설정은 Unity의 정적 플래그입니다. 이를 올바르게 설정하면 가시성 컬링, 라이트 베이킹 및 배칭과 같은 작업에 큰 영향을 미칠 수 있습니다. Blender에서 커스텀 프로퍼티를 사용하면 재사용 가능한 프랍을 포함하여 레벨의 모든 부분에 정적 플래그를 설정할 수 있으며, 해당 정보가 레벨 전체에 걸쳐 Unity로 전달되도록 할 수 있습니다.
마지막으로 저희가 콜라이더를 어떻게 설정했는지 알려 드리겠습니다. Unity는 모델 에셋 이름에 _LOD0, _LOD1 등의 접미사를 붙이면 모델의 디테일 수준(LOD) 배리언트를 자동으로 감지하는 간단하지만 효과적인 시스템을 갖추고 있습니다. 저희는 여기서 아이디어를 얻어 비슷한 콜라이더 시스템을 만들었습니다. 지오메트리 이름에 _BoxCollider 또는 _NoCollision을 붙이는 것만으로도 Blender의 메시를 Unity의 콜라이더로 대체할 수 있습니다.
구체적인 예로, 커스텀 프로퍼티를 읽고 임포트된 각 오브젝트에 적합한 정적 플래그를 할당하는 LevelSetupPostprocessor의 스니핏을 살펴보겠습니다.
public class LevelSetupPostprocessor : AssetPostprocessor
{
// Dictionary of each object that is using a custom property.
private readonly Dictionary<string, (string[], object[])> _userPropertyMap = new ();
// List of all the custom properties we support
private static readonly string[] SupportedPropNames = new []
{
"Surface",
"Layer",
"PrefabLink",
"Collision",
"StaticFlags",
"LightmapScale",
"LightMeshPreset"
};
// Unity Event from AssetPostprocessor
// Called for each object in the model
private void OnPostprocessGameObjectWithUserProperties(GameObject go, string[] propNames, object[] values)
{
// Check if the custom properties contain any that we are interested in and add them to the dictionary.
if (SupportedPropNames.Select(x => x.ToLowerInvariant()).Intersect(propNames.Select(x => x.ToLowerInvariant())).Any())
{
_userPropertyMap.Add(go.name, (propNames, values));
}
}
// Unity Event from AssetPostprocessor
private void OnPostprocessModel(GameObject model)
{
// For each of the discovered custom properties,
// find the corresponding gameobject in the Model Prefab Variant
// and apply the appropriate logic
for(int i = _userPropertyMap.Count -1; i >= 0; i--)
{
var kvp = _userPropertyMap.ElementAt(i);
GameObject go = FindGameObjectInHierarchy(model, kvp.Key); // searches the model's children by name
string[] propNames = kvp.Value.Item1;
object[] values = kvp.Value.Item2;
for(int j = 0; j < propNames.Length; j++)
{
object value = values[j];
switch (propNames[j])
{
case "staticflags":
HandleStaticFlags(go, value);
break;
// ...
}
}
}
}
// Applies StaticFlags on the object based on custom properties from Blender
private void HandleStaticFlags(GameObject go, object value)
{
string[] staticFlags = value.ToString().Split(',');
StaticEditorFlags activeFlags = 0;
for(int i = 0; i < staticFlags.Length; ++i)
{
string flag = staticFlags[i].ToLower().Trim();
switch (flag)
{
case "batching static":
activeFlags |= StaticEditorFlags.BatchingStatic;
break;
// ...
default:
LogWarning($"Unknown static flag {flag} detected when importing {go.name}", go);
break;
}
}
GameObjectUtility.SetStaticEditorFlags(go, activeFlags);
}
}
이 모든 작업이 원활하게 이루어질 수 있도록 Blender 쪽에서도 몇 가지 작업을 해야 했습니다.
커스텀 프로퍼티는 Blender의 UI에 약간 숨겨져 있어 아티스트가 매번 커스텀 프로퍼티를 수동으로 입력해야 한다는 점에서 사용자 경험이 좋지는 않습니다. 수동 텍스트 입력에 의존하면 오류가 발생하기 쉬우며, 처음부터 Blender에서 설정했을 경우에 얻을 수 있는 많은 이점이 사라지게 됩니다. 프리팹 기반 워크플로에서 Blender로 전환하면서, 원하는 오브젝트를 찾아 보고 선택할 수 있는 멋진 오브젝트 라이브러리와 같은 프리팹의 장점도 활용하기가 어려워졌습니다. 다행히도 Blender는 Unity와 마찬가지로 매우 유연하고 쉽게 확장할 수 있습니다.
프리팹 구성 문제에 대한 해답은 에셋 라이브러리가 포함된 Blender 3.2에서 나왔습니다. 이 시스템은 Unity의 프리팹 시스템과 비슷하게 작동하는 면이 있습니다. 별도의 파일에 에셋을 생성한 다음 Blender 씬으로 임포트할 수 있으며, 에셋 파일의 변경 사항은 Blender 씬에 자동으로 반영됩니다. 또한 커스텀 프로퍼티 또는 콜라이더가 Blender에서 이 에셋의 각 인스턴스에 올바르게 적용되도록 합니다.
Blender의 경우, 커스텀 프로퍼티를 보다 명확한 사용자 인터페이스에서 설정할 수 있도록 자체 애드온을 개발했습니다. 이렇게 하면 각 프로퍼티를 수동으로 입력하는 대신 관련 Blender 오브젝트를 선택하고 버튼을 누르기만 하면 커스텀 프로퍼티를 간편하게 설정할 수 있습니다.
Bundle Exporter 애드온은 클릭 한 번으로 모든 FBX 파일을 익스포트하는 데 사용하는 오픈 소스 애드온입니다. 저희는 이 애드온을 커스텀 프로퍼티와 함께 사용할 수도 있도록 수정하고 요구 사항에 맞게 더 빠르게 익스포트할 수 있도록 UI를 업데이트했습니다.
Breachers의 레벨 디자인 워크플로를 설정하는 데 처음에는 많은 시간을 투자해야 했지만, 이 프로젝트에 적합한 선택이었다고 생각합니다. 꽤 재미있기도 했고요.
초기 블록아웃부터 알파 테스트, 몇 달 뒤의 최종 출시까지 오랫동안 게임을 개발해 왔기 때문에 레벨에서의 반복 작업은 빠르고 수월했습니다. 디자이너와 아티스트의 오버헤드와 바쁜 업무를 없애는 동시에, 이전에는 개발자가 필요했던 업무를 이들에게 이관할 수 있었습니다.
Unity와 Blender가 이렇게 원활하게 통합할 수 있다는 사실에 깊은 인상을 받았으며, 이러한 통합이 Breachers를 전 세계와 공유할 수 있는 자랑스러운 게임으로 만드는 데 결정적인 역할을 했다고 확신합니다.
지금까지 읽어 주셔서 감사 드리며, 게임을 즐겨 주세요!
Is this article helpful for you?
Thank you for your feedback!