Skip to content

Custom Types

Debby's Adjust module includes built-in support for common Unity and C# types (int, float, string, Vector3, Color, etc.), but you can extend it to work with your own custom types or types not natively supported by Debby by creating type adapters.

What is a Type Adapter?

A type adapter is a class that tells Debby how to:

  1. Display your custom type in the UI (create visual elements)
  2. Convert your type to/from strings (for console commands)

Anatomy of a Type Adapter

Type adapters inherit from AdjustTypeAdapter<TValue>, or AdjustTypeAdapter<TValue, TAttribute> if you want extra customization options via a custom attribute.

csharp
using Debology.Debby.Modules.Adjust.Adapters;
using Debology.Debby.Modules.Adjust.Attributes;
using UnityEngine.UIElements;

public class MyTypeAdapter : AdjustTypeAdapter<MyCustomType>
{
    // Format hint shown when parsing fails (e.g., "x,y,z" or "name:value")
    protected override string FormatHint => "format description";

    // Parse string input to your type (for console commands)
    protected override bool FromString(string str, out MyCustomType value)
    {
        // Parse string and return true if successful
    }

    // Convert your type to string (for console output)
    protected override string ToString(MyCustomType value)
    {
        return value.ToString();
    }

    // Create the UI element for this type
    protected override VisualElement CreateElement(
        string name,
        IAdjustableValue<MyCustomType> av,
        AdjustAttribute attribute)
    {
        // Create and configure your visual element
        var element = new VisualElement();

        // Bind it to the adjustable value
        av.BindTwoWay(element); // if using INotifyValueChanged<T>

        return element;
    }
}

Required Methods

FormatHint

A short string describing the expected format for string parsing. This is shown to users when parsing fails.

Examples:

  • "x,y,z" for Vector3
  • "r,g,b,a | (0-1)" for Color
  • "min-max" for a range type

FromString(string str, out TValue value)

Parses a string into your custom type. Used by console commands.

Returns: true if parsing succeeded, false otherwise.

ToString(TValue value)

Converts your type to a string representation for console output.

CreateElement(string name, IAdjustableValue<TValue> av, AdjustAttribute attribute)

Creates the UI element that represents your type in the Adjust module.

Parameters:

  • name: Display name for the element
  • av: The adjustable value wrapper (use for two-way binding)
  • attribute: The attribute instance (contains Category, Group, Width, etc.)

Returns: A VisualElement that displays and controls your value.

Creating a Simple Adapter

Here's a complete example for a custom Range type adapter.

csharp
public struct Range
{
    public float Min;
    public float Max;

    public Range(float min, float max)
    {
        Min = min;
        Max = max;
    }

    public override string ToString() => $"[{Min}-{Max}]";
}
csharp
using Debology.Debby.Modules.Adjust.Adapters;
using Debology.Debby.Modules.Adjust.Attributes;
using Debology.Debby.Elements;
using Debology.Debby.Modules.Adjust;
using UnityEngine.UIElements;

public class RangeTypeAdapter : AdjustTypeAdapter<Range>
{
    protected override string FormatHint => "min-max (e.g., 0-100)";

    protected override bool FromString(string str, out Range value)
    {
        value = default;

        var parts = str.Split('-');
        if (parts.Length != 2) return false;

        if (!float.TryParse(parts[0], out var min)) return false;
        if (!float.TryParse(parts[1], out var max)) return false;

        value = new Range(min, max);
        return true;
    }

    protected override string ToString(Range value) => value.ToString();

    protected override VisualElement CreateElement(
        string name,
        IAdjustableValue<Range> av,
        AdjustAttribute attribute)
    {
        // Create a group to hold both fields
        var group = new DebbyGroup(name);

        // Create min field
        var minField = new DebbyFloatPicker("Min");
        minField.SetValueWithoutNotify(av.value.Min);
        minField.RegisterValueChangedCallback(evt =>
        {
            var current = av.value;
            current.Min = evt.newValue;
            av.value = current;
        });

        // Create max field
        var maxField = new DebbyFloatPicker("Max");
        maxField.SetValueWithoutNotify(av.value.Max);
        maxField.RegisterValueChangedCallback(evt =>
        {
            var current = av.value;
            current.Max = evt.newValue;
            av.value = current;
        });

        // Update UI when value changes externally
        av.OnValueChanged += newValue =>
        {
            minField.SetValueWithoutNotify(newValue.Min);
            maxField.SetValueWithoutNotify(newValue.Max);
        };

        // Disable if read-only
        var isReadOnly = av.ReadOnly || attribute.ForceReadOnly;
        minField.SetEnabled(!isReadOnly);
        maxField.SetEnabled(!isReadOnly);

        group.Add(minField);
        group.Add(maxField);

        return group;
    }
}

Using Custom Attributes

For more control, you can create a custom attribute for your type, which you can use to customize the UI and behavior of your type on a per-field basis. The attribute must inherit from AdjustAttribute and be marked with AttributeUsage(AttributeTargets.Field | AttributeTargets.Property).

csharp
using System;
using Debology.Debby.Modules.Adjust.Attributes;

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class AdjustRangeAttribute : AdjustAttribute
{
    public float AbsoluteMin { get; set; }
    public float AbsoluteMax { get; set; }

    public AdjustRangeAttribute(
        float absoluteMin = float.MinValue,
        float absoluteMax = float.MaxValue)
    {
        AbsoluteMin = absoluteMin;
        AbsoluteMax = absoluteMax;
    }
}

Then use the two-parameter version of AdjustTypeAdapter:

csharp
public class RangeAdapter : AdjustTypeAdapter<Range, AdjustRangeAttribute>
{
    protected override VisualElement CreateElement(
        string name,
        IAdjustableValue<Range> av,
        AdjustAttribute attribute)
    {
        var group = new DebbyGroup(name);

        // Access custom attribute properties
        float absMin = float.MinValue;
        float absMax = float.MaxValue;

        if (attribute is AdjustRangeAttribute rangeAttr)
        {
            absMin = rangeAttr.AbsoluteMin;
            absMax = rangeAttr.AbsoluteMax;
        }

        // Create clamped sliders using the bounds
        var minSlider = new DebbyFloatSlider("Min", absMin, absMax);
        var maxSlider = new DebbyFloatSlider("Max", absMin, absMax);

        // ... binding code ...

        return group;
    }

    // ... other methods ...
}

TIP

For clarity and consistency, it's recommended to name the attribute the same as the type with an Adjust prefix. So type Range would have an attribute named AdjustRange.

Registering Your Adapter

Register your custom adapter during initialization:

csharp
using Debology.Debby;
using UnityEngine;

public class GameInitializer : MonoBehaviour
{
    void Start()
    {
        // Initialize Debby if not auto-loaded
        Debby.Initialize();

        // Register your custom adapter
        Debby.Adjust.RegisterTypeAdapter<RangeAdapter, Range>();
        
        // If using a custom attribute
        Debby.Adjust.RegisterTypeAdapter<RangeAdapter, Range, AdjustRangeAttribute>();
    }
}

Using Your Custom Type

Once registered, you can use your custom type with the [Adjust] attribute, or your custom attribute if you used one:

csharp
using Debology.Debby.Modules.Adjust.Attributes;

public class GameSettings
{
    [Adjust(Command = "difficulty")]
    public Range DifficultyRange = new Range(0, 100);

    [AdjustRange(AbsoluteMin = 0, AbsoluteMax = 1000)]
    public Range HealthRange = new Range(50, 500);
}

Two-Way Binding with INotifyValueChanged

For simpler cases where your UI element implements INotifyValueChanged<T>, use BindTwoWay:

csharp
protected override VisualElement CreateElement(
    string name,
    IAdjustableValue<MyType> av,
    AdjustAttribute attribute)
{
    var element = new MyCustomElement(name);

    // Automatically syncs changes in both directions
    av.BindTwoWay(element);

    element.SetEnabled(!(av.ReadOnly || attribute.ForceReadOnly));

    return element;
}

The BindTwoWay extension method:

  • Sets the initial value of the UI element
  • Updates the adjustable value when the UI changes
  • Updates the UI when the adjustable value changes

Complex Example: Item Stack

Here's a more complex example showing a custom type with validation and custom styling:

csharp
// Custom type
public class ItemStack
{
    public string ItemId;
    public int Quantity;

    public ItemStack(string itemId, int quantity)
    {
        ItemId = itemId;
        Quantity = Mathf.Max(0, quantity);
    }
}
csharp
// Custom attribute
public class AdjustItemStackAttribute : AdjustAttribute
{
    public int MaxQuantity { get; set; } = 999;
}
csharp
// Custom adapter
public class ItemStackAdapter : AdjustTypeAdapter<ItemStack, AdjustItemStackAttribute>
{
    protected override string FormatHint => "itemId:quantity (e.g., sword:5)";

    protected override bool FromString(string str, out ItemStack value)
    {
        value = null;

        var parts = str.Split(':');
        if (parts.Length != 2) return false;

        if (!int.TryParse(parts[1], out var quantity)) return false;
        if (string.IsNullOrWhiteSpace(parts[0])) return false;

        value = new ItemStack(parts[0].Trim(), quantity);
        return true;
    }

    protected override string ToString(ItemStack value)
    {
        if (value == null) return "null";
        return $"{value.ItemId}:{value.Quantity}";
    }

    protected override VisualElement CreateElement(
        string name,
        IAdjustableValue<ItemStack> av,
        AdjustAttribute attribute)
    {
        var group = new DebbyGroup(name);

        // Get max quantity from attribute
        var maxQuantity = 999;
        if (attribute is AdjustItemStackAttribute stackAttr)
        {
            maxQuantity = stackAttr.MaxQuantity;
        }

        // Item ID field
        var idField = new DebbyTextField("Item ID");
        idField.SetValueWithoutNotify(av.value?.ItemId ?? "");
        idField.RegisterValueChangedCallback(evt =>
        {
            var current = av.value ?? new ItemStack("", 0);
            av.value = new ItemStack(evt.newValue, current.Quantity);
        });

        // Quantity picker with validation
        var quantityPicker = new DebbyIntPicker("Quantity", 0, maxQuantity);
        quantityPicker.SetValueWithoutNotify(av.value?.Quantity ?? 0);
        quantityPicker.RegisterValueChangedCallback(evt =>
        {
            var current = av.value ?? new ItemStack("", 0);
            var clamped = Mathf.Clamp(evt.newValue, 0, maxQuantity);
            av.value = new ItemStack(current.ItemId, clamped);
        });

        // Update UI when value changes
        av.OnValueChanged += newValue =>
        {
            idField.SetValueWithoutNotify(newValue?.ItemId ?? "");
            quantityPicker.SetValueWithoutNotify(newValue?.Quantity ?? 0);
        };

        // Handle read-only
        var isReadOnly = av.ReadOnly || attribute.ForceReadOnly;
        idField.SetEnabled(!isReadOnly);
        quantityPicker.SetEnabled(!isReadOnly);

        group.Add(idField);
        group.Add(quantityPicker);

        return group;
    }
}

Usage:

csharp
public class Inventory
{
    [AdjustItemStack(MaxQuantity = 100, Command = "sword")]
    public ItemStack Sword = new ItemStack("sword", 1);
}

Console commands:

> sword               // Prints: sword:1
> sword longsword:5   // Sets sword to longsword with quantity 5

Troubleshooting

Adapter not being used:

  • Ensure you've registered it with Debby.Adjust.RegisterTypeAdapter<TAdapter, TValue>()
  • Check that registration happens right after Debby.Initialize() (before you open the Adjust module)
  • Check that the type parameter matches exactly (including namespace)

UI not updating:

  • Make sure you're subscribing to av.OnValueChanged
  • Use SetValueWithoutNotify to avoid infinite loops
  • Check that you're modifying av.value, not a local copy

Console commands not working:

  • Implement FromString and ToString correctly
  • Set the Command property in the attribute: [Adjust(Command = "mycommand")]
  • Check console for parsing errors