Popcorning Guinea Pig
Search Results for

    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

    1. Keep hierarchies shallow: Avoid excessive nesting
    2. Logical grouping: Group related elements together
    3. Clear naming: Use descriptive names for elements
    4. Consistent structure: Follow consistent patterns across your UI

    Performance

    1. Minimize updates: Only update when necessary
    2. Use object pooling: Reuse UI elements when possible
    3. Efficient culling: Don't render invisible elements
    4. Batch operations: Group multiple changes together

    Maintainability

    1. Separation of concerns: Keep layout, styling, and logic separate
    2. Reusable components: Create composable UI components
    3. Clear interfaces: Define clear contracts between components
    4. 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.

    • Edit this page
    In this article
    Back to top © 2024 Popcorning Guinea Pig Game Engine. A custom C# game engine for indie developers.