본문 바로가기
Utils

Unity, PlayerPrefs 커스텀 클래스 (오버라이딩)

by Client. DJ 2023. 6. 1.
반응형

위와 같이 바뀐다.

유니티 제공 PlayerPrefs 한계

유니티에서 기본 제공하는 PlayerPrefs 클래스는 간단하게 사용이 가능하지만, 대체적으로 사용하는데 아래와 같은 불편함이 있습니다.

  1. 한정된 클래스 타입으로 string, int, float만 제공합니다.
  2. 보안에 취약합니다. 해킹 이슈에 노출되어있습니다.
  3. 파일 로드의 개념이지만 캐싱을 따로 하고 있지 않습니다.

위와 같은 문제를 해결하려면 다른 방법이 필요하기에 PlayerPrefs를 글로벌 클래스로 수정하여 사용하는 방식입니다. 근본적으로는 PlayerPrefs의 기능을 쓰지만, 다양한 타입에 사용이 가능하고, Serialize 및 base64 알고리즘를 통해서 보안을 좀 더 강화했습니다. 알고리즘을 추가하여 본인이 원하면 수정도 가능합니다. 또한, 내부적으로 Dictionary를 통해 관리를 하고 씬 전환 때마다 메모리를 정리하도록 하였습니다.

스크립트

PlayerPrefs.cs

using System;
using System.IO;
using System.Collections.Generic;

/// <summary>
/// 파일 저장 클래스
/// </summary>
public static class PlayerPrefs // UnityEngine.PlayerPrefs Overriding
{
    /// <summary>
    /// Dictionary 관리
    /// </summary>
    private static Dictionary<string, object> memory = new();

    static PlayerPrefs() 
    {
        // 씬이 전환될 때마다 메모리 정리 이벤트 발생
        UnityEngine.SceneManagement.SceneManager.sceneLoaded += (scene, mode) => Release();
    }

    /// <summary>
    /// 데이터 불러오기
    /// </summary>
    /// <typeparam name="T">데이터 타입</typeparam>
    /// <param name="name">이름</param>
    /// <returns>데이터 반환</returns>
    public static T Load<T>(string name, T defaultValue = default)
    {
        if (!RegisterKey(name)) Save(name, defaultValue);
        if (!memory.TryGetValue(name, out var data))
        {
            data = FileLoad<T>(name);
            memory.Add(name, data);
        }
        return (T)data;
    }

    /// <summary>
    /// 데이터 저장하기
    /// </summary>
    /// <typeparam name="T">데이터 타입</typeparam>
    /// <param name="name">이름</param>
    /// <param name="data">데이터</param>
    public static void Save<T>(string name, T data)
    {
        if (!memory.ContainsKey(name)) memory.Add(name, null);
        memory[name] = data;
        FileSave(name, data);
    }

    /// <summary>
    /// 메모리 정리
    /// </summary>
    public static void Release() => memory.Clear();

    /// <summary>
    /// 키 등록 여부
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    private static bool RegisterKey(string key) => HasKey(key);

    #region ## File I/O ##
    /// <summary>
    /// 파일 데이터 불러오기
    /// </summary>
    /// <typeparam name="T">데이터 타입</typeparam>
    /// <param name="name">이름</param>
    /// <returns>데이터 반환</returns>
    private static T FileLoad<T>(string name)
    {
        if (UnityEngine.PlayerPrefs.HasKey(name))
        {
            var loadData = UnityEngine.PlayerPrefs.GetString(name);
            if (loadData != null)
            {
                byte[] bytes = Convert.FromBase64String(loadData);
                using (var ms = new MemoryStream(bytes))
                {
                    object obj = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter().Deserialize(ms);
                    return (T)obj;
                }
            }
        }
        return default;
    }

    /// <summary>
    /// 파일 데이터 저장
    /// </summary>
    /// <typeparam name="T">데이터 타입</typeparam>
    /// <param name="name">이름</param>
    /// <param name="data">데이터</param>
    private static void FileSave<T>(string name, T data)
    {
        if (data != null)
        {
            using (var ms = new MemoryStream())
            {
                new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter().Serialize(ms, data);
                UnityEngine.PlayerPrefs.SetString(name, Convert.ToBase64String(ms.ToArray()));
            }
        }
        else
        {
            UnityEngine.PlayerPrefs.SetString(name, null);
        }
        UnityEngine.PlayerPrefs.Save();
    }
    #endregion

    #region ## PlayerPrefs original method ##
    public static void SetInt(string key, int value) => Save(key, value);
    public static int GetInt(string key, int defaultValue = 0) => Load(key, defaultValue);
    public static void SetFloat(string key, float value) => Save(key, value);
    public static float GetFloat(string key, float defaultValue = 0f) => Load(key, defaultValue);
    public static void SetString(string key, string value) => Save(key, value);
    public static string GetString(string key, string defaultValue = "") => Load(key, defaultValue);
    public static bool HasKey(string key) => UnityEngine.PlayerPrefs.HasKey(key);
    public static void DeleteKey(string key)
    {
        UnityEngine.PlayerPrefs.DeleteKey(key);
        memory.Remove(key);
    }
    public static void DeleteAll()
    {
        UnityEngine.PlayerPrefs.DeleteAll();
        memory.Clear();
    }
    public static void Save() => UnityEngine.PlayerPrefs.Save();
    #endregion
}

글로벌 클래스로 제작되었기 때문에 UnityEngine.PlayerPrefs는 가려집니다.

예제

private void Start()
{
    List<int> list = new List<int>();

    // 저장
    PlayerPrefs.Save("Numbers", list);

    // 불러오기
    List<int> newList = PlayerPrefs.Load<List<int>>("Numbers");
}

이렇게 한정된 string, int, float 타입 말고 다양한 형태의 저장이 가능합니다.

마무리

PlayerPrefs의 순수 기능만으로는 저장하여 사용하는 것에는 한계가 있어, 커스텀으로 수정했습니다. 다양한 클래스를 사용하게 되고 이에 따라 저장이 필요할 때 사용하면 좋습니다.


참고 : https://forum.unity.com/threads/c-serialization-playerprefs-mystery.72156/

반응형

댓글