Version: 2023.2
언어: 한국어
`PropertyVisitor`를 사용해 프로퍼티 방문자 생성
멀티플레이어 및 네트워킹

로우레벨 API를 사용하여 프로퍼티 방문자 생성

이 예시에서는 IPropertyBagVisitorIPropertyVisitor 인터페이스와 함께 로우레벨 API를 사용하여 프로퍼티 방문자를 생성하는 방법을 설명합니다. 이 예시는 PropertyVisitor 기본 클래스를 사용하여 프로퍼티 방문자를 생성하는 예시와 동일합니다.

개요 예시

이 예시에는 오브젝트의 현재 상태를 콘솔에 출력하는 프로퍼티 방문자를 생성하는 단계별 지침이 포함되어 있습니다.

다음과 같은 유형이 있다고 가정합니다.

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"}};
}

유틸리티 메서드 DebugUtilities를 다음과 같이 생성합니다.

public static class DebugUtilities
{
    public static void PrintObjectDump<T>(T value)
    {
        // Magic goes here.
    }
}

다음과 같이 Data 오브젝트로 PrintObjectDump 메서드를 호출합니다.

DebugUtilities.PrintObjectDump(new Data());

콘솔에 다음과 같이 출력됩니다.

- 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

방문자 생성

먼저 IPropertyBagVisitor를 구현하는 DumpObjectVisitor 클래스를 생성합니다. 클래스 안에서 StringBuilder를 사용하여 오브젝트의 현재 상태를 나타내는 문자열을 빌드합니다.

  1. IPropertyBagVisitor 인터페이스를 구현하는 DumpObjectVisitor 클래스를 생성합니다.

  2. 클래스에 StringBuilder 필드를 추가합니다.

  3. StringBuilder를 제거하고 인덴트 레벨을 재설정하는 Reset 메서드를 추가합니다.

  4. 오브젝트의 현재 상태를 나타내는 문자열을 반환하는 GetDump 메서드를 추가합니다.

    DumpObjectVisitor 클래스는 다음과 같습니다.

    public class DumpObjectVisitor
        : IPropertyBagVisitor
        , IPropertyVisitor
    {
        private const int k_InitialIndent = 0;
    
        private readonly StringBuilder m_Builder = new StringBuilder();
        private int m_IndentLevel = k_InitialIndent;
    
        public void Reset()
        {
            m_Builder.Clear();
            m_IndentLevel = k_InitialIndent;
        }
    
        public string GetDump()
        {
            return m_Builder.ToString();
        }
    }
    

프로퍼티 가져오기

DumpObjectVisitor 클래스 안에서 IPropertyBagVisitor.Visit 메서드를 오버라이드하여 컨테이너 오브젝트의 프로퍼티를 루핑합니다. 오브젝트 덤프 방문자에 값을 표시하고 프로퍼티에 대한 방문을 위임합니다.

this를 사용하여 프로퍼티의 Accept 메서드를 호출하려면 IPropertyVisitor 인터페이스를 구현합니다. 이 인터페이스를 사용하면 프로퍼티 방문 시 방문 동작을 지정할 수 있으며, PropertyVisitor 클래스의 VisitProperty 메서드와 유사합니다.

  1. DumpObjectVisitor 클래스 안에서 IPropertyBagVisitor.VisitIPropertyVisitor.Visit 메서드를 오버라이드합니다.

    void IPropertyBagVisitor.Visit<TContainer>(IPropertyBag<TContainer> propertyBag, ref TContainer container)
    {
        foreach (var property in propertyBag.GetProperties(ref container))
        {
            property.Accept(this, ref container);
        }
    }
        
    void IPropertyVisitor.Visit<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container)
    {
        var value = property.GetValue(ref container);
        // Code goes here.
    }
    
  2. 방문자 내부 상태에 대한 액세스 권한이 필요하므로 PropertyVisitor 기본 클래스와 함께 사용되는 IVisitPropertyAdapter 어댑터는 해당 클래스 외부에서 사용할 수 없습니다. 하지만 필요한 정보가 포함된 도메인별 어댑터를 정의할 수 있습니다. DumpObjectVisito 클래스 안에서 먼저 어댑터를 사용하도록 IPropertyVisitor 구현을 업데이트합니다.

    // Create the following methods to encapsulate the formatting of the message and display the value.
    public readonly struct PrintContext
    {
        private StringBuilder Builder { get; }
        private string Prefix { get; }
        public string PropertyName { get; }
    
        public void Print<T>(T value)
        {
            Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(value?.GetType() ?? typeof(T))}}} {value}");
        }
        
        public void Print(Type type, string value)
        {
            Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(type)}}} {value}");
        }
    
        public PrintContext(StringBuilder builder, string prefix, string propertyName)
        {
            Builder = builder;
            Prefix = prefix;
            PropertyName = propertyName;
        }
    }
    
    public interface IPrintValue
    {
    }
    
    public interface IPrintValue<in T> : IPrintValue
    {
        void PrintValue(in PrintContext context, T value);
    }
    
    public class DumpObjectVisitor
        : IPropertyBagVisitor
        , IPropertyVisitor
        , IPrintValue<Vector2>
        , IPrintValue<Color>
    {
        public IPrintValue Adapter { get; set; }
        
        public DumpObjectVisitor()
        {
            // For simplicity
            Adapter = this;
        }
        void IPropertyVisitor.Visit<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container)
        {
            // Here, we need to manually extract the value.
            var value = property.GetValue(ref container);
        
            var propertyName = GetPropertyName(property);
        
            // We can still use adapters, but we must manually dispatch the calls. 
            if (Adapter is IPrintValue<TValue> adapter)
            {
                var context = new PrintContext(m_Builder, Indent, propertyName);
                adapter.PrintValue(context, value);
                return;
            }
            
            // Fallback behaviour here 
        }
        
        void IPrintValue<Vector2>.PrintValue(in PrintContext context, Vector2 value)
        {
            context.Print(value);
        }
        void IPrintValue<Color>.PrintValue(in PrintContext context, Color value)
        {
            const string format = "F3";
            var formatProvider = CultureInfo.InvariantCulture.NumberFormat;
            context.Print(typeof(Color), $"RGBA({value.r.ToString(format, formatProvider)}, {value.g.ToString(format, formatProvider)}, {value.b.ToString(format, formatProvider)}, {value.a.ToString(format, formatProvider)})");
        }
    }
    

완성된 코드는 다음과 같습니다.

public readonly struct PrintContext
{
    // A context struct to hold information about how to print the property
    private StringBuilder Builder { get; }
    private string Prefix { get; }
    public string PropertyName { get; }

    // Method to print the value of type T with its associated property name
    public void Print<T>(T value)
    {
        Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(value?.GetType() ?? typeof(T))}}} {value}");
    }

    // Method to print the value with a specified type and its associated property name
    public void Print(Type type, string value)
    {
        Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(type)}}} {value}");
    }

    // Constructor to initialize the PrintContext
    public PrintContext(StringBuilder builder, string prefix, string propertyName)
    {
        Builder = builder;
        Prefix = prefix;
        PropertyName = propertyName;
    }
}

// Generic interface IPrintValue that acts as a marker interface for all print value adapters
public interface IPrintValue
{
}

// Generic interface IPrintValue<T> to define how to print values of type T
// This interface is used as an adapter for specific types (Vector2 and Color in this case)
public interface IPrintValue<in T> : IPrintValue
{
    void PrintValue(in PrintContext context, T value);
}

// DumpObjectVisitor class that implements various interfaces for property visiting and value printing
private class DumpObjectVisitor : IPropertyBagVisitor, IPropertyVisitor, IPrintValue<Vector2>, IPrintValue<Color>
{
    // (Other members are omitted for brevity)

    public IPrintValue Adapter { get; set; }

    public DumpObjectVisitor()
    {
        // The Adapter property is set to this instance of DumpObjectVisitor
        // This means the current DumpObjectVisitor can be used as a print value adapter for Vector2 and Color.
        Adapter = this;
    }

    // This method is called when visiting a property bag (a collection of properties)
    void IPropertyBagVisitor.Visit<TContainer>(IPropertyBag<TContainer> propertyBag, ref TContainer container)
    {
        foreach (var property in propertyBag.GetProperties(ref container))
        {
            // Call the Visit method of IPropertyVisitor to handle individual properties
            property.Accept(this, ref container);
        }
    }

    // This method is called when visiting each individual property of an object.
    // It tries to find a suitable adapter (IPrintValue<T>) for the property value type (TValue) and uses it to print the value.
    // If no suitable adapter is found, it falls back to displaying the value using its type name.
    void IPropertyVisitor.Visit<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container)
    {
        // Here, we need to manually extract the value.
        var value = property.GetValue(ref container);

        var propertyName = GetPropertyName(property);

        // We can still use adapters, but we must manually dispatch the calls.
        // Try to find an adapter for the current property value type (TValue).
        if (Adapter is IPrintValue<TValue> adapter)
        {
            // If an adapter is found, create a print context and call the PrintValue method of the adapter.
            var context = new PrintContext(m_Builder, Indent, propertyName);
            adapter.PrintValue(context, value);
            return;
        }

        // Fallback behavior here - if no adapter is found, handle printing based on type information.
        var type = value?.GetType() ?? property.DeclaredValueType();
        var typeName = TypeUtility.GetTypeDisplayName(type);

        if (TypeTraits.IsContainer(type))
            m_Builder.AppendLine($"{Indent}- {propertyName} {{{typeName}}}");
        else
            m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");

        // Recursively visit child properties (if any).
        ++m_IndentLevel;
        if (null != value)
            PropertyContainer.Accept(this, ref value);
        --m_IndentLevel;
    }

    // Method from IPrintValue<Vector2> used to print Vector2 values
    void IPrintValue<Vector2>.PrintValue(in PrintContext context, Vector2 value)
    {
        // Simply use the Print method of PrintContext to print the Vector2 value.
        context.Print(value);
    }

    // Method from IPrintValue<Color> used to print Color values
    void IPrintValue<Color>.PrintValue(in PrintContext context, Color value)
    {
        const string format = "F3";
        var formatProvider = CultureInfo.InvariantCulture.NumberFormat;
        
        // Format and print the Color value in RGBA format.
        context.Print(typeof(Color), $"RGBA({value.r.ToString(format, formatProvider)}, {value.g.ToString(format, formatProvider)}, {value.b.ToString(format, formatProvider)}, {value.a.ToString(format, formatProvider)})");
    }
}

하위 프로퍼티의 현재 상태 출력

데이터에 대한 방문자를 실행하면 기본적으로 지정된 오브젝트에 대한 방문이 바로 시작됩니다. 모든 프로퍼티 방문자의 경우 오브젝트의 하위 프로퍼티에 대한 방문을 시작하려면 PropertyPathPropertyContainer.Accept 메서드에 전달하십시오.

  1. DebugUtilities 메서드를 업데이트하여 선택적 PropertyPath를 가져옵니다.

    public static class DebugUtilities
    {
        private static readonly DumpObjectVisitor 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());
        }
    }
    
  2. Data 오브젝트로 PrintObjectDump 메서드를 호출합니다. 이렇게 하면 원하는 출력을 얻을 수 있습니다.

추가 리소스

`PropertyVisitor`를 사용해 프로퍼티 방문자 생성
멀티플레이어 및 네트워킹