PS: 老早前存在草稿箱里的文章,后边忘了写到哪了,没写完,看看得了
UE4重播系统
用于录制和播放游戏的重播系统
ue4有一套可用于战斗回放的重播系统,官方文档上有简单介绍
https://docs.unrealengine.com/zh-CN/TestingAndOptimization/ReplaySystem/index.html
在wiki上找的教程做了一遍,发现这篇教程较老,很多api都更新了,这里整理重做一边并记下详细说明,便于以后该功能的复用。
wiki教程链接:(需要梯子)
https://michaeljcole.github.io/wiki.unrealengine.com/Replay_System_Tutorial/
准备
在工程的配置文件DefaultEngine.ini中加入
+NetDriverDefinitions=(DefName=“DemoNetDriver”,DriverClassName=“/Script/Engine.DemoNetDriver”,DriverClassNameFallback=“/Script/Engine.DemoNetDriver”)
此步骤将启用并加载DemoNetDriver
按照教程,把关卡中所有静态网格体的static mesh replicate movement勾上,确保actor被正确复制。
提到了如果项目已经设置为多人游戏,可以跳过此步骤。
在创建代码之前,确保项目文件的build.cs中包含“Json”,如
PublicDependencyModuleNames.AddRange(new string[] { “Core”, “CoreUObject”, “Engine”, “InputCore”, “Json” });
这个很简单,不多说
GameInstance代码
重播的核心函数都在GameInstance里,那么肯定需要一个自己的GameInstanceC++类。
蓝图调用时可直接getgameinstance然后cast to调用,为避免耦合,也可以封装到BlueprintFunctionLibrary中写成静态函数
GameInstance.h:(并没有全部粘下来,只放了要用的函数声明及结构体定义)
#include "NetworkReplayStreaming.h" //新添加一个头文件
USTRUCT(BlueprintType) //新建ReplayInfo结构体,完全跟wiki教程的一样,此结构体为获得重播记录的信息
struct FS_ReplayInfo
{
GENERATED_USTRUCT_BODY()
UPROPERTY(BlueprintReadOnly)
FString ReplayName;
UPROPERTY(BlueprintReadOnly)
FString FriendlyName;
UPROPERTY(BlueprintReadOnly)
FDateTime Timestamp;
UPROPERTY(BlueprintReadOnly)
int32 LengthInMS;
UPROPERTY(BlueprintReadOnly)
bool bIsValid;
FS_ReplayInfo(FString NewName, FString NewFriendlyName, FDateTime NewTimestamp, int32 NewLengthInMS)
{
ReplayName = NewName;
FriendlyName = NewFriendlyName;
Timestamp = NewTimestamp;
LengthInMS = NewLengthInMS;
bIsValid = true;
}
FS_ReplayInfo()
{
ReplayName = "Replay";
FriendlyName = "Replay";
Timestamp = FDateTime::MinValue();
LengthInMS = 0;
bIsValid = false;
}
};
//instance类中:
public:
virtual void Init() override;
private:
TSharedPtr<INetworkReplayStreamer> EnumerateStreamsPtr;
FOnEnumerateStreamsComplete OnEnumerateStreamsCompleteDelegate;
void OnEnumerateStreamsComplete(const TArray<FNetworkReplayStreamInfo>& StreamInfos);
// for DeleteReplays(..)
FDeleteFinishedStreamCallback OnDeleteFinishedStreamCompleteDelegate;
void OnDeleteFinishedStreamComplete(const FDeleteFinishedStreamResult& Result);
public:
// Start recording a replay from blueprint. ReplayName = Name of file on disk, FriendlyName = Name of replay in UI
UFUNCTION(BlueprintCallable, Category = "Replays")
void StartRecordingReplayFromBP(FString ReplayName, FString FriendlyName);
// Start recording a running replay and save it, from blueprint.
UFUNCTION(BlueprintCallable, Category = "Replays")
void StopRecordingReplayFromBP();
// Start playback for a previously recorded Replay, from blueprint
UFUNCTION(BlueprintCallable, Category = "Replays")
void PlayReplayFromBP(FString ReplayName);
// Start looking for/finding replays on the hard drive
UFUNCTION(BlueprintCallable, Category = "Replays")
void FindReplays();
// Apply a new custom name to the replay (for UI only)
UFUNCTION(BlueprintCallable, Category = "Replays")
void RenameReplay(const FString &ReplayName, const FString &NewFriendlyReplayName);
// Delete a previously recorded replay
UFUNCTION(BlueprintCallable, Category = "Replays")
void DeleteReplay(const FString &ReplayName);
protected:
UFUNCTION(BlueprintImplementableEvent, Category = "Replays")
void BP_OnFindReplaysComplete(const TArray<FS_ReplayInfo> &AllReplays);
GameInstance.cpp:
void U***GameInstance::Init()
{
EnumerateStreamsPtr = FNetworkReplayStreaming::Get().GetFactory().CreateReplayStreamer();
OnEnumerateStreamsCompleteDelegate = FOnEnumerateStreamsComplete::CreateUObject(this, &UZomboyGameInstance::OnEnumerateStreamsComplete);
OnDeleteFinishedStreamCompleteDelegate = FDeleteFinishedStreamCallback::CreateUObject(this, &UZomboyGameInstance::OnDeleteFinishedStreamComplete);
}
void U***GameInstance::OnEnumerateStreamsComplete(const TArray<FNetworkReplayStreamInfo>& StreamInfos)
{
TArray<FS_ReplayInfo> AllReplays;
for (FNetworkReplayStreamInfo StreamInfo : StreamInfos)
{
if (!StreamInfo.bIsLive)
{
AllReplays.Add(FS_ReplayInfo(StreamInfo.Name, StreamInfo.FriendlyName, StreamInfo.Timestamp, StreamInfo.LengthInMS));
}
}
BP_OnFindReplaysComplete(AllReplays); //用于刷新Replays的回调
}
void U***GameInstance::OnDeleteFinishedStreamComplete(const FDeleteFinishedStreamResult& Result)
{
FindReplays();
}
void U***GameInstance::StartRecordingReplayFromBP(FString ReplayName, FString FriendlyName)
{
StartRecordingReplay(ReplayName, FriendlyName);
}
void U***GameInstance::StopRecordingReplayFromBP()
{
StopRecordingReplay();
}
void U***yGameInstance::PlayReplayFromBP(FString ReplayName)
{
PlayReplay(ReplayName);
}
void U***GameInstance::FindReplays()
{
if (EnumerateStreamsPtr)
{
EnumerateStreamsPtr->EnumerateStreams(FNetworkReplayVersion(), FString(), FString(), UpgradeEnumerateStreamsDelegate(OnEnumerateStreamsCompleteDelegate));
}
}
void U***GameInstance::RenameReplay(const FString &ReplayName, const FString &NewFriendlyReplayName)
{
//重命名这里没有按wiki写,直接调了一个函数,代理传的空,可以实现更改FriendlyReplayName,wiki教程里大佬做了个文件的读写操作,老版引擎是可以的,新版有变动不能那样用了
if (EnumerateStreamsPtr)
{
EnumerateStreamsPtr->RenameReplayFriendlyName(ReplayName, NewFriendlyReplayName, nullptr);
}
}
void U***GameInstance::DeleteReplay(const FString &ReplayName)
{
if (EnumerateStreamsPtr)
{
EnumerateStreamsPtr->DeleteFinishedStream(ReplayName, OnDeleteFinishedStreamCompleteDelegate);
}
}
关于Replay文件读写
上述gameinstance中已经写明了保存的.replay文件的Find、Delete、Rename等,依旧是参考wiki教程做的修改,改动都是因为版本更新导致的api更新,变动不算大。
新版本(可能是4.22以后,这里用的4.24)按上述流程StartRecording之后StopRecording后生成的录播文件是.replay格式的单个回放文件,存在项目的Saved/Demos下(如下图)
这里用4.16发现生成的录播文件是文件夹,文件夹下有四个子文件(如下图)
这里放出wiki的重命名函数,他这里应该还是用的较老版本引擎,所以新版不能这么用了,但是从这里能学到一些东西。
大佬先找到了重播文件路径,再使用CreateFileReader读出.replayinfo文件的Json,之后把NewFriendlyReplayName写入,使用CreateFileWriter写入一个新的.replayinfo文件替换掉原来的,相当于替换了四个子文件中的.replayinfo文件,实现了更换重播文件的FriendlyReplayName功能。新版文件只有一个.Replay文件,当然不能这么用了。
void UMyGameInstance::RenameReplay(const FString &ReplayName, const FString &NewFriendlyReplayName) { // Get File Info FNullReplayInfo Info;
{
const FString DemoPath = FPaths::Combine(\*FPaths::GameSavedDir(), TEXT("Demos/"));
const FString StreamDirectory = FPaths::Combine(\*DemoPath, \*ReplayName);
const FString StreamFullBaseFilename = FPaths::Combine(\*StreamDirectory, \*ReplayName);
const FString InfoFilename = StreamFullBaseFilename + TEXT(".replayinfo");
TUniquePtr<FArchive> InfoFileArchive(IFileManager::Get().CreateFileReader(\*InfoFilename));
if (InfoFileArchive.IsValid() && InfoFileArchive->TotalSize() != 0)
{
FString JsonString;
*InfoFileArchive << JsonString;
Info.FromJson(JsonString);
Info.bIsValid = true;
InfoFileArchive->Close();
}
// Set FriendlyName
Info.FriendlyName = NewFriendlyReplayName;
// Write File Info
TUniquePtr<FArchive> ReplayInfoFileAr(IFileManager::Get().CreateFileWriter(\*InfoFilename));
if (ReplayInfoFileAr.IsValid())
{
FString JsonString = Info.ToJson();
*ReplayInfoFileAr << JsonString;
ReplayInfoFileAr->Close();
}
}
PC_ReplaySpectator代码
还需要创建一个回放专用的PlayerController并在GameMode中选上,蓝图内容放在之后。
此PlayerController用于控制回放进度
PC_ReplaySpectator.h:
UCLASS()
class ***_API APC_ReplaySpectator : public APlayerController
{
GENERATED_BODY()
public:
/** we must set some Pause-Behavior values in the actor */
APC_ReplaySpectator(const FObjectInitializer& ObjectInitializer);
protected:
/** for saving Anti-Aliasing and Motion-Blur settings during Pause State */
int32 PreviousAASetting;
int32 PreviousMBSetting;
public:
/** Set the Paused State of the Running Replay to bDoPause. Return new Pause State */
UFUNCTION(BlueprintCallable, Category = "CurrentReplay")
bool SetCurrentReplayPausedState(bool bDoPause);
/** Gets the Max Number of Seconds that were recorded in the current Replay */
UFUNCTION(BlueprintCallable, Category = "CurrentReplay")
int32 GetCurrentReplayTotalTimeInSeconds() const;
/** Gets the Second we are currently watching in the Replay */
UFUNCTION(BlueprintCallable, Category = "CurrentReplay")
int32 GetCurrentReplayCurrentTimeInSeconds() const;
/** Jumps to the specified Second in the Replay we are watching */
UFUNCTION(BlueprintCallable, Category = "CurrentReplay")
void SetCurrentReplayTimeToSeconds(int32 Seconds);
/** Changes the PlayRate of the Replay we are watching, enabling FastForward or SlowMotion */
UFUNCTION(BlueprintCallable, Category = "CurrentReplay")
void SetCurrentReplayPlayRate(float PlayRate = 1.f);
};
PC_ReplaySpectator.cpp:
// Fill out your copyright notice in the Description page of Project Settings.
#include "***/Public/Player/PC_ReplaySpectator.h"
#include "Engine/DemoNetDriver.h"
APC_ReplaySpectator::APC_ReplaySpectator(const FObjectInitializer& ObjectInitializer)
{
bShowMouseCursor = true;
PrimaryActorTick.bTickEvenWhenPaused = true;
bShouldPerformFullTickWhenPaused = true;
}
bool APC_ReplaySpectator::SetCurrentReplayPausedState(bool bDoPause)
{
AWorldSettings* WorldSettings = GetWorldSettings();
// Set MotionBlur off and Anti Aliasing to FXAA in order to bypass the pause-bug of both
static const auto CVarAA = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DefaultFeature.AntiAliasing"));
static const auto CVarMB = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DefaultFeature.MotionBlur"));
if (bDoPause)
{
PreviousAASetting = CVarAA->GetInt();
PreviousMBSetting = CVarMB->GetInt();
// Set MotionBlur to OFF, Anti-Aliasing to FXAA
CVarAA->Set(1);
CVarMB->Set(0);
//WorldSettings->Pauser= PlayerState;
WorldSettings->SetPauserPlayerState(PlayerState);
return true;
}
// Rest MotionBlur and AA
CVarAA->Set(PreviousAASetting);
CVarMB->Set(PreviousMBSetting);
//WorldSettings->Pauser = NULL;
WorldSettings->SetPauserPlayerState(NULL);
return false;
}
int32 APC_ReplaySpectator::GetCurrentReplayTotalTimeInSeconds() const
{
if (GetWorld())
{
if (GetWorld()->DemoNetDriver)
{
return GetWorld()->DemoNetDriver->DemoTotalTime;
}
}
return 0.f;
}
int32 APC_ReplaySpectator::GetCurrentReplayCurrentTimeInSeconds() const
{
if (GetWorld())
{
if (GetWorld()->DemoNetDriver)
{
return GetWorld()->DemoNetDriver->DemoCurrentTime;
}
}
return 0.f;
}
void APC_ReplaySpectator::SetCurrentReplayTimeToSeconds(int32 Seconds)
{
if (GetWorld())
{
if (GetWorld()->DemoNetDriver)
{
GetWorld()->DemoNetDriver->GotoTimeInSeconds(Seconds);
}
}
}
void APC_ReplaySpectator::SetCurrentReplayPlayRate(float PlayRate /*= 1.f*/)
{
if (GetWorld())
{
if (GetWorld()->DemoNetDriver)
{
GetWorld()->GetWorldSettings()->DemoPlayTimeDilation = PlayRate;
}
}
}
蓝图
蓝图方面,需要分别创建GameInstance和PC_ReplaySpectator的BP类,还需要建一个GameMode
在GameMode里面的ReplaySpectatorPlayerController选择创建好的BP_PC_ReplaySpectator
在任意创建的可受控制的类中,或者关卡蓝图中,调用开始录制和停止录制函数,用于录制,注意是在服务器调用,FriendlyName暂时设为Test,按照需求可自行更改
建立"WID_MainMenu" 和 "WID_ReplaySlot"UMG
这里截的教程上的图,要把"ReplayFriendlyName"属性的 “Editable” and "Exposed On Spawn"勾上然后暴露。这样一个重播文件的UI插槽就建好了
WID_MainMenu