Best Practices

Updated 3 days ago

Best Practices

Design patterns, performance tips, and code organization for building quality ZUI integrations.


📋 Table of Contents


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:

  1. Sketch on paper - Draw your UI layout
  2. Use the Visual Designer - https://zanakinz.github.io/ZUI
  3. 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


Following these best practices will result in robust, maintainable, and user-friendly ZUI integrations!