Progression Tree Builder

v1.1.2

Customizing

Advanced customization guide for sub-controllers, custom layouts, and tree references.

Overview

Progression Tree Builder uses a modular architecture that separates concerns into specialized sub-controllers and interfaces. This guide covers the advanced customization options available through this architecture.

Prerequisites: This guide assumes you're familiar with the basic setup covered in the Quick Start Guide.

Sub-Controller Architecture

The UI Controller delegates specialized tasks to three sub-controllers, each with a specific responsibility:

Skill Node Renderer

Creates and positions skill node UI elements based on the selected layout mode.

Handles: Node instantiation, position calculation, node caching, custom layouts

Connection Renderer

Draws visual lines between nodes to show skill dependencies.

Handles: LineRenderer creation, connection updates, visual styling

Tree Navigation Controller

Manages the tab UI for switching between multiple unlocked trees.

Handles: Tab creation, tree switching, tab state updates

Skill Node Renderer

The SkillNodeRenderer is the most customizable sub-controller. It supports:

  • Multiple Layout Modes: Grid, Graph (Vertical/Horizontal/Radial), List, Latest
  • Custom Layout Strategies: Implement ILayoutStrategyfor custom positioning
  • Node Caching: Reuses GameObjects for performance
  • Event System: Fires OnSkillNodeClickedfor node selection
// Accessing the sub-controller
public class ProgressionTreeUIController : MonoBehaviour
{
    private SkillNodeRenderer skillNodeRenderer;
    
    void Awake()
    {
        skillNodeRenderer = new SkillNodeRenderer(
            uiReferences,
            layoutSettings,
            skillNodeCache,
            debugMode
        );
        
        skillNodeRenderer.SetUIController(this);
        skillNodeRenderer.OnSkillNodeClicked += SelectSkill;
    }
}

Connection Renderer

The ConnectionRenderer creates visual links between related skills:

// ConnectionRenderer usage
private ConnectionRenderer connectionRenderer;

void Awake()
{
    connectionRenderer = new ConnectionRenderer(
        uiReferences,
        connectionLines,  // LineRenderer prefab pool
        debugMode
    );
}

void UpdateConnections()
{
    connectionRenderer.DrawConnections(
        skillNodeCache,  // Dictionary<ProgressionSkill, GameObject>
        currentTree
    );
}

Tree Navigation Controller

The TreeNavigationController handles multi-tree UI when players have multiple trees unlocked:

// TreeNavigationController usage
private TreeNavigationController treeNavigationController;

void Awake()
{
    treeNavigationController = new TreeNavigationController(
        uiReferences,
        treeTabButtons,
        availableUnlockedTrees,
        debugMode
    );
    
    treeNavigationController.OnTreeTabClicked += SwitchToTree;
}

void UpdateAvailableTrees(List<IProgressionTreeReference> trees)
{
    treeNavigationController.SetupTreeTabs(trees);
}

Custom Layout Strategies

Create custom positioning logic by implementing the ILayoutStrategy interface.

The ILayoutStrategy Interface

public interface ILayoutStrategy
{
    // Called BEFORE layout components are created
    // Modify controller.layoutSettings here
    void Prepare(ProgressionTreeUIController controller);
    
    // Calculate positions for Graph mode or custom modes
    // Return empty dictionary for Grid/List (LayoutGroup handles it)
    Dictionary<ProgressionSkill, Vector2> CalculatePositions(
        ProgressionTree tree,
        LayoutSettings settings
    );
    
    // Display name for debugging/UI
    string GetLayoutName();
}

Understanding Prepare()

The Prepare() method is called at a critical time in the layout setup:

Execution Order:

  1. 1. User opens tree → OpenTree() called
  2. 2. customLayoutStrategy.Prepare(this) → Modify layoutSettings
  3. 3. SetupLayoutComponents() → Create GridLayoutGroup/etc with modified settings
  4. 4. BuildSkillTreeDisplay() → Create nodes

This timing is crucial: modifying layoutSettings after SetupLayoutComponents()has no effect!

Example: Wide Grid Strategy

[CreateAssetMenu(fileName = "WideGridStrategy", 
    menuName = "My Game/Wide Grid Layout")]
public class WideGridStrategy : ScriptableObject, ILayoutStrategy
{
    [Header("Grid Settings")]
    [Range(1, 5)]
    public int columns = 2;  // Fewer columns = wider layout
    
    public Vector2 cellSize = new Vector2(800f, 200f);  // Very wide cells
    public Vector2 cellSpacing = new Vector2(20f, 20f);
    
    public void Prepare(ProgressionTreeUIController controller)
    {
        // Apply custom settings
        controller.layoutSettings.gridColumns = columns;
        controller.layoutSettings.gridCellSize = cellSize;
        controller.layoutSettings.gridCellSpacing = cellSpacing;
        
        Debug.Log($"[WideGrid] Applied {columns} column layout");
    }
    
    public Dictionary<ProgressionSkill, Vector2> CalculatePositions(
        ProgressionTree tree,
        LayoutSettings settings)
    {
        // Grid mode uses LayoutGroup - return empty
        return new Dictionary<ProgressionSkill, Vector2>();
    }
    
    public string GetLayoutName() => "Wide Grid (2 columns)";
}

Tree References System

The tree reference system provides a unified way to work with both ProgressionTree ScriptableObjects and ProgressionTreeGraphAssets.

IProgressionTreeReference Interface

Both tree formats implement this interface:

public interface IProgressionTreeReference
{
    string TreeID { get; }      // Unique identifier
    string TreeName { get; }    // Display name
    Sprite TreeIcon { get; }    // UI icon
    
    ProgressionTree GetProgressionTree();  // Get runtime tree
}

This allows the system to handle both formats transparently:

// Works with BOTH ProgressionTree and GraphAsset
public void OpenTree(IProgressionTreeReference treeRef)
{
    // Get metadata without loading full tree
    Debug.Log($"Opening: {treeRef.TreeName}");
    
    // Load runtime tree when needed
    ProgressionTree runtimeTree = treeRef.GetProgressionTree();
    
    // Use the tree
    BuildDisplay(runtimeTree);
}

ProgressionTreeReference Wrapper

For serialized fields that need to accept any tree type, useProgressionTreeReference:

[Serializable]
public class MyComponent : MonoBehaviour
{
    // Accepts ProgressionTree OR GraphAsset
    [SerializeField]
    private ProgressionTreeReference myTreeRef;
    
    void Start()
    {
        // Get the runtime tree
        ProgressionTree tree = myTreeRef.GetProgressionTree();
        
        // Get metadata
        string name = myTreeRef.GetTreeName();
        string id = myTreeRef.GetTreeID();
        
        // Check if valid
        if (myTreeRef.IsValid())
        {
            // Use the tree
        }
    }
}

Best Practice: UseIProgressionTreeReference for method parameters and ProgressionTreeReference for serialized fields. This provides maximum flexibility.