Skip to main content Link Search Menu Expand Document (external link)

Day 5: UXML Serialization

Today, we implemented UXML Generators for our SkillTree and SkillNodes. Additionally, we updated the Skill Nodes to be intractable. Lastly, for convenience, we updated the UXML generators to be aware of position changes by deserializing the existing UXML prior to regeneration.

Table of contents
  1. Today’s Tasks
  2. Implementing UXML Generators
  3. Adding Listeners to the Skill Nodes
  4. Deserializing UXML for Meta Data

Today’s Tasks

  1. Implement UXML Generators for each type
  2. Manually add Skill Tree Name / Description Information
  3. Refactor types

Implementing UXML Generators

On Day 4, we implemented a rather long algorithm and unorganized method for generating a UXML file given a skill tree. To help manage this complexity, we refactored this into two smaller classes, one for the overall SkillTree and a second for the nested SkillNodes

public class SkillTreeGenerator : IUXMLGenerator<ScriptableSkillTree>
{
    private readonly ISkillTreeMetaData _metaData;
    public SkillTreeGenerator(ISkillTreeMetaData metaData) => _metaData = metaData;
    public XmlElement ToXMLElement(ScriptableSkillTree toConvert)
    {
        XmlElement el = UXML.CreateContainer("SkillTree");
        XmlElement edges = UXML.CreateContainer("Edges");
        el.AppendChild(edges);

        XmlElement nodes = UXML.CreateContainer("Nodes");
        el.AppendChild(nodes);

        SkillNodeGenerator generator = new(_metaData, edges);
        foreach (var node in toConvert.SkillTree.Nodes)
        {
            nodes.AppendChild(generator.ToXMLElement(node));
        }

        return el;
    }
}
public class SkillNodeGenerator : IUXMLGenerator<ISkillNode<ISkilledEntity<ISkill>, ISkill>>
{
    public static readonly string s_SkillNodeElement = "CaptainCoder.SkillTree.UnityEngine.SkillNodeElement";
    private readonly ISkillTreeMetaData _metaData;
    private readonly XmlElement _edgeContainer;
    public SkillNodeGenerator(ISkillTreeMetaData metaData, XmlElement edgeContainer) 
        => (_metaData, _edgeContainer) = (metaData, edgeContainer);

    public XmlElement ToXMLElement(ISkillNode<ISkilledEntity<ISkill>, ISkill> toConvert)
    {         
        XmlElement skillNode = UXML.XML.CreateElement(s_SkillNodeElement);
        skillNode.SetAttribute("name", UXML.SanitizeSkillName(toConvert.Skill));
        skillNode.SetAttribute("style", Style(toConvert.Skill));
        skillNode.SetAttribute("display-name", toConvert.Skill.Name);
        skillNode.SetAttribute("display-description", toConvert.Skill.Description);
        foreach (var child in toConvert.Children)
        {
            XmlElement lineNode = UXML.CreateLineElement(toConvert.Skill, child.Skill);
            _edgeContainer.AppendChild(lineNode);
        }     
        return skillNode;
    }

    // These two methods generate the necessary styling for the skill nodes
    private string Style(ISkill skill);
    private string ImageStyle(Sprite sprite);
}

Adding Listeners to the Skill Nodes

With the new generators working, it was time to connect them with the UI so the player is able to interact with them in the scene. To do this, we implemented a simple UIDocument that has two text labels: one for the name of the skill and another for the description.

To communicate with the two we implemented a SkillNodeElement class which has two events: OnPointerEntered and OnClicked.

public class SkillNodeElement : VisualElement
{
    public event Action<SkillNodeElement> OnPointerEntered;
    public event Action<SkillNodeElement> OnClicked;

    public SkillNodeElement()
    {
        RegisterCallback<PointerDownEvent>(OnPointerDown);
        RegisterCallback<PointerEnterEvent>(OnPointerEnter);
    }

    private void OnPointerEnter(PointerEnterEvent evt) => OnPointerEntered?.Invoke(this);
    private void OnPointerDown(PointerDownEvent evt) => OnClicked?.Invoke(this);

    private void Init(string name, string description) => (DisplayName, DisplayDescription) = (name, description);

    public string DisplayName { get; set; }
    public string DisplayDescription { get; set; }
}

Next, we implemented a SkillInfoController which registers itself on the skill nodes such that when they are clicked, the labels of the UIDocument are updated:

public class SkillInfoController : MonoBehaviour
{
    [field: SerializeField]
    public UIDocument SkillLayout { get; private set; }
    private Label _skillName;
    private Label _skillDescription;

    public void Awake()
    {
        VisualElement root = SkillLayout.rootVisualElement;
        _skillName = root.Q<Label>("SkillName");
        Debug.Assert(_skillName != null, "Could not find SkillName node.");
        _skillDescription = root.Q<Label>("SkillDescription");
        Debug.Assert(_skillDescription != null, "Could not find SkillDescription node.");
        AddListeners(root);
    }

    // Recursively scan for SkillNodeElements and register on them
    private void AddListeners(VisualElement toScan)
    {
        if (toScan is SkillNodeElement)
        {
            SkillNodeElement asSkillNode = toScan as SkillNodeElement;
            asSkillNode.OnClicked += OnSelect;
        }
        foreach (VisualElement child in toScan.Children())
        {
            AddListeners(child);
        }
    }

    // When selected, update the labels
    private void OnSelect(SkillNodeElement selected)
    {
        _skillName.text = selected.DisplayName;
        _skillDescription.text = selected.DisplayDescription;
    }
}

Deserializing UXML for Meta Data

At this point, each time a SkillTree was converted to a UXML file, the resulting tree placed all of the SkillNodes at the top left corner. This would overwrite any existing information and was quite annoying.

To fix this, we implemented a SkllTreeDeserializer which scans an existing UXML file for nodes and extracts the position information to be used during regeneration:

public class SkillTreeDeserializer
{
    public static SkillTreeDeserializer Default = new();

    public ISkillTreeMetaData ParseMetaData(string path)
    {
        if (!File.Exists(path)) { throw new FileNotFoundException($"Could not load metadata {path}"); }
        XmlDocument doc = new();
        doc.Load(path);
        SkillTreeMetaData metaData = new();
        ScanDocument(doc.DocumentElement, metaData);
        return metaData;
    }

    private void ScanDocument(XmlElement node, SkillTreeMetaData accumulator)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            if (child.NodeType != XmlNodeType.Element) { continue; }
            XmlElement childElement = (XmlElement)child;
            ScanDocument(childElement, accumulator);
            if (childElement.Name != SkillNodeGenerator.s_SkillNodeElement) { continue; }
            string name = childElement.GetAttribute("name");
            Debug.Assert(name != string.Empty, "Discovered SkillNode without a name attribute.");
            string style = childElement.GetAttribute("style");
            Match leftMatch = Regex.Match(style, "left: [0-9]+px;");
            Match topMatch = Regex.Match(style, "top: [0-9]+px;");
            int x = leftMatch.Success ? ExtractInt(leftMatch) : 0;
            int y = topMatch.Success ? ExtractInt(topMatch) : 0;
            accumulator._positions[name] = new Vector2(x, y);
            Debug.Log($"{name}: {x} {y}");
        }
    }

    private int ExtractInt(Match match) 
        => int.Parse(Regex.Match(match.Groups[0].Value, "[0-9]+").Groups[0].Value, 
                        CultureInfo.InvariantCulture);

    private class SkillTreeMetaData : ISkillTreeMetaData
    {
        internal Dictionary<string, Vector2> _positions = new();
        public Vector2 PositionOf(string skillName)
        {
            if (_positions.TryGetValue(skillName, out Vector2 result))
            {
                return result;
            }
            return new Vector2(0, 0);
        }
    }
}

public interface ISkillTreeMetaData
{
    public Vector2 PositionOf(string skillName) => Vector2.zero;

    public static ISkillTreeMetaData Default = new DefaultMetaData();

    private class DefaultMetaData : ISkillTreeMetaData {};
}

With this in place, it was now possible to update the Skill Trees in the inspector without their UI positions resetting.

We’ve made a lot of great progress today. However, there are still several unanswered questions. The biggest of which is “How do we access the ISkill methods / requirements from the UXML?” Currently the name and description are encoded as strings but we have lost the reference to the Skill itself. We need to somehow be able to load the Skill’s ScriptableObject at runtime with information that is encoded in the UXML document.

Join the Discussion

Before commenting, you will need to authorize giscus. Alternatively, you can add a comment directly on the GitHub Discussion Board.