Popcorning Guinea Pig
Search Results for

    Layouts and Positioning

    The Popcorning Guinea Pig engine provides a flexible positioning and layout system for UI elements, supporting multiple coordinate systems, anchor points, and automatic layout containers.

    Overview

    The layout system features:

    • Multiple coordinate systems (absolute, relative, normalized)
    • Anchor-based positioning with automatic adjustment
    • Flexible sizing options (fixed, percentage, content-based)
    • Layout containers for automatic arrangement
    • Responsive design support for different screen sizes
    • Z-order management for layering

    Coordinate Systems

    Absolute Positioning

    Position elements using pixel coordinates:

    var button = new Button
    {
        Position = new Vector2(100, 50), // 100 pixels from left, 50 from top
        Size = new Vector2(200, 40)      // 200 pixels wide, 40 pixels tall
    };
    

    Relative Positioning

    Position elements relative to their parent:

    var panel = new Panel
    {
        Position = new Vector2(50, 50),
        Size = new Vector2(400, 300)
    };
    
    var childButton = new Button
    {
        Position = new Vector2(20, 20), // 20 pixels from panel's top-left
        Size = new Vector2(100, 30)
    };
    
    panel.AddChild(childButton);
    

    Normalized Positioning

    Use normalized coordinates (0.0 to 1.0) for responsive layouts:

    var centerButton = new Button
    {
        Position = new Vector2(0.5f, 0.5f), // Center of screen
        Size = new Vector2(200, 40),
        Anchor = AnchorPoint.Center, // Anchor at center
        CoordinateSystem = UICoordinateSystem.Normalized
    };
    

    Anchor Points

    Anchor points determine how elements are positioned relative to their coordinates:

    public enum AnchorPoint
    {
        TopLeft,     // Default: position is top-left corner
        TopCenter,   // Position is top-center
        TopRight,    // Position is top-right corner
        CenterLeft,  // Position is center-left
        Center,      // Position is center of element
        CenterRight, // Position is center-right
        BottomLeft,  // Position is bottom-left corner
        BottomCenter,// Position is bottom-center
        BottomRight  // Position is bottom-right corner
    }
    
    // Usage examples
    var topRightButton = new Button
    {
        Position = new Vector2(800, 0),
        Size = new Vector2(100, 40),
        Anchor = AnchorPoint.TopRight // Button's top-right at (800, 0)
    };
    
    var centerLabel = new Label
    {
        Position = new Vector2(400, 300),
        Anchor = AnchorPoint.Center, // Label centered at (400, 300)
        Text = "Centered Text"
    };
    

    Sizing Options

    Fixed Sizing

    Set explicit pixel dimensions:

    var fixedPanel = new Panel
    {
        Size = new Vector2(300, 200) // Always 300x200 pixels
    };
    

    Percentage Sizing

    Size relative to parent container:

    var parentPanel = new Panel
    {
        Size = new Vector2(400, 300)
    };
    
    var childPanel = new Panel
    {
        SizeMode = SizeMode.Percentage,
        Size = new Vector2(0.5f, 0.8f), // 50% width, 80% height of parent
        Parent = parentPanel
    };
    

    Content-Based Sizing

    Automatically size to fit content:

    var autoLabel = new Label
    {
        Text = "This text determines the size",
        SizeMode = SizeMode.ContentFit, // Size to fit text
        Font = myFont
    };
    
    var autoPanel = new Panel
    {
        SizeMode = SizeMode.ContentFit, // Size to fit all children
        Padding = new Padding(10) // Add padding around content
    };
    

    Layout Containers

    StackPanel

    Arrange children in a vertical or horizontal stack:

    public class StackPanel : UIElement
    {
        public StackOrientation Orientation { get; set; } = StackOrientation.Vertical;
        public float Spacing { get; set; } = 0f;
        public StackAlignment Alignment { get; set; } = StackAlignment.Start;
        
        public override void UpdateLayout()
        {
            ArrangeChildren();
            base.UpdateLayout();
        }
        
        private void ArrangeChildren()
        {
            float currentOffset = 0f;
            
            foreach (var child in Children)
            {
                if (!child.Visible) continue;
                
                if (Orientation == StackOrientation.Vertical)
                {
                    child.Position = new Vector2(GetAlignmentOffset(child), currentOffset);
                    currentOffset += child.Size.Y + Spacing;
                }
                else
                {
                    child.Position = new Vector2(currentOffset, GetAlignmentOffset(child));
                    currentOffset += child.Size.X + Spacing;
                }
            }
        }
    }
    
    // Usage
    var menuStack = new StackPanel
    {
        Position = new Vector2(100, 100),
        Orientation = StackOrientation.Vertical,
        Spacing = 10f,
        Alignment = StackAlignment.Center
    };
    
    menuStack.AddChild(new Button { Text = "Play", Size = new Vector2(150, 40) });
    menuStack.AddChild(new Button { Text = "Options", Size = new Vector2(150, 40) });
    menuStack.AddChild(new Button { Text = "Exit", Size = new Vector2(150, 40) });
    

    GridLayout

    Arrange children in a grid pattern:

    public class GridLayout : UIElement
    {
        public int Rows { get; set; } = 1;
        public int Columns { get; set; } = 1;
        public Vector2 CellSpacing { get; set; } = Vector2.Zero;
        public GridSizeMode SizeMode { get; set; } = GridSizeMode.Uniform;
        
        public void AddChild(UIElement child, int row, int column)
        {
            child.SetGridPosition(row, column);
            AddChild(child);
            UpdateLayout();
        }
        
        protected override void UpdateLayout()
        {
            var cellSize = CalculateCellSize();
            
            foreach (var child in Children)
            {
                var gridPos = child.GetGridPosition();
                var position = new Vector2(
                    gridPos.Column * (cellSize.X + CellSpacing.X),
                    gridPos.Row * (cellSize.Y + CellSpacing.Y)
                );
                
                child.Position = position;
                if (SizeMode == GridSizeMode.Uniform)
                    child.Size = cellSize;
            }
            
            base.UpdateLayout();
        }
    }
    
    // Usage
    var inventoryGrid = new GridLayout
    {
        Rows = 5,
        Columns = 8,
        CellSpacing = new Vector2(2, 2),
        Position = new Vector2(50, 50)
    };
    
    // Add items to specific grid positions
    for (int row = 0; row < 5; row++)
    {
        for (int col = 0; col < 8; col++)
        {
            var slot = new InventorySlot();
            inventoryGrid.AddChild(slot, row, col);
        }
    }
    

    DockPanel

    Dock children to edges of the container:

    public class DockPanel : UIElement
    {
        public override void UpdateLayout()
        {
            var availableArea = new Rectangle(0, 0, (int)Size.X, (int)Size.Y);
            
            foreach (var child in Children.OrderBy(c => c.DockPriority))
            {
                if (!child.Visible) continue;
                
                var dock = child.Dock;
                var childSize = child.Size;
                
                switch (dock)
                {
                    case Dock.Top:
                        child.Position = new Vector2(availableArea.X, availableArea.Y);
                        child.Size = new Vector2(availableArea.Width, childSize.Y);
                        availableArea.Y += (int)childSize.Y;
                        availableArea.Height -= (int)childSize.Y;
                        break;
                        
                    case Dock.Bottom:
                        child.Position = new Vector2(availableArea.X, availableArea.Bottom - childSize.Y);
                        child.Size = new Vector2(availableArea.Width, childSize.Y);
                        availableArea.Height -= (int)childSize.Y;
                        break;
                        
                    case Dock.Left:
                        child.Position = new Vector2(availableArea.X, availableArea.Y);
                        child.Size = new Vector2(childSize.X, availableArea.Height);
                        availableArea.X += (int)childSize.X;
                        availableArea.Width -= (int)childSize.X;
                        break;
                        
                    case Dock.Right:
                        child.Position = new Vector2(availableArea.Right - childSize.X, availableArea.Y);
                        child.Size = new Vector2(childSize.X, availableArea.Height);
                        availableArea.Width -= (int)childSize.X;
                        break;
                        
                    case Dock.Fill:
                        child.Position = new Vector2(availableArea.X, availableArea.Y);
                        child.Size = new Vector2(availableArea.Width, availableArea.Height);
                        break;
                }
            }
        }
    }
    
    // Usage
    var gameUI = new DockPanel
    {
        Size = new Vector2(800, 600)
    };
    
    var topBar = new Panel { Dock = Dock.Top, Size = new Vector2(0, 50) };
    var bottomBar = new Panel { Dock = Dock.Bottom, Size = new Vector2(0, 30) };
    var sidePanel = new Panel { Dock = Dock.Right, Size = new Vector2(200, 0) };
    var mainArea = new Panel { Dock = Dock.Fill };
    
    gameUI.AddChild(topBar);
    gameUI.AddChild(bottomBar);
    gameUI.AddChild(sidePanel);
    gameUI.AddChild(mainArea);
    

    Responsive Design

    Screen Size Adaptation

    Create layouts that adapt to different screen sizes:

    public class ResponsiveLayout : UIElement
    {
        private readonly Dictionary<ScreenSize, LayoutConfig> _layouts;
        private ScreenSize _currentScreenSize;
        
        public ResponsiveLayout()
        {
            _layouts = new Dictionary<ScreenSize, LayoutConfig>();
            SetupLayouts();
        }
        
        private void SetupLayouts()
        {
            // Mobile layout (small screens)
            _layouts[ScreenSize.Small] = new LayoutConfig
            {
                ButtonSize = new Vector2(120, 40),
                Spacing = 8,
                Orientation = StackOrientation.Vertical,
                FontSize = 14
            };
            
            // Desktop layout (large screens)
            _layouts[ScreenSize.Large] = new LayoutConfig
            {
                ButtonSize = new Vector2(150, 50),
                Spacing = 15,
                Orientation = StackOrientation.Horizontal,
                FontSize = 18
            };
        }
        
        public void UpdateForScreenSize(Vector2 screenSize)
        {
            var newScreenSize = DetermineScreenSize(screenSize);
            
            if (newScreenSize != _currentScreenSize)
            {
                _currentScreenSize = newScreenSize;
                ApplyLayout(_layouts[newScreenSize]);
            }
        }
        
        private ScreenSize DetermineScreenSize(Vector2 screenSize)
        {
            return screenSize.X < 800 ? ScreenSize.Small : ScreenSize.Large;
        }
    }
    

    Aspect Ratio Handling

    Maintain aspect ratios across different screen sizes:

    public class AspectRatioContainer : UIElement
    {
        public float AspectRatio { get; set; } = 16f / 9f; // Default 16:9
        public AspectRatioMode Mode { get; set; } = AspectRatioMode.FitInside;
        
        protected override void UpdateLayout()
        {
            var availableSize = Size;
            var targetSize = CalculateTargetSize(availableSize, AspectRatio, Mode);
            var offset = (availableSize - targetSize) * 0.5f;
            
            foreach (var child in Children)
            {
                child.Position = offset;
                child.Size = targetSize;
            }
            
            base.UpdateLayout();
        }
        
        private Vector2 CalculateTargetSize(Vector2 available, float aspectRatio, AspectRatioMode mode)
        {
            var availableAspect = available.X / available.Y;
            
            return mode switch
            {
                AspectRatioMode.FitInside => availableAspect > aspectRatio
                    ? new Vector2(available.Y * aspectRatio, available.Y)
                    : new Vector2(available.X, available.X / aspectRatio),
                
                AspectRatioMode.FillContainer => availableAspect < aspectRatio
                    ? new Vector2(available.Y * aspectRatio, available.Y)
                    : new Vector2(available.X, available.X / aspectRatio),
                
                _ => available
            };
        }
    }
    

    Advanced Positioning

    Constraint-Based Layout

    Create layouts with constraints and relationships:

    public class ConstraintLayout : UIElement
    {
        private readonly List<LayoutConstraint> _constraints;
        
        public void AddConstraint(UIElement element, ConstraintType type, UIElement target, float offset = 0)
        {
            _constraints.Add(new LayoutConstraint
            {
                Element = element,
                Type = type,
                Target = target,
                Offset = offset
            });
        }
        
        protected override void UpdateLayout()
        {
            // Sort constraints by dependency order
            var sortedConstraints = TopologicalSort(_constraints);
            
            foreach (var constraint in sortedConstraints)
            {
                ApplyConstraint(constraint);
            }
            
            base.UpdateLayout();
        }
        
        private void ApplyConstraint(LayoutConstraint constraint)
        {
            var element = constraint.Element;
            var target = constraint.Target ?? this;
            
            switch (constraint.Type)
            {
                case ConstraintType.LeftOf:
                    element.Position = new Vector2(
                        target.Position.X - element.Size.X - constraint.Offset,
                        element.Position.Y);
                    break;
                    
                case ConstraintType.RightOf:
                    element.Position = new Vector2(
                        target.Position.X + target.Size.X + constraint.Offset,
                        element.Position.Y);
                    break;
                    
                case ConstraintType.Above:
                    element.Position = new Vector2(
                        element.Position.X,
                        target.Position.Y - element.Size.Y - constraint.Offset);
                    break;
                    
                case ConstraintType.Below:
                    element.Position = new Vector2(
                        element.Position.X,
                        target.Position.Y + target.Size.Y + constraint.Offset);
                    break;
                    
                case ConstraintType.CenterHorizontal:
                    element.Position = new Vector2(
                        target.Position.X + (target.Size.X - element.Size.X) * 0.5f,
                        element.Position.Y);
                    break;
                    
                case ConstraintType.CenterVertical:
                    element.Position = new Vector2(
                        element.Position.X,
                        target.Position.Y + (target.Size.Y - element.Size.Y) * 0.5f);
                    break;
            }
        }
    }
    
    // Usage
    var constraintLayout = new ConstraintLayout
    {
        Size = new Vector2(400, 300)
    };
    
    var titleLabel = new Label { Text = "Game Title", Size = new Vector2(200, 30) };
    var playButton = new Button { Text = "Play", Size = new Vector2(100, 40) };
    var optionsButton = new Button { Text = "Options", Size = new Vector2(100, 40) };
    
    constraintLayout.AddChild(titleLabel);
    constraintLayout.AddChild(playButton);
    constraintLayout.AddChild(optionsButton);
    
    // Set up constraints
    constraintLayout.AddConstraint(titleLabel, ConstraintType.CenterHorizontal, null);
    constraintLayout.AddConstraint(playButton, ConstraintType.CenterHorizontal, null);
    constraintLayout.AddConstraint(playButton, ConstraintType.Below, titleLabel, 20);
    constraintLayout.AddConstraint(optionsButton, ConstraintType.Below, playButton, 10);
    constraintLayout.AddConstraint(optionsButton, ConstraintType.CenterHorizontal, null);
    

    Animation-Aware Positioning

    Handle positioning during animations:

    public class AnimatedLayout : UIElement
    {
        private readonly Dictionary<UIElement, Vector2> _targetPositions;
        private readonly Dictionary<UIElement, Vector2> _animationVelocities;
        
        public void AnimateToPosition(UIElement element, Vector2 targetPosition, float duration)
        {
            _targetPositions[element] = targetPosition;
            
            var currentPosition = element.Position;
            var distance = Vector2.Distance(currentPosition, targetPosition);
            var velocity = distance / duration;
            
            _animationVelocities[element] = (targetPosition - currentPosition).Normalized() * velocity;
        }
        
        public override void Update(GameTime gameTime)
        {
            var deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
            
            foreach (var kvp in _animationVelocities.ToList())
            {
                var element = kvp.Key;
                var velocity = kvp.Value;
                var targetPosition = _targetPositions[element];
                
                var newPosition = element.Position + velocity * deltaTime;
                var distanceToTarget = Vector2.Distance(newPosition, targetPosition);
                
                if (distanceToTarget < 1f) // Close enough
                {
                    element.Position = targetPosition;
                    _animationVelocities.Remove(element);
                    _targetPositions.Remove(element);
                }
                else
                {
                    element.Position = newPosition;
                }
            }
            
            base.Update(gameTime);
        }
    }
    

    Layout Performance

    Layout Caching

    Cache layout calculations to improve performance:

    public class CachedLayout : UIElement
    {
        private bool _layoutDirty = true;
        private Vector2 _lastSize;
        private readonly Dictionary<UIElement, Rectangle> _cachedBounds;
        
        public override void UpdateLayout()
        {
            if (!_layoutDirty && Size == _lastSize)
                return; // Use cached layout
            
            RecalculateLayout();
            _layoutDirty = false;
            _lastSize = Size;
        }
        
        private void RecalculateLayout()
        {
            _cachedBounds.Clear();
            
            // Perform expensive layout calculations
            foreach (var child in Children)
            {
                var bounds = CalculateChildBounds(child);
                _cachedBounds[child] = bounds;
                
                child.Position = new Vector2(bounds.X, bounds.Y);
                child.Size = new Vector2(bounds.Width, bounds.Height);
            }
        }
        
        public void InvalidateLayout()
        {
            _layoutDirty = true;
            Parent?.InvalidateLayout();
        }
    }
    

    Lazy Layout Updates

    Update layouts only when necessary:

    public class LazyLayout : UIElement
    {
        private readonly HashSet<UIElement> _dirtyElements;
        private bool _layoutUpdateScheduled;
        
        public void MarkElementDirty(UIElement element)
        {
            _dirtyElements.Add(element);
            ScheduleLayoutUpdate();
        }
        
        private void ScheduleLayoutUpdate()
        {
            if (!_layoutUpdateScheduled)
            {
                _layoutUpdateScheduled = true;
                // Schedule update for next frame
                GameLoop.Schedule(() => PerformLayoutUpdate());
            }
        }
        
        private void PerformLayoutUpdate()
        {
            foreach (var element in _dirtyElements)
            {
                UpdateElementLayout(element);
            }
            
            _dirtyElements.Clear();
            _layoutUpdateScheduled = false;
        }
    }
    

    Best Practices

    Layout Design

    1. Consistent Spacing

      // Use consistent spacing values
      public static class LayoutConstants
      {
          public const float SmallSpacing = 4f;
          public const float MediumSpacing = 8f;
          public const float LargeSpacing = 16f;
      }
      
    2. Responsive Breakpoints

      public static class ScreenBreakpoints
      {
          public const int Mobile = 480;
          public const int Tablet = 768;
          public const int Desktop = 1024;
      }
      
    3. Layout Hierarchies

      // Keep layout hierarchies reasonable depth
      // Prefer composition over deep nesting
      var mainLayout = new DockPanel();
      var contentArea = new StackPanel();
      var buttonRow = new GridLayout { Columns = 3 };
      

    Performance Optimization

    1. Minimize Layout Updates

      • Only recalculate when necessary
      • Use layout invalidation sparingly
      • Batch layout updates when possible
    2. Efficient Layout Algorithms

      • Choose appropriate layout containers
      • Avoid complex constraint solving in tight loops
      • Cache layout calculations when possible
    3. Memory Management

      • Reuse layout objects when possible
      • Clean up event subscriptions
      • Monitor memory usage in complex layouts

    The layout and positioning system provides the foundation for creating professional, responsive user interfaces that work across different screen sizes and device types 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.