게임오브젝트의 트랜스폼의 데이터를 저장했다가 다시 로드해서 리플레이 적용
[자체홍보 : 저의 현재 프로젝트] Chase and Destory it
https://www.youtube.com/watch?v=moI8KUuTUFI 현재는 일반 레이싱 모드에서 위 영상 같이 도...
blog.naver.com
주로 레이싱게임 같은데서 내가 했던 최고 기록을 고스트 플레이어로 보여 준다거나 하는 용도로 많이 사용되는 방법이죠.
일단 이를 어떻게 구현하는지 생각해 봤습니다.
리플레이가 필요한 오브젝트의 트랜스폼의 값인 x, y, z 축의 위치 값과 회전값을 프레임마다 구해내서 이 값을 저장해 놓고 필요할 때 로드한 다음에 매 프레임마다 다시 위치/회전의 값을 적용해 주면 되겠죠.
기본적인 컨셉은 이렇습니다만, 여기에 문제가 있습니다.
예를 들어 완벽하게 정해진 프레임 즉, FPS(초당 프레임수)를 유지한다는게 불가능하다는 것입니다.
60FPS를 완벽하게 유지한다면, 1초동안의 오브젝트 움직임(위치/회전) 데이터를 가져오는 경우 60개의 위치/회전 데이터를 저장하면 되지만, FPS란게 계속 바뀌기에 그렇게 쉽지만은 않습니다.
다음으로 에셋스토어에서 해당 기능을 수행할 수 있는 에셋을 찾아봤습니다.
아주 오래전에 제가 이미 구입해서 가지고 있는 리플레이 기능 에셋이 있는게 생각나서 한 번 찾아봤는데요. (2013년 9월 18일에 구입했던 에셋으로 나오네요)
제 기억으로 꽤나 오래전에 구입했고, 가격도 싸진 않습니다만, 데이터를 파일로도 저장/로드 할 수 있는 딱 제가 원하는 기능이 가능한 에셋입니다.
말씀드린데로 오래된 에셋인데, 아직 Deprecated 에셋은 아니고, 판매 서비스를 하는 에셋이지만, 업데이트 된지는 2019년 4월 17일이 마지막으로 오래된 에셋이 맞습니다.
문제는 오래되면 최신 유니티 버전과의 호환성 문제가 생기기 마련이죠.
제가 2022.3 버전에서 가져와 본 결과로는 앞으로 obsolete될 거라는 warning 이 아닌 이미 obsolete 된 API를 사용하고 있는 몇 개의 에러가 있네요. 치명적인 부분이 아니기에 그 부분을 아예 지워버리거나 수정하면 사용할 수 있는 정도로 작동되는 것은 확인했습니다.
EZ Replay Manager | Utilities Tools | Unity Asset Store
Use the EZ Replay Manager from SoftRare on your next project. Find this utility tool & more on the Unity Asset Store.
prf.hn
그 외에도 에셋스토어에서 ghost replay 로 검색해 보면,
Ultimate Replay 2.0/3.0 등이 $35 수준으로 조금 비싸지만, 단순히 트랜스폼을 넘어 애니메이션 등 많은 리플레이를 가능하게 하는 에셋이 있습니다. (무료버전으로 테스트 해 볼 수 있네요)
Ultimate Replay 3.0 | Camera | Unity Asset Store
Get the Ultimate Replay 3.0 package from Trivial Interactive and speed up your game development process. Find this & other Camera options on the Unity Asset Store.
prf.hn
비슷하게 GhostToolPro 도 있습니다.
조금 더 싼 $20 수준의 ShadowGhostPro 에셋도 있는데 이건 아래 저가 수준과 크게 다르지 않은 기능 정도만 지원하는거 같은데, 가격은 꽤 비싸네요.
그 외에도 $5 수준에서 Ghost Recording and replay script, RJ Ghost Replay System 및 Ghost Recorder 등도 딱 제가 위에서 말씀드린 트랜스폼 데이터를 저장하고 리플레이 하기에 적당한 기능을 제공합니다.
그 외에도 RWND같은 무료 에셋도 있습니다.
제가 현재 개발하는 프로젝트인 Chase and Destory it 이라는 경영 액션 레이싱 게임은 장기 개발기간이 필요하기 때문에 상대적으로 단기 프로젝트로 러너게임을 같이 개발하고 있는데요.
이 러너게임은 막바지 단계에 있습니다. 이 러너게임에서 플레이어의 최고 기록을 고스트로 리플레이 해서 경쟁을 유도하거나 할 수 있으면 다른 유저의 기록을 고스트로서 리플레이해서 경쟁을 시키는 기능을 넣고 싶어서 이런 에셋을 찾아봤습니다.
제가 필요한 기능은 정말 단순하게 위치값과 회전값 데이터만 저장하고 로드해서 리플레이 할 수 있으면 됩니다.
위에서 살펴본 가장 저가의 에셋만 있으면 충분히 구현할 수 있을 것으로 보입니다만, 그 전에 구글에 Unity Ghost replay system을 찾아보니 튜토리얼과 소스를 공개한 유튜브영상이 하나 있네요.
Tarodev 님이 제공하는
https://www.youtube.com/watch?v=GkAQh2QzdJA&t=1s
입니다.
깃허브에서 소스도 받으실 수 있어요
https://github.com/Matthew-J-Spencer/Timetrial-Ghosts
GitHub - Matthew-J-Spencer/Timetrial-Ghosts: Time trial ghosts for any kind of speed running game.
Time trial ghosts for any kind of speed running game. - Matthew-J-Spencer/Timetrial-Ghosts
github.com
이 소스코드는 예제를 포함하고 있지만, 실제 시스템 구현에 필요한 소스는 딱 두 개 입니다.
Recording.cs 와 ReplaySystem.cs 입니다.
이렇게 스크립트 두 개만 있으면 제가 필요한 기능을 수행할 수 있는 점이 딱 맘에 들었습니다.
이 개발자 Patreon에 가입해서 약간의 금액으로 도움을 주면 디테일 한 서비스를 받을 수도 있겠죠.
하여간 위 두개의 소스를 프로젝트에 넣고, 플레이 하면서 데이터를 레코딩하고 다시 불러서 리플레이를 실행하는 부분은 필요에 따라 스크립트를 작성해 주면 됩니다.
저는 ReplayManager라는 오브젝트랑 스크립트를 하나 만들어 씬에 넣어주었습니다.
스크립트는
private ReplaySystem _system;
[SerializeField] private Transform _recordTarget;
[SerializeField] private GameObject _ghostPrefab;
[SerializeField] private int _captureEveryNFrames;
private void Awake ()
{
_system = new ReplaySystem(this);
}
을 넣어줬습니다.
그리고 키보드에서 'R' 키를 누르면 레코딩 시작을 'S' 키를 누르면 레코딩 정지 'P' 키를 누르면 리플레이를 시작하도록 만들었습니다.
// Update is called once per frame
void Update()
{
if (Input.GetKeyUp(KeyCode.R))
_system.StartRun(_recordTarget.transform.GetChild(6), _captureEveryNFrames);
else if (Input.GetKeyUp(KeyCode.S))
_system.FinishRun();
else if (Input.GetKeyUp(KeyCode.P))
{
_system.SetSavedRun(recording);
_system.PlayRecording(RecordingType.Saved, Instantiate(_ghostPrefab));
}
}
_captureEveryNFrames 값은 2 정도 줘도 충분히 부드럽게 나오네요.
_recordTarget 은 위치/회전 값 데이터를 저장할 플레이어의 캐릭터를 연결해 주면 되고, _ghostPrefab은 나중에 리플레이할 때, 고스트로 나타날 오브젝트의 prefab을 만들어 연결해 주면 됩니다(보통 반투명으로 보이는 재질로 만듭니다)
지금은 Instantiate api를 사용하는데, 나중에 풀링시스템에서 활성화 시켜주는 방식으로 바꿔줄 것이고요.
이렇게 해서 테스트 해 보니 잘 작동됩니다.
다만 실제 제 프로젝트에 적용하려고 하니 몇 가지 문제점이 있어서 소스코드를 수정해야 되더군요.
소스코드는 2D 게임에서 레코딩과 리플레이 하도록 만들어져 있어서 다루는 데이터가 플레이어의 위치값 x, y 그리고 회전값 z 뿐입니다. 제 프로젝트는 3D지만, 사실상 2D 게임과 같은 뷰이기 때문에 문제는 없지만, 다뤄야 하는 값이 위치값 y, z 그리고 회전값 x 입니다. 이건 제가 필요한 축으로 어렵지 않게 수정했습니다. (Recording.cs 코드를 수정했습니다)
소스코드를 보면 데이터를 저장하는 것을 고려해서 작성된 코드인 것으로 보이지만, 실제 파일로 저장하는 것이 필요한데, 그렇게 구현되어 있지는 않습니다. 거기에 제 프로젝트에 Easy Save를 사용해 데이터를 저장하고 있기에 이를 통합하는 작업도 필요했습니다. 이거 구현하느라 소스코드 파악하고 이리저리 테스트 하는데 2틀 이상 걸렸네요. 그래도 심플한게 너무 맘에 들어서 제가 이미 갖고 있는 에셋인 EZ Replay Manager를 사용하는 것 보다도 이 걸로 해결하고 싶었습니다.
해결방법은 그렇게 어려운 건 아니었네요.
원래 소스코드가 저장을 염두해서 Serialize 와 Deserialize 하도록 만들어져 있었어요.
소스코드의 ReplaySystem.cs 의 FinishRun 메소드 부분에서 Easy Save를 이용한 파일로 저장이 일어나도록 만들었습니다. (SheepyReplay.es3 라는 파일로 저장)
간단하게 아래 처럼 수정했습니다.
public bool FinishRun(bool save = true) {
if (_currentRun == null) return false;
if (!save) {
_currentRun = null;
return false;
}
_runs[RecordingType.Last] = _currentRun;
// 아래 한 줄 추가한 것으로 간단히 저장
ES3.Save<string>("Replay", _currentRun.Serialize(), "SheepyReplay.es3");
_currentRun = null;
if (!GetRun(RecordingType.Best, out var best) || _runs[RecordingType.Last].Duration <= best.Duration) {
_runs[RecordingType.Best] = _runs[RecordingType.Last];
return true;
}
return false;
}
이렇게 Serialize된 데이터로서 저장하는 것 까지는 됐는데, 파일을 로드해서 다시 Replay에 적용하려면 Deserialize 해야 하는 것 때문에 좀 헤맸습니다.
역시 제가 프로그래머로서 게임 개발을 시작한게 아니라서 제대로 된 코스로 기본부터 공부한게 아니다 보니 쉽지 않네요. 프로그래밍을 제대로 공부하신 분이 보시면 아주 쉽게 끝냈을 걸 헤맨거 같습니다.
파일을 로드하고 Deserialize 를 적용하는 것은 앞서 제가 작성한 스크립트인 ReplayManager.cs 에서 하면 됩니다.
그래서 이렇게 만들어진 최종 코드는 아래처럼 됩니다.
아! Serialize 되면 데이터들이 string으로서 변환됩니다. Easy Save에서 string 필드로서 저장하고 로드도 그렇게 해야 하기 때문에 아래에서 SerializedRecording 이라는 string 필드를 주고 여기에 로드값을 담은 다음에 실제로 리플레이를 하려면 Recording 클래스의 데이터로서 넣어줘야 (_system.SetSavedRun(recording)이 그 역할) 하기 때문에 recording = new Recording(SerializedRecording) 으로 Deserialize를 했습니다.
이 간단한 한 줄 알아내려고 하루종일 고생했네요. ^^;
using System.Collections;
using System.Collections.Generic;
using TarodevGhost;
using UnityEngine;
public class ReplayManager : MonoBehaviour
{
private ReplaySystem _system;
[SerializeField] private Transform _recordTarget;
[SerializeField] private GameObject _ghostPrefab;
[SerializeField] private int _captureEveryNFrames;
private Recording recording;
private string SerializedRecording;
private void Awake()
{
_system = new ReplaySystem(this);
if (ES3.KeyExists("Replay"))
{
SerializedRecording = ES3.Load<string>("Replay", "SheepyReplay.es3");
recording = new Recording(SerializedRecording);
}
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyUp(KeyCode.R))
_system.StartRun(_recordTarget.transform.GetChild(6), _captureEveryNFrames);
else if (Input.GetKeyUp(KeyCode.S))
_system.FinishRun();
else if (Input.GetKeyUp(KeyCode.P))
{
_system.SetSavedRun(recording);
_system.PlayRecording(RecordingType.Saved, Instantiate(_ghostPrefab));
}
}
}
이렇게 해서 아주 간단한 오브젝트의 위치값 Y, Z와 회전값 X 를 플레이하면서 파일로 저장했다가 언제든지 다시 파일을 로드해서 필요할 때, 리플레이 하는 시스템을 구현 해 보았습니다.
아! 물론 제가 한 건, 제 프로젝트에 맞게 극히 일부분의 수정 한 거고, Tarodev 님이 만든 스크립트를 사용하는 방법에 관한 내용이었습니다!
단순히 달리고 점프하는 게임이 아니고, 캐릭터를 R/G/B 의 색상 사이에서 전환할 수 있는데 바닥 플랫폼 및 장애물도 R/G/B 컬러 속성이 있기에 이를 매치시키면서 앞으로 나아가는 게임 입니다. 색상이 불일치되면 데미지를 입어요.
여백지미@Self Expression Arts