上周做了FirstPerson模板的回放系统,本周我主要在主项目中编写回放系统。遇到了很多问题。还没有完全解决。
1)打开Config/DefaultEngine.ini,添加如下语句保存以允许使用DemoNetDriver:
[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver")
创建一个父类为MyGameInstance的C++类,命名为 MyGameInstance。打开Visual Studio,先打开 项目名.Build.cs 这个C#文件,添加语句:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Json" });
然后完成头文件与CPP文件:
//头文件
// 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 TAICHIPROJECT_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 EnumerateStreamsPtr;
FOnEnumerateStreamsComplete OnEnumerateStreamsCompleteDelegate;
void OnEnumerateStreamsComplete(const TArray& StreamInfos);
// for DeleteReplays(..)
FOnDeleteFinishedStreamComplete OnDeleteFinishedStreamCompleteDelegate;
void OnDeleteFinishedStreamComplete(const bool bDeleteSucceeded);
protected:
UFUNCTION(BlueprintImplementableEvent, Category = "Replays")
void BP_OnFindReplaysComplete(const TArray &AllReplays);
};
//CPP
// Fill out your copyright notice in the Description page of Project Settings.
#include "MyGameInstance.h"
#include "TaiChiProject.h"
#include "Runtime/NetworkReplayStreaming/NullNetworkReplayStreaming/Public/NullNetworkReplayStreaming.h"
#include "NetworkVersion.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& StreamInfos)
{
TArray 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 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 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中生成解决方案。这里我犯了一个错,我选择了重新生成解决方案。这样将整个项目重新编译,因为我的UE是安装版而不是源码版,重新编译的话会导致dll文件丢失,用到的一些插件也会报错。还好有github能回退到之前的版本,但是发现以及问题的过程浪费了很多时间。
MyGameInstance写好后就可以进行测试,在UE中创建一个MyGameInstance的子蓝图类BP_MyGameInstance。将项目的GameInstance设置为BP_MyGameInstance。然后在UserCharacter中调用相关函数:
运行开始时调用StartRecording,通过键盘事件结束录制然后播放。注意要在独立窗口运行。这里又遇到一个问题,根据查看文件目录可以确定录制没有问题,但是播放时场景为空。经过添加一些辅助物体作为参照物,发现原因是播放回放时的旁观者对象spectator生成位置与userCharacter位置不一致,为了解决这个问题我将场景位置做了一个平移以适配spectator的位置。再次测试能够看到场景但是只是静态的,并没有动态的录像。经过论坛上的查询,可能是gamemode中的default pawn class与spectator class不一致的原因。于是我将UserCharacter的父类从pawn改为spectator pawn,将gamemode的default pawn class与spectator class都设为UserCharacter,结果还是不行。我怀疑是不能在园关卡直接回放,两者会造成冲突。于是我新建了一个关卡,专门用来播放回放。然后发现可行。那么按照这个方案的话我需要再额外做一些工作,将录制好的文件复制到名为NewReplay的文件夹中,然后所有文件重命名为NewReplay,这样直接在回放关卡中播放NewReplay就可以了。
先贴一下相关函数的代码:
//头文件的函数声明
UFUNCTION(BlueprintCallable, Category = "Save")
static bool CreateDirectory() ;
UFUNCTION(BlueprintCallable, Category = "Save")
static FString CopyDirectory(FString oldPath);
//CPP中的函数定义
bool USaveToTxt::CreateDirectory()
{
FString Dir = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay"));
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
if (!PlatformFile.DirectoryExists(*Dir))
{
PlatformFile.CreateDirectory(*Dir);
if (!PlatformFile.DirectoryExists(*Dir))
{
return false;
//~~~~~~~~~~~~~~
}
}
return true;
}
FString USaveToTxt::CopyDirectory(FString oldPath)
{
FString AbsoluteSourcePath = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/"));
AbsoluteSourcePath = FPaths::Combine(AbsoluteSourcePath, oldPath);
FString AbsoluteDestinationPath = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay"));
/*if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*AbsoluteSourcePath))
{
return;
}
if (!FPlatformFileManager::Get().GetPlatformFile().MoveFile(*AbsoluteDestinationPath, *AbsoluteSourcePath))
{
}*/
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
PlatformFile.CopyDirectoryTree(*AbsoluteDestinationPath, *AbsoluteSourcePath, true);
FString oldName = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay/"));
oldName = FPaths::Combine(oldName, oldPath);
FString temp = oldPath + ".demo";
FString oldName1 = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay/"), temp);
temp = oldPath + ".header";
FString oldName2 = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay/"), temp);
temp = oldPath + ".replayinfo";
FString oldName3 = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay/"), temp);
temp = oldPath + ".final";
FString oldName4 = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay/"), temp);
FString newName = FPaths::Combine(AbsoluteDestinationPath, TEXT("/newReplay"));
FString newName1 = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay/newReplay.demo"));
FString newName2 = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay/newReplay.header"));
FString newName3 = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay/newReplay.replayinfo"));
FString newName4 = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/newReplay/newReplay.final"));
PlatformFile.MoveFile(*newName1, *oldName1);
PlatformFile.MoveFile(*newName2, *oldName2);
PlatformFile.MoveFile(*newName3, *oldName3);
PlatformFile.MoveFile(*newName4, *oldName4);
FString s= FPaths::Combine(*FPaths::GameSavedDir(), TEXT("Demos/Replay_2018-5-6-20-5/Replay_2018-5-6-20-5.demo"));
FString x = FPaths::Combine(AbsoluteDestinationPath, TEXT("/Replay_2018-5-6-20.demo"));
PlatformFile.MoveFile(*AbsoluteDestinationPath, *AbsoluteSourcePath);
return oldName1;
}
然后解释一下函数的意思。首先需要一个创建一个C++类,我之前写个一个SaveToTxt,这里就继续使用。头文件中要注意UFUNCTION的第一个参数,设为BlueprintPure,生成的蓝图节点不会带有执行引脚。设为BlueprintCallable则会有执行引脚。然后是CPP文件,首先关于UE4的文件操作可以参考:点击打开链接。CreateDirectory() 用来创建NewReplay 文件夹,FPaths::GameSavedDir()用来获取项目文件下的saved目录,注意combine()会默认添加"\"字符,比如combine("newReplay","xxx")的结果就是"newReplay\xxx",如果是combine("newReplay\","xxx")结果还是"newReplay\xxx",也就是说如果你没有自己加上"\",该函数会自己加上,这个问题又是花了很多时间才发现。还有就是moveFile既可以移动文件,也可以起到重命名的作用,具体请看参考链接。
文件操作写完了后,再次进行测试,结果发现还是没有解决问题,没办法只好到官方论坛提问,希望能有人帮忙解决。接下来先把这个问题放下,完成回放系统界面操作。