This example demonstrates how to use the PropertyVisitor base class to create a property visitor. For an equivalent example that uses the IPropertyBagVisitor and IPropertyVisitor interfaces, refer to Use low-level APIs to create a property visitor.
Create a simple data class and a utility class that uses the visitor to visit and print its properties:
Create the following C# class called Data with properties that can be visited:
using System.Collections.Generic;
using UnityEngine;
public class Data
{
public string Name = "Henry";
public Vector2 Vec2 = Vector2.one;
public List<Color> Colors = new List<Color> { Color.green, Color.red };
public Dictionary<int, string> Dict = new Dictionary<int, string> { { 5, "zero" } };
}
Create the following utility class called DebugUtilities:
using UnityEngine;
public static class DebugUtilities
{
public static void PrintObjectDump<T>(T value)
{
// Magic goes here.
}
}
Create the visitor class as follows:
Create a class named ObjectVisitor that inherits from PropertyVisitor.
Add a StringBuilder field to the class. This can be used to build a string that represents the current state of an object.
Add a Reset method that clears the StringBuilder and resets the indent level.
Add a GetDump method that returns the string representation of the current state of an object.
The ObjectVisitor class now looks as follows:
// `PropertyVisitor` is an abstract class that you must derive from.
public class ObjectVisitor: PropertyVisitor
{
private const int k_InitialIndent = 0;
private readonly StringBuilder m_Builder = new StringBuilder();
private int m_IndentLevel = k_InitialIndent;
private string Indent => new (' ', m_IndentLevel * 2);
public void Reset()
{
m_Builder.Clear();
m_IndentLevel = k_InitialIndent;
}
public string GetDump()
{
return m_Builder.ToString();
}
}
Inside the ObjectVisitor class, override the VisitProperty method to visit each property of an object and log the property name. The PropertyVisitor doesn’t require any members to implement and by default, it simply visits every property and performs no action.
Inside the ObjectVisitor class, add the following override VisitProperty method:
protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
m_Builder.AppendLine($"- {property.Name}");
}
Now that you have a minimal visitor, you can implement the utility method. Update the PrintObjectDump method in the DebugUtilities class to create a new ObjectVisitor instance and use it to visit the properties of the given object:
public static class DebugUtilities
{
private static readonly ObjectVisitor s_Visitor = new();
public static void PrintObjectDump<T>(T value)
{
s_Visitor.Reset();
// This is the main entry point to run a visitor.
PropertyContainer.Accept(s_Visitor, ref value);
Debug.Log(s_Visitor.GetDump());
}
}
From an appropriate place in your code, call the PrintObjectDump method and pass a Data object to it. For example, from a simple MonoBehaviour component this might look as follows:
using UnityEngine;
public class MyMonoBehaviour : MonoBehaviour
{
void Start()
{
DebugUtilities.PrintObjectDump(new Data());
}
}
This prints the following output to the console:
- Name
- Vec2
- Colors
- Dict
As the output from the previous section shows, when you override the VisitProperty method, it doesn’t automatically visit the sub-properties of an object. To get the sub-properties, use the PropertyContainer.Accept method to apply the visitor on each of the values recursively.
In the ObjectVisitor class, update the VisitProperty method to apply the visitor recursively on the value to nest in:
protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
m_Builder.AppendLine($"{Indent}- {property.Name}");
++m_IndentLevel;
// Apply this visitor recursively on the value to nest in.
if (null != value)
PropertyContainer.Accept(this, ref value);
--m_IndentLevel;
}
The console output is now as follows:
- Name
- Vec2
- x
- y
- Colors
- 0
- r
- g
- b
- a
- 1
- r
- g
- b
- a
- Dict
- 5
- Key
- Value
To further enhance the printed output, get the property name of collection items, and the type and value of each property. Certain properties have special names, especially when dealing with collection items. The conventions for property names are as follows:
To make this distinction more explicit, enclose the property name in square brackets.
Inside the ObjectVisitor class, add the following method:
private static string GetPropertyName(IProperty property)
{
return property switch
{
// You can also treat `IListElementProperty`, `IDictionaryElementProperty`, and `ISetElementProperty` separately.
ICollectionElementProperty => $"[{property.Name}]",
_ => property.Name
};
}
Update the VisitProperty method to use TypeUtility.GetTypeDisplayName to retrieve the display name of a given type.
protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
var propertyName = GetPropertyName(property);
// Get the concrete type of the property or its declared type if value is null.
var typeName = TypeUtility.GetTypeDisplayName(value?.GetType() ?? property.DeclaredValueType());
m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");
++m_IndentLevel;
if (null != value)
PropertyContainer.Accept(this, ref value);
--m_IndentLevel;
}
The console output is now as follows:
- Name = {string} Henry
- Vec2 = {Vector2} (1.00, 1.00)
- x = {float} 1
- y = {float} 1
- Colors = {List<Color>} System.Collections.Generic.List`1[UnityEngine.Color]
- [1] = {Color} RGBA(0.000, 1.000, 0.000, 1.000)
- r = {float} 0
- g = {float} 1
- b = {float} 0
- a = {float} 1
- [1] = {Color} RGBA(1.000, 0.000, 0.000, 1.000)
- r = {float} 1
- g = {float} 0
- b = {float} 0
- a = {float} 1
- Dict = {Dictionary<int, string>} System.Collections.Generic.Dictionary`2[System.Int32,System.String]
- [5] = {KeyValuePair<int, string>} [5, five]
- Key = {int} 5
- Value = {string} five
Because List<T> doesn’t override the ToString() method, the list value is displayed as System.Collections.Generic.List1[UnityEngine.Color]. To reduce the amount of information displayed, update the VisitProperty to use the TypeTraits.IsContainer utility method to only display the value for types that don’t contain sub-properties, such as primitives, enums, and strings.
In the ObjectVisitor class, update the VisitProperty method to use TypeTraits.IsContainer to determine whether the value is a container type. If it is, display the type name without the value. Otherwise, display the type name and the value:
protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
var propertyName = GetPropertyName(property);
var type = value?.GetType() ?? property.DeclaredValueType();
var typeName = TypeUtility.GetTypeDisplayName(type);
// Only display the values for primitives, enums and strings.
if (TypeTraits.IsContainer(type))
m_Builder.AppendLine($"{Indent}- {propertyName} {{{typeName}}}");
else
m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");
++m_IndentLevel;
if (null != value)
PropertyContainer.Accept(this, ref value);
--m_IndentLevel;
}
The console output is now as follows:
- Name = {string} Henry
- Vec2 {Vector2}
- x = {float} 1
- y = {float} 1
- Colors {List<Color>}
- [0] {Color}
- r = {float} 0
- g = {float} 1
- b = {float} 0
- a = {float} 1
- [1] {Color}
- r = {float} 1
- g = {float} 0
- b = {float} 0
- a = {float} 1
- Dict {Dictionary<int, string>}
- [5] {KeyValuePair<int, string>}
- Key = {int} 5
- Value = {string} five
Tip: To reduce the amount of information displayed, you can also use the following methods to override a Visit specialization for the collection types:
PropertyVisitor.VisitCollectionPropertyVisitor.VisitListPropertyVisitor.VisitDictionaryPropertyVisitor.VisitSetThese are similar to the VisitProperty method, but they expose the generic parameters of their respective collections types.
Add type-specific overrides to display Vector2 and Color types in a more compact manner.
Use PropertyVisitor along with IVisitPropertyAdapter. When an adapter is registered for a specific type, if the targeted type is encountered during the visitation, the adapter is called instead of the VisitProperty method.
In the ObjectVisitor class, add the IVisitPropertyAdapter for Vector2 and Color:
public class DumpObjectVisitor
: PropertyVisitor
, IVisitPropertyAdapter<Vector2>
, IVisitPropertyAdapter<Color>
{
public DumpObjectVisitor()
{
AddAdapter(this);
}
void IVisitPropertyAdapter<Vector2>.Visit<TContainer>(in VisitContext<TContainer, Vector2> context, ref TContainer container, ref Vector2 value)
{
var propertyName = GetPropertyName(context.Property);
m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Vector2)}}} {value}");
}
void IVisitPropertyAdapter<Color>.Visit<TContainer>(in VisitContext<TContainer, Color> context, ref TContainer container, ref Color value)
{
var propertyName = GetPropertyName(context.Property);
m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Color)}}} {value}");
}
}
When you run a visitor on data, by default, it starts the visitation on the top-level object. For any property visitor, to start the visitation on sub-properties of an object, pass a PropertyPath to the PropertyContainer.Accept method.
Update the DebugUtilities method to take an optional PropertyPath:
public static class DebugUtilities
{
private static readonly ObjectVisitor s_Visitor = new();
public static void PrintObjectDump<T>(T value, PropertyPath path = default)
{
s_Visitor.Reset();
if (path.IsEmpty)
PropertyContainer.Accept(s_Visitor, ref value);
else
PropertyContainer.Accept(s_Visitor, ref value, path);
Debug.Log(s_Visitor.GetDump());
}
}
Call the PrintObjectDump method with the Data object. The console output is now as follows:
- Name {string} = Henry
- Vec2 {Vector2} = (1.00, 1.00)
- Colors {List<Color>}
- [0] = {Color} RGBA(0.000, 1.000, 0.000, 1.000)
- [1] = {Color} RGBA(1.000, 0.000, 0.000, 1.000)
- Dict {Dictionary<int, string>}
- [5] {KeyValuePair<int, string>}
- Key {int} = 5
- Value {string} = five
The complete code for the visitor class now looks as follows:
using System.Text;
using Unity.Properties;
using UnityEngine;
public class ObjectVisitor
: PropertyVisitor
, IVisitPropertyAdapter<Vector2>
, IVisitPropertyAdapter<Color>
{
private const int k_InitialIndent = 0;
// StringBuilder to store the dumped object's properties and values.
private readonly StringBuilder m_Builder = new StringBuilder();
private int m_IndentLevel = k_InitialIndent;
// Helper property to get the current indentation.
private string Indent => new(' ', m_IndentLevel * 2);
public ObjectVisitor()
{
// Constructor, it initializes the ObjectVisitor and adds itself as an adapter
// to handle properties of type Vector2 and Color.
AddAdapter(this);
}
// Reset the visitor, clearing the StringBuilder and setting indentation to initial level.
public void Reset()
{
m_Builder.Clear();
m_IndentLevel = k_InitialIndent;
}
// Get the string representation of the dumped object.
public string GetDump()
{
return m_Builder.ToString();
}
// Helper method to get the property name, handling collections and other property types.
private static string GetPropertyName(IProperty property)
{
return property switch
{
// If it's a collection element property, display it with brackets
ICollectionElementProperty => $"[{property.Name}]",
// For other property types, display the name as it is
_ => property.Name
};
}
// This method is called when visiting each property of an object.
// It determines the type of the value and formats it accordingly for display.
protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
var propertyName = GetPropertyName(property);
// Get the type of the value or property.
var type = value?.GetType() ?? property.DeclaredValueType();
var typeName = TypeUtility.GetTypeDisplayName(type);
// Only display the values for primitives, enums, and strings, and treat other types as containers.
if (TypeTraits.IsContainer(type))
m_Builder.AppendLine($"{Indent}- {propertyName} {{{typeName}}}");
else
m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");
// Increase indentation level before visiting child properties (if any).
++m_IndentLevel;
if (null != value)
PropertyContainer.Accept(this, ref value);
// Decrease indentation level after visiting child properties.
--m_IndentLevel;
}
// This method is a specialized override for Vector2 properties.
// It displays the property name and its value as a Vector2.
void IVisitPropertyAdapter<Vector2>.Visit<TContainer>(in VisitContext<TContainer, Vector2> context, ref TContainer container, ref Vector2 value)
{
var propertyName = GetPropertyName(context.Property);
m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Vector2)}}} {value}");
}
// This method is a specialized override for Color properties.
// It displays the property name and its value as a Color.
void IVisitPropertyAdapter<Color>.Visit<TContainer>(in VisitContext<TContainer, Color> context, ref TContainer container, ref Color value)
{
var propertyName = GetPropertyName(context.Property);
m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Color)}}} {value}");
}
}