플레이어 전용 데이터 저장 및 가져오기

에픽게임즈 테크니컬 어카운트 매니저 Rajen Kishna |
2021년 12월 7일
EOS 시작하기의 이전 연재글에서는 타이틀 스토리지를 사용하여 클라우드에서 게임 전용 데이터를 가져오는 방법에 대해 알아봤습니다. 타이틀 스토리지플레이어 데이터 스토리지의 차이점에 대해 다뤘으므로, 이번에는 플레이어 데이터 스토리지를 사용하여 플레이어 데이터를 저장하고 가져오는 방법에 대해 알아봅니다. 플레이어 데이터 스토리지에는 플레이어 권한이 있다는 점에 유의하시기 바랍니다. 따라서 플레이어의 조작이 부정 행위로 이어질 수 있는 상황(예: 인벤토리 관리)에서는 사용하지 않아야 합니다. 이 글에서는 다음과 같은 내용을 다룹니다.
 

플레이어 데이터 스토리지 vs. 에픽게임즈 스토어 클라우드 저장

플레이어 데이터 스토리지에 대해 알아보기 전에, 관련 기능인 에픽게임즈 스토어(EGS)의 클라우드 저장 기능에 대해 먼저 알아보겠습니다. EGS 클라우드 저장 기능은 에픽게임즈 스토어용으로 직접 환경설정되어 있고 에픽게임즈 런처를 통해 관리되므로 API 구현이 필요하지 않습니다.

두 가지 구현 모두 클라우드에 플레이어 저장 데이터를 저장하는 데 사용할 수 있지만, 플레이어 데이터 스토리지는 게임이 배포되는 스토어에 관계없이 모든 플랫폼에서 모든 파일을 저장하고 가져오는 데 사용할 수 있어 더욱 유연합니다. 플레이어 데이터 스토리지를 사용하면 모든 플레이어를 위한 크로스 세이브(cross-save) 기능을 구현할 수 있습니다. 두 구현의 차이점에 대한 자세한 내용은 이 문서를 참고하세요.

클라이언트 정책 변경하기

모든 서비스와 마찬가지로 클라이언트 정책에 적절한 작업을 추가해야 합니다.
  1. https://dev.epicgames.com/portal/에서 개발자 포털에 로그인합니다.
  2. 왼쪽 메뉴에서 '제품 세팅(Product Settings)'으로 이동하여 '제품 세팅' 화면에서 '클라이언트(Clients)' 탭을 클릭합니다.
  3. 사용할 클라이언트 정책(Client Policy) 옆에 있는 점 세 개 모양의 아이콘을 클릭하고 '세부사항(Details)'을 클릭합니다.
  4. 스크롤을 내려 '기능(Features)'으로 이동하여 '플레이어 데이터 스토리지(Player Data Storage)' 옆에 있는 토글 버튼을 클릭합니다.
  5. '액세스(access)', '삭제(delete)', '복사(copy)' 기능을 모두 구현할 것이기 때문에 세 동작 옆에 있는 각각의 상자를 체크합니다.
  6. '저장 및 종료(Save & Exit)'를 클릭하여 확인합니다.
Developer Portal Client Policy Player Data Storage
플레이어 데이터 스토리지 클라이언트 정책에서 허용되는 기능 및 작업

파일 쿼리하기 및 메타데이터 가져오기

타이틀 스토리지에 대한 이전 연재글을 아직 읽지 않았다면 'SDK 초기화, EncryptionKey 및 CacheDirectory' 섹션에서 파일을 암호화하고 캐시하는 방법을 읽어 보세요. 해당 섹션을 모두 읽은 것으로 간주하고 파일 쿼리하기부터 시작하겠습니다.
  1. Views 폴더에서 PlayerDataStorageView라는 새로운 사용자 컨트롤을 생성합니다.

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

    <StackPanel Grid.Column="1">
        <Button Width="100" Height="23" Margin="2" Content="Query files" Command="{Binding PlayerDataStorageQueryFileList}" />
        <Button Width="100" Height="23" Margin="2" Content="Upload file" Command="{Binding PlayerDataStorageWriteFile}" />
        <StackPanel Orientation="Horizontal">
            <Button Width="100" Height="23" Margin="2" Content="Download file" Command="{Binding PlayerDataStorageReadFile}" />
            <Button Width="100" Height="23" Margin="2" Content="Duplicate file" Command="{Binding PlayerDataStorageDuplicateFile}" />
        </StackPanel>
        <Button Width="100" Height="23" Margin="2" Content="Delete file" Command="{Binding PlayerDataStorageDeleteFile}" />
    </StackPanel>

    <ListView x:Name="PlayerDataStorageFilesListView" Grid.Column="0" Margin="2" ItemsSource="{Binding PlayerDataStorageFiles}" SelectedItem="{Binding SelectedPlayerDataStorageFile}" SelectionChanged="PlayerDataStorageFilesListView_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="LastModifiedTime" Width="175" DisplayMemberBinding="{Binding LastModifiedTime}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
                <GridViewColumn Header="FileSizeBytes" Width="75" 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가 있는 간단한 UI와 파일을 쿼리, 업로드, 다운로드, 복제 및 삭제하는 버튼이 있습니다.
 
  1. PlayerDataStorageView.xaml.cs를 열고 DataContext와 자리표시자 이벤트 핸들러를 설정합니다.

public partial class PlayerDataStorageView : UserControl
{
    public PlayerDataStorageViewModel ViewModel { get { return ViewModelLocator.PlayerDataStorage; } }

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

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

 
  1. PlayerDataStorageViewModel.cs 클래스를 ViewModels 폴더에 추가합니다.

public class PlayerDataStorageViewModel : BindableBase
{
    private ObservableCollection<FileMetadata> _playerDataStorageFiles;
    public ObservableCollection<FileMetadata> PlayerDataStorageFiles
    {
        get { return _playerDataStorageFiles; }
        set { SetProperty(ref _playerDataStorageFiles, value); }
    }

    private FileMetadata _selectedPlayerDataStorageFile;
    public FileMetadata SelectedPlayerDataStorageFile
    {
        get { return _selectedPlayerDataStorageFile; }
        set { SetProperty(ref _selectedPlayerDataStorageFile, value); }
    }
}

 
  1. ViewModelLocator.cs에서 PlayerDataStorageViewModel에 레퍼런스를 추가합니다.

private static PlayerDataStorageViewModel _playerDataStorage;
public static PlayerDataStorageViewModel PlayerDataStorage
{
    get { return _playerDataStorage ??= new PlayerDataStorageViewModel(); }
}

 
  1. 쿼리 로직을 저장하기 위해 Services 폴더에 PlayerDataStorageService.cs 클래스를 추가합니다.

public static class PlayerDataStorageService
{
    public static void QueryFileList()
    {
        var queryFileListOptions = new QueryFileListOptions()
        {
            LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId)
        };

        ViewModelLocator.Main.StatusBarText = "Querying player data storage file list...";

        App.Settings.PlatformInterface.GetPlayerDataStorageInterface()
.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.GetPlayerDataStorageInterface()
.CopyFileMetadataAtIndex(copyFileMetadataAtIndexOptions, out var metadata);

                    if (result == Result.Success)
                    {
                        ViewModelLocator.PlayerDataStorage.PlayerDataStorageFiles
.Add(metadata);
                    }
                }
            }

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

 
  • 본 시리즈의 다른 연재글을 보셨다면, 비슷한 패턴이 보일 것입니다. 즉, Options 구조체를 초기화한 다음 PlayerDataStorage.QueryFileList로 전달합니다.
  • 다음, PlayerDataStorage.CopyFileMetadataAtIndex를 호출하고 반환된 파일마다 모든 메터데이터를 가져오도록 패턴을 반복​​​​합니다.
 
  1. PlayerDataStorageQueryFileListCommand.cs 클래스를 Commands 폴더에 추가합니다.

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

    public override void Execute(object parameter)
    {
        ViewModelLocator.PlayerDataStorage.PlayerDataStorageFiles = new ObservableCollection<FileMetadata>();
        PlayerDataStorageService.QueryFileList();
    }
}

 
  1. PlayerDataStorageViewModel.cs를 열고 다음 명령을 선언하여 인스턴스화합니다.

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
}

 
  1. 연결 인터페이스를 통해 성공적으로 로그인한 후에만 쿼리할 수 있도록 ViewModelLocator.cs에서 RaiseConnectCanExecuteChanged() 메서드에 다음 줄을 추가합니다.

PlayerDataStorage.PlayerDataStorageQueryFileList.RaiseCanExecuteChanged();
 
  1. 마지막으로, MainWindow.xaml에서 TabControl에 PlayerDataStorageView를 추가합니다.

<TabItem x:Name="PlayerDataStorage" Header="Player Data Storage">
    <views:PlayerDataStorageView />
</TabItem>


이제 인증 후 앱을 실행하고 플레이어 데이터 스토리지 파일을 쿼리하면 로그에서 다음과 같은 내용을 확인할 수 있습니다.

[Warning] LogEOSPlayerDataStorage - Querying file failed, got 0 results.
QueryFileList NotFound


아직 아무 파일도 업로드하지 않았기 때문에 이렇게 보이는 것이 정상입니다. 이제 다음 단계에서 파일을 업로드해 보겠습니다.

파일 업로드하기

파일을 업로드하는 로직은 경우에 따라 다를 수 있지만, 아래에서는 이 API를 구현하는 방법에 대한 일반적인 방식에 대해 설명합니다.
 
  1. PlayerDataStorageService.cs에 다음 메서드를 추가합니다.

public static void WriteFile(OpenFileDialog openFileDialog)
{
    var bytesWritten = 0;

    var writeFileOptions = new WriteFileOptions()
    {
        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
        Filename = openFileDialog.SafeFileName,
        ChunkLengthBytes = 10485760,
        WriteFileDataCallback = (WriteFileDataCallbackInfo writeFileDataCallbackInfo, out byte[] buffer) =>
        {
            using var fs = new FileStream($"{openFileDialog.FileName}", FileMode.Open, FileAccess.Read);
            if (fs.Length > bytesWritten)
            {
                var readBytes = new byte[System.Math.Min(writeFileDataCallbackInfo.DataBufferLengthBytes, fs.Length)];
                fs.Seek(bytesWritten, SeekOrigin.Begin);
                bytesWritten += fs.Read(readBytes, 0, (int)System.Math.Min(writeFileDataCallbackInfo
.DataBufferLengthBytes, fs.Length));
                buffer = readBytes;
            }
            else
            {
                buffer = new byte[0];
                return WriteResult.CompleteRequest;
            }
            return WriteResult.ContinueWriting;
        },
        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 = $"Uploading file <{writeFileOptions.Filename}> (creating buffer)...";

    var fileTransferRequest = App.Settings.PlatformInterface
.GetPlayerDataStorageInterface().WriteFile(writeFileOptions, null, (WriteFileCallbackInfo writeFileCallbackInfo) =>
    {
        Debug.WriteLine($"WriteFile {writeFileCallbackInfo.ResultCode}");

        if (writeFileCallbackInfo.ResultCode == Result.Success)
        {
            ViewModelLocator.PlayerDataStorage
.PlayerDataStorageQueryFileList.Execute(null);
            Debug.WriteLine($"Successfully uploaded {writeFileCallbackInfo.Filename}.");
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }
        else
        {
            Debug.WriteLine($"Error uploading {writeFileCallbackInfo.Filename}: {writeFileCallbackInfo.ResultCode}.");
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }    
    });

    if (fileTransferRequest == null)
    {
        Debug.WriteLine("Error uploading file: bad handle");
        ViewModelLocator.Main.StatusBarText = string.Empty;
    }
}

 
  • 이 패턴은 타이틀 스토리지에서 파일을 읽기 위해 구현한 것과 유사합니다. 파일을 청크로 업로드하고 진행 상태를 보고하는 로직을 저장하는 WriteFileOptions를 초기화합니다.
  • PlayerDataStorage.WriteFile을 사용하여 실제 업로드를 시작하고, Success 외에 아래에서 설명된 사용 제한을 일으키는 다른 결과가 발생하는지 관찰합니다.
  1. Commands 폴더에 PlayerDataStorageWriteFileCommand.cs 클래스를 추가합니다.

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

    public override void Execute(object parameter)
    {
        OpenFileDialog openFileDialog = new OpenFileDialog();
        if (openFileDialog.ShowDialog() == true)
        {
            PlayerDataStorageService.WriteFile(openFileDialog);
        }
    }
}

 
  1. PlayerDataStorageViewModel.cs를 열고 새로운 명령을 선언하여 인스턴스화합니다.

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }
public PlayerDataStorageWriteFileCommand PlayerDataStorageWriteFile { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
    PlayerDataStorageWriteFile = new PlayerDataStorageWriteFileCommand();
}

 
  1. 마지막으로, ViewModelLocator.cs를 열고 RaiseConnectCanExecuteChanged() 메서드에 다음을 추가합니다.

PlayerDataStorage.PlayerDataStorageWriteFile.RaiseCanExecuteChanged();

이제 앱을 실행하고 아래에 추가로 나열된 사용 제한 사항을 고려하여 파일을 업로드할 수 있습니다. 파일이 업로드되면 코드가 쿼리를 트리거하고 ListView에 새로운 파일이 표시됩니다.
App Player Data Storage Added Files
플레이어 데이터 스토리지에 업로드된 파일

개발자 포털로 이동하여 해당 제품 > '게임 서비스(Game Services)' > '플레이어 데이터 스토리지(Player Data Storage)'으로 이동한 후 앱 UI에서 PUID를 복사하여 플레이어를 찾는 방법도 있습니다. 여기에서 이 사용자의 파일 목록과 현재 및 최대 사용량을 확인할 수 있습니다.
Developer Portal Player Data Storage Added Files
에픽게임즈 개발자 포털에 업로드된 파일

파일 읽기

다음은 클라우드에서 파일을 읽는 단계로, 타이틀 스토리지에서 진행했던 작업과 유사합니다. 여기에서도 로직은 대체로 동일합니다.
  1. PlayerDataStorageService.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.ContinueReading;
        },
        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
.GetPlayerDataStorageInterface().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;
    }
}

 
  • 파일을 청크로 다운로드하고 진행 상태를 보고하는 로직을 저장하는 ReadFileOptions를 초기화합니다.
  • PlayerDataStorage.ReadFile을 사용하여 구성된 CacheDirectory 다운로드를 시작합니다.
 
  1. Commands 폴더에 PlayerDataStorageReadFileCommand.cs 클래스를 추가합니다.

public class PlayerDataStorageReadFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return ViewModelLocator.PlayerDataStorage.SelectedPlayerDataStorageFile != null;
    }

    public override void Execute(object parameter)
    {
        PlayerDataStorageService.ReadFile(ViewModelLocator
.PlayerDataStorage.SelectedPlayerDataStorageFile);
    }
}

 
  1. PlayerDataStorageViewModel.cs를 열고 새로운 명령을 선언하여 인스턴스화합니다.

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }
public PlayerDataStorageWriteFileCommand PlayerDataStorageWriteFile { get; set; }
public PlayerDataStorageReadFileCommand PlayerDataStorageReadFile { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
    PlayerDataStorageWriteFile = new PlayerDataStorageWriteFileCommand();
    PlayerDataStorageReadFile = new PlayerDataStorageReadFileCommand();
}

 
  1. 마지막으로, PlayerDataStorageView.xaml.cs를 열고 PlayerDataStorageFilesListView_SelectionChanged() 메서드에 다음을 추가합니다.

ViewModel.PlayerDataStorageReadFile.RaiseCanExecuteChanged();

파일을 쿼리한 후에는 ListView에서 파일 중 하나를 선택하고 'Download file' 버튼을 클릭하여 클라우드에서 파일을 가져올 수 있습니다. 다운로드가 완료되었음을 나타내는 출력이 '디버그' 창에 다음과 같이 표시됩니다.

ReadFile Success
Successfully downloaded test_file.txt to C:\Users\<User>\AppData\Local\Temp\.

파일 복제하기

다운로드하거나 업로드하지 않고도 플레이어의 스토리지 공간에 있는 파일을 복제하고 싶을 때가 있습니다. 이 경우 복제 API를 사용하면 편리합니다.
  1. PlayerDataStorageService.cs에 다음 메서드를 추가합니다.

public static void DuplicateFile(FileMetadata fileMetadata)
{
    var duplicateFileOptions = new DuplicateFileOptions()
    {
        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
        SourceFilename = fileMetadata.Filename,
        DestinationFilename = $"{fileMetadata.Filename}_(copy)"
    };

    ViewModelLocator.Main.StatusBarText = $"Copying <{duplicateFileOptions.SourceFilename}> as <{duplicateFileOptions.DestinationFilename}>...";

    App.Settings.PlatformInterface.GetPlayerDataStorageInterface()
.DuplicateFile(duplicateFileOptions, null, (DuplicateFileCallbackInfo duplicateFileCallbackInfo) =>
    {
        Debug.WriteLine($"DuplicateFile {duplicateFileCallbackInfo.ResultCode}");

        if (duplicateFileCallbackInfo.ResultCode == Result.Success)
        {
            ViewModelLocator.PlayerDataStorage
.PlayerDataStorageQueryFileList.Execute(null);
            ViewModelLocator.Main.StatusBarText = "Successfully copied file.";
        }
        else
        {
            Debug.WriteLine("Copying file failed: " + duplicateFileCallbackInfo.ResultCode);
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }
    });
}

   
  1. Commands 폴더에 PlayerDataStorageDuplicateFileCommand.cs 클래스를 추가합니다.

public class PlayerDataStorageDuplicateFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return ViewModelLocator.PlayerDataStorage.SelectedPlayerDataStorageFile != null;
    }

    public override void Execute(object parameter)
    {
        PlayerDataStorageService.DuplicateFile(ViewModelLocator
.PlayerDataStorage.SelectedPlayerDataStorageFile);
    }
}

 
  1. PlayerDataStorageViewModel.cs를 열고 새로운 명령을 선언하여 인스턴스화합니다.

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }
public PlayerDataStorageWriteFileCommand PlayerDataStorageWriteFile { get; set; }
public PlayerDataStorageReadFileCommand PlayerDataStorageReadFile { get; set; }
public PlayerDataStorageDuplicateFileCommand PlayerDataStorageDuplicateFile { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
    PlayerDataStorageWriteFile = new PlayerDataStorageWriteFileCommand();
    PlayerDataStorageReadFile = new PlayerDataStorageReadFileCommand();
    PlayerDataStorageDuplicateFile = new PlayerDataStorageDuplicateFileCommand();
}

 
  1. 마지막으로, PlayerDataStorageView.xaml.cs를 열고 PlayerDataStorageFilesListView_SelectionChanged() 메서드에 다음을 추가합니다.

ViewModel.PlayerDataStorageDuplicateFile.RaiseCanExecuteChanged();

ListView에서 파일을 선택하고 'Duplicate file' 버튼을 누르면 동일한 이름 뒤에 '_(copy)'가 추가된 새 파일이 표시됩니다.
App Player Data Storage Duplicated File
플레이어 데이터 스토리지 클라우드 스토리지에서 파일 복제하기

파일 삭제하기

마지막으로 구현할 것은 파일을 삭제하는 기능입니다.
  1. PlayerDataStorageService.cs에 다음 메서드를 추가합니다.

public static void DeleteFile(FileMetadata fileMetadata)
{
    var deleteFileOptions = new DeleteFileOptions()
    {
        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
        Filename = fileMetadata.Filename
    };

    ViewModelLocator.Main.StatusBarText = $"Deleting <{deleteFileOptions.Filename}>...";

    App.Settings.PlatformInterface.GetPlayerDataStorageInterface()
.DeleteFile(deleteFileOptions, null, (DeleteFileCallbackInfo deleteFileCallbackInfo) =>
    {
        Debug.WriteLine($"DeleteFile {deleteFileCallbackInfo.ResultCode}");

        if (deleteFileCallbackInfo.ResultCode == Result.Success)
        {
            ViewModelLocator.PlayerDataStorage
.PlayerDataStorageQueryFileList.Execute(null);
            ViewModelLocator.Main.StatusBarText = "Successfully deleted file.";
        }
        else
        {
            Debug.WriteLine("Deleting file failed: " + deleteFileCallbackInfo.ResultCode);
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }
    });
}

 
  • PlayerDataStorage.DeleteFile API는 매우 간단합니다. 파일 이름을 입력으로 받은 다음, 파일이 영구적으로 삭제됩니다.
  1. Commands 폴더에 PlayerDataStorageDeleteFileCommand.cs 클래스를 추가합니다.

public class PlayerDataStorageDeleteFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return ViewModelLocator.PlayerDataStorage.SelectedPlayerDataStorageFile != null;
    }

    public override void Execute(object parameter)
    {
        PlayerDataStorageService.DeleteFile(ViewModelLocator
.PlayerDataStorage.SelectedPlayerDataStorageFile);
    }
}

 
  1. PlayerDataStorageViewModel.cs를 열고 새로운 명령을 선언하여 인스턴스화합니다.

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }
public PlayerDataStorageWriteFileCommand PlayerDataStorageWriteFile { get; set; }
public PlayerDataStorageReadFileCommand PlayerDataStorageReadFile { get; set; }
public PlayerDataStorageDuplicateFileCommand PlayerDataStorageDuplicateFile { get; set; }
public PlayerDataStorageDeleteFileCommand PlayerDataStorageDeleteFile { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
    PlayerDataStorageWriteFile = new PlayerDataStorageWriteFileCommand();
    PlayerDataStorageReadFile = new PlayerDataStorageReadFileCommand();
    PlayerDataStorageDuplicateFile = new PlayerDataStorageDuplicateFileCommand();
    PlayerDataStorageDeleteFile = new PlayerDataStorageDeleteFileCommand();
}

 
  1. 마지막으로, PlayerDataStorageView.xaml.cs를 열고 PlayerDataStorageFilesListView_SelectionChanged() 메서드에 다음을 추가합니다.

ViewModel.PlayerDataStorageDeleteFile.RaiseCanExecuteChanged();

이제 ListView에서 파일을 선택한 후 'Delete file' 버튼으로 파일을 삭제할 수 있습니다.

사용 제한

플레이어 데이터 스토리지에는 모든 사용자의 신뢰성과 가용성을 보장하기 위해 몇 가지 사용 제한이 있습니다. 이 연재글을 작성하는 시점에는 다음과 같은 제한 사항이 존재합니다. 이와 관련된 최신 정보를 알아보려면 이 문서를 참고하세요.
  • 분당 읽기 또는 쓰기 요청 1,000회
  • 개별 파일 크기 최대 200MB
  • 사용자당 총 스토리지 공간 400MB
  • 사용자당 최대 파일 개수 1,000개

최대 개별 파일 크기 제한인 200MB를 초과하는 파일 업로드를 시도하는 경우 PlayerDataStorage.WriteFile 호출이 실패하며 PlayerDataStorageFileSizeTooLarge 결과를 반환합니다. 사용자당 총 스토리지 공간이 400MB를 초과하면 PlayerDataStorage.WriteFile에 대한 추가 호출이 실패하고, 해당 사용자의 제한 상태를 나타내는 PlayerDataStorageUserThrottled 결과를 반환합니다. 이 사용자의 스토리지 공간은 파일이 충분히 제거되고 스토리지 공간 활용도가 400MB 미만이 될 때까지 제한된 상태로 유지됩니다. 스토리지 정리를 위해 제한된 상태에서 파일을 쿼리하고, 파일 메타데이터를 가져오고, 파일을 삭제할 수 있습니다.

코드 다운로드

아래 링크를 통해 이 연재글에서 사용된 코드를 받을 수 있습니다. 다운로드한 코드를 설정하려면 GitHub Repository의 사용 지침을 따르세요.
타이틀 스토리지와 플레이어 데이터 스토리지의 차이점에 대해 알아봤습니다. 다음 연재글에서는 플레이어 데이터 스토리지를 사용하여 플레이어 전용 데이터를 저장하고 가져오는 것을 구현해 보겠습니다.

전체 연재글은 시리즈 참조에서 확인할 수 있습니다. 질문이나 피드백은 커뮤니티 포럼에 올려주세요.

    여러분의 성공이 곧 에픽의 성공입니다.

    에픽은 개방되고 통합된 게이밍 커뮤니티를 지향합니다. 에픽 온라인 서비스를 누구에게나 무료로 제공함으로써 더 많은 개발자들이 플레이어 커뮤니티에 더 나은 서비스를 제공할 수 있도록 지원하고자 합니다.