- Overview
- Core Concepts
- View Service
- Presenter System
- Widget System
- Populator System
- UI Animation
- Performance Considerations
- Common Issues and Solutions
- Best Practices
- Examples
- Summary
The CherryFramework UI system provides a comprehensive, modular approach to building user interfaces in Unity. It implements a variant of the MVVM (Model-View-ViewModel) pattern with a focus on navigation, state management, and animation.
Please see the Sample (Assets/Sample/Scenes/dinoscene.unity, GameObject UIRoot) for details how to setup the sytem! It is pretty easy and straightforward.
- View Service: Centralized navigation and view stack management
- Presenter System: Screen-based UI with hierarchy support
- Widget System: Reusable, stateful UI components
- Populator System: Dynamic list/collection rendering with pooling
- Animation System: Declarative show/hide animations with sequencing
- Modal/Popup Support: Special handling for modal dialogs and popups
- Dependency Injection: Seamless integration with DI container
- Data Binding: Automatic UI updates via Accessor system
| Layer | Purpose | Components |
|---|---|---|
| View Service | Navigation and view management | ViewService, RootPresenterBase |
| Presenters | Screen-level UI containers | PresenterBase, PresenterErrorBase, PresenterLoadingBase |
| Widgets | Reusable UI components | WidgetBase, WidgetElement, WidgetState |
| Populators | Dynamic collection rendering | PopulatorBase<T>, PopulatorElementBase<T> |
| Animation | Visual transitions | UiAnimationBase, various animators |
┌─────────────────────────────────────────────────────────────────┐
│ ViewService │
│ - Manages view stack │
│ - Handles navigation (Pop/Back) │
│ - Tracks active view │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ RootPresenterBase │ │ PresenterBase │
│ - Root view container │ │ - Screen UI │
│ - Loading/Error screens│ │ - Child presenters │
└─────────────────────────┘ │ - Show/Hide animations │
│ └─────────────────────────┘
│ │
│ ┌───────────┴───────────┐
│ ▼ ▼
│ ┌─────────────────┐ ┌─────────────────┐
│ │ WidgetBase │────▶│ WidgetElement │
│ │ - Multiple states │ - Single element│
│ │ - State machine │ │ - Show/Hide │
│ └─────────────────┘ └─────────────────┘
│ │ │
│ └───────────┬───────────┘
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ PopulatorBase<T> │──────▶│ PopulatorElementBase<T>│
│ - Object pool │ │ - Data container │
│ - List rendering │ │ - Refresh/Update │
│ - Animation sequencing │ │ - Show/Hide animations │
└─────────────────────────┘ └─────────────────────────┘
│
▼
┌─────────────────┐
│ UiAnimationBase│
│ - Show() │
│ - Hide() │
│ - Sequencing │
└─────────────────┘
| Component | Purpose |
|---|---|
ViewService |
Central navigation service managing view stack |
RootPresenterBase |
Root container for all presenters |
PresenterBase |
Base class for screen-level UI |
WidgetBase |
Stateful UI component with multiple visual states |
WidgetElement |
Individual UI element with show/hide |
PopulatorBase<T> |
Dynamic list renderer with pooling |
UiAnimationBase |
Base class for all UI animations |
Namespace: CherryFramework.UI.Views
Purpose: Central navigation service that manages the view stack, handles presenter transitions, and tracks the active view.
public class ViewService : GeneralClassBase
{
public delegate void OnViewChangedDelegate();
public event OnViewChangedDelegate OnAnyViewBecameActive;
public event OnViewChangedDelegate OnAllViewsBecameInactive;
public bool IsViewActive { get; }
public bool IsLastView { get; }
public PresenterBase ActiveView { get; private set; }
public ViewService(RootPresenterBase root, bool debugMessages);
// Pop views by type
public Sequence PopView<T>(PresenterBase mountingPoint = null, bool skipAnimation = false) where T : PresenterBase;
public Sequence PopView<T>(out T newView, PresenterBase mountingPoint = null, bool skipAnimation = false);
public Sequence PopView(Type type, PresenterBase mountingPoint = null, bool skipAnimation = false);
// Pop views by instance
public Sequence PopView(PresenterBase view, PresenterBase mountingPoint = null, bool skipAnimation = false);
// Navigation
public Sequence Back(bool skipAnimation = false);
public Sequence HideAndReset(bool skipAnimation = false);
public void ClearHistory();
// Special screens
public Sequence PopLoadingView();
public Sequence PopErrorView(string title, string message);
}public ViewService(RootPresenterBase root, bool debugMessages)Example:
// In installer
var rootPresenter = FindObjectOfType<RootPresenterBase>();
var viewService = new ViewService(rootPresenter, debugMessages: true);
DependencyContainer.Instance.BindAsSingleton(viewService);public Sequence PopView<T>(PresenterBase mountingPoint = null, bool skipAnimation = false) where T : PresenterBaseExample:
// Show settings screen
_viewService.PopView<SettingsPresenter>();
// Show inventory as child of HUD
_viewService.PopView<InventoryPresenter>(hudPresenter);public Sequence PopView<T>(out T newView, PresenterBase mountingPoint = null, bool skipAnimation = false)Example:
_viewService.PopView<SettingsPresenter>(out var settingsView);
settingsView.SetConfiguration(currentSettings);public Sequence Back(bool skipAnimation = false)Example:
// Go back to previous screen
_viewService.Back();
// Go back without animation
_viewService.Back(skipAnimation: true);public Sequence HideAndReset(bool skipAnimation = false)Example:
// Hide all views and reset to empty state
_viewService.HideAndReset();public class GameUI : MonoBehaviour
{
[Inject] private ViewService _viewService;
private void Start()
{
_viewService.OnAnyViewBecameActive += OnViewOpened;
_viewService.OnAllViewsBecameInactive += OnAllViewsClosed;
}
private void OnViewOpened()
{
Debug.Log($"View opened: {_viewService.ActiveView?.name}");
}
private void OnAllViewsClosed()
{
Debug.Log("All views closed - showing main menu?");
}
}Namespace: CherryFramework.UI.InteractiveElements.Presenters
Purpose: Base class for all screen-level UI components. Manages child presenters, animations, and view hierarchy. All child presenters must added to childPresenters manually in the Editor
public abstract class PresenterBase : InteractiveElementBase
{
[Inject] protected ViewService ViewService;
[Header("Hierarchy settings")]
[SerializeField] private Canvas childrenContainer;
[SerializeField] protected List<PresenterBase> childPresenters = new();
public Canvas ChildrenContainer => childrenContainer;
public List<PresenterBase> ChildPresenters => childPresenters;
public virtual bool Modal { get; private set; }
public List<PresenterBase> uiPath { get; set; } = new();
public PresenterBase currentChild { get; set; }
public void InitializePresenter();
public virtual Sequence ShowFrom(PresenterBase previous, bool skipAnimation = false);
public virtual Sequence HideTo(PresenterBase next, bool skipAnimation = false);
}Example:
public class MainMenuPresenter : PresenterBase
{
[SerializeField] private Button _playButton;
[SerializeField] private Button _settingsButton;
[SerializeField] private Button _quitButton;
protected override void OnPresenterInitialized()
{
_playButton.onClick.AddListener(OnPlayClicked);
_settingsButton.onClick.AddListener(OnSettingsClicked);
_quitButton.onClick.AddListener(OnQuitClicked);
}
private void OnPlayClicked()
{
ViewService.PopView<GameplayPresenter>();
}
private void OnSettingsClicked()
{
ViewService.PopView<SettingsPresenter>();
}
private void OnQuitClicked()
{
Application.Quit();
}
public override Sequence ShowFrom(PresenterBase previous, bool skipAnimation = false)
{
Debug.Log($"Showing main menu, previous: {previous?.name}");
return base.ShowFrom(previous, skipAnimation);
}
}Namespace: CherryFramework.UI.Views
Purpose: Root container that holds all presenters and provides access to special screens. All child presenters must added to childPresenters manually in the Editor
public class RootPresenterBase : PresenterBase
{
[SerializeField] private PresenterLoadingBase loadingScreen;
[SerializeField] private PresenterErrorBase errorScreen;
public PresenterLoadingBase LoadingScreen => loadingScreen;
public PresenterErrorBase ErrorScreen => errorScreen;
}Example:
// In your scene hierarchy:
// Canvas (RootPresenterBase)
// ├── LoadingScreen (PresenterLoadingBase)
// ├── ErrorScreen (PresenterErrorBase)
// ├── Menus
// │ ├── MainMenu (PresenterBase)
// │ └── Settings (PresenterBase)
// └── Gameplay
// ├── HUD (PresenterBase)
// └── PauseMenu (PresenterBase)Namespace: CherryFramework.UI.InteractiveElements.Presenters
Purpose: Specialized presenter for error screens with predefined UI elements.
public abstract class PresenterErrorBase : PresenterBase, IPopUp
{
[SerializeField] private TMP_Text errorTitle;
[SerializeField] private TMP_Text errorMsg;
[SerializeField] private Button backButton;
public void SetError(string title, string message)
{
errorTitle.text = title;
errorMsg.text = message;
}
}Example:
// Show error screen
_viewService.PopErrorView("Connection Failed", "Please check your internet connection.");Namespace: CherryFramework.UI.InteractiveElements.Presenters
Purpose: Specialized presenter for loading screens.
public abstract class PresenterLoadingBase : PresenterBase, IPopUp
{
// Can be extended with progress bars, tips, etc.
}Example:
// Show loading screen while async operation completes
_viewService.PopLoadingView();
// Later, when loading completes
_viewService.Back();Namespace: CherryFramework.UI.InteractiveElements.Widgets
Purpose: Stateful UI component that can switch between multiple visual states with smooth transitions.
public class WidgetBase : InteractiveElementBase
{
[SerializeField] private WidgetStartupBehaviour startupBehaviour;
[SerializeField] protected List<WidgetState> widgetStates = new();
public int CurrentState { get; private set; }
public int StatesCount => widgetStates.Count;
public bool Playing { get; private set; }
public event OnStateChangedDelegate OnStartStateChange;
public event OnStateChangedDelegate OnFinishStateChange;
public void SetState(int state);
public void SetState(string stateName);
public string GetStateName(int state);
}Example:
[Serializable]
public class ButtonState
{
public Sprite backgroundSprite;
public Color textColor;
public AudioClip clickSound;
}
public class StatefulButton : WidgetBase
{
[SerializeField] private Image _background;
[SerializeField] private TMP_Text _label;
[SerializeField] private Button _button;
[SerializeField] private List<ButtonState> _buttonStates;
protected override void OnEnable()
{
base.OnEnable();
_button.onClick.AddListener(OnClick);
}
protected override void OnDisable()
{
base.OnDisable();
_button.onClick.RemoveListener(OnClick);
}
public void SetState(int stateIndex)
{
SetState(stateIndex);
// WidgetBase will handle the state transition
}
protected override void OnShowComplete()
{
base.OnShowComplete();
ApplyCurrentState();
}
private void ApplyCurrentState()
{
var state = _buttonStates[CurrentState];
_background.sprite = state.backgroundSprite;
_label.color = state.textColor;
}
private void OnClick()
{
var stateName = GetStateName(CurrentState);
Debug.Log($"Button clicked in state: {stateName}");
}
}Namespace: CherryFramework.UI.InteractiveElements.Widgets
Purpose: Defines a single state for a widget, containing the UI elements that should be active in that state.
[Serializable]
public class WidgetState
{
public string stateName = "";
public List<WidgetElement> stateElements = new();
}Example:
// In Unity Inspector:
// WidgetState (Normal)
// - NormalIcon (WidgetElement)
// - NormalLabel (WidgetElement)
//
// WidgetState (Hover)
// - HoverIcon (WidgetElement)
// - HoverLabel (WidgetElement)
//
// WidgetState (Pressed)
// - PressedIcon (WidgetElement)
// - PressedLabel (WidgetElement)Namespace: CherryFramework.UI.InteractiveElements.Widgets
Purpose: Determines how the widget initializes its states.
| Value | Description |
|---|---|
ExecuteShowOnCurrentState |
Only show the current state |
SimultaneouslyExecuteShowOnSelfAndCurrentState |
Show widget and current state together |
SequentiallyExecuteShowOnSelfAndCurrentState |
Show widget, then current state |
JustSetCurrentState |
Set state without animations |
Namespace: CherryFramework.UI.InteractiveElements.Widgets
Purpose: Individual UI element that can be shown/hidden, typically used as part of a widget state.
public class WidgetElement : InteractiveElementBase
{
public virtual Sequence Show()
{
return CreateSequence(animators, Purpose.Show);
}
public virtual Sequence Hide()
{
return CreateSequence(animators, Purpose.Hide);
}
}Example:
public class AnimatedIcon : WidgetElement
{
[SerializeField] private Image _icon;
[SerializeField] private float _pulseAmount = 1.2f;
public void SetIcon(Sprite sprite)
{
_icon.sprite = sprite;
}
public Sequence Pulse()
{
var seq = DOTween.Sequence();
seq.Append(transform.DOScale(_pulseAmount, 0.2f));
seq.Append(transform.DOScale(1f, 0.2f));
return seq;
}
}Namespace: CherryFramework.UI.InteractiveElements.Populators
Purpose: Dynamically renders collections of data using pooled UI elements.
public abstract class PopulatorBase<T> where T : class
{
protected T[] Data;
protected PopulatorElementBase<T> ElementSample;
protected Transform ElementsRoot;
public IReadOnlyCollection<PopulatorElementBase<T>> Active => active;
protected PopulatorBase(PopulatorElementBase<T> elementSample, Transform root);
public virtual void UpdateElements(IEnumerable<T> data, float delayEveryElement = 0f);
public void Clear();
}Example:
public class InventoryPopulator : PopulatorBase<ItemData>
{
public InventoryPopulator(InventoryItemElement sample, Transform root)
: base(sample, root)
{
}
public override void UpdateElements(IEnumerable<ItemData> items, float delayEveryElement = 0f)
{
base.UpdateElements(items, delayEveryElement);
// Additional logic after population
Debug.Log($"Inventory updated with {Data.Length} items");
}
}
// Usage
public class InventoryUI : MonoBehaviour
{
[SerializeField] private InventoryItemElement _itemPrefab;
[SerializeField] private Transform _contentRoot;
private InventoryPopulator _populator;
private void Awake()
{
_populator = new InventoryPopulator(_itemPrefab, _contentRoot);
}
public void ShowInventory(List<ItemData> items)
{
_populator.UpdateElements(items, 0.05f); // Staggered animation
}
}Namespace: CherryFramework.UI.InteractiveElements.Populators
Purpose: Base class for elements that are populated by a Populator.
public abstract class PopulatorElementBase<T> : WidgetElement where T : class
{
public T data;
public virtual void SetData(T data)
{
this.data = data;
}
public virtual Sequence Refresh()
{
var seq = CreateSequence(animators, Purpose.Hide);
seq.Append(CreateSequence(animators, Purpose.Show));
seq.AppendCallback(OnRefreshComplete);
return seq;
}
protected virtual void OnRefreshComplete() { }
}Example:
public class InventoryItemElement : PopulatorElementBase<ItemData>
{
[SerializeField] private TMP_Text _nameText;
[SerializeField] private TMP_Text _countText;
[SerializeField] private Image _iconImage;
public override void SetData(ItemData data)
{
base.SetData(data);
_nameText.text = data.itemName;
_countText.text = $"x{data.count}";
_iconImage.sprite = data.icon;
}
public override Sequence Refresh()
{
// Custom refresh animation
var seq = DOTween.Sequence();
seq.Append(transform.DOScale(0.8f, 0.1f));
seq.Append(transform.DOScale(1f, 0.2f));
seq.AppendCallback(() => Debug.Log($"Refreshed {data.itemName}"));
return seq;
}
}Namespace: CherryFramework.UI.UiAnimation
Purpose: Base class for all UI animations.
public abstract class UiAnimationBase : MonoBehaviour
{
[SerializeField] protected float duration = 0.3f;
[SerializeField] protected Ease showEasing = Ease.OutQuad;
[SerializeField] protected Ease hideEasing = Ease.OutQuad;
protected RectTransform Target { get; }
protected Sequence MainSequence;
public void Initialize();
public abstract Sequence Show(float delay = 0f);
public abstract Sequence Hide(float delay = 0f);
}Namespace: CherryFramework.UI.UiAnimation
Purpose: Configures an animator with delay and launch mode.
[Serializable]
public class UiAnimationSettings
{
public UiAnimationBase animator;
public float delay = 0f;
public LaunchMode launchMode;
}Namespace: CherryFramework.UI.UiAnimation.Enums
Purpose: Determines when an animation plays in a sequence.
| Value | Description |
|---|---|
AtGlobalAnimationStart |
Starts at the beginning of the sequence |
AtPreviousAnimatorStart |
Starts at the same time as previous animator |
AfterPreviousAnimatorFinished |
Starts after previous animator completes |
[RequireComponent(typeof(CanvasGroup))]
public class UiFade : UiAnimationBase
{
// Fades the CanvasGroup alpha
}
// Usage in inspector:
// Add to any UI element with CanvasGroup[RequireComponent(typeof(RectTransform))]
public class UiScale : UiAnimationBase
{
[SerializeField] private UiAnimatorEndValueTypes type;
[SerializeField] private Vector3 value;
}
// Scales the RectTransform[RequireComponent(typeof(RectTransform))]
public class UiSlide : UiAnimationBase
{
[SerializeField] private Vector2 positionDelta;
[SerializeField] private bool reverseDirectionOnHide = true;
}
// Slides based on percentage of element size[RequireComponent(typeof(TMP_Text))]
public class UiTextFade : UiAnimationBase
{
// Fades TMP_Text alpha
}[RequireComponent(typeof(RectTransform))]
public class UiActive : UiAnimationBase
{
// Simply sets gameObject active/inactive
}public class AnimatedPanel : InteractiveElementBase
{
[SerializeField] private List<UiAnimationSettings> _customAnimations; // Fill in the Editor
protected override void OnShowStart()
{
base.OnShowStart();
Debug.Log("Panel show started");
}
protected override void OnShowComplete()
{
base.OnShowComplete();
Debug.Log("Panel show completed");
}
public void PlayCustomSequence()
{
var seq = CreateSequence(_customAnimations, Purpose.Show);
seq.Play();
}
}// GOOD - Reasonable stack depth
_viewService.PopView<MenuPresenter>();
_viewService.PopView<SettingsPresenter>();
_viewService.Back(); // Back to menu
// BAD - Deep stacks can use memory
for (int i = 0; i < 50; i++)
{
_viewService.PopView<DeepPresenter>(); // Don't do this!
}public class OptimizedPopulator<T> : PopulatorBase<T> where T : class
{
private int _maxPoolSize = 50;
public OptimizedPopulator(PopulatorElementBase<T> sample, Transform root, int maxSize)
: base(sample, root)
{
_maxPoolSize = maxSize;
}
public override void UpdateElements(IEnumerable<T> data, float delayEveryElement = 0f)
{
var dataArray = data as T[] ?? data.ToArray();
// Limit data size
if (dataArray.Length > _maxPoolSize)
{
Debug.LogWarning($"Truncating data from {dataArray.Length} to {_maxPoolSize}");
dataArray = dataArray.Take(_maxPoolSize).ToArray();
}
base.UpdateElements(dataArray, delayEveryElement);
}
}// GOOD - Batch animations
public Sequence ShowAll()
{
var seq = DOTween.Sequence();
foreach (var element in _elements)
{
seq.Join(element.Show()); // All play together
}
return seq;
}
// BAD - Simultaneous animations for many elements
public Sequence ShowAllSequential()
{
var seq = DOTween.Sequence();
foreach (var element in _elements)
{
seq.Insert(0f, element.Show()); // Plays one after another - slow!
}
return seq;
}// GOOD - Simple states
public class SimpleButton : WidgetBase
{
[SerializeField] private List<WidgetState> _states; // 2-3 states
}
// BAD - Too many states, consider using nested widgets
public class ComplexWidget : WidgetBase
{
[SerializeField] private List<WidgetState> _states; // 20+ states - hard to manage
}Symptoms: PopView() called but nothing appears, or errors are displayed in Console
Solutions:
// SOLUTION 1: Ensure presenter is registered
public class MyInstaller : InstallerBehaviourBase{
[SerializeField] private RootPresenterBase _root;
protected override void Install()
{
// Make sure all child presenters are assigned in inspector
Debug.Log($"Root has {_root.ChildPresenters.Count} child presenters");
}
}
// SOLUTION 2: Check if view exists in container
public bool CanShowPresenter<T>() where T : PresenterBase
{
var root = FindObjectOfType<RootPresenter>();
return root.ChildPresenters.Any(p => p is T);
}Symptoms: Back() doesn't work, can't navigate past certain screens
Solution:
public class ModalPresenter : PresenterBase
{
[SerializeField] private bool _isModal = true;
public override bool Modal => _isModal;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
// Handle modal-specific back behavior
ViewService.Back();
}
}
}
// ViewService automatically blocks navigation past modal
// _history.TryPeek(out var current)
// if (current.Last() is IModal || current.Last().Modal)
// {
// return; // Navigation blocked
// }// GOOD - Clear hierarchy
// RootPresenter
// ├── MainMenuPresenter
// │ └── SettingsPresenter (child of MainMenu)
// ├── GameplayPresenter
// │ ├── HUDPresenter
// │ └── PausePresenter (modal)
// └── LoadingPresenter
// In code
public class GameplayPresenter : PresenterBase
{
[SerializeField] private HUDPresenter _hud;
[SerializeField] private PausePresenter _pause;
protected override void OnPresenterInitialized()
{
childPresenters.Add(_hud);
childPresenters.Add(_pause);
}
}// Create reusable widgets
public class HealthBarWidget : WidgetBase
{
[SaveGameData] private float _health;
public void SetHealth(float health)
{
_health = health;
UpdateState();
}
private void UpdateState()
{
if (_health > 50) SetState("Healthy");
else if (_health > 20) SetState("Warning");
else SetState("Critical");
}
}
// Reuse everywhere
public class PlayerHUD : PresenterBase
{
[SerializeField] private HealthBarWidget _playerHealth;
[SerializeField] private HealthBarWidget _bossHealth;
}public class ShopUI : PresenterBase
{
[SerializeField] private ShopItemElement _itemPrefab;
[SerializeField] private Transform _itemContainer;
private PopulatorBase<ShopItem> _populator;
protected override void OnPresenterInitialized()
{
base.OnPresenterInitialized();
_populator = new PopulatorBase<ShopItem>(_itemPrefab, _itemContainer);
}
public void ShowItems(List<ShopItem> items)
{
_populator.UpdateElements(items, 0.03f); // Staggered appearance
}
}public class OnboardingFlow : PresenterBase
{
[SerializeField] private List<InteractiveElementBase> _steps;
public Sequence PlayOnboarding()
{
var seq = DOTween.Sequence();
foreach (var step in _steps)
{
seq.Append(step.Show());
seq.AppendInterval(2f);
seq.Append(step.Hide());
}
seq.OnComplete(() => {
ViewService.Back(); // Return to previous screen
});
return seq;
}
}public class ConfirmationDialog : PresenterBase, IModal
{
[SerializeField] private Button _confirmButton;
[SerializeField] private Button _cancelButton;
private System.Action _onConfirm;
private System.Action _onCancel;
public void Show(string message, System.Action onConfirm, System.Action onCancel = null)
{
_onConfirm = onConfirm;
_onCancel = onCancel;
// Set message text
ViewService.PopView(this);
}
public void OnConfirm()
{
_onConfirm?.Invoke();
ViewService.Back();
}
public void OnCancel()
{
_onCancel?.Invoke();
ViewService.Back();
}
}public class UISoundManager : MonoBehaviour
{
[Inject] private ViewService _viewService;
[Inject] private SoundService _sound;
private void Start()
{
_viewService.OnAnyViewBecameActive += OnViewChanged;
}
private void OnViewChanged()
{
var view = _viewService.ActiveView;
if (view is SettingsPresenter)
_sound.Play("ui_settings_open");
else if (view is InventoryPresenter)
_sound.Play("ui_inventory_open");
}
}public class AsyncOperationPresenter : PresenterBase
{
[SerializeField] private Slider _progressBar;
[SerializeField] private TMP_Text _statusText;
public async void LoadAsync()
{
ViewService.PopLoadingView();
_statusText.text = "Loading...";
var operation = SomeAsyncOperation();
while (!operation.IsCompleted)
{
_progressBar.value = operation.Progress;
await Task.Delay(100);
}
_statusText.text = "Complete!";
await Task.Delay(500);
ViewService.Back();
}
}public static class ViewDebugger
{
public static void LogViewHierarchy(ViewService viewService)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("--- View Hierarchy ---");
var root = DependencyContainer.Instance.GetInstance<RootPresenterBase>();
LogPresenter(sb, root, 0);
Debug.Log(sb.ToString());
}
private static void LogPresenter(System.Text.StringBuilder sb, PresenterBase presenter, int depth)
{
var indent = new string(' ', depth * 2);
sb.AppendLine($"{indent}├─ {presenter.name} ({presenter.GetType().Name})");
foreach (var child in presenter.ChildPresenters)
{
LogPresenter(sb, child, depth + 1);
}
}
}// 1. Define presenters
public class MainMenuPresenter : PresenterBase
{
[SerializeField] private Button _playButton;
[SerializeField] private Button _settingsButton;
[SerializeField] private Button _quitButton;
protected override void OnPresenterInitialized()
{
_playButton.onClick.AddListener(OnPlayClicked);
_settingsButton.onClick.AddListener(OnSettingsClicked);
_quitButton.onClick.AddListener(OnQuitClicked);
}
private void OnPlayClicked()
{
ViewService.PopView<GameplayPresenter>();
}
private void OnSettingsClicked()
{
ViewService.PopView<SettingsPresenter>(this);
}
private void OnQuitClicked()
{
Application.Quit();
}
}
public class SettingsPresenter : PresenterBase
{
[SerializeField] private Slider _volumeSlider;
[SerializeField] private Toggle _fullscreenToggle;
[SerializeField] private Button _backButton;
[Inject] private SettingsModel _settings;
protected override void OnPresenterInitialized()
{
_volumeSlider.value = _settings.Volume;
_fullscreenToggle.isOn = _settings.Fullscreen;
_volumeSlider.onValueChanged.AddListener(OnVolumeChanged);
_fullscreenToggle.onValueChanged.AddListener(OnFullscreenChanged);
_backButton.onClick.AddListener(() => ViewService.Back());
}
private void OnVolumeChanged(float volume)
{
_settings.Volume = volume;
}
private void OnFullscreenChanged(bool fullscreen)
{
_settings.Fullscreen = fullscreen;
}
}
public class GameplayPresenter : PresenterBase
{
[SerializeField] private HUDPresenter _hud;
[SerializeField] private PauseMenuPresenter _pauseMenu;
private bool _isPaused;
protected override void OnPresenterInitialized()
{
childPresenters.Add(_hud);
childPresenters.Add(_pauseMenu);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
TogglePause();
}
}
private void TogglePause()
{
_isPaused = !_isPaused;
if (_isPaused)
{
ViewService.PopView<PauseMenuPresenter>(this);
Time.timeScale = 0f;
}
else
{
ViewService.Back();
Time.timeScale = 1f;
}
}
}
// 2. Widget for HUD
public class HUDPresenter : PresenterBase
{
[SerializeField] private HealthBarWidget _healthBar;
[SerializeField] private ScoreWidget _scoreWidget;
[SerializeField] private AmmoWidget _ammoWidget;
[Inject] private PlayerModel _player;
protected override void OnPresenterInitialized()
{
// Bind to model
_player.HealthAccessor.BindDownwards(health => _healthBar.SetHealth(health));
_player.ScoreAccessor.BindDownwards(score => _scoreWidget.SetScore(score));
_player.AmmoAccessor.BindDownwards(ammo => _ammoWidget.SetAmmo(ammo));
}
}
public class HealthBarWidget : WidgetBase
{
[SerializeField] private Slider _slider;
[SerializeField] private TMP_Text _text;
public void SetHealth(float health)
{
_slider.value = health / 100f;
_text.text = $"{health:F0}%";
if (health < 20) SetState("Critical");
else if (health < 50) SetState("Warning");
else SetState("Healthy");
}
}
// 3. Populator for inventory
public class InventoryPresenter : PresenterBase
{
[SerializeField] private InventoryItemElement _itemPrefab;
[SerializeField] private Transform _contentRoot;
private PopulatorBase<ItemData> _populator;
protected override void OnPresenterInitialized()
{
_populator = new PopulatorBase<ItemData>(_itemPrefab, _contentRoot);
}
public void ShowInventory(List<ItemData> items)
{
_populator.UpdateElements(items, 0.03f);
}
}
public class InventoryItemElement : PopulatorElementBase<ItemData>
{
[SerializeField] private TMP_Text _nameText;
[SerializeField] private TMP_Text _countText;
[SerializeField] private Image _iconImage;
public override void SetData(ItemData data)
{
base.SetData(data);
_nameText.text = data.itemName;
_countText.text = $"x{data.count}";
_iconImage.sprite = data.icon;
}
}
// 4. Installer
[DefaultExecutionOrder(-10000)]
public class UIInstaller : InstallerBehaviourBase
{
[SerializeField] private RootPresenterBase _rootPresenter;
[SerializeField] private bool _debugViews = true;
protected override void Install()
{
var viewService = new ViewService(_rootPresenter, _debugViews);
BindAsSingleton(viewService);
}
}
// 5. Usage in game
public class GameController : MonoBehaviour
{
[Inject] private ViewService _viewService;
[Inject] private InventoryPresenter _inventory;
private void Start()
{
// Start with main menu
_viewService.PopView<MainMenuPresenter>();
}
public void OpenInventory(List<ItemData> items)
{
_viewService.PopView<InventoryPresenter>(out var inventory);
inventory.ShowInventory(items);
}
}┌─────────────────────────────────────────────────────────────────┐
│ ViewService │
│ (Navigation) │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ RootPresenterBase │──────▶│ PresenterBase │
│ (Container) │ │ (Screen) │
└─────────────────────────┘ └─────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ WidgetBase │ │ PopulatorBase │
│ (Stateful) │ │ (Dynamic) │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ WidgetElement │ │PopulatorElement │
│ (Element) │ │ (Element) │
└─────────────────┘ └─────────────────┘
│ │
└───────────────┬──────────────────┘
▼
┌─────────────────┐
│ UiAnimationBase │
│ (Animation) │
└─────────────────┘
| Component | Purpose | Key Methods |
|---|---|---|
ViewService |
Navigation | PopView<T>(), Back(), ClearHistory() |
PresenterBase |
Screen UI | ShowFrom(), HideTo(), InitializePresenter() |
WidgetBase |
Stateful component | SetState(), state change events |
WidgetElement |
UI element | Show(), Hide() |
PopulatorBase<T> |
Dynamic lists | UpdateElements(), Clear() |
PopulatorElementBase<T> |
List item | SetData(), Refresh() |
UiAnimationBase |
Animation | Show(), Hide() |
| # | Key Point | Why It Matters |
|---|---|---|
| 1 | ViewService manages a stack of presenters | Enables back navigation and view history |
| 2 | Presenters can have child presenters | Creates hierarchical UI structures |
| 3 | Modal presenters block back navigation | Ensures modal dialogs behave correctly |
| 4 | Widgets maintain multiple visual states | Perfect for tabs, buttons, toggles, status indicators |
| 5 | Populators use object pooling | Efficient rendering of large lists |
| 6 | Animations are declarative and sequenced | Complex transitions with minimal code |
| 7 | All UI components are injectable | Seamless integration with DI container |
| 8 | Loading and error screens are built-in | Consistent user experience |
| Component | Use When |
|---|---|
PresenterBase |
Creating a new screen/menu |
WidgetBase |
Building reusable components with multiple states |
WidgetElement |
Creating individual UI pieces |
PopulatorBase<T> |
Displaying dynamic lists (inventory, shop, leaderboards) |
| Custom Animator | Creating unique transition effects |
The CherryFramework UI system provides a robust, scalable foundation for building complex user interfaces with clean separation of concerns, efficient rendering, and smooth animations.