유니티(Unity) 엔진으로 리플레이 기능을 구현하는 방법

The definition of insanity is doing the same thing over and over and expecting different results. (정신 이상의 정의는 같은 것을 반복하면서 다른 결과를 기대하는 것이다.) – Albert Einstein (알버트 아인슈타인)

어떤 것을 재현하는 방법은 두 가지가 있습니다. 하나는 결과를 따라하는 것이고, 다른 하나는 과정을 따라하는 것입니다. 리플레이 기능을 구현하는 방법도 크게 두 가지가 있습니다. 결과를 따라하기 위해 상태를 기록하는 방법이 있고, 과정을 따라하기 위해 사건을 기록하는 방법이 있습니다.

상태를 기록하는 방법은 사건을 기록하는 방법보다 구현하기 쉽고 임의의 시간으로 건너 뛰는 기능을 만들 수 있다는 장점이 있습니다. 상태 기록 방법은 다시 두 가지로 나눌 수 있습니다. 하나는 동영상 녹화를 하는 방법인데 구현하기 제일 쉽지만 제약이 많은 방법이고, 다른 하나는 객체마다 일일이 상태를 저장하는 방법인데 게임 객체가 많아질수록 비효율적인 방법입니다. 객체마다 상태를 저장하는 방법은 다음처럼 구현하면 됩니다. 게임 상태와 연관된 모든 게임 객체의 위치, 회전, 축척, 그리고 애니메이션 상태(스키닝을 사용하는 경우) 등을 매 프레임마다 기록합니다. 좀 더 효율적으로 기록하려면 일관성을 활용해서 값에 변화가 있을 때만 기록한 후 무손실 압축을 적용하면 됩니다.

여기서 주로 설명하려는 리플레이 기능 구현 방법은 사건을 기록하는 방법이므로, 앞으로는 사건을 기록하는 방법만 다루도록 하겠습니다. 사건을 기록하는 방법을 구현하려면 초기 상태를 기록한 후에 사건이 발생할 때마다 그걸 추가로 기록하면 됩니다. 임의의 시간으로 건너 뛰는 기능을 추가하려면 일정 간격으로 상태 기록도 하면 됩니다.

유니티 엔진을 사용해서 리플레이 기능을 구현하는 방법은 기본적인 리플레이 기능 구현 방법과 크게 다르지 않으며, 결정론적 알고리즘(deterministic algorithm)에 의해 동작하도록 신경써야 할 것은 다음과 같습니다. 참고로, 제가 실제로 리플레이 기능을 구현해 본 적은 없으므로 잘못된 내용이 있을지도 모르겠습니다.

첫째, 리플레이에서 잘못된 부분을 빨리 찾을 수 있는 도구를 만들어야 합니다. 눈으로 확인하는 것은 시간이 오래 걸리고 부정확하므로, 전체 상태를 저장 후 복구한 것과 리플레이를 저장 후 복구한 것의 차이를 매 프레임마다 자동으로 비교하는 도구를 만드는 게 좋습니다.

둘째, 이전 리플레이 버젼을 지원할 것인지 생각해야 합니다. 리플레이가 게임에서 차지하는 비중이 작다면 이전 리플레이 버젼을 지원하지 않는 게 좋습니다. 만약 지원하고 싶다면 게임 상태에 영향을 주는 모든 자산과 소스 코드를 리플레이 버젼별로 보존해야 합니다.

셋째, 실행 시마다 프레임 레이트가 다르므로, 그에 대한 처리를 해야 합니다. 그 방법은 두 가지가 있습니다. 하나는 프레임 간의 시간 차도 기록했다가 복원하는 방법인데 리플레이 재생 시 부드럽지 않아서 안 좋고, 다른 하나는 고정 프레임 레이트를 사용하는 방법입니다. 고정 프레임 레이트를 사용할 땐 Update를 사용하되 실제 처리는 고정 프레임 레이트를 사용하는 방법이 있는데 어렵고, FixedUpdate를 사용하는 방법도 있는데 이게 더 나은 것 같습니다. FixedUpdate를 사용한다면 다음처럼 처리해야 합니다. 게임 상태와 관련된 모든 처리를 Update나 LateUpdate 대신에 FixedUpdate에서 처리해야 합니다. 플레이어의 입력은 Update에서 받아야 하므로 그렇게 하되, 즉시 처리하지 말고 모아 두었다가 FixedUpdate에서 처리해야 합니다. 코루틴 사용 시에도 yield return null;나 yield return new WaitForSeconds(…); 대신에 yield return new WaitForFixedUpdate();를 사용해야 합니다.

넷째, 스크립트가 실제 플레이 시와 리플레이 시에 같은 프레임에 같은 순서로 동작하는지 확인해야 합니다. 리플레이 시 게임 객체 생성 순서를 실제 플레이 시와 같게 처리했더라도, 각 게임 객체의 스크립트가 실제 플레이 시와 같은 프레임에 같은 순서로 처리되는지 기록을 남겨서 확인해 봐야 합니다. 만약 같은 프레임에 같은 순서대로 처리되는 게 보장되지 않는다면, 그렇게 처리하는 관리자를 만들어야 합니다.

다섯째, 물리가 실제 플레이 시와 리플레이 시에 같게 동작하는지 확인해야 합니다. 만약 같지 않다면 유니티의 물리 처리를 제거하고 직접 구현해야 합니다. 여기에서 물리 처리엔 충돌 판정도 포함됩니다. 충돌 판정을 직접 구현하기 힘들다면 충돌 시엔 충돌 사건만 기록하고, 실제 충돌 처리는 기록된 사건을 토대로 따로 하는 방법도 생각해 볼 수 있습니다.

여섯째, 외부 플러그인 사용 시 그 플러그인이 실제 플레이 시와 리플레이 시에 같게 동작하는지 확인해야 합니다. 만약 다르게 동작한다면 그 문제를 해결할 수 있는 방법이 세 가지 있습니다. 첫 번째 방법은 그 플러그인을 수정하는 것이고, 두 번째 방법은 직접 구현하는 것이며, 세 번째 방법은 처리 결과만 얻어 내서 실제 플레이 시와 리플레이 시에 동일하게 적용하는 것입니다.

일곱째, 난수 처리를 확인해야 합니다. 리플레이 시 난수 발생기의 씨앗(seed)을 실제 플레이 시와 같게 설정하는 것은 기본이고, 난수 발생기가 정확히 같은 시점에 불려지게 해야 합니다. 따라서 유니티의 난수 발생기 대신에 직접 구현한 것을 게임 상태 전용으로 사용하는 게 좋을 것입니다.

여덟째, 부동소수점 연산을 주의해야 합니다. 똑같은 부동소수점 연산도 부동 소수점 연산 장치(FPU), 운영체제, 또는 컴파일러의 차이 등으로 결과가 달라질 수 있다고 합니다. 실제 플레이 시와 리플레이 시의 부동소수점 연산 결과가 똑같은지 확인해 보고, 만약 그렇지 않다면 고정소수점이나 정수를 사용하게 바꿔야 합니다. 문제가 덜 생기도록 하려면 리플레이를 저장한 플레이어의 기계에서만 그걸 재생할 수 있게 하는 게 좋습니다.

아홉째, 애니메이션이 게임 상태에 영향을 준다면 고정 프레임 레이트로 동작하게 해야 합니다. 게임 객체에서 참조하는 뼈(bone)의 위치를 바꾸는 애니메이션은 게임 상태에 영향을 줍니다. 애니메이션이 고정 프레임 레이트로 동작하게 하려면 다음처럼 하면 될 것 같습니다. Legacy 애니메이션 시스템을 사용한다면 Animation.animatePhysics을 true로 설정하고, Mecanim 애니메이션 시스템을 사용한다면 Animator.updateMode를 AnimatorUpdateMode.AnimatePhysics로 설정합니다. 만약 그래도 안 되면 애니메이션 처리를 직접 구현해야 합니다.

열째, 사건 중에 꼭 기록해야 할 것 중 하나는 플레이어의 입력인데, 하드웨어 입력을 직접 기록하는 것보다는 그걸 게임 명령으로 바꾼 것을 기록하는 게 더 유연하므로 좋습니다.

참고:

Advertisements

2 thoughts on “유니티(Unity) 엔진으로 리플레이 기능을 구현하는 방법

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중