Hierarchical UI
The Popcorning Guinea Pig engine uses a hierarchical UI system where elements are organized in a parent-child tree structure, enabling complex layouts, event propagation, and efficient rendering.
Overview
The hierarchical UI system provides:
- Parent-child relationships with inheritance of properties
- Automatic coordinate system transformations
- Event propagation up and down the hierarchy
- Efficient rendering through culling and batching
- State management and lifecycle events
- Modular component composition
UI Element Hierarchy
Basic Parent-Child Relationships
Every UI element can contain child elements:
// Create parent container
var mainPanel = new Panel
{
Position = new Vector2(100, 100),
Size = new Vector2(400, 300),
BackgroundColor = Color.DarkBlue
};
// Create child elements
var titleLabel = new Label
{
Text = "Settings",
Position = new Vector2(20, 20), // Relative to parent
TextColor = Color.White
};
var closeButton = new Button
{
Text = "X",
Position = new Vector2(360, 10), // Relative to parent
Size = new Vector2(30, 30)
};
// Build hierarchy
mainPanel.AddChild(titleLabel);
mainPanel.AddChild(closeButton);
Deep Hierarchies
Create complex nested structures:
// Game UI hierarchy
var gameUI = new Panel { Name = "GameUI", Size = screenSize };
var topBar = new Panel { Name = "TopBar", Dock = Dock.Top, Size = new Vector2(0, 60) };
var healthBar = new ProgressBar { Position = new Vector2(10, 10), Size = new Vector2(200, 20) };
var manaBar = new ProgressBar { Position = new Vector2(10, 35), Size = new Vector2(200, 20) };
topBar.AddChild(healthBar);
topBar.AddChild(manaBar);
var inventoryPanel = new Panel { Name = "Inventory", Position = new Vector2(50, 50), Size = new Vector2(300, 400) };
var inventoryGrid = new GridContainer { Rows = 8, Columns = 6 };
inventoryPanel.AddChild(inventoryGrid);
var chatPanel = new Panel { Name = "Chat", Position = new Vector2(400, 300), Size = new Vector2(300, 200) };
var chatScrollView = new ScrollView();
var chatContent = new StackPanel { Orientation = StackOrientation.Vertical };
chatScrollView.Content = chatContent;
chatPanel.AddChild(chatScrollView);
// Build complete hierarchy
gameUI.AddChild(topBar);
gameUI.AddChild(inventoryPanel);
gameUI.AddChild(chatPanel);
Coordinate Transformations
Local vs World Coordinates
The hierarchy system automatically transforms coordinates:
public abstract class UIElement
{
public Vector2 Position { get; set; } // Local position relative to parent
public Vector2 WorldPosition
{
get
{
if (Parent != null)
return Parent.WorldPosition + Position;
return Position;
}
}
public Rectangle WorldBounds
{
get
{
var worldPos = WorldPosition;
return new Rectangle((int)worldPos.X, (int)worldPos.Y, (int)Size.X, (int)Size.Y);
}
}
public Vector2 LocalToWorld(Vector2 localPoint)
{
return WorldPosition + localPoint;
}
public Vector2 WorldToLocal(Vector2 worldPoint)
{
return worldPoint - WorldPosition;
}
}
Transformation Matrices
For advanced transformations, use matrix-based coordinate conversion:
public class TransformableUIElement : UIElement
{
public float Rotation { get; set; }
public Vector2 Scale { get; set; } = Vector2.One;
public Vector2 Origin { get; set; } = Vector2.Zero;
public Matrix LocalTransform
{
get
{
return Matrix.CreateTranslation(-Origin.X, -Origin.Y, 0) *
Matrix.CreateScale(Scale.X, Scale.Y, 1) *
Matrix.CreateRotationZ(Rotation) *
Matrix.CreateTranslation(Position.X + Origin.X, Position.Y + Origin.Y, 0);
}
}
public Matrix WorldTransform
{
get
{
var local = LocalTransform;
return Parent is TransformableUIElement transformableParent
? local * transformableParent.WorldTransform
: local;
}
}
public Vector2 TransformPoint(Vector2 point)
{
return Vector2.Transform(point, WorldTransform);
}
}
Event Propagation
Bubbling Events
Events propagate from child to parent by default:
public class UIEventManager
{
public void ProcessClickEvent(Vector2 clickPosition)
{
var hitElement = FindDeepestHitElement(rootElement, clickPosition);
if (hitElement != null)
{
PropagateClickEvent(hitElement, clickPosition);
}
}
private void PropagateClickEvent(UIElement element, Vector2 position)
{
var currentElement = element;
while (currentElement != null)
{
var handled = currentElement.OnClick?.Invoke(currentElement, position);
if (handled == true || currentElement.StopsEventPropagation)
break;
currentElement = currentElement.Parent;
}
}
private UIElement FindDeepestHitElement(UIElement element, Vector2 position)
{
if (!element.Visible || !element.Contains(position))
return null;
// Check children first (front to back)
for (int i = element.Children.Count - 1; i >= 0; i--)
{
var child = element.Children[i];
var hit = FindDeepestHitElement(child, position);
if (hit != null)
return hit;
}
// Return this element if no children were hit
return element.CanReceiveEvents ? element : null;
}
}
Capture Events
Implement event capturing (parent to child):
public class UIElement
{
public event Func<UIElement, UIEventArgs, bool> OnPreviewClick; // Capturing phase
public event Func<UIElement, UIEventArgs, bool> OnClick; // Bubbling phase
public bool HandleCaptureEvent(UIEventArgs e)
{
// Capture phase: propagate down from root to target
var path = GetPathToRoot().Reverse().ToList();
foreach (var element in path)
{
var handled = element.OnPreviewClick?.Invoke(element, e);
if (handled == true)
return true;
}
return false;
}
public bool HandleBubbleEvent(UIEventArgs e)
{
// Bubble phase: propagate up from target to root
var currentElement = this;
while (currentElement != null)
{
var handled = currentElement.OnClick?.Invoke(currentElement, e);
if (handled == true)
return true;
currentElement = currentElement.Parent;
}
return false;
}
}
Rendering System
Z-Order Management
Control rendering order with Z-indices:
public class UIElement
{
public int ZIndex { get; set; } = 0;
public bool ClipChildren { get; set; } = false;
public virtual void Draw(SpriteBatch spriteBatch)
{
if (!Visible) return;
// Set up clipping if needed
Rectangle? previousScissorRect = null;
if (ClipChildren)
{
previousScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
spriteBatch.GraphicsDevice.ScissorRectangle = WorldBounds;
}
// Draw this element
DrawSelf(spriteBatch);
// Draw children sorted by Z-index
var sortedChildren = Children
.Where(c => c.Visible)
.OrderBy(c => c.ZIndex)
.ToList();
foreach (var child in sortedChildren)
{
child.Draw(spriteBatch);
}
// Restore clipping
if (ClipChildren && previousScissorRect.HasValue)
{
spriteBatch.GraphicsDevice.ScissorRectangle = previousScissorRect.Value;
}
}
protected virtual void DrawSelf(SpriteBatch spriteBatch)
{
// Override in derived classes to draw element-specific content
}
}
Culling and Optimization
Optimize rendering with viewport culling:
public class OptimizedUIRenderer
{
private Rectangle _viewport;
public void DrawWithCulling(UIElement root, SpriteBatch spriteBatch, Rectangle viewport)
{
_viewport = viewport;
DrawElementWithCulling(root, spriteBatch);
}
private void DrawElementWithCulling(UIElement element, SpriteBatch spriteBatch)
{
if (!element.Visible) return;
var worldBounds = element.WorldBounds;
// Cull elements outside viewport
if (!worldBounds.Intersects(_viewport))
return;
// Draw this element if visible
element.DrawSelf(spriteBatch);
// Draw visible children
foreach (var child in element.Children.OrderBy(c => c.ZIndex))
{
DrawElementWithCulling(child, spriteBatch);
}
}
}
State Management
Inherited Properties
Properties can be inherited down the hierarchy:
public class UIElement
{
private bool? _enabled;
private float? _alpha;
public bool Enabled
{
get => _enabled ?? Parent?.Enabled ?? true;
set => _enabled = value;
}
public float Alpha
{
get => _alpha ?? Parent?.Alpha ?? 1.0f;
set => _alpha = value;
}
public float EffectiveAlpha => Alpha * (Parent?.EffectiveAlpha ?? 1.0f);
public bool IsEffectivelyEnabled => Enabled && (Parent?.IsEffectivelyEnabled ?? true);
}
State Propagation
Propagate state changes through the hierarchy:
public class UIElement
{
public virtual void SetVisibility(bool visible, bool recursive = false)
{
Visible = visible;
if (recursive)
{
foreach (var child in Children)
{
child.SetVisibility(visible, true);
}
}
OnVisibilityChanged?.Invoke(this, visible);
}
public virtual void SetEnabled(bool enabled, bool recursive = false)
{
Enabled = enabled;
if (recursive)
{
foreach (var child in Children)
{
child.SetEnabled(enabled, true);
}
}
OnEnabledChanged?.Invoke(this, enabled);
}
protected virtual void OnParentChanged(UIElement oldParent, UIElement newParent)
{
// Update inherited properties when parent changes
InvalidateInheritedProperties();
// Notify children of hierarchy change
foreach (var child in Children)
{
child.OnAncestorChanged();
}
}
}
Component Composition
Composite UI Elements
Create complex components from simpler ones:
public class DialogBox : Panel
{
private Label _titleLabel;
private Panel _contentArea;
private Panel _buttonArea;
private Button _okButton;
private Button _cancelButton;
public string Title
{
get => _titleLabel.Text;
set => _titleLabel.Text = value;
}
public UIElement Content
{
get => _contentArea.Children.FirstOrDefault();
set
{
_contentArea.Children.Clear();
if (value != null)
_contentArea.AddChild(value);
}
}
public event Action OnOK;
public event Action OnCancel;
public DialogBox()
{
InitializeComponents();
LayoutComponents();
SetupEvents();
}
private void InitializeComponents()
{
// Title bar
_titleLabel = new Label
{
Font = Fonts.Bold,
TextColor = Color.White
};
// Content area
_contentArea = new Panel
{
BackgroundColor = Color.Transparent
};
// Button area
_buttonArea = new Panel
{
BackgroundColor = Color.DarkGray
};
_okButton = new Button
{
Text = "OK",
Size = new Vector2(80, 30)
};
_cancelButton = new Button
{
Text = "Cancel",
Size = new Vector2(80, 30)
};
// Build hierarchy
AddChild(_titleLabel);
AddChild(_contentArea);
AddChild(_buttonArea);
_buttonArea.AddChild(_okButton);
_buttonArea.AddChild(_cancelButton);
}
private void LayoutComponents()
{
_titleLabel.Dock = Dock.Top;
_titleLabel.Size = new Vector2(0, 30);
_buttonArea.Dock = Dock.Bottom;
_buttonArea.Size = new Vector2(0, 50);
_contentArea.Dock = Dock.Fill;
_okButton.Anchor = AnchorPoint.BottomRight;
_okButton.Position = new Vector2(-90, -35);
_cancelButton.Anchor = AnchorPoint.BottomRight;
_cancelButton.Position = new Vector2(-10, -35);
}
private void SetupEvents()
{
_okButton.OnClick = () => OnOK?.Invoke();
_cancelButton.OnClick = () => OnCancel?.Invoke();
}
}
// Usage
var dialog = new DialogBox
{
Title = "Confirm Action",
Size = new Vector2(300, 200),
Content = new Label { Text = "Are you sure you want to continue?" }
};
dialog.OnOK = () => PerformAction();
dialog.OnCancel = () => CloseDialog();
Template-Based Components
Create reusable component templates:
public class UITemplate
{
public string Name { get; set; }
public Dictionary<string, object> Properties { get; set; }
public List<UITemplate> Children { get; set; }
public UIElement Instantiate()
{
var element = CreateElement();
ApplyProperties(element);
foreach (var childTemplate in Children ?? new List<UITemplate>())
{
var child = childTemplate.Instantiate();
element.AddChild(child);
}
return element;
}
private UIElement CreateElement()
{
return Name switch
{
"Button" => new Button(),
"Label" => new Label(),
"Panel" => new Panel(),
"StackPanel" => new StackPanel(),
_ => throw new InvalidOperationException($"Unknown element type: {Name}")
};
}
}
// Load template from JSON
var template = JsonSerializer.Deserialize<UITemplate>(templateJson);
var menuUI = template.Instantiate();
Lifecycle Management
Element Lifecycle
Manage element lifecycle events:
public abstract class UIElement
{
public bool IsLoaded { get; private set; }
public bool IsInitialized { get; private set; }
public event Action<UIElement> Loaded;
public event Action<UIElement> Unloaded;
public event Action<UIElement> ParentChanged;
internal void Load()
{
if (IsLoaded) return;
OnLoad();
IsLoaded = true;
Loaded?.Invoke(this);
// Load children
foreach (var child in Children)
{
child.Load();
}
}
internal void Unload()
{
if (!IsLoaded) return;
// Unload children first
foreach (var child in Children)
{
child.Unload();
}
OnUnload();
IsLoaded = false;
Unloaded?.Invoke(this);
}
protected virtual void OnLoad()
{
// Override in derived classes
}
protected virtual void OnUnload()
{
// Override in derived classes
// Clean up resources, unsubscribe events, etc.
}
public virtual void AddChild(UIElement child)
{
if (child.Parent != null)
child.Parent.RemoveChild(child);
Children.Add(child);
child.Parent = this;
child.OnParentChanged();
if (IsLoaded)
child.Load();
}
public virtual void RemoveChild(UIElement child)
{
if (Children.Remove(child))
{
child.Unload();
child.Parent = null;
child.OnParentChanged();
}
}
}
Resource Management
Manage resources throughout the hierarchy:
public class ResourceManagedUIElement : UIElement
{
private readonly List<IDisposable> _resources = new();
protected T LoadResource<T>(string path) where T : class, IDisposable
{
var resource = AssetRegistry.LoadAsset<T>(path);
_resources.Add(resource);
return resource;
}
protected override void OnUnload()
{
// Dispose all resources
foreach (var resource in _resources)
{
resource?.Dispose();
}
_resources.Clear();
base.OnUnload();
}
}
Performance Optimization
Dirty Rectangle Updates
Only update changed regions:
public class UIElement
{
private Rectangle _lastBounds;
private bool _isDirty = true;
public Rectangle DirtyRegion { get; private set; }
public void MarkDirty()
{
if (!_isDirty)
{
_isDirty = true;
DirtyRegion = WorldBounds;
Parent?.MarkChildDirty(this);
}
}
private void MarkChildDirty(UIElement child)
{
if (!_isDirty)
{
DirtyRegion = Rectangle.Union(DirtyRegion, child.DirtyRegion);
Parent?.MarkChildDirty(this);
}
}
public override void Draw(SpriteBatch spriteBatch)
{
if (_isDirty)
{
// Only redraw dirty regions
DrawDirtyRegions(spriteBatch);
_isDirty = false;
_lastBounds = WorldBounds;
}
}
}
Batch Operations
Batch hierarchy operations for efficiency:
public class UIBatchOperations
{
private readonly List<Action> _pendingOperations = new();
private bool _batchMode = false;
public void BeginBatch()
{
_batchMode = true;
}
public void EndBatch()
{
_batchMode = false;
foreach (var operation in _pendingOperations)
{
operation();
}
_pendingOperations.Clear();
}
public void AddChild(UIElement parent, UIElement child)
{
if (_batchMode)
{
_pendingOperations.Add(() => parent.AddChild(child));
}
else
{
parent.AddChild(child);
}
}
public void RemoveChild(UIElement parent, UIElement child)
{
if (_batchMode)
{
_pendingOperations.Add(() => parent.RemoveChild(child));
}
else
{
parent.RemoveChild(child);
}
}
}
Best Practices
Hierarchy Design
- Keep hierarchies shallow: Avoid excessive nesting
- Logical grouping: Group related elements together
- Clear naming: Use descriptive names for elements
- Consistent structure: Follow consistent patterns across your UI
Performance
- Minimize updates: Only update when necessary
- Use object pooling: Reuse UI elements when possible
- Efficient culling: Don't render invisible elements
- Batch operations: Group multiple changes together
Maintainability
- Separation of concerns: Keep layout, styling, and logic separate
- Reusable components: Create composable UI components
- Clear interfaces: Define clear contracts between components
- Resource cleanup: Always dispose of resources properly
The hierarchical UI system provides a powerful foundation for creating complex, maintainable user interfaces with proper event handling, state management, and performance optimization in your Popcorning Guinea Pig games.