Runtime Content Saves & Data Reads

Alright, so this one’s for the slate & programming teams.

Would the team consider implementing a way to read and write in capture data for slate / UI menus to use, without having to go through the content browser? Or, if failing that, would the devs consider allowing for SceneCaptures, Matinee Movie Dumps and/or tiledshots to be stored in the content browser at runtime for slate and native access?

I was looking through FileManager.h and FileHelpers.h based on an old discussion I had with Bob Tellez. There are some functions in there related to file handling, but there doesn’t appear to be much you can really do with the list of functions currently available. You have your basic move/copy stuff and accessor functions for file handling. But there’s nothing really in terms of reading or direct writing to files, or the serialization (and encryption!) of data as might be expected from BasicSaveObject/BasicLoadObject. If it’s handled somewhere else and I’m just unaware, I’d appreciate some insight.

Anyways, I’ll point to an example of how this might be used in practice. in my DataHive project, I wanted to enhance my current save system by getting a snapshot of the current save area and reading them into my Flash save menu*. I would give the saved thumbnails a specific naming convention to distinguish them from other files. In this way, the thumbnail system would be completely procedural. Unfortunately that proved to be unworkable b/c of how UE3’s content packaging system was designed.

Now in terms of implementation, one way is to provide accessor functions for reading in files and doing the usual vetting to check for bad or invalid content. Treat it almost like a content browser import, except this would operate at the native level.

Or if direct reads are too risky, then perhaps allowing for a new and improved screencapture2D, matinee dump, or shot command to save its captures into the content browser at runtime.

The basic premise here would be that you could create a file that is vetted by native architecture, and thus is assured to be clean (or as clean as this architecture can make a file…). All that’s left is a way for slate menus, blueprints, etc. to read in the newly generated capture files.

  • (Yes, I’m aware Scaleform isn’t supported anymore, I’m just talking about the general idea now as it pertains to Slate)

seems very much like what I requested here, point no. 2, to which I was answered this. except I only asked for reading of files, and saving of such files seems needed as well. but if you keep reading further below, you’ll see my intentions for this were for game save thumbnails (yeah, guess who I thought would benefit from it)

btw I still haven’t tried that suggestion (I only know I can call GFileManager->CreateFileWriter() from anywhere, but I still haven’t looked into how to actually use it), so maybe some example would be great.

anyway, looking forward to this answer

ok I tested it out, and I got some limited success. at some function I added the following code:

FArchive* SaveFile = GFileManager->CreateFileWriter(TEXT("test.txt"));
SaveFile->Close();

and the file test.txt was created at Rocket/Engine/Binaries/Win64. of course the file contains nothing, and Intellisense gave me no real hints as to how to use Save->Something() to write something to the file.

That’s exactly my point. Where do you go from there? I looked at Archive.h too earlier before my first post. I don’t see how it comes together, or it doesn’t appear to be far enough along for you to write data line by line, or to otherwise serialize/deserialize it in a specific way.

Yeah, you can access the file, and manipulate it. But what else can you do with it? That’s what I was getting at.

One other thing I’d add is how you might notice how in Archive.h that the FArchive class itself is absent. The FArchive class definition isn’t anywhere to be found… and yet all the readers / writers extend from it. So there may be additional stuff inherent in the FArchive base class, but it’s just not been made available. So that’s another issue there…

SceneCapture2Ds write their data to a UTextureRenderTarget2D (you create in the content browser) which can be used directly in slate by making a brush then using it like so:

Do this once at startup:

FSlateStyle& Style = FSlateStyle::GetInstance();
Style.Set("MyGame.MyCaptureBrush", new FSlateImageBrush(TEXT("texture://Game/SomePath/SomeCaptureRenderTarget2D"), FVector2D(128,128), FLinearColor(1,1,1,1)));

Now just use the brush like so:

SNew(SImage)
.Image(FSlateStyle::GetBrush("MyGame.MyCaptureBrush"))

Matinee movie dumps and tiledshots were not meant to be used progamatically as they just write their output to disk. If you want to write some logic to work with thier output, you may want to start by using one of the techniques below.

Now, since you asked, here is some more general info about saving/loading:

Once you have an FArchive, you can serialize primitive types or UObjects into it by using the << operator like so:

FArchive* SaveFile = GFileManager->CreateFileWriter(TEXT("test.txt"));
if ( SaveFile )
{
    FString MyString = TEXT("TestString");
    *SaveFile << MyString;
    SaveFile->Close();
    delete SaveFile;
    SaveFile = NULL;
}

Loading with an FArchive is similar

FArchive* FileReader = GFileManager->CreateFileReader(TEXT("test.txt"));
if(FileReader != NULL)
{
    FString MyString;
    *FileReader << MyString;
    FileReader->Close();
    delete FileReader;
    FileReader = NULL;
}

As an alternative to dealing with data at this low level, you may want to use the package system. Here is saving.

UPackage* MyDataPackage = CreatePackage(NULL, TEXT("/Temp/MyPackage"));
UMyCoolObject* MyDataObject = NewObject(MyDataPackage);
UPackage::SavePackage(MyDataPackage, MyDataObject, RF_Standalone, FPaths::GameSavedDir() + TEXT("SomeFile.uasset"));

Where MyCoolObject is a class containing all the data you care about. Now here is loading.

UPackage* MyDataPackage = ::LoadPackage( NULL, FPaths::GameSavedDir() + TEXT("SomeFile.uasset"), LOAD_None );
TArray ObjectsInPackage;
GetObjectsWithOuter(MyDataPackage, ObjectsInPackage, false);
if ( ObjectsInPackage.Num() > 0 )
{
    UMyCoolObject* MyDataObject = Cast(ObjectsInPackage[0]);
}

You will want to use the same UPackage returned from LoadPackage when calling UPackage::SavePackage.

Forgive me if there are any syntax errors in the code samples above, I just typed them out in this message.

Finally, you may want some more power over your saved objects. In this case you will want to make a custom asset type, then saving and loading is managed automatically for you. One of the easiest ways to do this is to make your data class extend UDataAsset. Then it can be created in the content browser and saved just like normal packages. If you want to get more involved in making an asset type, you can also make a UFactory class and IAssetTypeActions class to add extra content browser functionality. I do not see any doc pages about creating a new asset type, so I will ping the documentation team about adding one.

Awesome response. I’ll try all of this stuff out. Thanks Bob!

awesome :slight_smile:

I’ve been experimenting with this for a savegame system, much like UDN’s SaveGameState Gem for UDK. so I tried the FArchive way and some other way using TJsonWriterFactory inside the FArchive way (thanks to joat and SolidSnake for their help on IRC), and then the UAsset way.

I decided the UAsset way is better because it’s the less human-readable of the 3, which is the best intended way for my savegame files (and the FArchive way offers no encryption options). The UAsset way also seems much easier to get vars and values off from code.

so I successfully have SomeFile.uasset saved, but then I can’t load it. I get the following warning in my game log:

LogUObjectGlobals:Warning: LoadPackage can't find package SomeFile.uasset

the file exists, it’s as if it wasn’t looking in the right dir or something. I haven’t changed any of the file or package paths from your code, so I’m lost here.

A save game system is currently being worked on, so I wouldn’t suggest writing your own at the moment. If you want to continue with your current code, I would make sure you saved and loaded your asset from the FPaths::GameSavedDir() like so:

FString SaveGamePath = FPaths::Combine(FPaths::GameSavedDir(), TEXT("SomeFile.uasset");

LoadPackage probably has an issue with loading content that is not in one of the valid content roots (Engine/Content, Game/Content, Game/Saved)

at this point it won’t hurt me to finish my implementation, so thanks for the info.

I however can’t use that line of code:

error C2039: 'Combine' : is not a member of 'FPaths'

Sorry about that, I guess FPaths::Combine isn’t in your build yet. You should be safe to use this instead:

FString SaveGamePath = FPaths::GameSavedDir() + TEXT("SomeFile.uasset");

ah, I was struggling to concatenate strings like that just now. thanks!

ok I got it working! thanks!

I’ve updated the answer to include the changes discussed in these replies. Passing in MyDataObject to SavePackage should let it be discovered as an object to be saved, since it doesn’t have the RF_Standalone flag at creation time.

I’ll take a second look at that when I have time.

but for now I wanted to say that whatever I load I get as null. it’s probably useful to mention that what I’m trying to save is a UObject-extending class where I added some custom vars. maybe I need to typecast it?

sorry to bother you again, but I’m getting all zero values while loading, and I’m believe it’s not even saving any of my vars at all. and the reason is that no matter how many vars I populate the generated UAsset file is always the same file size.

for this I have a class USaveGameChar where I’ve declared variables (in this case I’m populating Health with my character’s health). here’s what I’m doing for saving:

ACustomCharacter* SavedPawn = Cast(GEngine->GetFirstLocalPlayerController()->GetPawn());
UPackage* SaveDataPackage = CreatePackage(NULL, TEXT("/Temp/MyPackage"));
USaveGameChar* SaveDataObject = NewObject(SaveDataPackage);
SaveDataObject->Health = SavedPawn->Health;
FString SaveGamePath = FPaths::GameSavedDir() + TEXT("Save.uasset");
bool bSaved = UPackage::SavePackage(SaveDataPackage, SaveDataObject, RF_Standalone, *SaveGamePath);

and loading:

FString SaveGamePath = FPaths::GameSavedDir() + TEXT("Save.uasset");
UPackage* SaveDataPackage = ::LoadPackage( NULL, *SaveGamePath, LOAD_None );
TArray ObjectsInPackage;
GetObjectsWithOuter(SaveDataPackage, ObjectsInPackage, false);
if ( ObjectsInPackage.Num() > 0 )
{
   USaveGameChar* SaveDataObject = Cast(ObjectsInPackage[0]);
   if (SaveDataObject)
   {
      UE_LOG(LogCustomGame, Log, TEXT("LoadChar Health %f "), SaveDataObject->Health); // this logs 0.000000
   }
}

also I found a crash where if you Save after Loading, the game crashes as it complains it can’t delete the file - understandable but IMO shouldn’t cause a crash. it happens if a Save is done within less than 30 seconds after a Load. maybe I need to somehow manually release the handle for the file after loading?

thanks again!

ugh. a similar thing happens if I use the FArchive approach, any vars I load have a value of zero. except here the filesize is indeed changing the more vars I save.

here’s saving:

ACustomCharacter* SavedPawn = Cast(GEngine->GetFirstLocalPlayerController()->GetPawn());
FString SaveGamePath = FPaths::GameSavedDir() + TEXT("Save.txt");
FArchive* SaveFile = GFileManager->CreateFileWriter(*SaveGamePath);
if (SaveFile != NULL)
{
	float Health = SavedPawn->Health;
	UE_LOG(LogCustomGame, Log, TEXT("SaveChar 1 Health %f "), Health); // logs 100.000000
	*SaveFile << Health;

	SaveFile->Close();
	delete SaveFile;
	SaveFile = NULL;
}

and loading:

ACustomCharacter* LoadedPawn = Cast(GEngine->GetFirstLocalPlayerController()->GetPawn());
FString SaveGamePath = FPaths::GameSavedDir() + TEXT("Save.txt");
FArchive* FileReader = GFileManager->CreateFileReader(*SaveGamePath);
if(FileReader != NULL)
{
	float Health;
	*FileReader << Health;
	FileReader->Close();
	delete FileReader;
	FileReader = NULL;

	UE_LOG(LogCustomGame, Log, TEXT("LoadChar %f "), Health); // logs 0.000000
}

ok somehow I got the FArchive approach working “just the same”, my code now saves and loads and it looks the same as what I posted here -_-’

also I got the UAsset approach working as well. I figured my custom class’ variables needed to be defined as UPROPERTY instead of regular vars, else they don’t get saved.

for testing purposes I’m saving 3 vars (the pawn’s Location, Rotation and Health), and the output FArchive text file is 28 bytes in size, while the UAsset file is 630 bytes. also the FArchive text file produces pure encrypted human-unreadable garbage characters, while if I open the UAsset in a text editor the var names are still human-readable.

bottom line: the FArchive produces much smaller and much better encrypted files. so for a custom savegame system the FArchive approach seems to be the winner (in the meantime while UE4 doesn’t have a savegame system, or perhaps if doesn’t suit my needs in the future).

Cool - good effort. That’s an interesting thread. How do you feel about it now (few months later) ? Now there’s the Eventgraph nodes under Function > SaveGame such as Create Save Game Object. And Save Game to Slot… I’d like to know more about all that.

Hi Tom,

The included save game system is similar to the approach mentioned here in that is serialises an object to disk as a series of bytes, much like using the archive system.

The object serialised to disk must be a USaveGame object (a blank class stub that you should extend and fill out yourself).

It’s functional but not great - it doesn’t actually include any functionality to handle things like recreating actors in their entirety, you have to do all this yourself in your own implementation. I’d say it’s only really useful as a starting point or for games that require very limited save information.

If you’re interested in more comprehensive save systems, feel free to send me an e-mail to ambershee@gmail.com if you’d like to discuss your needs, as it could be very helpful with our implementation :slight_smile: - we’re rolling one in house with a view to licensing it to other developers as a plugin in future.