UnityにおけるPlayerPrefsデータ管理システムの実装

UnityにおけるPlayerPrefsによるデータ保存の基本原理:

PlayerPrefsはUnityエンジンに組み込まれた非常に軽量なローカルデータ保存ソリューションです。以下の実装コードに基づき、その基本的な動作原理は5つの核心点に要約できます:

1. 核心的な保存メカニズム:キーと値のペア

PlayerPrefsの本質は単純な「辞書」構造に似ており、データは常に「キーと値のペア」の形式で保存と検索が行われます。キー: データの唯一の識別子で、文字列形式である必要があります。値: 保存したい具体的なデータ内容です。保存でも読み込みでも、対応する値を操作するにはこの一意のキーを提供する必要があります。

2. サポートされるデータ型

その基層APIは非常にシンプルで、3つの基本的なデータ型のみを直接保存できます:int、float、string。この特性により、複数のデータ型を保存する際には特殊な処理が必要になります。

3. 基層の物理的保存場所(クロスプラットフォームでの差異)

UnityのC#コードでPlayerPrefs.SetInt()のような統一されたAPIを呼び出していても、Unityエンジンは基層でビルド対象プラットフォームに応じて、データを自動的に異なる物理的位置に書き込みます:

Windows (PC): データはシステムのレジストリに書き込まれます。一般的なパスは HKCU\Software\[会社名]\[製品名] です。

macOS: ~/Library/Preferences フォルダ内の.plist設定ファイルに保存されます。

Android: ネイティブのSharedPreferencesメカニズムを使用し、アプリケーションのサンドボックスディレクトリ内に.xmlファイルとして保存されます(通常は /data/data/[パッケージ名]/shared_prefs/ パス下)。

iOS: AppleのUserDefaultsメカニズムを使用し、最終的には.plistファイルとして保存されます。

4. メモリとディスクの相互作用のタイミング

読み込みタイミング: ゲームが起動し、最初にPlayerPrefsにアクセスした際、Unityは対応する位置のデータを一度にすべてメモリに読み込みます。その後の読み込み操作は直接メモリにアクセスするため、読み取り速度が非常に速くなります。

書き込みタイミング: SetIntなどの保存メソッドを呼び出すと、データは一時的にメモリ内の状態のみを変更します。PlayerPrefs.Save()メソッドを明示的に呼び出すか、ゲームが正常にプロセスを終了した場合のみ、Unityが実際にI/O操作を開始し、メモリ内の変更をディスク(レジストリまたはファイル)に永続化書き込みします。

5. 適用シナリオとセキュリティ上の懸念事項

最も適している用途: プレイヤーの基本的な設定(例:音量、言語設定、画質選択)、単純なハイスコア記録、またはチュートリアルが完了したかどうかなどの散在した機密性の低いマークの保存に最適です。

欠点とリスク: すべてのデータは平文で保存されており、暗号化保護は何もありません。プレイヤーはレジストリエディタ(Windows)を開いたり、ローカルのXMLファイルを変更したりすることで容易にデータを改ざんできます(例:金貨の数を強制的に999999に変更)。さらに、無制限に多くのキーを生成すると、レジストリやローカルファイルが異常に大きくなり、パフォーマンス低下や読み込みの遅延を引き起こす可能性があります。これが、多くの商業ゲームが複雑なオブジェクトをJSON、XMLまたはバイナリストリームにシリアライズして保存する理由です。

以下に実装コードを示します

//名前空間の参照
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

Reflectionはこのスクリプトで最も重要なコンポーネントです。リフレクションは、コードが実行時に動的にオブジェクトの型情報(含まれるフィールドやプロパティなど)を取得できるようにする機能で、これによりマネージャーは事前にデータ構造を知らなくても保存と読み取りが可能になります。System.CollectionsIListIDictionaryの処理をサポートするために使用されます。

private static LocalDataManager instance = new LocalDataManager();

public static LocalDataManager Instance
{
    get { return instance; }
}

private LocalDataManager() { }
  • これは古典的な「イーガーモード」シングルトンパターン(クラスロード時にインスタンス化)です。ゲーム全体でLocalDataManagerのインスタンスが1つしか存在しないことを保証し、LocalDataManager.Instanceというグローバルアクセスポイントを提供して、あらゆるスクリプトから保存機能を簡単に呼び出せるようにします。コンストラクタをprivate LocalDataManager()とプライベート化することで、外部からのnewキーワードによる新しいインスタンスの作成を阻止します。
public void StoreData(object data,string dataKey)
{
   Type dataType = data.GetType(); // データオブジェクトの型情報を取得
   FieldInfo[] fields = dataType.GetFields(); // リフレクションを使用して型のすべてのパブリックフィールドを取得
   string fullKey = "";
   for (int i = 0; i < fields.Length; i++)
   {
       FieldInfo field = fields[i];
       // 重複しない一意のキー名を生成し、異なるクラスの同名フィールドの競合を防ぐ
       fullKey = dataKey + "_" + dataType.Name + "_" + field.FieldType.Name + "_" + field.Name;
       
       // field.GetValue(data)は、現在のオブジェクトインスタンス内のフィールドの具体的な値を取得するために使用されます
       StoreFieldValue(field.GetValue(data),fullKey); 
   }
}
  • これは外部に公開された保存メソッドです。保存したいクラスオブジェクト(例:PlayerStats)を渡すだけで、このクラスを「分解」し、すべてのパブリックフィールドを抽出し、各フィールドに対して重複しない長い文字列キーを生成し、値を基層のStoreFieldValueに渡します。
private void StoreFieldValue(object value,string key)
{
    Type valueType = value.GetType();
    // 基本型の処理
    if (valueType == typeof(int)) { PlayerPrefs.SetInt(key,(int)value); }
    else if (valueType == typeof(float)) { PlayerPrefs.SetFloat(key,(float)value); }
    else if (valueType == typeof(string)) { PlayerPrefs.SetString(key,value.ToString()); }
    else if (valueType == typeof(bool)) { PlayerPrefs.SetInt(key,(bool)value ? 1 : 0); }

分析: UnityのPlayerPrefsintfloatstringのみをサポートしています。そのためbool型は、trueの場合は1、falseの場合は0として整数に変換して保存します。

// コレクション型の処理
    else if (typeof(ICollection).IsAssignableFrom(valueType))
    {
        if (value is IList list)
        {
            PlayerPrefs.SetInt(key,list.Count); // まずコレクションの要素数を保存
            int index = 0;
            foreach (object item in list)
            {
                StoreFieldValue(item,key + "_" + index); // 再帰呼び出しでコレクション内の各要素を保存
                ++index;
            }
        }
        else if (value is IDictionary dictionary)
        {
            PlayerPrefs.SetInt(key, dictionary.Count); // 辞書の長さを保存
            int index = 0;
            foreach (object dictKey in dictionary.Keys)
            {
                StoreFieldValue(dictKey, key + "_Key_" + index); // キーを保存
                StoreFieldValue(dictionary[dictKey], key + "_Value_" + index); // 値を保存
                ++index;
            }
        }
    }

分析: IsAssignableFromは、その型がICollectionインターフェースを実装しているかどうかを判断するために使用されます(List<T>、配列など)。保存戦略は、まずキーで要素の総数を保存し、次にコレクションを反復処理し、各要素にインデックスサフィックスを追加して新しいキーを生成し、再帰的にStoreFieldValueを呼び出します。

// カスタムクラス(ネスト)の処理
    else { StoreData(value,key); }
}

分析: データが基本型でもコレクションでもない場合、それはカスタムクラスオブジェクトだと見なされます。コードは前のStoreDataメソッドを再帰的に呼び出し、それをさらに分解します。これは非常に巧妙な相互再帰呼び出しです。

データ読み込み型

public object RetrieveData(Type type,string dataKey)
{
    object data = Activator.CreateInstance(type); // リフレクションを使用して空のオブジェクトをインスタンス化
    FieldInfo[] fields = type.GetFields();
    string loadKey = "";
    FieldInfo field;
    for (int i = 0; i < fields.Length; i++)
    {
        field = fields[i];
        loadKey = dataKey + "_" + type.Name + "_" + field.FieldType.Name + "_" + field.Name;
        field.SetValue(data,RetrieveFieldValue(field.FieldType,loadKey)); 
    }
    return data;
}

分析: Activator.CreateInstanceを使用してメモリ内に空のオブジェクト「殻」を作成し、保存時とまったく同じルールでキー名を連結し、データを抽出してこのオブジェクトに戻し込みます。

private object RetrieveFieldValue(Type fieldType, string key)
{
    if (fieldType == typeof(int)) { return PlayerPrefs.GetInt(key,0); }
    else if (fieldType == typeof(float)) { return PlayerPrefs.GetFloat(key, 0); }
    else if (fieldType == typeof(string)) { return PlayerPrefs.GetString(key, ""); }
    else if (fieldType == typeof(bool)) { return PlayerPrefs.GetInt(key, 0) == 1 ? true : false; }

分析: 型に応じて対応するPlayerPrefs.Getメソッドを呼び出します。

else if (typeof(ICollection).IsAssignableFrom(fieldType))
    {
        if (fieldType.IsGenericType)
        {
            int count = PlayerPrefs.GetInt(key, 0);
            Type listType = typeof(List<>).MakeGenericType(fieldType.GetGenericArguments()[0]);
            IList list = Activator.CreateInstance(listType) as IList;
            for (int i = 0; i < count; i++)
            {
                // fieldType.GetGenericArguments()[0]はList<T>内部のジェネリックTの型を取得するために使用されます
                list.Add(RetrieveFieldValue(fieldType.GetGenericArguments()[0],key + "_" + i));      
            }
            return list;
        }
        else if (typeof(IDictionary).IsAssignableFrom(fieldType))
        {
            int count = PlayerPrefs.GetInt(key, 0);
            Type dictType = typeof(Dictionary<,>).MakeGenericType(fieldType.GetGenericArguments());
            IDictionary dict = Activator.CreateInstance(dictType) as IDictionary;
            for (int i = 0; i < count; i++)
            {
                dict.Add(RetrieveFieldValue(fieldType.GetGenericArguments()[0], key + "_Key_" + i),
                    RetrieveFieldValue(fieldType.GetGenericArguments()[1], key + "_Value_" + i));
            }
            return dict;
        }
    }

分析: Listの読み取りは保存の逆プロセスです。まず長さを読み取り、次にリフレクションを使用してListを作成し、ジェネリックパラメータ型を取得して、ループで再帰的に各要素を読み取りListに追加します。辞書(IDictionary)の読み込みロジックはこれと全く同じです。

完全なコード

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

/// <summary>
/// ローカルデータマネージャクラス - データの保存と読み取りを一元管理
/// </summary>
public class LocalDataManager
{
    private static LocalDataManager instance = new LocalDataManager();

    public static LocalDataManager Instance
    {
        get { return instance; }
    }

    private LocalDataManager() { }
    
    /// <summary>
    /// データを保存
    /// </summary>
    /// <param name="data">保存するデータオブジェクト</param>
    /// <param name="dataKey">データオブジェクトの一意なキー(呼び出し側で制御)</param>
    public void StoreData(object data, string dataKey)
    {
       Type dataType = data.GetType();
       FieldInfo[] fields = dataType.GetFields();
       string fullKey = "";
       for (int i = 0; i < fields.Length; i++)
       {
           FieldInfo field = fields[i];
           fullKey = dataKey + "_" + dataType.Name + "_" + field.FieldType.Name + "_" + field.Name;
           StoreFieldValue(field.GetValue(data), fullKey);
       }
    }

    private void StoreFieldValue(object value, string key)
    {
        Type valueType = value.GetType();
        if (valueType == typeof(int))
        {
            PlayerPrefs.SetInt(key, (int)value);
        }
        else if (valueType == typeof(float))
        {
            PlayerPrefs.SetFloat(key, (float)value);
        }
        else if (valueType == typeof(string))
        {
            PlayerPrefs.SetString(key, value.ToString());
        }
        else if (valueType == typeof(bool))
        {
            PlayerPrefs.SetInt(key, (bool)value ? 1 : 0);
        }
        else if (typeof(ICollection).IsAssignableFrom(valueType))
        {
            if (value is IList list)
            {
                PlayerPrefs.SetInt(key, list.Count);
                int index = 0;
                foreach (object item in list)
                {
                    StoreFieldValue(item, key + "_" + index);
                    ++index;
                }
            }
            else if (value is IDictionary dictionary)
            {
                PlayerPrefs.SetInt(key, dictionary.Count);
                int index = 0;
                foreach (object dictKey in dictionary.Keys)
                {
                    StoreFieldValue(dictKey, key + "_Key_" + index);
                    StoreFieldValue(dictionary[dictKey], key + "_Value_" + index);
                    ++index;
                }
            }
        }
        else
        {
            StoreData(value, key);
        }
    }
    
    /// <summary>
    /// データを読み込む
    /// </summary>
    /// <param name="dataType">読み込むデータの型情報</param>
    /// <param name="dataKey">データオブジェクトの一意なキー(呼び出し側で制御)</param>
    /// <returns>読み込まれたデータオブジェクト</returns>
    public object RetrieveData(Type dataType, string dataKey)
    {
        object data = Activator.CreateInstance(dataType);
        FieldInfo[] fields = dataType.GetFields();
        string loadKey = "";
        FieldInfo field;
        for (int i = 0; i < fields.Length; i++)
        {
            field = fields[i];
            loadKey = dataKey + "_" + dataType.Name + "_" + field.FieldType.Name + "_" + field.Name;
            field.SetValue(data, RetrieveFieldValue(field.FieldType, loadKey));
        }
        return data;
    }
    
    /// <summary>
    /// 単一の値を取得するメソッド
    /// </summary>
    /// <param name="fieldType">フィールドの型</param>
    /// <param name="key">データを取得するためのキー</param>
    /// <returns>取得したデータ</returns>
    private object RetrieveFieldValue(Type fieldType, string key)
    {
        if (fieldType == typeof(int))
        {
            return PlayerPrefs.GetInt(key, 0);
        }
        else if (fieldType == typeof(float))
        {
            return PlayerPrefs.GetFloat(key, 0);
        }
        else if (fieldType == typeof(string))
        {
            return PlayerPrefs.GetString(key, "");
        }
        else if (fieldType == typeof(bool))
        {
            return PlayerPrefs.GetInt(key, 0) == 1 ? true : false;
        }
        else if (typeof(ICollection).IsAssignableFrom(fieldType))
        {
            if (fieldType.IsGenericType)
            {
                int count = PlayerPrefs.GetInt(key, 0);
                Type listType = typeof(List<>).MakeGenericType(fieldType.GetGenericArguments()[0]);
                IList list = Activator.CreateInstance(listType) as IList;
                for (int i = 0; i < count; i++)
                {
                    list.Add(RetrieveFieldValue(fieldType.GetGenericArguments()[0], key + "_" + i));      
                }
                return list;
            }
            else if (typeof(IDictionary).IsAssignableFrom(fieldType))
            {
                int count = PlayerPrefs.GetInt(key, 0);
                Type dictType = typeof(Dictionary<,>).MakeGenericType(fieldType.GetGenericArguments());
                IDictionary dict = Activator.CreateInstance(dictType) as IDictionary;
                for (int i = 0; i < count; i++)
                {
                    dict.Add(RetrieveFieldValue(fieldType.GetGenericArguments()[0], key + "_Key_" + i),
                        RetrieveFieldValue(fieldType.GetGenericArguments()[1], key + "_Value_" + i));
                }
                return dict;
            }
        }
        return RetrieveData(fieldType, key);  
    }
}

タグ: Unity PlayerPrefs データ管理 リフレクション シリアライズ

7月30日 18:43 投稿