ET Entity Tree 工具

目标

  • 对整体 Entity 组件的树形图进行绘制
  • 多个 Child 要有效区分,如 Unit、Scene
  • 选中 Entity 时,可以查看 Entity 的属性
  • 有办法查看 Unit 上挂载的 NubericComponent 中具体的属性

具体实现

第一条树形图的绘制没什么好说的,本身就是 Unity 自带的 TreeView 的基础实现,这里直接略过,感兴趣可以自行阅读源码

不同实体区分

日常在 Debug 时,经常会碰到一个问题,这个 Unit 到底是哪个?每次手动添加 Debug 的 watch,或者鼠标放置查看某个属性都很烦,这里要借助 DebugDisplay 的方式,自定义当前 Unit 显示的名称

[DebuggerDisplay("DebuggerDisplay,nq")]
public class Unit : Entity, IAwake<int>
{
    // ...
    
    private string DebuggerDisplay => this.Config.Name;
}

这样我们在日常 Debug 时,外部看到的 Unit 会直接显示 this.Config.Name 中的值,所以在树的绘制时,我们也要利用好这个规则,在 Item 的名字上,根据是否定义了 DebuggerDisplay 对名字做追加

这里要注意的是,虽然 DebuggerDisplay 支持其他方式,如:[DebuggerDisplay("{this.Config.Name}")],但目前仅支持示例中提供的方式,且变量名必须是 DebuggerDisplay,如果需要自定义,需要去 EntityTreeViewItem 修改默认的 propertyName

Entity Inspector

这个功能实现起来也比较简单,ET 原本就已经有了针对绘制的 API,此时我们需要做的就是,动态创建一个 ComponentView 对象,放在场景中,当 Tree 窗口监听到点击后,把这个 Entity 赋值给 ComponentView,最后再将 Selection 的对象设为 ComponentView

在此种设计下 ComponentView 可以考虑从 Mono 中移除,完全作为 Editor 代码使用

右键菜单

注意!目前这里的功能虽然实现了,但是因为 ET7 的机制导致这里不工作,需要等后面解决 Editor 直接引用 Model 等代码的问题才可以

在日常开发中,策划往往需要查看当前 NumbericComponent 中这个单位具体属性,所以需要提供一个通用的解决方案来解决这里 Editor 绘制的需求,这类问题的本质,其实是解决这个 Unit 到底是哪个 Unit 的问题,通过对 Tree 的扩展,这类问题解决方案就非常简单了

namespace ET
{
    [EntityMenu(typeof (Unit), "打开属性菜单")]
    public class UnitNumericWindowMenu: AEntityMenuHandler
    {
        public override void OnClick(Entity entity)
        {
            var unit = entity as Unit;

            Log.Debug(unit.Config.Name);
        }
    }
}

这里是在 Editor 下,指定这个菜单是哪种 Entity 的,同时提供一个菜单名,在右键点击 Unit 时,会弹出 打开属性菜单 的 Menu,点击后,会执行这里的 OnClick 函数

PS: 这里的菜单,针对同一个类型支持多个定义噢,只是示例没有展示

NumericComponent 绘制代码

这里是我在 ET6 版本的基础上改了很多内容,仅供参考,而且使用到了 Odin,具体显示如下:

都弄好后,上面针对 Unit 的右键菜单中的具体实现就可以变成这样了


[EntityMenu(typeof(Unit), "打开属性菜单")]
public class UnitNumericWindowMenu : AEntityMenuHandler
{
    public override void OnClick(Entity entity) 
    { 
        PropertyViewWindow.Open(entity as Unit); 
    }
}

NumericGroup

[Serializable]
public class NumericGroup
{
    [SerializeField]
    [ValueDropdown(nameof(_numeric))]
    [TableColumnWidth(100, false)]
    [HideLabel]
    [VerticalGroup("槽位")]
    private NumericType _slot;

    private ValueDropdownList<NumericType> _numeric => numeric;

    [HideLabel]
    [VerticalGroup("最终值")]
    [TableColumnWidth(70, false)]
    [SerializeField]
    private long _final;

    [HideLabel]
    [ShowIf("@_Show(NumericType.Base)")]
    [VerticalGroup("基础值")]
    [TableColumnWidth(70, false)]
    [SerializeField]
    private long _base_value;

    [HideLabel]
    [ShowIf("@_Show(NumericType.BaseAdd)")]
    [VerticalGroup("基础 Add")]
    [TableColumnWidth(70, false)]
    [SerializeField]
    private long _base_add;

    [HideLabel]
    [ShowIf("@_Show(NumericType.BasePct)")]
    [VerticalGroup("基础 Pct")]
    [TableColumnWidth(70, false)]
    [SerializeField]
    private float _base_pct;

    [HideLabel]
    [ShowIf("@_Show(NumericType.Add)")]
    [VerticalGroup("Add")]
    [TableColumnWidth(70, false)]
    [SerializeField]
    private long _add;

    [HideLabel]
    [ShowIf("@_Show(NumericType.Pct)")]
    [VerticalGroup("Pct")]
    [TableColumnWidth(70, false)]
    [SerializeField]
    private float _pct;

    [HideLabel]
    [ShowIf("@_Show(NumericType.FinalAdd)")]
    [VerticalGroup("最终 Add")]
    [TableColumnWidth(70, false)]
    [SerializeField]
    private long _final_add;

    [HideLabel]
    [ShowIf("@_Show(NumericType.FinalPct)")]
    [VerticalGroup("最终 Pct")]
    [TableColumnWidth(70, false)]
    [SerializeField]
    private float _final_pct;

    public NumericGroup(NumericType start) { _slot = start; }

    public void Update(NumericComponent numeric)
    {
        int times = (int) NumericType.Times;
        int start = (int) _slot;

        numeric.Get(start,                                      out _final);
        numeric.Get(start * times + (int) NumericType.Base,     out _base_value);
        numeric.Get(start * times + (int) NumericType.BaseAdd,  out _base_add);
        numeric.Get(start * times + (int) NumericType.BasePct,  out _base_pct);
        numeric.Get(start * times + (int) NumericType.Add,      out _add);
        numeric.Get(start * times + (int) NumericType.Pct,      out _pct);
        numeric.Get(start * times + (int) NumericType.FinalAdd, out _final_add);
        numeric.Get(start * times + (int) NumericType.FinalPct, out _final_pct);
    }

    private bool _Show(NumericType padding)
    {
        int times = (int) NumericType.Times;
        int start = (int) _slot;

        int final = start * times + (int) padding;

        return Enum.IsDefined(typeof(NumericType), final);
    }
}

NumbericChange_UpdateGUI

这里需要注意,这个代码写在 Editor 下,需要自己去 EventSystem 注册一下~

public class NumbericChange_UpdateGUI : AEvent<EventType.NumericChange>
{
    protected override async UniTask Run(EventType.NumericChange args)
    {
        PropertyViewWindow.UpdateGUI(args.parent.InstanceId);
        await UniTask.CompletedTask;
    }
}

PropertyViewWindow

public class PropertyViewWindow : OdinEditorWindow
{
    [FoldoutGroup("数值", false)]
    [HideLabel]
    [TableList(AlwaysExpanded = true, IsReadOnly = true)]
    [SerializeField]
    private List<NumericGroup> _numeric_groups;

    private Unit             _unit;
    private NumericComponent _numeric;

    private static Dictionary<long, PropertyViewWindow> _opened = new();

    private void Awake() { TestCaseWindow.on_change += _ => { _CloseAll(); }; }

    private static void _CloseAll()
    {
        foreach(var window in _opened.Values)
        {
            window.Close();
        }

        _opened.Clear();
    }

    public static void Open(Unit unit)
    {
        if(unit is null || unit.IsDisposed)
        {
            return;
        }

        _opened.TryGetValue(unit.InstanceId, out var window);

        if(window == null)
        {
            window = CreateWindow<PropertyViewWindow>(unit.ToEditorString(), typeof(PropertyViewWindow));

            if(!window.docked)
            {
                window.position = GUIHelper.GetEditorWindowRect().AlignCenter(700, 650);
            }

            _opened.Add(unit.InstanceId, window);

            window.OnClose += () =>
            {
                long id = unit.InstanceId;
                _opened.Remove(id);
            };
        }

        window._Init(unit);
    }

    public static void UpdateGUI(long instance_id)
    {
        _opened.TryGetValue(instance_id, out var window);

        if(window == null)
        {
            return;
        }

        window._UpdateGUI();
    }

    private void _Init(Unit unit)
    {
        _unit               = unit;
        _numeric            = unit.GetComponent<NumericComponent>();
        _numeric_groups     = new List<NumericGroup>();

        foreach(NumericType type in Enum.GetValues(typeof(NumericType)))
        {
            if(type <= NumericType.Start)
            {
                continue;
            }

            if(type >= NumericType.Max)
            {
                continue;
            }

            _numeric_groups.Add(new NumericGroup(type));
        }

        _UpdateGUI();
    }

    private void Update()
    {
        if(_unit is null || _unit.IsDisposed || !EditorApplication.isPlaying)
        {
            Close();
        }
    }

    private void _UpdateGUI()
    {
        foreach(var group in _numeric_groups)
        {
            group.Update(_numeric);
        }

        Repaint();
    }
}