目次 - SDL 3.0 API(機能別) - ストレージ

ストレージ

概要

ストレージAPIは低レベルAPI(SDLの場合ならばファイルシステムI/Oストリーム)を使用する場合に生じる移植性の問題を抽象化するために設計された高レベルAPIである. このAPIはいくつかの理由で一般的なファイルシステムAPIと比べて厳格になっている:

  1. アクセスの対象: ファイルシステムAPIのよくある落とし穴は, ストレージは全て同じとみなしているところである. しかし, 他の多くの環境(特にゲーム機)では, アクセスするファイルシステムの種類についてより厳格である. 例えば, ゲームデータとユーザデータは通常は, 全く異なる性質の2つのストレージに分けられている. (そして, 例レベルAPIもまったく異なる可能性がある.)
  2. アクセスの方法: 別のよくある間違いは, アプリケーションは全てのストレージが書き込み可能と見なしている点である. 繰り返しになるが, 多くの環境ではゲームデータとユーザデータは2つのストレージに分けられていて, 書き込み可能なのはユーザデータのみで, ゲームデータは読み込み専用である.
  3. アクセスのタイミング: ファイルシステムへのアクセスの移植性の問題のほとんどは, タイミングに関するものである――ストレージデバイスは常にいつでもアクセスできるとみなしてはならないし, 特定のデバイスに対してどれだけ長くアクセスしてもよいとみなしてはならない.

次の例について考えてみる:

void ReadGameData(void)
{
    extern char** fileNames;
    extern size_t numFiles;
    for (size_t i = 0; i < numFiles; i += 1) {
        FILE *data = fopen(fileNames[i], "rwb");
        if (data == NULL) {
            // 何か問題が発生した!
        } else {
            // ここで様々な処理を行う
            fclose(data);
        }
    }
}

void ReadSave(void)
{
    FILE *save = fopen("saves/save0.sav", "rb");
    if (save == NULL) {
        // 何か問題が発生した!
    } else {
        // ここで様々な処理を行う
        fclose(save);
    }
}

void WriteSave(void)
{
    FILE *save = fopen("saves/save0.sav", "wb");
    if (save == NULL) {
        // 何か問題が発生した!
    } else {
        // ここで様々な処理を行う
        fclose(save);
    }
}

もう一度問題点を箇条書きする:

  1. アクセスの対象: このコードはファイルシステム全体にアクセスしている. ゲームデータとセーブデータは全てカレントディレクトリ(ここはゲームをインストールしたフォルダかもしれないし, そうではないかもしれない)にあるとみなしている.
  2. アクセスの方法: このコードはゲームデータのパスが書き込み可能と見なしていて, セーブデータもゲームデータと同じ場所に書き込んでる.
  3. アクセスのタイミング: このコードは, ファイルシステムは常にアクセス可能で, アクセスの時間制限もないとみなしている.

この思い込みのせいで, ファイルシステムに関するコードは移植性が低く, 次のような場合に失敗する:

SDL_Storageを使うと, これらの問題につまづくことはなくなる:

void ReadGameData(void)
{
    extern char** fileNames;
    extern size_t numFiles;

    SDL_Storage *title = SDL_OpenTitleStorage(NULL, 0);
    if (title == NULL) {
        // 何か問題が発生した!
    }
    while (!SDL_StorageReady(title)) {
        SDL_Delay(1);
    }

    for (size_t i = 0; i < numFiles; i += 1) {
        void* dst;
        Uint64 dstLen = 0;

        if (SDL_GetStorageFileSize(title, fileNames[i], &dstLen) && dstLen > 0) {
            dst = SDL_malloc(dstLen);
            if (SDL_ReadStorageFile(title, fileNames[i], dst, dstLen)) {
                // ここで様々な処理を行う
            } else {
                // 何か問題が発生した!
            }
            SDL_free(dst);
        } else {
            // 何か問題が発生した!
        }
    }

    SDL_CloseStorage(title);
}

void ReadSave(void)
{
    SDL_Storage *user = SDL_OpenUserStorage("libsdl", "Storage Example", 0);
    if (user == NULL) {
        // 何か問題が発生した!
    }
    while (!SDL_StorageReady(user)) {
        SDL_Delay(1);
    }

    Uint64 saveLen = 0;
    if (SDL_GetStorageFileSize(user, "save0.sav", &saveLen) && saveLen > 0) {
        void* dst = SDL_malloc(saveLen);
        if (SDL_ReadStorageFile(user, "save0.sav", dst, saveLen)) {
            // ここで様々な処理を行う
        } else {
            // 何か問題が発生した!
        }
        SDL_free(dst);
    } else {
        // 何か問題が発生した!
    }

    SDL_CloseStorage(user);
}

void WriteSave(void)
{
    SDL_Storage *user = SDL_OpenUserStorage("libsdl", "Storage Example", 0);
    if (user == NULL) {
        // 何か問題が発生した!
    }
    while (!SDL_StorageReady(user)) {
        SDL_Delay(1);
    }

    extern void *saveData; // ここで様々な処理を行う...
    extern Uint64 saveLen;
    if (!SDL_WriteStorageFile(user, "save0.sav", saveData, saveLen)) {
        // 何か問題が発生した!
    }

    SDL_CloseStorage(user);
}

SDL_Storageによる改善点は:

  1. アクセスの対象: このコードは明確に関数の結果に基づいたタイトルまたはユーザストレージデバイスから読み込んでいる.
  2. アクセスの方法: このコードは明確に関数の結果に基づいて読み込みと書き込みの関数を使用している.
  3. アクセスのタイミング: このコードは明確に必要なときデバイスをオープンし, ファイルシステムでの作業が終了したときクローズしている.

その結果として, アプリケーションは環境とそのファイルシステムの増大する要求に対してより堅牢になった.

パブリックにアクセス可能なSDL_Storageバックエンドの例はSteamクラウドバックエンドである――プログラムを開始するとSteamworksを初期化できる. その後, SDLはSteamworksが初期化されたと認識し, アプリケーションがユーザストレージをオープンするとき自動的にISteamRemoteStorageを使用する. さらに重要なのは, ストレージをオープンするとファイルシステムの「バッチ」操作が始まったと認識され, ストレージをクローズすると終了したと認識されバッチが掃き出されることである. これはSteamでDynamic Cloud Syncに対応するために使用される. ユーザは自分のPCにデータを保存し, デバイスをスリープさせ, 全てのデバイスでセーブデータを完全に同期させて別のPCでゲームを続けることができるため, プログラムを再スタートさせずに途切れることなくプレーすることができる.

正しいパスについて

ストレージAPIの全てのパスは, UNIXスタイルの区切り文字('/')を使用する. パスで異なる区切り文字を使用すると, その環境の下層が受け入れる場合でも, 動作しない. これはストレージAPIのストレージ間と環境間の移植性の維持と, アプリケーションのコードをシンプルにするためである.

ストレージAPIでは相対ディレクトリ("."と"..")は使用できない.

全ての正しいUTF-8文字列(NUL文字が終端でパスが'/'で区切られている)はファイル名として使用できるが, 下層のストレージが対応していないため, その名前でのファイルの生成が拒否される場合もある.

関数

  1. SDL_CloseStorage
  2. SDL_CopyStorageFile
  3. SDL_CreateStorageDirectory
  4. SDL_EnumerateStorageDirectory
  5. SDL_GetStorageFileSize
  6. SDL_GetStoragePathInfo
  7. SDL_GetStorageSpaceRemaining
  8. SDL_GlobStorageDirectory
  9. SDL_OpenFileStorage
  10. SDL_OpenStorage
  11. SDL_OpenTitleStorage
  12. SDL_OpenUserStorage
  13. SDL_ReadStorageFile
  14. SDL_RemoveStoragePath
  15. SDL_RenameStoragePath
  16. SDL_StorageReady
  17. SDL_WriteStorageFile

  1. SDL_Storage

構造体

  1. SDL_StorageInterface

SDL Wikiへのリンク

SDL3/CategoryStorage - SDL Wiki