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
на переполнении бросает исключение, а не заворачивается, и одно начисление уронило бы игру.
У каждого 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 этой разницы не покажет, если совпадёт с сохранённым значением.
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.
| Группа | Методы |
|---|---|
| запись | 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) и удаление значений не несут — только имя, а для удаления ещё и тип.