ue4重播回放系统Replay System


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
在这里插入图片描述
在这里插入图片描述

  • 15
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值