从云端检索游戏的特定数据

Epic Games技术客户经理Rajen Kishna |
2021年11月30日
上一篇文章中,我们设置了游戏服务身份验证,现在我们可以开始使用作品存储服务安全地从云端检索游戏的特定数据。我们将介绍:
 

作品存储与玩家数据存储的对比

Epic在线服务提供两种加密的云端数据存储方式:作品存储玩家数据存储。加密和解谜将使用由你指定的加密密钥完成,因此,云端永远无法读取文件。作品存储可用于在云端集中存储数据,供任何玩家访问。作品存储数据是通过开发人员门户保存的,但这些数据可使用EOS SDK API检索。例如,假设你想在不更新游戏的情况下将配置数据提供给玩家,这将很有用。

玩家数据存储,顾名思义,是针对每个玩家的,可在玩家登录的任何设备上通过API进行保存和检索。它可用于实现云存储(甚至可以跨越多个平台)。下一篇文章将重点介绍玩家数据存储。

使用限制

一般来说,为了确保向所有用户提供稳定可靠的生态系统,Epic在线服务存在一定的服务使用限制。部分服务更是有额外的使用限制,而作品存储服务就是其中之一。撰写本文时,存在下列限制,但请参阅文档了解最新信息:
 
  • 部署中所有文件的尺寸总计不超过10GB
  • 部署中文件的总数不超过100个

更改我们的客户端策略

在我们的示例应用程序中调用作品存储接口API前,首先必须调整我们的客户端策略,以允许对该服务的访问:
 
  1. 访问https://dev.epicgames.com/portal/登录开发者门户。
  2. 在左侧菜单中导航到你的产品,进入“产品设置”,在“产品设置”界面中点击“客户端”选项卡。
  3. 在你使用的客户端策略旁边点击三点图标,然后点击“详情”。
 
Developer Portal Client Policy Details
客户端策略详情
 
  1. 向下滚动至“功能”,点击“产品存储”旁的开关按钮。
    • 注意,右侧将显示“允许的操作”列表,我们可以单独勾选其中的选项,为客户端的行为提供更精细的访问权限。
  2. 勾选“列出”和“访问”操作旁的复选框,我们将实现这两个功能。
  3. 点击“保存并退出”。
 
Developer Portal Client Policy Title Storage
作品存储客户端策略允许的功能和操作

在开发人员门户中设置文件

下一步是在开发者门户中设置文件。如前所述,作品存储API仅用于查询和检索数据,所以上传和管理文件是通过门户网站完成的:
 
  1. 在左侧菜单中导航到你的产品,进入“游戏服务”>“作品存储”。
  2. 我们可以在这里了解一些信息:
    • 之前添加的文件(以及文件计数)。目前,界面中可能会显示“找不到文件”。
    • 10GB配额中的已使用量。
    • 当前查看的部署。在这里,你可能只会看到“Release in Live Sandbox”,因为这是默认部署,但如果添加了更多部署,可在这里进行选择。
    • 用于添加新文件的按钮。
    • 指向文档的链接。
 
Developer Portal Title Storage
开发人员门户中的作品存储设置
 
  1. 点击“添加新文件”按钮添加新文件。

在弹出窗口中,你会看到一个提示输入加密密钥的屏幕。这串密钥应为256位的十六进制字符串,由你使用64个字符(仅包含数字0-9和字母A-F)定义。我们将在代码中使用这串密钥,从而通过SDK加密和解密。你必须知道,Epic不会存储这串密钥,并且加密和解密时必须使用它,所以请务必记住它。为了方便起见,弹出窗口中还提供了将密钥保存为txt文件的选项。
 
  1. 出于演示目的,我们将使用以下加密密钥,但在生产环境中使用SDK时,请务必生成一个安全的密钥:1111111111111111111111111111111111111111111111111111111111111111
  2. 点击“下一步”移动到文件细节设置。

在这里,我们可以为文件指定名称(最多64个字符),添加标记(当通过API查询文件时将很有用),上传实际的文件,或指定覆盖任何现有的文件。 

标记用于同时查询多个文件,并帮助你避免在游戏中硬编码特定的文件名。你可以在作品存储中为文件添加多个标记,因此,如果你想查询所有可用的文件,只需为所有文件添加一个通用标记,并查询该标记。

覆盖是指我们希望根据特定的PUID、身份提供商ID(如Epic账户ID)或EOS客户端使用该文件特定版本的情况。例如,你可以用它为游戏客户端和受信任的服务器提供特定的文件,而不需要维护单独的文件和代码/配置差异。另一个例子是,如果你为Android和PC使用了不同的EOS客户端,它们将拥有各自的文件版本。
 
  1. 由于我们只是要创建用于示例应用程序的文件,可以输入以下内容:
    • 文件名:test_file
    • 添加标记:test_tag(输入后点击“添加”)
    • 上传文件:可以是任何文件,你可以创建一个简单的txt文件,其中包含几个单词文本
    • 覆盖:不作改动
  2. 点击“保存并关闭”提交。
 
Developer Portal Title Storage Test File
向作品存储添加新文件
 
  1. 最后,再重复一次步骤6和步骤7:
    • 加密密钥:1111111111111111111111111111111111111111111111111111111111111111
    • 文件名:another_test
    • 标记:test_tag
    • 添加标记:second_tag
    • 上传文件:任何文件
    • 覆盖:不作改动

我们将使用这两个文件,通过不同的标记来验证我们的查询逻辑。你现在应该看到在作品存储设置界面中显示了两个文件。
 
Developer Portal Title Storage Two Files Added
添加到作品存储中的两个文件

在SDK初始化中添加EncryptionKey和CacheDirectory

在开发人员门户中设置策略和文件后,我们可以开始实现代码了。我们首先需要将加密密钥(已在前一小节中定义)和缓存目录的位置添加到SDK初始化代码中,让它知道如何加密和解密文件,以及使用哪个文件夹缓存下载。

作品存储和玩家数据存储将被缓存在指定缓存目录下的独立子文件夹中,所以在SDK初始化过程中,我们只需要设置一个目录。如果缓存中已存在某个文件(MD5哈希值相同),在后续的读取请求中,只会再次从缓存中获取该文件的内容,而不会从云端重新下载。
 
  1. 打开ApplicationSettings.cs,并添加以下两个成员。注意,我们将使用默认的Temp目录作为缓存目录,但你可以将它替换为你的应用程序有权读写的任何文件夹。

public string EncryptionKey = "1111111111111111111111111111111111111111111111111111111111111111";
public string CacheDirectory = Path.GetTempPath();

 
  1. 打开MainWindow.xaml.cs,定位到options变量的初始化部分,在“IsServer = false”后添加以下内容:

,
EncryptionKey = App.Settings.EncryptionKey,
CacheDirectory = App.Settings.CacheDirectory


我们可以运行该应用程序,确保没有错误,但在功能上还没有任何变化。

按名称或标记查询文件

  1. 在Views文件夹中创建名为“TitleStorageView”的新用户控件:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>

    <StackPanel Grid.Column="1">
        <TextBox x:Name="TitleStorageFileNameTextBox" Margin="2" TextChanged="TitleStorageFileNameTextBox_TextChanged" Text="{Binding TitleStorageFileName, UpdateSourceTrigger=PropertyChanged}" />
        <Button Width="100" Height="23" Margin="2" Content="Query file" Command="{Binding TitleStorageQueryFile}" />
        <TextBox x:Name="TitleStorageTagTextBox" Margin="2" TextChanged="TitleStorageTagTextBox_TextChanged" Text="{Binding TitleStorageTag, UpdateSourceTrigger=PropertyChanged}" />
        <Button Width="100" Height="23" Margin="2" Content="Query by tag" Command="{Binding TitleStorageQueryFileList}" />
        <Button Width="100" Height="23" Margin="2" Content="Download file" Command="{Binding TitleStorageReadFile}" />
    </StackPanel>

    <ListView x:Name="TitleStorageFilesListView" Grid.Column="0" Margin="2" ItemsSource="{Binding TitleStorageFiles}" SelectedItem="{Binding SelectedTitleStorageFile}" SelectionChanged="TitleStorageFilesListView_SelectionChanged">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Filename" Width="150" DisplayMemberBinding="{Binding Filename}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
                <GridViewColumn Header="MD5Hash" Width="250" DisplayMemberBinding="{Binding MD5Hash}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
                <GridViewColumn Header="FileSizeBytes" Width="100" DisplayMemberBinding="{Binding FileSizeBytes}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
                <GridViewColumn Header="UnencryptedDataSizeBytes" Width="150" DisplayMemberBinding="{Binding UnencryptedDataSizeBytes}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
            </GridView>
        </ListView.View>
    </ListView>
</Grid>

 
  • 我们将使用ListView显示从查询中返回的文件
  • 我们有两个带有按钮的TextBox,用于按名称或标记查询文件
  • 最后,我们还有一个按钮,用于下载在ListView中选择的文件
 
  1. 打开TitleStorageView.xaml.cs,连接ViewModel,并添加UI事件句柄:

public partial class TitleStorageView : UserControl
{
    public TitleStorageViewModel ViewModel { get { return ViewModelLocator.TitleStorage; } }

    public TitleStorageView()
    {
        InitializeComponent();
        DataContext = ViewModel;
    }

    private void TitleStorageFilesListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
    }

    private void TitleStorageFileNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        ViewModel.TitleStorageQueryFile.RaiseCanExecuteChanged();
    }

    private void TitleStorageTagTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        ViewModel.TitleStorageQueryFileList.RaiseCanExecuteChanged();
    }
}

 
  1. 在ViewModels文件夹中添加TitleStorageViewModel.cs类:

public class TitleStorageViewModel : BindableBase
{
    private ObservableCollection<FileMetadata> _titleStorageFiles;
    public ObservableCollection<FileMetadata> TitleStorageFiles
    {
        get { return _titleStorageFiles; }
        set { SetProperty(ref _titleStorageFiles, value); }
    }

    private FileMetadata _selectedTitleStorageFile;
    public FileMetadata SelectedTitleStorageFile
    {
        get { return _selectedTitleStorageFile; }
        set { SetProperty(ref _selectedTitleStorageFile, value); }
    }

    private string _titleStorageFileName;
    public string TitleStorageFileName
    {
        get { return _titleStorageFileName; }
        set { SetProperty(ref _titleStorageFileName, value); }
    }

    private string _titleStorageTag;
    public string TitleStorageTag
    {
        get { return _titleStorageTag; }
        set { SetProperty(ref _titleStorageTag, value); }
    }
}

 
  1. 在ViewModelLocator.cs中添加对TitleStorageViewModel的引用:

private static TitleStorageViewModel _titleStorage;
public static TitleStorageViewModel TitleStorage
{
    get { return _titleStorage ??= new TitleStorageViewModel(); }
}

 
  1. 在Services文件夹中添加TitleStorageService.cs类来保存查询逻辑:

public static class TitleStorageService
{
    public static void QueryFile(string fileName)
    {
        var queryFileOptions = new QueryFileOptions()
        {
            LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
            Filename = fileName
        };

        ViewModelLocator.Main.StatusBarText = $"Querying title storage file <{queryFileOptions.Filename}>...";

        App.Settings.PlatformInterface.GetTitleStorageInterface()
.QueryFile(queryFileOptions, null, (QueryFileCallbackInfo queryFileCallbackInfo) =>
        {
            Debug.WriteLine($"QueryFile {queryFileCallbackInfo.ResultCode}");

            if (queryFileCallbackInfo.ResultCode == Result.Success)
            {
                var copyFileMetadataByFilenameOptions = new CopyFileMetadataByFilenameOptions()
                {
                    LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
                    Filename = fileName
                };
                var result = App.Settings.PlatformInterface.GetTitleStorageInterface()
.CopyFileMetadataByFilename(copyFileMetadataByFilenameOptions, out var metadata);

                if (result == Result.Success)
                {
                    ViewModelLocator.TitleStorage.TitleStorageFiles.Add(metadata);
                }
            }

            ViewModelLocator.Main.StatusBarText = string.Empty;
        });
    }

    public static void QueryFileList(string tag)
    {
        var queryFileListOptions = new QueryFileListOptions()
        {
            LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
            ListOfTags = new string[] { tag }
        };

        ViewModelLocator.Main.StatusBarText = $"Querying title storage files by tag <{tag}>...";

        App.Settings.PlatformInterface.GetTitleStorageInterface()
.QueryFileList(queryFileListOptions, null, (QueryFileListCallbackInfo queryFileListCallbackInfo) =>
        {
            Debug.WriteLine($"QueryFileList {queryFileListCallbackInfo.ResultCode}");

            if (queryFileListCallbackInfo.ResultCode == Result.Success)
            {
                for (uint i = 0; i < queryFileListCallbackInfo.FileCount; i++)
                {
                    var copyFileMetadataAtIndexOptions = new CopyFileMetadataAtIndexOptions()
                    {
                        Index = i,
                        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId)
                    };
                    var result = App.Settings.PlatformInterface.GetTitleStorageInterface()
.CopyFileMetadataAtIndex(copyFileMetadataAtIndexOptions, out var metadata);

                    if (result == Result.Success)
                    {
                        ViewModelLocator.TitleStorage.TitleStorageFiles.Add(metadata);
                    }
                }
            }

            ViewModelLocator.Main.StatusBarText = string.Empty;
        });
    }
}

   
  1. 在Commands文件夹中添加TitleStorageQueryFileCommand.cs类:

public class TitleStorageQueryFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return !string.IsNullOrWhiteSpace(ViewModelLocator.Main.ProductUserId) && !string.IsNullOrWhiteSpace(ViewModelLocator.TitleStorage
.TitleStorageFileName);
    }

    public override void Execute(object parameter)
    {
        ViewModelLocator.TitleStorage.TitleStorageFiles = new ObservableCollection<FileMetadata>();
        TitleStorageService.QueryFile(ViewModelLocator.TitleStorage
.TitleStorageFileName);
    }
}

 
  1. 在Commands文件夹中添加TitleStorageQueryFileListCommand.cs类:

public class TitleStorageQueryFileListCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return !string.IsNullOrWhiteSpace(ViewModelLocator.Main.ProductUserId) && !string.IsNullOrWhiteSpace(ViewModelLocator
.TitleStorage.TitleStorageTag);
    }

    public override void Execute(object parameter)
    {
        ViewModelLocator.TitleStorage.TitleStorageFiles = new ObservableCollection<FileMetadata>();
        TitleStorageService.QueryFileList(ViewModelLocator.TitleStorage
.TitleStorageTag);
    }
}

 
  1. 打开TitleStorageViewModel.cs声明并实例化两个命令:

public TitleStorageQueryFileCommand TitleStorageQueryFile { get; set; }
public TitleStorageQueryFileListCommand TitleStorageQueryFileList { get; set; }

public TitleStorageViewModel()
{
    TitleStorageQueryFile = new TitleStorageQueryFileCommand();
    TitleStorageQueryFileList = new TitleStorageQueryFileListCommand();
}

 
  1. 在ViewModelLocator.cs的RaiseConnectCanExecuteChanged()方法中添加以下两行,确保只有在通过连接接口成功登录后,才能调用作品存储API:

TitleStorage.TitleStorageQueryFile.RaiseCanExecuteChanged();
TitleStorage.TitleStorageQueryFileList.RaiseCanExecuteChanged();

 
  1. 最后,在MainWindow.xaml中添加TitleStorageView:

<TabItem x:Name="TitleStorage" Header="Title Storage">
    <views:TitleStorageView />
</TabItem>


现在,当我们运行应用程序,并通过身份验证接口或连接接口进行身份验证时,我们可以导航到“Title Storage”选项卡,按文件名“test_file”或“another_test”进行查询。你还可以按标记查询,注意,如果使用“test_tag”,这两个文件都会返回。
 
App Title Storage Queried Files
按标记查询的作品存储文件

读取文件

现在,我们将实现从云存储中读取文件。实现方法将根据游戏需求而有所不同,但我们现在要简单地实现将文件下载到缓存目录(即Temp目录):
 
  1. 在TitleStorageService.cs中添加以下方法:

public static void ReadFile(FileMetadata fileMetadata)
{
    var readFileOptions = new ReadFileOptions()
    {
        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
        Filename = fileMetadata.Filename,
        ReadChunkLengthBytes = 1048576,
        ReadFileDataCallback = (ReadFileDataCallbackInfo readFileDataCallbackInfo) =>
        {
            using var fs = new FileStream($"{App.Settings.CacheDirectory}{readFileDataCallbackInfo.Filename}", FileMode.Append, FileAccess.Write);
            fs.Write(readFileDataCallbackInfo.DataChunk, 0, readFileDataCallbackInfo.DataChunk.Length);
            return ReadResult.RrContinuereading;
        },
        FileTransferProgressCallback = (FileTransferProgressCallbackInfo fileTransferProgressCallbackInfo) =>
        {
            var percentComplete = (double)fileTransferProgressCallbackInfo.BytesTransferred / (double)fileTransferProgressCallbackInfo.TotalFileSizeBytes * 100;
            ViewModelLocator.Main.StatusBarText = $"Downloading file <{fileTransferProgressCallbackInfo.Filename}> ({System.Math.Ceiling.(percentComplete)}%)...";
        }
    };

    ViewModelLocator.Main.StatusBarText = $"Downloading file <{readFileOptions.Filename}> (creating buffer)...";

    var fileTransferRequest = App.Settings.PlatformInterface.GetTitleStorageInterface().ReadFile(readFileOptions, null, (ReadFileCallbackInfo readFileCallbackInfo) =>
    {
        Debug.WriteLine($"ReadFile {readFileCallbackInfo.ResultCode}");

        if (readFileCallbackInfo.ResultCode == Result.Success)
        {
            Debug.WriteLine($"Successfully downloaded {readFileCallbackInfo.Filename} to {App.Settings.CacheDirectory}.");
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }
    });

    if (fileTransferRequest == null)
    {
        Debug.WriteLine("Error downloading file: bad handle");
        ViewModelLocator.Main.StatusBarText = string.Empty;
    }
}
 
  • 正如这篇文档中所述,TitleStorage.ReadFile按块读取文件。在TitleStorage.ReadFileOptions中,我们指定了一个文件块大小,以及一个用于读取数据的回调方法和一个用于报告进度的回调方法。
  • 我们要使用Temp文件夹(默认为C:\Users\\AppData\Local\Temp\或%Temp%)存储文件。
 
  1. 在Commands文件夹中添加TitleStorageReadFileCommand.cs类:

public class TitleStorageReadFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return ViewModelLocator.TitleStorage.SelectedTitleStorageFile != null;
    }

    public override void Execute(object parameter)
    {
        TitleStorageService.ReadFile(ViewModelLocator.TitleStorage
.SelectedTitleStorageFile);
    }
}

 
  1. 打开TitleStorageViewModel.cs,声明并实例化新命令:

public TitleStorageQueryFileCommand TitleStorageQueryFile { get; set; }
public TitleStorageQueryFileListCommand TitleStorageQueryFileList { get; set; }
public TitleStorageReadFileCommand TitleStorageReadFile { get; set; }

public TitleStorageViewModel()
{
    TitleStorageQueryFile = new TitleStorageQueryFileCommand();
    TitleStorageQueryFileList = new TitleStorageQueryFileListCommand();
    TitleStorageReadFile = new TitleStorageReadFileCommand();
}

 
  1. 最后,打开TitleStorageView.xaml.cs,在TitleStorageFilesListView_SelectionChanged()方法中添加以下内容:

ViewModel.TitleStorageReadFile.RaiseCanExecuteChanged();

现在,当我们运行应用程序、验证身份、查询并下载文件后,该文件将存储于磁盘的%Temp%目录中。在本例中,我们可以重命名“test_file”或“another_test”,添加.txt文件扩展名,这样就能打开该文件并查看其中内容。
 
App Title Storage Downloaded File
通过作品存储服务下载的文件

获取代码

在下方获取本文的代码。请按照GitHub仓库中的使用说明设置下载的代码。
我们讨论了作品存储和玩家数据存储之间的区别,在下一篇文章中,我们将使用玩家数据存储实现存储和检索特定于玩家的数据。

你可以在系列目录中找到包含本系列所有文章的完整列表,如果你对本文有任何疑问或反馈,请前往社区论坛告知我们。

    你的成功就是我们的成功

    Epic秉承开放、整合的游戏社区理念。通过免费向所有人提供在线服务,我们致力于帮助更多开发者服务好他们自己的玩家社区。