The game uses a custom MVVM (Model-View-ViewModel) system for UI.
- The *Model layer is formed by the data structures in DataObjects.cs and the other helper functions and gameplay state scattered throughout the codebase.
- The ViewModel layer sits on top of that, and lives in ViewModels folder. These adapt the data from the Model into a format that is tailored for displaying in the UI. For example, the ViewModel layer might take a character's social network and transform that into an integer for how many cities they are connected to, and the UI shows that number in a text object.
- The View layer has two parts:
- UGUI components on the GameObject - define the structure, layout, and style of the UIs, (as XAML would in WPF, or HTML in ASP.NET or javascript MVVM frameworks)
- Code-Behind - which lives in the Views folder, are MonoBehaviours that live on the UGUI UI GameObjects. These View classes are the "code-behind" for the Views and are mostly just responsible for binding the View's components (such as Buttons, Text, etc) to properties of the ViewModel
public class Test : MonoBehaviour
{
public void OpenTownScreen() {
// hide the PortScreen View
Globals.UI.Hide<PortScreen>();
// show the TownScreen view, passing in a new TradeViewModel, which can take in any information to be sent into the UI as the model
Globals.UI.Show<TownScreen, TradeViewModel>(new TradeViewModel("My Town"));
}
}
This MVVM system may be overkill for some very simple UIs, and it's not unreasonable to set up small UIs in a simpler way, but for more complex UIs this can simplify the code a lot. The major benefits are:
- Readability, maintainability
- Clear separation of the presentation logic (animations, visual state) from the "behavior" of the UI (what data drives it, how it reacts to user input commands)
- Views are completely decoupled from the game logic and underlying data structure
- For example, a view could bind to an interface or abstract model that could have many implementatiions
- Likewise, the ViewModel is completely independent of the View, so the behavior of a UI can be unit tested without needing the actual front-end View
- Easy to have the same data drive multtiple views, ie. they are all "views" of the data
- Iteration speed
- Very easy to make a UI that automatically updates from external state changes (such as the quantity of food and water depleting while you sail)
- For example,
BoundModel
- For example,
- Automatic population of lists, again with automatic updating as items are added/removed
- For example, see the ObservableCollection logic in CityViewModel
- Lightweight adaptation of model data into whatever format is needed in the UI using a pipeline of data transformation, inspired by reactive programming
- For example,
BoundModel.AsString
, or the time formatting transformation in TimePassingView
- For example,
- Very easy to make a UI that automatically updates from external state changes (such as the quantity of food and water depleting while you sail)
The game's MVVM system has built in support for these Views, which have their own ViewModel pre-defined. You can use these as "controls" in larger UIs:
- ButtonView
- Binds to label string and an OnClick action
- StringView
- Binds to a string
- ImageView
- Binds to a Sprite asset
- ListView
- Binds to an ObservableCollection, and takes a ViewModel type and a View prefab set in the inspector for its items
- Automatically handles populating/adding/removing item views when the backing ObservableCollection is changed
- MessageBoxView
- Binds to a title and message string, and an OK and Cancel ButtonViewModel (each having a label string and OnClick action)
- SliderView
- Binds to a float value to drive the UGUI slider component
We're using the following standard canvas scaler settings for most of our UIs: