Коротко: лёгкий и производительный MVVM-фреймворк для Unity uGUI, вдохновлённый Unity Weld, но реализующий привязки через генерацию runtime expressions и делегатов для более высокой производительности.
Этот фреймворк был вдохновлён проектом Unity Weld (ссылка на репозиторий), но реализует привязки иначе - не через тяжёлую чистую рефлексию, а через генерацию runtime expressions, которые в рантайме конвертируются в делегаты с прямым вызовом методов. Это даёт заметное преимущество по производительности на сложных UI. Также реализована гибкая система Binder'ов и поддержка SubViewModel, а Editor часть сделана на UI Toolkit и максимально универсальна настолько, насколько это возможно.
- Реализация привязок через runtime-expressions → делегаты.
- Гибкая архитектура Binder'ов - легко расширяется под ваши нужды.
- Поддержка SubViewModel и коллекций ViewModel.
- Editor-инструменты на UI Toolkit (расширяемые).
- Работает на IL2CPP (при соблюдении некоторых требований) и на мобильных платформах.
- Model - бизнес-логика и данные (POCO, ECS-компоненты и т.п.).
- View - UI-элементы (TextMeshPro, Image, Toggle, Slider и т.д.).
- ViewModel - посредник между Model и View, содержит состояние, доступное для биндинга.
- Binders - компоненты, связывающие ViewModel и View (OneWay, TwoWay, Command, Collection и т.д.).
- Converters - преобразуют данные (например: формат валюты, DateTime и т.д.).
- ViewModelTemplates - провайдеры ViewModel'ей для коллекций и SubViewModel.
- Unity 6000.0.33f1+ (рекомендуется LTS).
- Для IL2CPP + aggressive stripping помечайте ViewModel и их члены атрибутом
[UnityEngine.Scripting.Preserve]. - Просто добавьте папку фреймворка в ваш проект Unity и откройте примеры сцен/префабов (если мне не будет лень их добавлять :) ).
Есть два основных варианта:
ViewModelMonoBehaviour- база для ViewModel, которые живут на GameObject (удобно для быстрого прототипирования и сцен).ViewModel- чистые классы (POCO), используются как SubViewModel или элементы коллекций.
public class ExampleViewModelBehaviour : ViewModelMonoBehaviour
{
[Bindable]
public string PlayerName
{
get => _playerName;
set => Set(ref _playerName, value);
}
private string _playerName;
[Bindable]
public int Health
{
get => _health;
set => Set(ref _health, value);
}
private int _health;
[Bindable]
public Sprite PlayerIcon
{
get => _playerIcon;
set => Set(ref _playerIcon, value);
}
private Sprite _playerIcon;
[Bindable]
public SomePocoExampleViewModel SubViewModel => _subViewModel;
private SomePocoExampleViewModel _subViewModel = new SomePocoExampleViewModel("placeholder");
[Bindable]
public ObservableDataList<SomePocoExampleViewModel> SomeViewModelCollection { get; } = new ObservableDataList<SomePocoExampleViewModel>();
[Bindable]
public ObservableDataList<int> DropdownOptions { get; } = new ObservableDataList<int>();
[Bindable]
public int SelectedOption
{
get => _selected;
set => SetProperty(ref _selected, value);
}
private int _selected;
[Bindable]
private void TestMethod()
{
Debug.Log(nameof(TestMethod));
SomeViewModelCollection.Add(new SomePocoExampleViewModel("Меч") { Count = 1 });
}
}[Serializable]
[Bindable]
public class SomePocoExampleViewModel : ViewModel
{
[Bindable]
[Preserve]
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
[Bindable]
[Preserve]
public Sprite Icon
{
get => _icon;
set => SetProperty(ref _icon, value);
}
[Bindable]
[Preserve]
public int Count
{
get => _count;
set => SetProperty(ref _count, value);
}
private string _name;
private Sprite _icon;
private int _count;
public SomePocoExampleViewModel(string name)
{
Name = name;
}
[Bindable]
[Preserve]
public void FirstTestMethod()
{
Debug.Log(nameof(FirstTestMethod));
}
[Bindable]
[Preserve]
public void SecondTestMethod()
{
Debug.Log(nameof(SecondTestMethod));
}
}Примечание: на чистых (non-Mono) ViewModel обязательно добавлять атрибут
[Bindable], чтобы SubViewModelTemplate/CollectionTemplate корректно обрабатывали члены класса.
-
Создайте ViewModel (наследуйтесь от
ViewModelMonoBehaviourили создайте POCO и проставьте Bindable). -
Добавьте UI-элемент на сцену (Text, TMP, Image, Toggle и т.д.).
-
Добавьте соответствующий
Binderк UI-элементу. -
В инспекторе укажите:
- Источник данных (ваша ViewModel)
- Имя свойства (например
PlayerName)
- На UI создайте контейнер (GameObject).
- Добавьте компонент
SubViewModelTemplateна префаб-контейнер. - В корневой ViewModel выберите свойство SubViewModel.
- Внутри контейнера можно биндить свойства SubViewModel как обычно.
Фреймворк предоставляет набор стандартных Binder'ов и позволяет реализовать свои, унаследовавшись от BinderBase, OneWayBinder<TSource,TTarget>, TwoWayBinder<TSource,TTarget> и т.д.
[AddComponentMenu("MVVM/Binders/GameObjectActivity Binder")]
public class GameObjectActivityBinder : OneWayBinder<bool, bool>
{
[SerializeField] private GameObject _gameObject;
private void Awake()
{
_gameObject ??= gameObject;
//обязательно вызываем base.OnAwake после кеширования данных
base.Awake();
}
protected override void ApplyTargetValue(bool value)
{
_gameObject.SetActive(value);
}
}[AddComponentMenu("MVVM/Binders/Slider Binder")]
[RequireComponent(typeof(Slider))]
[SupportedBindingTypes(typeof(float))]
public sealed class SliderBinder : TwoWayBinder<float, float>
{
private Slider _slider;
private void Awake()
{
_slider = GetComponent<Slider>();
base.Awake();
}
protected override void ApplyTargetValue(float value)
{
if (_slider != null && !_slider.wholeNumbers && Mathf.Abs(_slider.value - value) > 0.001f)
_slider.value = value;
}
protected override void SubscribeToUiEvents()
{
if (_slider != null)
_slider.onValueChanged.AddListener(OnUiValueChanged);
}
protected override void UnsubscribeFromUiEvents()
{
if (_slider != null)
_slider.onValueChanged.RemoveListener(OnUiValueChanged);
}
}Для коллекций используется ObservableDataList<T> - поведение похоже на обычный List<T>, но с уведомлениями об изменениях.
[Bindable]
public class InventoryViewModel : ViewModel
{
[Bindable]
public ObservableDataList<ItemViewModel> Items { get; } = new ObservableDataList<ItemViewModel>();
public InventoryViewModel()
{
Items.Add(new ItemViewModel("Меч") { Count = 1 });
Items.Add(new ItemViewModel("Зелье здоровья") { Count = 3 });
// использование RemoveAt, Clear, Swap и т.д.
}
}- Создайте префаб элемента списка с компонентом
CollectionViewModelTemplate. - На сцене разместите контейнер с компонентом
ObservableViewModelListBinder. - В инспекторе укажите источник данных (ViewModel), имя свойства-коллекции и префаб.
Создание конвертера - наследуйте от ValueConverterT<TSource, TTarget> и реализуйте Convert/ConvertBack.
[CreateAssetMenu(menuName = "MVVM/Converters/Enum To String")]
public sealed class EnumToStringConverterT<TEnum> : ValueConverterT<TEnum, string> where TEnum : Enum
{
public override string Convert(TEnum source) => source.ToString();
public override TEnum ConvertBack(string target) => (TEnum)Enum.Parse(typeof(TEnum), target);
}Чтобы использовать конвертер - добавьте созданный ScriptableObject конвертер в список конвертеров PropertyBinder'а в инспекторе.
- Могу ли я писать свою сложную логику Binder'ов? - Да, унаследуйтесь от
BinderBaseили нужного базового класса и реализуйте логику. - Работает ли с IL2CPP и aggressive stripping? - Да, но пометьте классы и члены атрибутом
[UnityEngine.Scripting.Preserve]. - Работает ли на WebGL / Android / iOS? - Гарантированно работает на Android, PC, iOS. WebGL не тестировался полноценно — используйте с осторожностью.
- Могу ли я используя данный MVVM фреймворк делать сложные анимации? - В теории - да, если вы сделаете свою обертку над Stateful UI который может через StateRole воспроизводить анимации
Если хотите внести улучшения или нашли баг — присылайте PR/Issue в репозиторий (или напишите мне в телеграм).