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:
- Display your custom type in the UI (create visual elements)
- 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.
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 elementav: 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.
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}]";
}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).
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:
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:
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:
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:
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:
// Custom type
public class ItemStack
{
public string ItemId;
public int Quantity;
public ItemStack(string itemId, int quantity)
{
ItemId = itemId;
Quantity = Mathf.Max(0, quantity);
}
}// Custom attribute
public class AdjustItemStackAttribute : AdjustAttribute
{
public int MaxQuantity { get; set; } = 999;
}// 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:
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 5Troubleshooting
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
SetValueWithoutNotifyto avoid infinite loops - Check that you're modifying
av.value, not a local copy
Console commands not working:
- Implement
FromStringandToStringcorrectly - Set the
Commandproperty in the attribute:[Adjust(Command = "mycommand")] - Check console for parsing errors