Nine-Slice Textures
Nine-slice textures are a powerful technique for creating scalable UI elements that maintain their visual quality at any size. The Popcorning Guinea Pig engine provides comprehensive support for nine-slice textures in UI components.
Overview
Nine-slice textures allow you to:
- Scale UI elements without distorting corners and edges
- Create buttons and panels that look good at any size
- Reduce the number of texture assets needed
- Maintain consistent visual style across different screen sizes
- Support complex border designs with minimal memory usage
How Nine-Slice Works
A nine-slice texture divides an image into nine sections:
┌─────┬─────────────┬─────┐
│ 1 │ 2 │ 3 │ Top row
├─────┼─────────────┼─────┤
│ 4 │ 5 │ 6 │ Middle row
├─────┼─────────────┼─────┤
│ 7 │ 8 │ 9 │ Bottom row
└─────┴─────────────┴─────┘
- Corners (1, 3, 7, 9): Never scaled, maintain original size
- Edges (2, 4, 6, 8): Scaled in one dimension only
- Center (5): Scaled in both dimensions
Setting Up Nine-Slice Textures
Texture Requirements
Create textures with clear slice boundaries:
Button texture example (64x32 pixels):
- Left border: 8 pixels
- Right border: 8 pixels
- Top border: 8 pixels
- Bottom border: 8 pixels
- Center: 48x16 pixels (scalable area)
NineSliceSprite Class
The engine provides a NineSliceSprite class for handling nine-slice rendering:
public class NineSliceSprite
{
public Texture2D Texture { get; set; }
public Rectangle SourceRectangle { get; set; }
public Padding Borders { get; set; }
public Color Tint { get; set; }
public NineSliceSprite(Texture2D texture, int left, int right, int top, int bottom)
{
Texture = texture;
Borders = new Padding(left, right, top, bottom);
SourceRectangle = new Rectangle(0, 0, texture.Width, texture.Height);
Tint = Color.White;
}
public void Draw(SpriteBatch spriteBatch, Rectangle destinationRectangle)
{
DrawNineSlice(spriteBatch, destinationRectangle, Tint);
}
}
UI Components with Nine-Slice Support
NineSliceButton
A button component that uses nine-slice textures for scalability:
public class NineSliceButton : Button
{
private NineSliceSprite _normalSprite;
private NineSliceSprite _hoverSprite;
private NineSliceSprite _pressedSprite;
private NineSliceSprite _disabledSprite;
public void SetNineSliceTextures(
Texture2D normal,
Texture2D hover,
Texture2D pressed,
Texture2D disabled,
int borderLeft, int borderRight, int borderTop, int borderBottom)
{
_normalSprite = new NineSliceSprite(normal, borderLeft, borderRight, borderTop, borderBottom);
_hoverSprite = new NineSliceSprite(hover, borderLeft, borderRight, borderTop, borderBottom);
_pressedSprite = new NineSliceSprite(pressed, borderLeft, borderRight, borderTop, borderBottom);
_disabledSprite = new NineSliceSprite(disabled, borderLeft, borderRight, borderTop, borderBottom);
}
public override void Draw(SpriteBatch spriteBatch)
{
var sprite = State switch
{
ButtonState.Normal => _normalSprite,
ButtonState.Hover => _hoverSprite,
ButtonState.Pressed => _pressedSprite,
ButtonState.Disabled => _disabledSprite,
_ => _normalSprite
};
sprite?.Draw(spriteBatch, GetAbsoluteBounds());
// Draw text on top
DrawText(spriteBatch);
}
}
// Usage
var button = new NineSliceButton
{
Text = "Scalable Button",
Position = new Vector2(100, 100),
Size = new Vector2(200, 50) // Can be any size
};
button.SetNineSliceTextures(
assetRegistry.LoadAsset<Texture2D>("ui/button_normal.png"),
assetRegistry.LoadAsset<Texture2D>("ui/button_hover.png"),
assetRegistry.LoadAsset<Texture2D>("ui/button_pressed.png"),
assetRegistry.LoadAsset<Texture2D>("ui/button_disabled.png"),
8, 8, 8, 8 // Border sizes
);
NineSlicePanel
A panel component with nine-slice background:
public class NineSlicePanel : Panel
{
private NineSliceSprite _backgroundSprite;
private NineSliceSprite _borderSprite;
public void SetNineSliceBackground(Texture2D texture, int borderLeft, int borderRight, int borderTop, int borderBottom)
{
_backgroundSprite = new NineSliceSprite(texture, borderLeft, borderRight, borderTop, borderBottom);
}
public void SetNineSliceBorder(Texture2D texture, int borderLeft, int borderRight, int borderTop, int borderBottom)
{
_borderSprite = new NineSliceSprite(texture, borderLeft, borderRight, borderTop, borderBottom);
}
public override void Draw(SpriteBatch spriteBatch)
{
var bounds = GetAbsoluteBounds();
// Draw background
_backgroundSprite?.Draw(spriteBatch, bounds);
// Draw border
_borderSprite?.Draw(spriteBatch, bounds);
base.Draw(spriteBatch);
}
}
// Usage
var dialogPanel = new NineSlicePanel
{
Position = new Vector2(200, 150),
Size = new Vector2(400, 300)
};
dialogPanel.SetNineSliceBackground(
assetRegistry.LoadAsset<Texture2D>("ui/dialog_background.png"),
12, 12, 12, 12
);
Advanced Nine-Slice Techniques
Animated Nine-Slice Components
Create animated UI elements that scale properly:
public class AnimatedNineSliceButton : NineSliceButton
{
private float _pulseTimer;
private Vector2 _originalSize;
private bool _isPulsing;
public void StartPulseAnimation(float intensity = 0.1f, float speed = 2f)
{
_originalSize = Size;
_isPulsing = true;
_pulseIntensity = intensity;
_pulseSpeed = speed;
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
if (_isPulsing)
{
_pulseTimer += (float)gameTime.ElapsedGameTime.TotalSeconds * _pulseSpeed;
var scale = 1f + Mathf.Sin(_pulseTimer) * _pulseIntensity;
Size = _originalSize * scale;
}
}
}
Multi-State Nine-Slice Components
Handle complex UI states with multiple nine-slice textures:
public class ProgressBarNineSlice : ProgressBar
{
private NineSliceSprite _backgroundSprite;
private NineSliceSprite _fillSprite;
private NineSliceSprite _overlaySprite;
public void SetNineSliceTextures(
Texture2D background,
Texture2D fill,
Texture2D overlay,
int borderSize)
{
_backgroundSprite = new NineSliceSprite(background, borderSize, borderSize, borderSize, borderSize);
_fillSprite = new NineSliceSprite(fill, borderSize, borderSize, borderSize, borderSize);
_overlaySprite = new NineSliceSprite(overlay, borderSize, borderSize, borderSize, borderSize);
}
public override void Draw(SpriteBatch spriteBatch)
{
var bounds = GetAbsoluteBounds();
// Draw background
_backgroundSprite?.Draw(spriteBatch, bounds);
// Draw fill based on progress
var fillWidth = (int)(bounds.Width * GetProgressPercentage());
var fillBounds = new Rectangle(bounds.X, bounds.Y, fillWidth, bounds.Height);
if (fillWidth > 0)
{
_fillSprite?.Draw(spriteBatch, fillBounds);
}
// Draw overlay
_overlaySprite?.Draw(spriteBatch, bounds);
base.Draw(spriteBatch);
}
}
Custom Nine-Slice Shapes
Create non-rectangular nine-slice elements:
public class RoundedNineSlicePanel : UIElement
{
private NineSliceSprite _sprite;
private float _cornerRadius;
public float CornerRadius
{
get => _cornerRadius;
set
{
_cornerRadius = value;
UpdateNineSliceBorders();
}
}
private void UpdateNineSliceBorders()
{
var borderSize = (int)Math.Ceiling(_cornerRadius);
if (_sprite != null)
{
_sprite.Borders = new Padding(borderSize, borderSize, borderSize, borderSize);
}
}
public void SetRoundedTexture(Texture2D texture, float cornerRadius)
{
_cornerRadius = cornerRadius;
var borderSize = (int)Math.Ceiling(cornerRadius);
_sprite = new NineSliceSprite(texture, borderSize, borderSize, borderSize, borderSize);
}
public override void Draw(SpriteBatch spriteBatch)
{
_sprite?.Draw(spriteBatch, GetAbsoluteBounds());
base.Draw(spriteBatch);
}
}
Creating Nine-Slice Textures
Design Guidelines
When creating nine-slice textures, follow these guidelines:
Clear Border Definition
- Make border areas visually distinct - Ensure corner elements don't have scalable content - Keep border thickness consistentScalable Center
- Design center area to tile seamlessly - Avoid directional elements in the center - Use patterns or solid colors that scale wellEdge Considerations
- Top/bottom edges should tile horizontally - Left/right edges should tile vertically - Maintain visual consistency when tiled
Texture Creation Tool
Create a utility to help generate nine-slice textures:
public static class NineSliceTextureGenerator
{
public static Texture2D CreateRoundedRectangle(
GraphicsDevice graphicsDevice,
int width, int height,
int cornerRadius,
Color fillColor,
Color borderColor,
int borderThickness = 1)
{
var texture = new Texture2D(graphicsDevice, width, height);
var data = new Color[width * height];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
var color = GetPixelColor(x, y, width, height, cornerRadius, fillColor, borderColor, borderThickness);
data[y * width + x] = color;
}
}
texture.SetData(data);
return texture;
}
private static Color GetPixelColor(int x, int y, int width, int height,
int cornerRadius, Color fillColor, Color borderColor, int borderThickness)
{
// Implement rounded rectangle pixel generation logic
// Return appropriate color based on position
var isInBorder = x < borderThickness || x >= width - borderThickness ||
y < borderThickness || y >= height - borderThickness;
var distanceFromCorner = GetDistanceFromNearestCorner(x, y, width, height, cornerRadius);
if (distanceFromCorner <= cornerRadius)
{
if (distanceFromCorner >= cornerRadius - borderThickness)
return borderColor;
else
return fillColor;
}
return isInBorder ? borderColor : fillColor;
}
}
Performance Optimization
Batch Rendering
Optimize nine-slice rendering with batching:
public class NineSliceBatchRenderer
{
private readonly List<NineSliceDrawCall> _drawCalls;
private readonly Dictionary<Texture2D, List<NineSliceDrawCall>> _batchedCalls;
public void AddDrawCall(NineSliceSprite sprite, Rectangle destination)
{
var drawCall = new NineSliceDrawCall
{
Sprite = sprite,
Destination = destination
};
_drawCalls.Add(drawCall);
}
public void FlushBatch(SpriteBatch spriteBatch)
{
// Group draw calls by texture
_batchedCalls.Clear();
foreach (var call in _drawCalls)
{
if (!_batchedCalls.ContainsKey(call.Sprite.Texture))
_batchedCalls[call.Sprite.Texture] = new List<NineSliceDrawCall>();
_batchedCalls[call.Sprite.Texture].Add(call);
}
// Render batches
foreach (var batch in _batchedCalls)
{
RenderBatch(spriteBatch, batch.Key, batch.Value);
}
_drawCalls.Clear();
}
private void RenderBatch(SpriteBatch spriteBatch, Texture2D texture, List<NineSliceDrawCall> calls)
{
// Optimize rendering of multiple nine-slice sprites with the same texture
foreach (var call in calls)
{
call.Sprite.Draw(spriteBatch, call.Destination);
}
}
}
Texture Atlasing
Combine multiple nine-slice textures into texture atlases:
public class NineSliceAtlas
{
public Texture2D AtlasTexture { get; private set; }
public Dictionary<string, NineSliceInfo> NineSlices { get; private set; }
public class NineSliceInfo
{
public Rectangle SourceRectangle { get; set; }
public Padding Borders { get; set; }
}
public NineSliceSprite GetNineSlice(string name)
{
if (NineSlices.TryGetValue(name, out var info))
{
return new NineSliceSprite(AtlasTexture,
info.Borders.Left, info.Borders.Right,
info.Borders.Top, info.Borders.Bottom)
{
SourceRectangle = info.SourceRectangle
};
}
return null;
}
}
// Usage
var atlas = LoadNineSliceAtlas("ui/ui_atlas.json");
var button = new NineSliceButton();
button.SetNineSliceSprite(
atlas.GetNineSlice("button_normal"),
atlas.GetNineSlice("button_hover"),
atlas.GetNineSlice("button_pressed")
);
Common Use Cases
Dialog Boxes
Create scalable dialog boxes with nine-slice textures:
public class DialogBox : NineSlicePanel
{
private Label _titleLabel;
private Label _contentLabel;
private List<Button> _buttons;
public DialogBox(string title, string content)
{
SetNineSliceBackground(
AssetRegistry.LoadAsset<Texture2D>("ui/dialog_bg.png"),
16, 16, 16, 16
);
CreateComponents(title, content);
LayoutComponents();
}
public void AddButton(string text, Action onClick)
{
var button = new NineSliceButton
{
Text = text,
OnClick = onClick
};
button.SetNineSliceTextures(
AssetRegistry.LoadAsset<Texture2D>("ui/button_normal.png"),
AssetRegistry.LoadAsset<Texture2D>("ui/button_hover.png"),
AssetRegistry.LoadAsset<Texture2D>("ui/button_pressed.png"),
null,
8, 8, 8, 8
);
_buttons.Add(button);
AddChild(button);
LayoutComponents();
}
}
Inventory Slots
Create consistent inventory slot appearances:
public class InventorySlot : NineSliceButton
{
private Texture2D _itemTexture;
private int _quantity;
public InventorySlot()
{
Size = new Vector2(50, 50);
SetNineSliceTextures(
AssetRegistry.LoadAsset<Texture2D>("ui/slot_empty.png"),
AssetRegistry.LoadAsset<Texture2D>("ui/slot_hover.png"),
AssetRegistry.LoadAsset<Texture2D>("ui/slot_selected.png"),
null,
4, 4, 4, 4
);
}
public void SetItem(Texture2D itemTexture, int quantity)
{
_itemTexture = itemTexture;
_quantity = quantity;
}
public override void Draw(SpriteBatch spriteBatch)
{
base.Draw(spriteBatch); // Draw nine-slice background
if (_itemTexture != null)
{
var itemBounds = GetContentBounds();
spriteBatch.Draw(_itemTexture, itemBounds, Color.White);
if (_quantity > 1)
{
DrawQuantityText(spriteBatch);
}
}
}
}
Health and Mana Bars
Create scalable status bars:
public class StatusBar : UIElement
{
private NineSliceSprite _backgroundSprite;
private NineSliceSprite _fillSprite;
private float _currentValue;
private float _maxValue;
public StatusBar(string backgroundTexture, string fillTexture)
{
_backgroundSprite = new NineSliceSprite(
AssetRegistry.LoadAsset<Texture2D>(backgroundTexture),
4, 4, 4, 4
);
_fillSprite = new NineSliceSprite(
AssetRegistry.LoadAsset<Texture2D>(fillTexture),
4, 4, 4, 4
);
}
public void SetValue(float current, float max)
{
_currentValue = current;
_maxValue = max;
}
public override void Draw(SpriteBatch spriteBatch)
{
var bounds = GetAbsoluteBounds();
// Draw background
_backgroundSprite.Draw(spriteBatch, bounds);
// Draw fill based on current value
if (_maxValue > 0)
{
var fillPercentage = _currentValue / _maxValue;
var fillWidth = (int)(bounds.Width * fillPercentage);
var fillBounds = new Rectangle(bounds.X, bounds.Y, fillWidth, bounds.Height);
if (fillWidth > 0)
{
_fillSprite.Draw(spriteBatch, fillBounds);
}
}
}
}
Best Practices
Design Practices
Consistent Border Sizes
- Use the same border thickness across related UI elements
- Document border sizes for your UI style guide
- Consider different screen DPIs when setting border sizes
Texture Optimization
- Keep nine-slice textures as small as possible
- Use power-of-two dimensions when possible
- Consider compression settings for different platforms
Visual Consistency
- Maintain consistent corner radius across similar elements
- Use the same color palette for related UI components
- Ensure borders and fills complement each other
Performance Practices
Texture Reuse
- Share nine-slice textures between similar components
- Use texture atlases to reduce draw calls
- Cache NineSliceSprite instances when possible
Rendering Optimization
- Batch nine-slice draws by texture
- Avoid unnecessary texture switches
- Use appropriate blend states and sampler states
Memory Management
- Dispose of unused nine-slice textures
- Monitor texture memory usage
- Consider streaming for large UI texture sets
Nine-slice textures provide a powerful way to create scalable, professional-looking UI elements that maintain their visual quality across different screen sizes and resolutions in your Popcorning Guinea Pig games.