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
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; }Responsive Breakpoints
public static class ScreenBreakpoints { public const int Mobile = 480; public const int Tablet = 768; public const int Desktop = 1024; }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
Minimize Layout Updates
- Only recalculate when necessary
- Use layout invalidation sparingly
- Batch layout updates when possible
Efficient Layout Algorithms
- Choose appropriate layout containers
- Avoid complex constraint solving in tight loops
- Cache layout calculations when possible
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.