Lukomor provides Editor and script tools for easy and convenient using MVVM pattern in your projects. Lukomor was made for separating code work from editor setupping work with binders system (this work can be done by non programmers actually). Also this framework has a lightweight DI system, it's powerful and nice addition for Lukomor.
Note
If you would like to support me, please feel free to do that with PayPal
- Short description
- Installation
- What is the MVVM
- ViewModels
- Views
- Binders
- What kind of binders you can expand
- DI in the Lukomor
- Recommendations
Lukomor is an architectural framework for Unity game engine that helps you apply MVVM pattern to your project easy and convenient reducing the leaks of Model into View. This framework suits to any kind of project: small and large. The most cool part of this framework (in my opinion) is separating programmers part of work from artists. Programmers can write ViewModels for features and artists can just setup binders and get a workable feature. For more information read documentation and watch the examples inside the imported framework (Examples.unitypackage)
For installation, just use unity Package Manager, at the left top corner click "+" and choose "Add package from git URL..."
Then paste the link below in the field and just press Enter.
https://github.com/vavilichev/Lukomor.git
Ready for work!
MVVM (Model - View - ViewModel) is a simple architectural programming pattern. You can find a lot of information about it in the internet. For example in Wikipedia. Therefore, I just place a scheme of work here without any additional info.
ViewModels is a non MonoBehaviour class that connects View and Model. In the Lukomor you must inherit the ViewModel class in each of your *ViewModel realization. It's required because the Editor scripts works with interfaces for showing relevant information about existing ViewModels as well as ViewModel already contains Subscriptions field and IDisposables realization to dispose the subscriptions. Therefore your class can be look like this:
public class MyCoolViewModel : ViewModel
{
}View is a basic MonoBehaviour component that must be attached to a GameObject that represents the visualization of some ViewModel`s work. It can be object on scene or prefab.
Every View can be a parent View (root) or SubView (child). If SourceView field is empty, the View is root. You can attach the parent view here.
Parent View and child View look simmilar but work really different.
Parent View awaits ViewModel that you choose in the ViewModel property in the Editor. This ViewModel will be placed in the reactive property to notify all the subscriptions about that.
After attaching the SourceView into Child View, you can see the property called PropertyName. This ChildView can see the SourceView selected ViewModel type and see it's IObservable<*ViewModel> properties, you can select it:
Warning
You must use required class as a generic parameter in the IObservable<*ViewModel> property. Otherwise other child Views and Binders will not understand what viewModel is the source for binding, you just will not be able to see the property names.

Child View waits the SourceView.ViewModel reactive property. When the property is filled, child view catch this moment and set it to it's own ViewModel property for notifying it's subscriptions futher.
Tip
The Search feature helps you find your ViewModels and property names really fast.
Binders is the main feature of this framework. Binders help you to connect View and ViewModel: visualize data from ViewModel and send commands to the ViewModel for interacting with Model for example. Binders can be binded to IObservable public property, or IReadonlyReactiveCollection public property, or ICommand public propty of the ViewModel. Also, Binder can be binded to another binder as a source of the data. You should understand that each binder can be the source of data for the other binders. It's convenient to build the sequences of data streaming and sharing.
public class ScreenExampleSimpleBindersViewModel : ViewModel
{
private readonly BehaviorSubject<bool> _booleanValue = new(false);
public IObservable<bool> BooleanValue => _booleanValue;
public ICommand CmdSwitchBoolean { get; private set; }
public ScreenExampleSimpleBindersViewModel()
{
CmdSwitchBoolean = new Command(SwitchBoolean);
SwitchBoolean();
}
private void SwitchBoolean()
{
_booleanValue.OnNext(!_booleanValue.Value);
}
}
There are there are several types of binders you should know:
As I said above, each binder can be the source of data for other binders. It means that each binder has OuputStream property:

Output stream can differ from the Input data type, when we need to convert the data. It's frequent case, for example: use boolean data value and convert it to the Color type (typical case for the enabled-disabled visualization). Input data type will be boolean, output - Color.
You can see that abstract part in the ObservableBinder<TIn, TOut> classs.
From the other hand, there are binders that have the same Input data type and output data type. The example is the DataToUnityEvent binders. These binders took the data and stream it to the UnityEvent<T> and stream the result further without changing the data type.
How to scale the binders and write your own implementation, read in the section How To Scale Binders.
This variant of binders can be binded to the IReadonlyReactiveCollection<T> public property of ViewModel. It reacts on adding and removing values from the collection. Usually this type of binders uses for widgets lists, or some collections and stores viewModels. In the project you can find ExampleCollectionsUI scene where this case is handled.
ObservableCollectionToViewBinder subscribes on IReadonlyReactiveCollectio<T> binder and takes the viewModels from there and creates views in the container using mapper attached. Mapper has the mappings which prefab should we use to create the View.
Important
There is only one implementationm of the collection binders that can create Views by IReadonlyReactiveCollection<T> property subscription. It contains direct links to the prefabs, it's not really optimized, so you can write your own variant of the binder and use addressables and so on. Or you can wait the next updates of the Lukomor framework :)
This type of binders uses for the input streaming to the ViewModels layer. These binders should connect to the ICommand property inside the ViewModel. At the moment you can find the only one type of Command binders: ButtonToCommandBinder, but you can also implement your own variant of the command binders
Important
Full list of prepared binders you can see in the Packages/Lukomor Architecture/Lukomor/Scripts/MVVM/Binders section.
Simple binder with the same Input data type and output data type:

Simple binder with different Input data type and output data type:

The reactions of the collection changing

To broadcast user's input to the ViewModels. It can have no arguments (CommandBinder) or have one argument (CommandBinder)

Lukomor also has a simple Dependency Injection implementation. It's optional and it's not integrated in the MVVM system. DI is just nice addition for the main framework. Lukomor DI based on factories and has an inheritance of DI containers. Lets watch how it works in detail
All dependencies (means factories) registers in DIContainer. Containers can inherit one from another with using composition, it means that you can create a tree of dependencies where child containers know about parent and can request instances from it, but parent containers doesn't know about childs. It's convenient to create one container for whole game that work like a project context, and different containers for each scene of your game. When you destroy scene, you destroy it's contaier, so all of dependencies of that scene destroyes too.
DIContainer stores factories, therefore you have to register factories into id DIContainer. Factory is a delegate that receives a reference to a DIContainer instance (you can get another instances from this container) and returns an instance of requested type.
Func<DIContainer, T> factoryYou can register factory that produces singleton (not static, of course, just instance with flag) and cache it and uses cached value every Resolve() running.
var container = new DIContainer();
container.RegisterSingleton(c => new MyAwesomeClass());Also you can use tags system for creating unique instances of the same type for different needs:
container.RegisterSingleton("my_owesome_tag_A", c => new MyAwesomeClass());
container.RegisterSingleton("my_owesome_tag_B", c => new MyAwesomeClass());Important
You should know that registration doesn't creates instances instantly. Container creates instances when you request classes by calling Resolve().
Important
You can create instance with registration by using CreateInstance() method. Each registration returns DIBuilder instance so you can do it with the registration.
var myAwesomeClassInstance = container
.RegisterSingleton(c => new MyAwesomeClass())
.CreateInstance();Also you can register factory for resolving multiple times for getting new instance every time when you do Resolve().
container.Register(c => new MyAwesomeClass());
...
var instanceA = container.Resolve<MyAwesomeClass>();
var instanceB = container.Resolve<MyAwesomeClass>(); // instanceA != instanceBAnd transient variant has a tags approach as well.
container.Register("variant_1", c => new MyAwesomeClass(parameter_1)); // factory with parameter 1
container.Register("variant_2", c => new MyAwesomeClass(parameter_2)); // factory with parameter 2
...
var instanceA = container.Resolve<MyAwesomeClass>("variant_1");
var instanceB = container.Resolve<MyAwesomeClass>("variant_1"); // instanceA != instanceB
var instanceC = container.Resolve<MyAwesomeClass>("variant_2");
var instanceD = container.Resolve<MyAwesomeClass>("variant_2"); // instanceC != instanceDYou can make project context with own parent container and scene context with own child container for making access from scene context to project context by using inheritance with simple composition:
var projectContainer = new DIContainer();
projectContainer.RegisterSingleton<GameSettingsService>();
...
var sceneContainer = new DIContainer(projectContainer);
sceneContainer.RegisterSingleton<CoolSceneFeatureService>();
...
var gameSettingsService = projectContainer.Resolve<GameSettingsService>(); // Work
var gameSettingsService = sceneContainer.Resolve<GameSettingsService>(); // Work
var coolSceneFeatureService = sceneContainer.Resolve<CoolSceneFeatureService>(); // Work
var coolSceneFeatureService = projectContainer.Resolve<CoolSceneFeatureService>(); // Doesn't workThe only one recommendation: use Entry Point pattern for your projects if you use Lukomor framework. It's really convenient to separate different scopes of entities (for project, for scenes) using Lukomor DI and MVVM. Just imagine the tree of dependencies where each branch (DIContainer) can be a scope of entities (for example scene) and if you want to destroy all dependencies from this scope - destroy the branch (DIContainer).




