본문 바로가기
게임개발/가마수트라

심층 분석: 섀도우 갬빗의 저장 시스템 생성 및 미세 조정하기

by 아수랑 2023. 12. 14.
728x90
분야: 프로그래밍 / 18분 읽기


이 프로그래밍 챌린지의 기술 프로세스에서 얻은 5가지 핵심 사항을 소개합니다.


게임 개발자 심층 분석은 비디오 게임의 특정 디자인, 아트 또는 기술적 특징을 조명하여 겉으로 보기에 간단해 보이는 기본적인 디자인 결정이 실제로는 전혀 간단하지 않다는 것을 보여주기 위해 진행 중인 시리즈입니다.

이번 에피소드에서는 미미미 게임즈가 어떻게 섀도우 갬빗에서 스컴 친화적인 세이브 시스템을 만들었는지 알아보세요: 수석 개발자 필립 위터샤겐과 함께 섀도우 갬빗: 더 커즈드 크루의 세이브 시스템을 개발했습니다. 

안녕하세요, 저는 독일의 소규모 독립 게임 스튜디오 미미미 게임즈(Mimimi Games)의 수석 개발자인 필립 비터샤겐입니다. 저희는 섀도우 택틱스로 스텔스 전략 장르를 부활시킨 것으로 잘 알려져 있습니다: 블레이드 오브 더 쇼군으로 비평가들의 찬사를 받았고, 그 뒤를 이어 데스페라도스 3와 가장 최근에는 가장 야심차게 자체 개발한 타이틀인 섀도우 갬빗으로 성공을 거두었습니다: 저주받은 크루는 다시 한 번 비평가들의 찬사를 받았지만, 안타깝게도 저희의 마지막 게임이 될 것입니다.

섀도우 택틱스를 개발할 당시 4명으로 구성된 소규모 개발팀이 이후 타이틀의 토대를 마련했습니다. 나중에 많은 부분이 최적화되었지만, 세이브 시스템의 아키텍처는 그 당시에 정의되었기 때문에 소규모 팀 규모와 매우 효율적이어야 한다는 요구사항의 영향을 받았습니다.

"대부분의 게임에는 세이브 시스템이 있습니다. 유니티만의 특별한 점은 무엇인가요?"라고 생각할 수 있습니다. 먼저 잠입 전략 장르와 세이브 시스템에 대한 특별한 요구 사항을 간략하게 소개한 다음, 저희 시스템이 어떻게 작동하는지, 요구 사항을 어떻게 해결했는지, 프로덕션을 위해 이 모든 것을 어떻게 최적화했는지 설명해드리겠습니다.


스텔스 전략 장르


스텔스 전략 장르, 특히 미미미 게임즈의 작품은 약 200x200m의 맵에서 탑다운 시점으로 플레이하는 것이 특징입니다. 그다지 넓지 않은 것 같지만, 맵에는 적과 상호작용이 촘촘하게 배치되어 있습니다. 이전 게임의 레벨에는 최대 100명의 NPC가 등장했지만, 섀도우 갬빗은 보다 개방적인 구조로 인해 한 맵에 최대 250명의 NPC가 등장하기도 합니다. 이 모든 NPC는 자체적인 감지, 루틴, AI 동작을 실행하며, 플레이어는 언제든지 스크롤하여 분석할 수 있습니다. 여기에 최대 8명(대부분의 경우 3명)의 플레이어가 제어하는 캐릭터가 동시에 계획하고 실행할 수 있는 스킬을 가지고 있으며, 다양한 스크립트 이벤트와 맵과의 상호작용을 통해 모든 종류의 복잡한 로직을 가지고 컷신, 애니메이션 또는 퀘스트 업데이트를 자유롭게 트리거할 수 있습니다.

스텔스 게임은 항상 저장 스커밍이 발생하기 쉽지만, 여러 플레이어 캐릭터와 다양한 스킬 시스템을 통해 우리 게임은 이를 적극적으로 수용하여 플레이어가 다양한 접근 방식을 시도하고 자주 저장 및 로드하도록 장려합니다.

퍼블리셔 이미지 제공.


이 모든 것이 저장 시스템에 대한 특정 요구 사항을 설정합니다:

맵에 있는 모든 NPC의 현재 동작, 사용 가능한 오브젝트와의 상호작용, 캐릭터의 스킬 실행 및 애니메이션 상태, 심지어 레벨 디자인에서 생성한 스크립트 맵 이벤트까지 모든 것을 매 순간 안정적으로 저장해야 합니다.

저장은 자주 실행될 가능성이 높으므로 빠르게 저장하세요.

더 빠르게 로드하세요.

레벨 디자인은 테스트를 위해 계속 필요하므로 개발 중에도 안정성을 유지하세요.

소규모 팀으로 야심찬 프로젝트를 출시하기 위해서는 매우 효율적이어야 하므로 개발 부서에 너무 많은 오버헤드를 발생시키지 마세요.


한 가지 예비 결정


항구 지역의 경우 저장 루트의 모든 동적 오브젝트가 오른쪽에 시각화되어 있습니다(왼쪽의 정적 오브젝트와 대조). 이러한 오브젝트는 모든 컴포넌트와 필드와 함께 저장되며 로드할 때마다 소멸되고 완전히 재구성됩니다. 퍼블리셔에서 제공한 이미지.

팀이 빠르고 효율적으로 움직이고 모든 동적 정보를 안정적으로 저장하기 위해 개발 초기에 내린 한 가지 기본 결정은 씬에 특정 루트 오브젝트(소위 '저장 루트'라고 함)를 정의하여 모든 하위 오브젝트를 모든 컴포넌트와 함께 저장 시스템에 도입하는 것이었습니다. 이러한 모든 객체와 객체가 참조하는 모든 필드는 저장하지 않도록 명시적으로 표시하지 않으면 기본적으로 저장됩니다.

그 결과 개발자는 특정 방식으로 코드를 작성해야 하지만 여전히 효율적인 방식으로 코드를 작성해야 했지만(해시셋이나 Func와 같은 일부 기본 제공 C# 클래스는 지원되지 않음), 동시에 매우 놀라운 결과를 얻었습니다:

 

코드가 어떻게든 저장할 수 없는 기능을 사용하는 경우, 첫 번째 저장-로드를 시도한 후 게임이 즉시 중단될 가능성이 높습니다.

코드가 지원되지 않는 기능을 사용하지 않는다면 로드 프로세스에서 모든 저장된 필드가 있는 모든 저장된 객체를 다시 생성할 수 있다는 확실한 보장 하에 저장-로드가 성공할 것입니다.


처음에는 어떤 기능이 지원되지 않는지 정확히 알 수 없었기 때문에 몇 달에 걸쳐 모든 프로그래머는 저장 로드를 몇 분 이상 테스트하지 않으면 사용자에게 경고하는 도구를 실행해야 했습니다. 이를 통해 모든 개발자가 사용을 자제해야 하는 언어 기능을 자동으로 학습할 수 있었습니다.

반응형

자세히 알아보기

이 모든 사용자 정의 코드를 자동으로 저장하고 로드할 수 있는 올바른 저장 로드 시스템을 만드는 것은 간단한 작업이 아니므로, 우리가 어떻게 접근했고 그 과정에서 어떤 결정을 내렸는지 살펴봅시다. 엔진과 무관하게 시스템을 설명하려고 노력했지만, 이것이 항상 가능한 것은 아닙니다. 미미미 게임즈에서는 Unity3D 엔진과 C#을 프로그래밍 언어로 사용한다는 점만 기억해 주세요. 리플렉션과 같은 일부 개념은 매직 매크로를 사용하여 C++ 엔진에도 도입할 수 있지만, 값 및 참조 유형과 같은 개념은 다른 언어에는 존재하지 않습니다.


저장

먼저 정의된 저장 루트 오브젝트의 자식인 모든 게임 오브젝트와 컴포넌트를 수집합니다. 그런 다음 해당 프로퍼티 또는 필드에 반영합니다. 이 과정에서 발견되는 일부 오브젝트는 슈퍼 커스텀 직렬화 메서드를 가질 수도 있습니다.

Save-Process.drawio.png


객체 유형에 따라 세 가지 직렬화 방법 중 하나가 사용됩니다:

속성 리플렉션: 대부분 미리 정의된 엔진 내 객체 또는 타사 패키지의 컴포넌트로, 공용 프로퍼티가 API를 정의하는 경우가 많으며 객체를 다시 생성하기에 충분합니다.

 

필드에 대한 리플렉션(역시 비공개): 모든 내부 프로세스를 포괄하는 API를 추구하지 않으면서 동시에 모든 내부 구조를 보존하고자 하는 사용자 정의 작성 컴포넌트가 대부분입니다.

 

사용자 정의 직렬화 방법: 코루틴-객체(미미미의 코루틴은 참조 객체가 없으면 저장 시스템이 이를 인식하지 못하기 때문에 절대 시작되지 않을 수 있음), 델리게이트, 머티리얼.


엔진에 등록되지 않은 오브젝트도 저장해야 하므로 이러한 필드와 프로퍼티에서 찾은 새 오브젝트를 리플렉션 프로세스에 재귀적으로 추가합니다. 따라서 '에셋'으로 정의할 수 있는 오브젝트(예: 메시, 그래픽 또는 단순한 세팅 컨테이너)는 무시합니다. 안타깝게도 유니티 엔진은 런타임 중에 에셋 오브젝트를 다른 오브젝트와 구별할 수 있는 방법을 제공하지 않기 때문에 게임이 빌드되기 전에 실행되는 또 다른 단계를 도입해야 했습니다. 저장 프로세스와 유사한 리플렉션의 도움으로 씬에서 사용되는 모든 에셋 오브젝트를 재귀적으로 수집하여 씬 내부의 거대한 사전에 저장하고 고유 ID를 할당합니다. 로딩 프로세스를 조금 더 간소화하기 위해 에셋과 유사하게 처리해야 하는 모든 저장 루트를 이 딕셔너리에 추가합니다.

에셋이 아닌 모든 오브젝트의 경우, 작고 단순한 데이터 구조(더 이상 객체 지향 기능이 없기 때문에 PDS(수동 데이터 구조) 또는 POD(Plain Old Data)라고도 함)를 생성합니다.
모든 참조를 소위 RefID(저장 파일의 다른 개체에 대한 ID) 또는 AssetID(자산에 대한 ID)로 대체할 수 있습니다. 이렇게 간단한 데이터 구조는 다양한 형식으로 쉽게 직렬화할 수 있습니다. 모든 대상 플랫폼과의 호환성과 최적화 가능성을 보장하기 위해 바이너리 버전뿐만 아니라 텍스트 버전으로도 제공되는 간단한 직렬화기를 직접 작성하기로 결정했습니다.

코루틴, 델리게이트, 머티리얼은 계속되는 도전이었지만(나중에 패치 섹션에서 한 가지 사례를 살펴보겠습니다), 코루틴을 저장할 수 있게 되면서 모든 종류의 비동기 코드를 쉽게 저장할 수 있었습니다. 모든 개발자가 외부 프로퍼티가 변경되기를 기다릴 때마다 상태 변수를 도입하거나 시간 지연을 기다려야 했다면 지금과 같은 상태로 게임을 출시할 수 없었을 것입니다.


로딩

로드 프로세스에서는 먼저 모든 세이브 루트를 정리하고 패시브 데이터 구조에서 모든 오브젝트를 다시 생성합니다. 그 후 직렬화 프로세스와 유사하게 저장된 모든 프로퍼티와 필드가 리플렉션을 통해 설정됩니다. 필요한 경우 사용자 정의 역직렬화 메서드가 사용됩니다.

로드-프로세스.drawio.png


물론 게임 엔진의 모든 내장 함수가 게임 오브젝트에 바인딩되어 있거나 그렇게 쉽게 복원할 수 있는 것은 아니기 때문에 그렇게 간단하지는 않습니다. 여기서는 저장, 로드 또는 두 가지 모두에서 추가 코드가 필요한 다양한 특수한 경우를 나열해 보겠습니다:

 

코루틴: 커스텀 직렬화 메서드가 있으며 로드 후 현재 열거자 위치에서 시작됩니다.

시간: 저장하는 순간에도 시간이 계속되기를 원했기 때문에 내부 시간 메서드를 감싸는 싱글톤 컴포넌트를 추가했습니다.

Random: 로드 후 시드를 복원합니다.

머티리얼: 머티리얼 인스턴스를 에셋 소스에 마술처럼 매핑해야 했기 때문에 결국 이름별 매핑이 필요했습니다.

엔진 오브젝트의 내부 깨어 있는 상태: 오브젝트가 처음 활성화된 후 일부 메서드가 자동으로 호출되는 방식(예: Awake 또는 Start)을 생각해 보세요. 이러한 메서드는 게임 로딩 후에 트리거되지 않거나 적어도 코드에 영향을 미치지 않아야 합니다.

물리: 깨어 있음 상태와 유사하게 물리 객체에도 내부 상태가 있습니다. 로드 프로세스 이후에 연속적으로 발생하는 트리거 및 충돌 콜백은 무시해야 합니다.

정적 필드: 정적 필드는 일반 필드처럼 발견한 모든 오브젝트에 포함됩니다(이전에는 전혀 저장할 필요가 없다고 생각했지만, 불안정성과 메모리 누수가 발생할 수 있다는 사실을 금방 깨달았습니다). 이론적으로는 정적 필드가 여러 번 저장되고 로드될 수 있지만, 한 번만 존재하는 기본 싱글톤 클래스를 제외하고는 정적 필드 사용을 권장하지 않습니다.

구조체: 앞서 제공한 샘플에서는 단순하게 유지하기 위해 조용히 무시했습니다. 구조체는 사용자 정의 참조 유형의 저장 프로세스와 일부 코드를 공유하지만 로드 프로세스에서 특별히 처리되며, 다른 객체보다 먼저 역직렬화됩니다.

 


최적화, 최적화, 최적화

기술적인 설명을 모두 마친 지금이 장르 소개에서 언급한 저장 시스템의 요구 사항을 다시 한 번 생각해 볼 수 있는 좋은 시기라고 생각합니다:

(완료) 모든 순간에 모든 것을 안정적으로 저장합니다: 처리해야 할 특수한 경우를 모두 파악하여 견고하고 안정적인 저장 로드 시스템을 구축할 수 있었습니다.

빠른 저장: 저장이 자주 트리거될 가능성이 높기 때문입니다.

더 빠르게 로드.

(완료) 개발 중 안정성을 유지합니다: 모든 것을 저장하도록 기본값을 설정하면 오류를 빠르게 감지할 수 있으며, 레벨 디자인은 개발 중에도 저장 시스템을 사용할 수 있어 오류가 거의 발생하지 않습니다.

(완료) 개발 부서에 너무 많은 오버헤드를 발생시키지 않습니다: 개발자는 세이브 시스템이 작동하려면 코드를 어떻게 작성해야 하는지(어떤 클래스가 지원되고 어떤 클래스가 지원되지 않는지) 배워야 했지만, 짧은 도입 기간이 지나면 세이브 시스템이 일상적인 업무에 부담을 주지 않습니다.



이 목록을 읽고 특정 줄에서 "(완료)" 태그가 누락된 것을 발견하기 전에도 숙련된 C# 개발자라면 이 글에서 "반영"이라는 단어가 사용될 때마다 이러한 시스템의 성능에 대한 의구심이 커졌을 것입니다. 리플렉션의 속도에 놀랐지만, 리플렉션이 특별히 유명한 것은 아닙니다. 그리고 현재 상태는 개발용으로는 괜찮았지만 프로덕션용으로는 완전히 재앙이었습니다. 그래서 가능한 모든 부분을 최적화하는 지루하지만 필수적인 작업이 시작되었습니다.

728x90


원인 찾기

모든 최적화 시도의 첫 번째 단계는 항상 최악의 기여자를 프로파일링하고 찾아내는 것입니다. 명백한 범인인 '리플렉션' 외에도 몇 가지 다른 후보를 발견했습니다:

텍스트 직렬화 및 디스크에 저장하고 디스크에서 로드하는 파일 크기입니다: 우리는 바이너리 직렬화 형식으로 전환하고 저장 파일을 GZip으로 추가로 압축했습니다.

C# 유형 역직렬화: 이제 유형 이름을 ID로 저장하고 처음 발생할 때 해당 C# 개체를 캐시합니다. 이로 인해 세션당 첫 번째 저장 로드 속도가 약간 느려지지만 다음 세션의 속도가 빨라집니다.

엔진 내 오브젝트 재생성


처음 두 가지 문제는 아주 쉽게 해결할 수 있었기 때문에 빠르게 해결 방법을 설명했으며 더 이상 설명하지 않겠습니다. 이제 리플렉션 성능에 대해 수행한 (역시 매우 당연한) 최적화에 대해 살펴보겠습니다.


리플렉션 최적화

게임에서 저장-로드 시스템을 구축하는 고전적인 방법은 몇 가지 "직렬화" 및 "역직렬화" 메서드를 노출하고 일종의 데이터베이스와 유사한 객체를 전달하는 일반적인 저장 가능 베이스 클래스를 정의하는 것입니다. 이러한 메서드 내에서 모든 파생 객체는 저장하거나 로드하려는 데이터를 일부 ID로 저장하거나 검색할 수 있습니다. 다음과 같이 보일 수 있습니다:

 

모든 것에 리플렉션을 사용했기 때문에 이런 종류의 메서드가 없었습니다. 하지만 엔진 내 오브젝트의 내부 상태를 복원할 수 있도록 모든 저장 가능한 컴포넌트에 대한 공통 베이스 클래스를 이미 가지고 있었습니다. 이러한 메서드를 수작업으로 작성하는 것은 "너무 많은 오버헤드를 생성하지 않는다"는 정의된 목표에 분명히 위배되지만, 안정적인 시스템이 구축되어 있고 런타임 정보가 필요하지 않은 리플렉션 기반 시스템 덕분에 빌드 과정에서 이러한 메서드를 쉽게 생성할 수 있습니다. 메서드 생성 외에도 유형 해상도를 미세하게 최적화하고 필드 이름의 미리 계산된 해시를 ID로 사용할 수 있게 되었습니다.


직렬화 및 역직렬화 메서드 생성은 보다 사용자 친화적인 로드 시간을 달성하는 데 큰 역할을 했습니다. 왼쪽은 대부분의 최적화가 비활성화되어 있는 개발 중 로드 프로세스를, 오른쪽은 최종 빌드입니다. 직렬화 및 역직렬화 메서드 자체만으로도 약 1.5초의 속도 향상을 가져왔습니다. 이미지 제공 게시자.

 

엔진 내 오브젝트 재생성 최적화하기

엔진 내 오브젝트와 컴포넌트를 재생성할 때 예상하지 못한 성능 병목 현상이 발생했습니다. Unity3D에서 이 작업을 위해 사용되는 메서드는 시간이 지나치게 오래 걸립니다. 이는 아마도 모든 호출이 통과해야 하는 C#과 C++ 엔진 사이의 브리지 때문일 것입니다. 이 이론을 염두에 두고 새로운 오브젝트와 컴포넌트를 생성하는 또 다른 방법, 즉 많은 컴포넌트가 포함된 프리팹 인스턴스화를 활용하려고 했습니다.

엔진 내 오브젝트를 생성하는 데 걸리는 시간은 실제로 엔진 호출 횟수에 거의 비례하는 것으로 보이기 때문에 컴포넌트가 많은 오브젝트의 경우 인스턴스화 호출이 훨씬 빠릅니다. 이 호출은 프리팹을 생성할 뿐만 아니라 씬의 다른 오브젝트도 복제할 수 있습니다. 씬의 오브젝트가 어떻게 생겼는지, 어떤 종류의 컴포넌트가 포함되어 있는지 알고 있었기 때문에 당면한 문제에 대한 해결책을 예상할 수 있었습니다. 씬의 비슷한 오브젝트에는 비슷한 컴포넌트가 포함되어 있습니다. 최대 250개에 달하는 NPC 중 상당수가 그렇고, 플레이어 캐릭터도 마찬가지이며, 레벨 디자이너가 구축하는 로직은 여러 게임 오브젝트로 구성되어 있고 게임 오브젝트당 정확히 하나의 명령 컴포넌트를 포함하는 경우가 많기 때문에 어느 정도는 마찬가지입니다.

이 지식을 바탕으로 한 가지 실험을 해보았습니다: 특정 컴포넌트가 포함된 템플릿 오브젝트를 만들면 이 컴포넌트 세트가 포함된 오브젝트가 로드될 경우에만 해당 오브젝트를 복제하면 됩니다. 이렇게 하면 컴포넌트를 추가하기 위한 모든 개별 호출을 절약할 수 있습니다. 대부분의 오브젝트에 컴포넌트가 하나만 있어도 필요한 호출 수가 절반으로 줄어들지만, 섀도우 갬빗의 NPC는 각각 약 100개의 컴포넌트를 가지고 있습니다. 엔진 호출 횟수와 그에 따른 시간도 엄청나게 절약할 수 있었습니다. 나중에는 첫 번째 로드 후 템플릿 사본의 사전 생성 기능까지 추가했습니다. 이제 다음 로드를 미리 예측하고 정상적인 게임플레이 중에 플레이어의 이전 로드에서 확인된 양을 기반으로 템플릿 오브젝트의 여러 복사본을 미리 생성합니다.

create_objects_02.gif


이 모든 최적화를 통해 가장 큰 맵에서도 최종 저장 및 로드 시간이 몇 초에 불과했습니다. 이로써 최종적으로 설정한 목표에 부합하고 원활한 플레이어 경험을 보장할 수 있었습니다.


패치

미미미에서는 가능한 한 버그 없는 게임을 출시하기 위해 항상 노력하지만, QA를 통과하지 못하거나 예상보다 심각한 문제가 발생하는 경우가 있습니다. 게임을 패치할 때 현재 플레이 중인 플레이어의 세이브 게임이 깨지면 플레이어가 게임을 포기하는 경우가 많기 때문에 이를 방지하고 싶지 않습니다. 섀도우 택틱스의 첫 번째 패치에서 세이브 게임 호환성을 보장하지 못한 후, 다시는 이런 일이 발생하지 않도록 툴을 구축했고 다음 프로젝트에서 세이브 코드의 특정 부분을 조정해야 했습니다.

패치 후 저장 코드가 호환되지 않는 경우가 가장 자주 발생하는 두 가지 이벤트가 있었습니다:

레벨에 필요한 에셋 목록이 변경된 경우.

코루틴을 위해 생성된 코드가 변경된 경우.

 

에셋 종속성 변경

이러한 종류의 문제는 대부분 씬의 작은 영역을 변경해야 하는 레벨 디자인에서 발생하며, 특히 스크립팅이 출시 버전에서 의도한 대로 작동하지 않거나 종속성이 잘못 연결되었을 때 발생합니다. 플레이어가 다가갈 때 음성 대사를 재생해야 하는 NPC를 상상해 보세요. 출시 버전에서 누군가 음성이 실제로 장면과 일치하지 않는 것을 발견하고 버그 리포트를 제출합니다. 설정된 연결을 검토하던 중 잘못된 사운드 파일이 연결되어 있다는 사실을 알게 되지만, 이를 교체하면 이전 사운드 파일에 대한 종속성이 제거됩니다. 그러면 저장 시스템이 변경 전에 저장한 모든 자산을 더 이상 해결하지 못하게 됩니다. 이 문제는 씬의 어딘가에 이전 사운드 파일에 대한 연결을 유지하면 쉽게 해결할 수 있지만, 이와 같은 변경이 발생하는 시점을 실제로 감지하여 적절하게 대응하는 것이 중요합니다. 그래서 빌드 파이프라인에 각 씬의 종속성을 나열하고 변경 사항이 발생할 경우 경고하는 작업을 추가했습니다.


생성된 코루틴 코드의 변경

코드 디컴파일을 보면 코루틴을 위해 컴파일러가 생성한 객체를 종종 볼 수 있습니다. 여기서 무슨 일이 일어나는지 더 잘 이해하기 위해 몇 가지 코드를 살펴봅시다. 이 코드는 섀도우 갬빗 DLC에 등장하는 플레이어블 캐릭터 자간의 어둠의 절제 기술에서 발췌한 것입니다: 자건의 소원(12월 6일에 출시)에 나오는 기술입니다.

code_screenshot_02.png


여기에는 다양한 애니메이션을 재생하는 코루틴이 있습니다. 코루틴 내부에 로컬 필드를 정의할 때 이러한 값을 저장하기 위해 어떤 객체가 필요합니다. 이를 위해 컴파일러는 "<coroAnimation>d__4"라는 새 클래스를 생성합니다. 이 클래스는 현재 실행 중인 코루틴을 위해 저장해야 하는 객체이기도 합니다. 이 오브젝트의 유형("<coroAnimation>d__4")은 게임을 패치해도 변경되지 않아야 하는데, 그렇지 않으면 코루틴을 올바르게 복원할 수 없기 때문입니다(Shadow Tactics 패치에서 상당히 광범위하게 발생했던 문제). 그렇다면 정확히 무엇이 유형을 변경할까요? 우리는 유형 이름의 숫자가 코루틴 이전에 같은 클래스에서 발생하는 필드, 속성 또는 메서드의 수에 따라 달라진다는 사실을 깨달았습니다:

0: m_actionExecuteLoop

1: m_actionExecuteEnd

2: m_coroAnimation

3: playPostExecutionAnimation

4: coroAnimation → 클래스 이름 <coroAnimation>d__4로 이어짐


이는 또한 코루틴 이전에 변경 사항을 추가하지 않는 한 코드에 변경 사항을 패치해도 안전하다는 것을 의미합니다. 모든 패치를 만들 때마다 소스 파일 하단에 새로운 필드와 메서드가 위치하는 특별한 섹션을 추가했습니다. 또한 빌드 후 외부 애플리케이션이 빌드된 모든 어셈블리에서 생성된 코루틴 객체 이름의 변경 사항을 확인하는 또 다른 작업을 추가했습니다.

패치 단계에서 규칙을 준수하는 것이 그리 어렵지 않다고 생각할 수도 있지만, 사실 오류는 일상에서 아주 쉽게 발생하며 빌드 파이프라인의 안전 점검 덕분에 결함이 있는 패치를 두 번 이상 릴리스하지 않아도 되었습니다.


요점

저장 시스템에 대해 자세히 알아보고 몇 가지 교훈을 얻으셨기를 바랍니다. 제가 강조하고 싶은 점은 다섯 가지입니다:

  1. 프로그래머의 시간을 존중하고 발생할 수 있는 마찰을 제거한다:
  2. 추가 코드 없이 모든 것을 저장하는 것을 기본값으로 설정합니다.
  3. 특별한 규칙을 최소한으로 유지하세요(예: 코루틴의 저장-로드 허용).
  4. 처음부터 최적화를 내장할 필요는 없습니다: 결국 모든 요구 사항을 알고 있다면 특정 병목 현상을 최적화하는 것이 더 쉽게 구현할 수 있는 경우가 많습니다.
  5. 오류는 일상 생활에서 발생할 수 있다는 점을 염두에 두세요: 항상 오류를 방지할 수 있는 파이프라인이 있어야 합니다.

저는 스텔스 전략 게임뿐만 아니라 저장 시스템을 작업하는 것이 정말 즐거웠고, 미미미 게임즈의 선구적인 프로젝트가 없어도 이 장르가 계속 발전할 수 있기를 바랍니다.



* 원문:

 

Fine tuning the scummy save system of Shadow Gambit: The Cursed Crew

It takes some serious planning to pull off a save system that supports the player's save scums. Here's how Mimimi Games did it.

www.gamedeveloper.com

 

* 게임 사이트: https://store.steampowered.com/app/1545560/Shadow_Gambit_The_Cursed_Crew/

 

Save 20% on Shadow Gambit: The Cursed Crew on Steam

Welcome to the Lost Caribbean! In this stealth strategy game, join a ghost ship with a living soul and assemble a cursed pirate crew. Embrace magical powers to defy the menacing army of the Inquisition, who stands between you and the mysterious treasure of

store.steampowered.com

 

// 오역이 있을 수 있습니다. 잘못된 번역은 댓글로 알려주세요.

댓글