Best Practices
Updated 3 days agoBest Practices
Design patterns, performance tips, and code organization for building quality ZUI integrations.
📋 Table of Contents
- Integration Patterns
- Code Organization
- UI Design Guidelines
- Performance Optimization
- Error Handling
- Testing Strategies
- Common Pitfalls
Integration Patterns
Always Use Soft Dependencies
✅ DO:
[BepInDependency("Zanakinz.ZUI", BepInDependency.DependencyFlags.SoftDependency)]
public class Plugin : BasePlugin
{
public override void Load()
{
if (InitZUI())
{
RegisterUI();
}
// Core functionality works regardless
InitializeCore();
}
}
❌ DON'T:
// Hard dependency - mod fails without ZUI
[BepInDependency("Zanakinz.ZUI", BepInDependency.DependencyFlags.HardDependency)]
public class Plugin : BasePlugin
{
public override void Load()
{
ZUI.SetPlugin("MyMod"); // Crashes if ZUI missing
}
}
Why: Soft dependencies maximize compatibility and provide better user experience.
Centralize ZUI Calls
✅ DO:
public class Plugin : BasePlugin
{
private static Type _zuiType;
private void Call(string methodName, params object[] args)
{
if (_zuiType == null) return;
try
{
var method = _zuiType.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m => m.Name == methodName &&
m.GetParameters().Length == args.Length);
if (method != null)
{
method.Invoke(null, args);
}
else
{
Log.LogWarning($"ZUI method not found: {methodName}({args.Length} params)");
}
}
catch (Exception ex)
{
Log.LogError($"Error calling ZUI.{methodName}: {ex.Message}");
}
}
}
Why: Single point of error handling, logging, and debugging.
Separate UI Registration
✅ DO:
public class Plugin : BasePlugin
{
public override void Load()
{
InitializeCore();
if (InitZUI())
{
RegisterMainMenuUI();
RegisterCustomWindows();
}
}
private void RegisterMainMenuUI()
{
Call("SetPlugin", "MyMod");
Call("SetTargetWindow", "Main");
Call("AddCategory", "Features");
Call("AddButton", "Feature 1", ".feat1");
}
private void RegisterCustomWindows()
{
RegisterControlPanel();
RegisterStatusWindow();
}
private void RegisterControlPanel()
{
Call("SetPlugin", "MyMod");
Call("SetTargetWindow", "ControlPanel");
Call("SetUI", 600, 400);
Call("SetTitle", "Control Panel");
// ... add elements
}
}
Why: Better organization, easier to maintain, clearer code structure.
Code Organization
Use Constants for Values
✅ DO:
public class ZUIConfig
{
// Plugin info
public const string PLUGIN_NAME = "MyMod";
// Window dimensions
public const int CONTROL_PANEL_WIDTH = 600;
public const int CONTROL_PANEL_HEIGHT = 400;
public const int STATUS_WINDOW_WIDTH = 400;
public const int STATUS_WINDOW_HEIGHT = 300;
// Layout constants
public const float MARGIN = 20f;
public const float LINE_HEIGHT = 30f;
public const float BUTTON_SPACING = 50f;
// Window names
public const string WINDOW_CONTROL = "ControlPanel";
public const string WINDOW_STATUS = "StatusWindow";
}
// Usage
Call("SetPlugin", ZUIConfig.PLUGIN_NAME);
Call("SetUI", ZUIConfig.CONTROL_PANEL_WIDTH, ZUIConfig.CONTROL_PANEL_HEIGHT);
Call("AddText", "Status", ZUIConfig.MARGIN, ZUIConfig.MARGIN);
❌ DON'T:
// Magic numbers scattered everywhere
Call("SetPlugin", "MyMod");
Call("SetUI", 600, 400);
Call("AddText", "Status", 20f, 20f);
Call("AddButton", "Test", ".test", 20f, 70f); // Where did 70 come from?
Why: Easier to update, self-documenting code, prevents inconsistencies.
Create Helper Classes
✅ DO:
public class ZUIHelper
{
private readonly Action<string, object[]> _call;
public ZUIHelper(Action<string, object[]> callMethod)
{
_call = callMethod;
}
public void CreateWindow(string name, int width, int height, string title)
{
_call("SetTargetWindow", new object[] { name });
_call("SetUI", new object[] { width, height });
_call("SetTitle", new object[] { title });
}
public void AddSection(string title, float x, float y, params ButtonInfo[] buttons)
{
_call("AddCategory", new object[] { title, x, y });
float buttonY = y + 40f;
foreach (var button in buttons)
{
_call("AddButton", new object[] { button.Text, button.Command, x, buttonY });
buttonY += 50f;
}
}
}
public class ButtonInfo
{
public string Text { get; set; }
public string Command { get; set; }
}
// Usage
var helper = new ZUIHelper(Call);
helper.CreateWindow("MyPanel", 600, 400, "My Panel");
helper.AddSection("Actions", 20f, 50f,
new ButtonInfo { Text = "Action 1", Command = ".act1" },
new ButtonInfo { Text = "Action 2", Command = ".act2" }
);
Why: Reduces code duplication, creates reusable components, cleaner code.
Use Layout Calculators
✅ DO:
public class LayoutHelper
{
private float _currentY;
private readonly float _margin;
private readonly float _spacing;
public LayoutHelper(float margin = 20f, float spacing = 50f)
{
_margin = margin;
_spacing = spacing;
_currentY = margin;
}
public float NextY()
{
float y = _currentY;
_currentY += _spacing;
return y;
}
public void Reset() => _currentY = _margin;
public float Margin => _margin;
}
// Usage
var layout = new LayoutHelper();
Call("AddText", "Header", layout.Margin, layout.NextY());
Call("AddButton", "Button 1", ".cmd1", layout.Margin, layout.NextY());
Call("AddButton", "Button 2", ".cmd2", layout.Margin, layout.NextY());
Call("AddButton", "Button 3", ".cmd3", layout.Margin, layout.NextY());
Why: Automatic spacing, consistent layout, easy to adjust.
UI Design Guidelines
Plan Your Layout First
✅ DO:
Before coding, either:
- Sketch on paper - Draw your UI layout
- Use the Visual Designer - https://zanakinz.github.io/ZUI
- Create mockups - Plan element positions
Why: Saves time, prevents rework, better results.
Use Consistent Spacing
✅ DO:
const float MARGIN = 20f;
const float HEADER_SPACING = 40f;
const float ELEMENT_SPACING = 50f;
const float SECTION_SPACING = 80f;
Call("SetUI", 600, 400);
float y = MARGIN;
// Header
Call("AddText", "Title", MARGIN, y);
y += HEADER_SPACING;
// Section 1
Call("AddCategory", "Section 1", MARGIN, y);
y += ELEMENT_SPACING;
Call("AddButton", "Button 1", ".cmd1", MARGIN, y);
y += ELEMENT_SPACING;
Call("AddButton", "Button 2", ".cmd2", MARGIN, y);
y += SECTION_SPACING; // Larger gap between sections
// Section 2
Call("AddCategory", "Section 2", MARGIN, y);
❌ DON'T:
// Inconsistent, random spacing
Call("AddText", "Title", 20f, 20f);
Call("AddCategory", "Section 1", 20f, 55f);
Call("AddButton", "Button 1", ".cmd1", 20f, 103f);
Call("AddButton", "Button 2", ".cmd2", 20f, 161f); // Why 161?
Why: Professional appearance, easier to read, maintainable.
Group Related Elements
✅ DO:
// Player management section
Call("AddCategory", "<color=#3498DB>Player Management</color>", 20f, 50f);
Call("AddButton", "Heal Player", ".heal", 20f, 90f);
Call("AddButton", "Kick Player", ".kick", 20f, 140f);
Call("AddButton", "Ban Player", ".ban", 20f, 190f);
// Server management section
Call("AddCategory", "<color=#E74C3C>Server Management</color>", 20f, 260f);
Call("AddButton", "Save World", ".save", 20f, 300f);
Call("AddButton", "Restart Server", ".restart", 20f, 350f);
Why: Logical organization, easier to navigate, better UX.
Use Visual Hierarchy
✅ DO:
// Main title - large, bold, colored
Call("SetTitle", "<size=18><b><color=#4ECDC4>Admin Panel</color></b></size>");
// Section headers - medium, colored
Call("AddCategory", "<size=14><color=#FFE66D>Quick Actions</color></size>", 20f, 50f);
// Regular text - normal size
Call("AddText", "Select an action below", 20f, 90f);
// Important warnings - red, italic
Call("AddText", "<color=#E74C3C><i>Warning: These actions are permanent</i></color>", 20f, 350f);
Why: Guides user attention, improves readability, professional look.
Respect Screen Boundaries
✅ DO:
// Reasonable window sizes for common resolutions
const int MAX_WIDTH = 1200; // Fits on 1920x1080
const int MAX_HEIGHT = 800;
// Add padding from window edges
const float MARGIN = 20f;
const float RIGHT_EDGE = MAX_WIDTH - MARGIN - 200f; // Account for element width
Call("SetUI", MAX_WIDTH, MAX_HEIGHT);
Call("AddButton", "Right Button", ".cmd", RIGHT_EDGE, 50f, 180f, 35f);
❌ DON'T:
// Window too large for most screens
Call("SetUI", 2500, 1800);
// Elements positioned outside window
Call("SetUI", 500, 300);
Call("AddText", "Off screen", 600f, 50f); // X=600 but window is only 500 wide
Why: Usability on all screen sizes, professional appearance.
Performance Optimization
Minimize Element Count
✅ DO:
// Use categories to organize, not excessive buttons
Call("AddCategory", "Player Actions");
Call("AddButton", "Heal", ".heal");
Call("AddButton", "Teleport", ".tp");
Call("AddButton", "Speed", ".speed");
Call("AddCategory", "Admin Actions");
Call("AddButton", "God Mode", ".god");
Call("AddButton", "Fly", ".fly");
❌ DON'T:
// Creating hundreds of buttons
for (int i = 0; i < 500; i++)
{
Call("AddButton", $"Item {i}", $".item{i}");
}
Why: Better performance, easier to navigate, cleaner UI.
Optimize Image Sizes
✅ DO:
// Use reasonably sized images
// Logo: 200x100 pixels, < 100KB
Call("AddImage", "logo.png", 20f, 20f, 200f, 100f);
// Background: 600x400 pixels, < 500KB
Call("AddImage", "background.png", 0f, 0f, 600f, 400f);
❌ DON'T:
// Using massive, unoptimized images
// logo_4k.png: 3840x2160, 15MB
Call("AddImage", "logo_4k.png", 20f, 20f, 200f, 100f); // Wastes memory
Why: Faster loading, better performance, smaller mod size.
Register UI Once
✅ DO:
private bool _uiRegistered = false;
public override void Load()
{
if (InitZUI() && !_uiRegistered)
{
RegisterUI();
_uiRegistered = true;
}
}
private void RegisterUI()
{
// Register once on load
Call("SetPlugin", "MyMod");
Call("SetTargetWindow", "Main");
Call("AddCategory", "Features");
Call("AddButton", "Action", ".action");
}
❌ DON'T:
// Registering UI every frame - VERY BAD!
public override void Update()
{
RegisterUI(); // Called 60+ times per second!
}
Why: Prevents performance issues, memory leaks, UI flicker.
Use Lazy Initialization
✅ DO:
private bool _zuiInitialized = false;
private bool InitZUI()
{
if (_zuiInitialized) return true;
// Expensive initialization only once
if (!IL2CPPChainloader.Instance.Plugins.ContainsKey("Zanakinz.ZUI"))
return false;
var assembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "ZUI");
if (assembly == null) return false;
_zuiType = assembly.GetType("ZUI.API.ZUI");
_zuiInitialized = _zuiType != null;
return _zuiInitialized;
}
Why: Avoid redundant work, better performance.
Error Handling
Always Handle Exceptions
✅ DO:
private void Call(string methodName, params object[] args)
{
if (_zuiType == null)
{
Log.LogWarning("ZUI not initialized");
return;
}
try
{
var method = _zuiType.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m => m.Name == methodName &&
m.GetParameters().Length == args.Length);
if (method == null)
{
Log.LogWarning($"Method not found: {methodName}({args.Length} params)");
return;
}
method.Invoke(null, args);
}
catch (TargetInvocationException ex)
{
Log.LogError($"Error invoking {methodName}: {ex.InnerException?.Message ?? ex.Message}");
}
catch (Exception ex)
{
Log.LogError($"Unexpected error calling {methodName}: {ex.Message}");
}
}
❌ DON'T:
// No error handling - crashes entire mod
private void Call(string methodName, params object[] args)
{
var method = _zuiType.GetMethod(methodName);
method.Invoke(null, args); // What if method is null?
}
Why: Graceful failures, better debugging, mod stability.
Validate Input
✅ DO:
private void CreateCustomWindow(string name, int width, int height)
{
// Validate inputs
if (string.IsNullOrEmpty(name))
{
Log.LogError("Window name cannot be empty");
return;
}
if (width < 100 || width > 2000)
{
Log.LogWarning($"Width {width} outside recommended range (100-2000)");
width = Math.Clamp(width, 100, 2000);
}
if (height < 100 || height > 1500)
{
Log.LogWarning($"Height {height} outside recommended range (100-1500)");
height = Math.Clamp(height, 100, 1500);
}
Call("SetTargetWindow", name);
Call("SetUI", width, height);
}
❌ DON'T:
// No validation - garbage in, garbage out
private void CreateCustomWindow(string name, int width, int height)
{
Call("SetTargetWindow", name); // What if name is null?
Call("SetUI", width, height); // What if width is -500?
}
Why: Prevents crashes, provides helpful warnings, better UX.
Provide Helpful Error Messages
✅ DO:
if (!InitZUI())
{
Log.LogWarning("ZUI not available. UI features will be disabled.");
Log.LogInfo("To enable UI features, install ZUI from: [link]");
return;
}
❌ DON'T:
if (!InitZUI())
{
Log.LogError("Failed"); // What failed? Why? What should user do?
return;
}
Why: Easier troubleshooting, better user experience.
Testing Strategies
Test With and Without ZUI
✅ DO:
// Test case 1: ZUI installed
// - UI should appear
// - Buttons should work
// - No errors in console
// Test case 2: ZUI not installed
// - Mod should still load
// - Core functionality works
// - Warning logged (not error)
// - No crashes
Why: Ensures soft dependency works correctly.
Test Different Window Sizes
✅ DO:
// Test at different resolutions
// 1920x1080 (most common)
// 2560x1440
// 3840x2160 (4K)
// Verify:
// - Window fits on screen
// - Elements visible
// - Text readable
// - Buttons clickable
Why: Ensures usability for all users.
Test With Other Mods
✅ DO:
// Test with:
// - Only your mod + ZUI
// - Your mod + other popular mods
// - Multiple ZUI-integrated mods
// Check for:
// - Load order issues
// - UI conflicts
// - Performance impact
// - Event handling
Why: Ensures compatibility and stability.
Common Pitfalls
Pitfall 1: Forgetting to Set Plugin Name
❌ Problem:
Call("SetTargetWindow", "Main");
Call("AddCategory", "Features"); // Where does this go?
✅ Solution:
Call("SetPlugin", "MyMod"); // Always call first!
Call("SetTargetWindow", "Main");
Call("AddCategory", "Features");
Pitfall 2: Wrong Parameter Count
❌ Problem:
Call("AddButton", "Test"); // Missing command parameter
✅ Solution:
Call("AddButton", "Test", ".test"); // Correct parameter count
Pitfall 3: Elements Outside Window
❌ Problem:
Call("SetUI", 500, 300);
Call("AddButton", "Test", ".test", 600f, 50f); // X=600 but window is 500 wide
✅ Solution:
const int WIDTH = 500;
const float MARGIN = 20f;
Call("SetUI", WIDTH, 300);
Call("AddButton", "Test", ".test", MARGIN, 50f); // Inside window bounds
Pitfall 4: Not Unsubscribing from Events
❌ Problem:
public override void Load()
{
ZUI.OnButtonsChanged += OnUIUpdated;
}
// Forgot to unsubscribe - memory leak!
✅ Solution:
public override void Load()
{
ZUI.OnButtonsChanged += OnUIUpdated;
}
public override bool Unload()
{
ZUI.OnButtonsChanged -= OnUIUpdated;
return true;
}
Pitfall 5: Hardcoded Values Everywhere
❌ Problem:
Call("AddText", "Status", 20f, 50f);
Call("AddButton", "Test1", ".test1", 20f, 100f);
Call("AddButton", "Test2", ".test2", 20f, 150f);
// What if I want to change margin from 20 to 30? Update everywhere!
✅ Solution:
const float MARGIN = 20f;
const float START_Y = 50f;
const float SPACING = 50f;
Call("AddText", "Status", MARGIN, START_Y);
Call("AddButton", "Test1", ".test1", MARGIN, START_Y + SPACING);
Call("AddButton", "Test2", ".test2", MARGIN, START_Y + SPACING * 2);
Quick Reference Checklist
Use this checklist when creating ZUI integrations:
- [ ] Use soft dependency pattern
- [ ] Centralize ZUI calls in helper method
- [ ] Create constants for layout values
- [ ] Validate all inputs
- [ ] Handle all exceptions
- [ ] Test with and without ZUI
- [ ] Test at different resolutions
- [ ] Use consistent spacing
- [ ] Group related elements
- [ ] Optimize image sizes
- [ ] Register UI only once
- [ ] Unsubscribe from events on unload
- [ ] Provide helpful error messages
- [ ] Document your UI layout
- [ ] Use the Visual Designer when appropriate
Related Pages
- Integration Guide - Integration patterns
- API Reference - Complete API documentation
- Custom Windows - Window creation guide
- Event System - Event handling patterns
- Troubleshooting - Common issues
Following these best practices will result in robust, maintainable, and user-friendly ZUI integrations!