Skip to content

Latest commit

 

History

History
218 lines (157 loc) · 14.1 KB

README.md

File metadata and controls

218 lines (157 loc) · 14.1 KB

ProjectPropertiesManager

ProjectPropertiesManager предоставляет типизированный доступ к произвольным значениям внутри ProjectData.ProjectProperties. Поддерживаются long, float, decimal, DateTime, string и bool; каждый тип хранится в отдельном словаре.

После готовности данных менеджер доступен через:

ProjectPropertiesManager properties = PRUnitySDK.Managers.ProjectProperties;

Строковые ключи

properties.SetLong("Coins", 100);
properties.AddLong("Coins", 25);

if (properties.TryGetLong("Coins", out long coins))
{
    // значение было сохранено
}

float playTime = properties.GetFloat("PlayTime"); // 0, если ключ отсутствует
properties.RemoveProperty<long>("Coins");

Для чтения доступны пары TryGet*/Get* для каждого поддерживаемого типа.

Для настройки, у которой значение из PRSDKSettings служит запасным, можно читать сохранённое переопределение через OverrideProperties.Get(key, fallback). Типизированный EnumerationType<T> задаёт и имя свойства, и тип значения; переопределение записывается через SetValue(key, value) и убирается через RemoveProperty(key). Если изменение влияет на вычисленное состояние, вызывающий модуль должен его пересчитать.

private static readonly EnumerationType<float> SpeedOverride = new("Example.Speed");

float speed = OverrideProperties.Get(SpeedOverride, settings.Speed);
properties.SetValue(SpeedOverride, 1.25f);
properties.RemoveProperty(SpeedOverride);

Пример с GrowthFactor и автоматическим пересчётом есть в документации опыта.

Какой тип брать для чисел

Тип Для чего
long счётчики и целые величины: сколько раз, сколько штук, какой уровень
float настройки и доли в пределах разумного: громкость, чувствительность, прогресс
decimal счёт, который копится дробными долями и растёт без предела: опыт, мягкая валюта

У float на большом числе мелкие начисления перестают доходить: сохранённое значение остаётся прежним. decimal держит 28 значащих цифр и вмещает почти на десять порядков больше long.

За это платят скоростью и размером: decimal считается заметно медленнее, поэтому для настроек и счётчиков он не нужен.

Складывать такой счёт лучше через AddDecimal или AddSafe из NumberExtensions: decimal на переполнении бросает исключение, а не заворачивается, и одно начисление уронило бы игру.

fallback

У каждого Get* есть необязательный параметр fallback — значение, которое вернётся, если ключ ещё ни разу не сохранялся. По умолчанию он равен default(T).

long coins = properties.GetLong("Coins");            // 0, если ключа нет
long start = properties.GetLong("Coins", 100);       // 100, если ключа нет

bool music = properties.GetBool("Music", true);      // по умолчанию включено
string nick = properties.GetString("Nick", "guest");
float volume = properties.GetFloat("Volume", 0.5f);

fallback подставляется только при отсутствии ключа. Сохранённое значение возвращается как есть — в том числе явно сохранённые 0, false и null.

Когда нужно именно отличить «ключа нет» от «сохранён 0/false/null», по-прежнему используйте TryGet*: fallback этой разницы не покажет, если совпадёт с сохранённым значением.

Enumeration и типизированные ключи

Legacy-перегрузки принимают Enumeration и используют его Value как строковый ключ. Для нового API предпочтительнее EnumerationType<T>: тип значения закреплён в самом ключе.

public static readonly EnumerationType<bool> ShowHints = new(nameof(ShowHints));

properties.SetValue(ShowHints, true);
bool showHints = properties.GetValue(ShowHints, fallback: false);
properties.RemoveProperty(ShowHints);

Параметр fallback есть у всех трёх видов ключа:

Enumeration coinsKey = Enumeration.GetOrCreate("Coins");
EnumerationType<long> typedCoinsKey = new("Coins");

long a = properties.GetValue("Coins", 100L);        // строковый ключ
long b = properties.GetValue(coinsKey, 100L);       // Enumeration
long c = properties.GetValue(typedCoinsKey, 100L);  // EnumerationType<long>

Для EnumerationType<long>, EnumerationType<float> и EnumerationType<decimal> есть AddLong, AddFloat и AddDecimal.

Сохранение

У методов записи и удаления три флага:

Параметр Текущее поведение
save при true вызывает GameManager.Instance.SaveProjectData()
ignoreSaveCooldown при true запись идёт не дожидаясь кулдауна сохранения
requiredNotify при true публикует событие изменения в EventBus (см. Уведомления)

Если нужно выполнить несколько изменений, передайте save: false, затем сохраните один раз:

properties.SetLong("Coins", 100, save: false);
properties.SetBool("TutorialDone", true, save: false);
PRUnitySDK.Managers.Game.SaveProjectData();

Когда обходить кулдаун

Флаги отвечают на разные вопросы и не заменяют друг друга:

Флаг Значит
save попытаться сохранить; внутри кулдауна вызов ничего не сделает, изменение уйдёт со следующей записью
ignoreSaveCooldown писать прямо сейчас, кулдаун не ждать

Обычной настройке хватает первого — она подождёт общего расписания.

Не подходит тому, за что игрок заплатил: покупке, отключению рекламы, награде. Между списанием и следующим разрешённым сохранением игрок может закрыть вкладку, и покупка пропадёт, а ресурсы останутся списанными — или наоборот.

properties.SetBool("RemoveAds", true, ignoreSaveCooldown: true);

Флаг передаётся в GameManager.SaveProjectData(ignoreCooldown). Не злоупотребляйте им: кулдаун не даёт писать на диск и в облако на каждое изменение.

Удаление отсутствующего ключа ничего не сохраняет. RemoveProperty(string, Type, ...) пишет warning для неподдерживаемого типа, а generic API выбрасывает NotSupportedException.

Уведомления

При requiredNotify: true изменение свойства публикуется в EventBus на двух уровнях — подписчик выбирает тот, который ему удобнее.

Уровень 1 — только имя свойства. Не зависит от типа: подписчик сам решает, нужно ли читать значение.

public class SaveIndicator : MonoBehaviour, IProjectPropertyChangedEvent
{
    public void OnProjectPropertyChanged(string propertyName)
    {
        // сюда приходят изменения свойств любого типа
    }
}

Уровень 2 — готовые значения нужного типа. По одному интерфейсу на поддерживаемый тип; один класс может реализовать сразу несколько. Вместе с новым приходит предыдущее значение, поэтому подписчику не нужно хранить свою копию, чтобы посчитать разницу.

public class CoinsView : MonoBehaviour, ILongProjectPropertyChangedEvent
{
    public void OnLongProjectPropertyChanged(string propertyName, long previousValue, long currentValue)
    {
        if (propertyName != "Coins")
            return;

        label.text = currentValue.ToString();
        PlayGainAnimation(currentValue - previousValue);
    }
}
Интерфейс Метод
IProjectPropertyChangedEvent OnProjectPropertyChanged(string)
ILongProjectPropertyChangedEvent OnLongProjectPropertyChanged(string, long previous, long current)
IFloatProjectPropertyChangedEvent OnFloatProjectPropertyChanged(string, float previous, float current)
IDecimalProjectPropertyChangedEvent OnDecimalProjectPropertyChanged(string, decimal previous, decimal current)
IBoolProjectPropertyChangedEvent OnBoolProjectPropertyChanged(string, bool previous, bool current)
IStringProjectPropertyChangedEvent OnStringProjectPropertyChanged(string, string previous, string current)
IDateTimeProjectPropertyChangedEvent OnDateTimeProjectPropertyChanged(string, DateTime previous, DateTime current)
IProjectPropertyRemovedEvent OnProjectPropertyRemoved(string, Type)

Порядок и условия рассылки:

  • сначала уведомляются типизированные подписчики, затем общие;
  • событие уходит после сохранения, поэтому подписчик видит уже зафиксированное состояние;
  • если значение не изменилось (сравнение через EqualityComparer<T>.Default), уведомления не будет — SetLong("Coins", 100) подряд дважды разошлёт событие один раз;
  • удаление несуществующего ключа события не публикует.

Фильтра по имени свойства на стороне шины нет: подписчик получает изменения всех свойств своего типа и сам сравнивает propertyName.

Публичный API

Группа Методы
запись SetDateTime, SetLong, SetString, SetFloat, SetDecimal, SetBool, SetValue<T>
накопление AddLong, AddFloat, AddDecimal
безопасное чтение TryGetDateTime, TryGetLong, TryGetString, TryGetFloat, TryGetDecimal, TryGetBool, TryGetValue<T>
чтение с fallback GetDateTime, GetLong, GetString, GetFloat, GetDecimal, GetBool, GetValue<T> — все с необязательным fallback
удаление RemoveProperty(string, Type), RemoveProperty<T>(string), RemoveProperty<T>(EnumerationType<T>)

Универсальное хранилище

Подробности хранения — в GameDataStorage. Тот же слой использует ResourceManager.

Ограничения

  • Доступ требует загруженного ProjectData; до GameManager.ReadySignal чтение и запись завершатся исключением.
  • Один строковый ключ может одновременно существовать в словарях разных типов. Тип является частью фактической идентичности свойства.
  • ObjectProperties присутствует в модели данных, но менеджер не предоставляет для него публичный API.
  • Уведомления не батчатся: серия Set* с save: false разошлёт столько событий, сколько было изменений. Если это критично, отключайте рассылку через requiredNotify: false и уведомляйте подписчиков самостоятельно после последнего изменения.
  • При первом сохранении свойства previousValue равен default(T), и этот случай неотличим от ранее сохранённого 0, false или null. Когда важно именно «свойства ещё не было», проверяйте через TryGet* до записи.
  • Общее событие (IProjectPropertyChangedEvent) и удаление значений не несут — только имя, а для удаления ещё и тип.