一、上周工作问题的解决
a) 观看模式已经成功合并到主项目当中,统一了一些公用变量。
b) 给用户一个准备的时间,准备动作做好之后说出开始指令后再开始学习。
c) 创建一个UserCharacter作为观看视角,设置类默认值locked to HMD为false以取消VR头盔对视角的控制。
二、回放系统测试
FirstPerson模板上的回放系统测试
首先新建一个FirstPerson的blueprint项目,我这里命名为ReplayTest.创建完成后打开ReplayTest/Config/DefaultEngine.ini,在文件最后添加如下语句然后保存。这个语句的作用就是允许使用DemoNetDriver这个录像功能。
[/Script/Engine.GameEngine] +NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver")
然后我们需要将所有Map里所有能移动的actor的Static Mesh Replicate Movement属性设置为true:
完成之后打开blueprint文件“FirstPersonProjectile”,将其类默认值值中的“Replicates”与“Replicate Movement”属性都设为true:
完成后打开blueprint文件“FirstPersonCharacter”,同样将“Replicates”与“Replicate Movement”属性设为true。然后找到“InputAction Fire”这个自定义事件,在该事件旁边创建一个自定义事件命名为“Server_Fire”,然后将该事件的属性设置为如图所示:
然后再在蓝图中做一些改动:
接下来我们需要把UE4中的录制与回放方法提供给蓝图以便我们在蓝图中调用。首先创建一个新的C++类,选择父类为GameInstance:
我这里命名为MyGameInstance,创建后会自动打开Visual Studio,先在项目中找到名为“ReplayTest.Build.cs”的C#文件,(注意第一个.前面的字符串为项目名称),打开该文件并添加语句:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Json" });
添加后的文件内容如下:
然后我们再打开之前创建的MyGameInstance.h文件,首先添加include语句:
#include "NetworkReplayStreaming.h"
注意有些include语句必须放在所有include的最前面或者最后面,如果之后编译出错可以根据错误提示调整其位置。然后我们来添加所需的函数:
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); virtual void Init() override; private: // for FindReplays() TSharedPtr<INetworkReplayStreamer> EnumerateStreamsPtr; FOnEnumerateStreamsComplete OnEnumerateStreamsCompleteDelegate; void OnEnumerateStreamsComplete(const TArray<FNetworkReplayStreamInfo>& StreamInfos); // for DeleteReplays(..) FOnDeleteFinishedStreamComplete OnDeleteFinishedStreamCompleteDelegate; void OnDeleteFinishedStreamComplete(const bool bDeleteSucceeded); protected: UFUNCTION(BlueprintImplementableEvent, Category = "Replays") void BP_OnFindReplaysComplete(const TArray<FS_ReplayInfo> &AllReplays);
最后再在文件开始处添加语句:
USTRUCT(BlueprintType) 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; } };那么MyGameInstance.h文件就已经全部写好了,再贴一下完整代码:
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Engine/GameInstance.h" #include "NetworkReplayStreaming.h" #include "MyGameInstance.generated.h" USTRUCT(BlueprintType) 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; } }; /** * */ UCLASS() class REPLAYTEST_API UMyGameInstance : public UGameInstance { GENERATED_BODY() 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); virtual void Init() override; private: // for FindReplays() TSharedPtr<INetworkReplayStreamer> EnumerateStreamsPtr; FOnEnumerateStreamsComplete OnEnumerateStreamsCompleteDelegate; void OnEnumerateStreamsComplete(const TArray<FNetworkReplayStreamInfo>& StreamInfos); // for DeleteReplays(..) FOnDeleteFinishedStreamComplete OnDeleteFinishedStreamCompleteDelegate; void OnDeleteFinishedStreamComplete(const bool bDeleteSucceeded); protected: UFUNCTION(BlueprintImplementableEvent, Category = "Replays") void BP_OnFindReplaysComplete(const TArray<FS_ReplayInfo> &AllReplays); };
然后我们再来编辑MyGameInstance.cpp文件,首先还是先添加include语句:
#include "Runtime/NetworkReplayStreaming/NullNetworkReplayStreaming/Public/NullNetworkReplayStreaming.h" #include "NetworkVersion.h" #include "MyGameInstance.h"
之后是所需函数:
void UMyGameInstance::Init() { Super::Init(); // create a ReplayStreamer for FindReplays() and DeleteReplay(..) EnumerateStreamsPtr = FNetworkReplayStreaming::Get().GetFactory().CreateReplayStreamer(); // Link FindReplays() delegate to function OnEnumerateStreamsCompleteDelegate = FOnEnumerateStreamsComplete::CreateUObject(this, &UMyGameInstance::OnEnumerateStreamsComplete); // Link DeleteReplay() delegate to function OnDeleteFinishedStreamCompleteDelegate = FOnDeleteFinishedStreamComplete::CreateUObject(this, &UMyGameInstance::OnDeleteFinishedStreamComplete); } void UMyGameInstance::StartRecordingReplayFromBP(FString ReplayName, FString FriendlyName) { StartRecordingReplay(ReplayName, FriendlyName); } void UMyGameInstance::StopRecordingReplayFromBP() { StopRecordingReplay(); } void UMyGameInstance::PlayReplayFromBP(FString ReplayName) { PlayReplay(ReplayName); } void UMyGameInstance::FindReplays() { if (EnumerateStreamsPtr.Get()) { EnumerateStreamsPtr.Get()->EnumerateStreams(FNetworkReplayVersion(), FString(), FString(), OnEnumerateStreamsCompleteDelegate); } } void UMyGameInstance::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); } 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(); } } void UMyGameInstance::DeleteReplay(const FString &ReplayName) { if (EnumerateStreamsPtr.Get()) { EnumerateStreamsPtr.Get()->DeleteFinishedStream(ReplayName, OnDeleteFinishedStreamCompleteDelegate); } } void UMyGameInstance::OnDeleteFinishedStreamComplete(const bool bDeleteSucceeded) { FindReplays(); }MyGameInstance.cpp完整代码:
// Fill out your copyright notice in the Description page of Project Settings. #include "ReplayTest.h" #include "Modules/ModuleManager.h" #include "Runtime/NetworkReplayStreaming/NullNetworkReplayStreaming/Public/NullNetworkReplayStreaming.h" #include "NetworkVersion.h" #include "MyGameInstance.h" IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ReplayTest, "ReplayTest" ); void UMyGameInstance::Init() { Super::Init(); // create a ReplayStreamer for FindReplays() and DeleteReplay(..) EnumerateStreamsPtr = FNetworkReplayStreaming::Get().GetFactory().CreateReplayStreamer(); // Link FindReplays() delegate to function OnEnumerateStreamsCompleteDelegate = FOnEnumerateStreamsComplete::CreateUObject(this, &UMyGameInstance::OnEnumerateStreamsComplete); // Link DeleteReplay() delegate to function OnDeleteFinishedStreamCompleteDelegate = FOnDeleteFinishedStreamComplete::CreateUObject(this, &UMyGameInstance::OnDeleteFinishedStreamComplete); } void UMyGameInstance::StartRecordingReplayFromBP(FString ReplayName, FString FriendlyName) { StartRecordingReplay(ReplayName, FriendlyName); } void UMyGameInstance::StopRecordingReplayFromBP() { StopRecordingReplay(); } void UMyGameInstance::PlayReplayFromBP(FString ReplayName) { PlayReplay(ReplayName); } void UMyGameInstance::FindReplays() { if (EnumerateStreamsPtr.Get()) { EnumerateStreamsPtr.Get()->EnumerateStreams(FNetworkReplayVersion(), FString(), FString(), OnEnumerateStreamsCompleteDelegate); } } void UMyGameInstance::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); } 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(); } } void UMyGameInstance::DeleteReplay(const FString &ReplayName) { if (EnumerateStreamsPtr.Get()) { EnumerateStreamsPtr.Get()->DeleteFinishedStream(ReplayName, OnDeleteFinishedStreamCompleteDelegate); } } void UMyGameInstance::OnDeleteFinishedStreamComplete(const bool bDeleteSucceeded) { FindReplays(); }
那么VS的编辑工作暂时告一段落,我们回到UE4中来使用刚刚写好的函数。首先编译,然后新建一个blueprint类,父类为之前创建的“MyGameInstance”,命名为“BP_MyGameInstance”,打开后在函数栏选择覆盖(override)“BP_OnFindReplaysComplete”:
之后我们会用这个函数来与UI连接显示回放列表。接下来新建一个关卡(map),这里命名“MainMenuMap”,打开项目设置,在“地图&模式”(map&mode)下进行如下设置:
然后打开blueprint文件"FirstPersonCharacter",添加如下节点:
这步完成后我们就实现了最基础的录制功能,可以在独立窗口下运行测试一下:
没有问题的话可以在项目目录下的saved/demos下找到录制文件。接下来我们需要做一个简单的UI界面来显示以及回放我们的录制文件。新建一个空间蓝图(widget)命名为”WID_ReplaySlot“,内容如下:
注意ReplayName与ReplayFriendlyName要进行设置:
接下来再新建一个widget,命名为WID_MainMenu,内容如下:
完成后打开MainMenuMap的关卡蓝图来实现widget:
然后我们再回到BP_MyGameInstance完成之前的BPonFindReplaysComplete函数:
这一步完成后我们就可以再次进行测试,检验回放删除等功能。没有问题的话继续下一步,再次新建一个C++类,父类为”Player Controller“,命名为”PC_ReplaySpectator”。在PC_ReplaySpectator.h的class中添加语句:
public: /** we must set some Pause-Behavior values in the ctor */ 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中添加语句:
#include "Engine/World.h" #include "Engine/DemoNetDriver.h"
APC_ReplaySpectator::APC_ReplaySpectator(const FObjectInitializer& ObjectInitializer) : Super(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; return true; } // Rest MotionBlur and AA CVarAA->Set(PreviousAASetting); CVarMB->Set(PreviousMBSetting); WorldSettings->Pauser = 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) { if (GetWorld()) { if (GetWorld()->DemoNetDriver) { GetWorld()->GetWorldSettings()->DemoPlayTimeDilation = PlayRate; } } }
完成后返回UE4编译。没有问题的话我们再次创建一个widget,命名为“WID_ReplaySpectator”:
对CurrentTime、Slider、MaxTime、PauseText分别添加绑定函数:
注意SliderPickedByUser、CurrentPauseState默认值为False.
事件蓝图:
其中PlayerRateComboBox需要设置:
那么这个控件终于弄好了,最后再创建一个蓝图类,父类为PC_ReplaySpectator,命名为BP_PC_ReplaySpectator:
打开FirstPersonGameMode:
所有工作到这就结束了。