Skip to main content

Authoring Screens Programmatically

Building a screen by hand — drag in a template, rename it, retarget its buttons, wire the graph — is quick for one screen and tedious for ten. HookUIScreenBuilder (JsonProductions.HookUI.EditorTools.AI, in Editor/AI/HookUIScreenBuilder.cs) is a small, stable Editor-time API for doing that from a script instead. It was originally written so an AI coding agent could add screens to a HookUI project without inventing its own UI-construction code, but it's just as useful for a human writing a one-off [MenuItem] to stamp out a batch of similar screens.

Why this exists

Building a UI hierarchy from raw GameObject/RectTransform/CanvasGroup/Image calls is bug-prone in ways that aren't obvious until you hit them: NaN-corrupted RectTransforms, invisible buttons caused by a bad inherited Image setting, CanvasGroup.alpha stuck at the wrong value after a botched setup. Every method in HookUIScreenBuilder clones an existing, already-working screen or button instead of constructing one from scratch — sidestepping that entire class of bug. If a screen already looks and behaves correctly in your project, cloning it can't reintroduce those failure modes.

The mental model

A screen is only reachable once three independent things agree:

  1. The HookView's ID — the component in the scene, with a Category/ID pair.
  2. An entry in HookUIDatabase — otherwise the ID won't show up in Inspector dropdowns.
  3. A ScreenNodeData in the Flow Graph, with a connection actually pointing at it — a View existing in the scene and a graph node existing are two independent things; only the ViewID string links them.

CloneScreen/CloneButton handle database registration (step 2) for you automatically. You still have to wire the graph yourself — that's what Connect/ConnectToQuit/SetStartScreen are for.

The API

namespace JsonProductions.HookUI.EditorTools.AI
{
public static class HookUIScreenBuilder
{
public static HookView CloneScreen(HookView template, string category, string id);
public static void SetTitle(HookView screen, string text);
public static HookButton CloneButton(HookButton template, Transform container, string category, string id, string labelText);
public static ScreenNodeData GetOrCreateScreenNode(HookUIFlowGraph graph, HookView screen);
public static void Connect(HookUIFlowGraph graph, HookView from, string buttonId, HookView to);
public static void ConnectToQuit(HookUIFlowGraph graph, HookView from, string buttonId);
public static void SetStartScreen(HookUIFlowGraph graph, HookView screen);
public static void RegisterID(HookUIIDType type, string category, string id);
}
}
  • CloneScreen duplicates an existing HookView, re-IDs it, strips HookSignalSender/HookSignalListener from the clone (their Signal IDs would otherwise still point at the template's signal), and registers the new View ID in HookUIDatabase.
  • SetTitle finds the biggest TextMeshProUGUI under the screen (by font size) and sets its text — themed templates usually have exactly one large title and one smaller subtitle, so this targets the title without needing to know the exact GameObject name.
  • CloneButton duplicates an existing HookButton under a container, re-IDs it, clears OnClick/OnHover, sets its label text, and registers the new Button ID.
  • GetOrCreateScreenNode finds or creates the ScreenNodeData for a view, placing new nodes at a free position to the right of every existing node. Safe to call repeatedly.
  • Connect wires a button on one screen to navigate to another: adds the button ID to the source node's OutgoingButtons and points a NodeConnection at the target node, creating either node if needed. Idempotent — re-running with the same arguments just re-points the same connection.
  • ConnectToQuit wires a button to quit the app, reusing a single shared QuitNodeData if one already exists in the graph.
  • SetStartScreen points the graph's start node at a screen — moves the existing start connection rather than adding a second one.
  • RegisterID is the low-level database registration CloneScreen/CloneButton call for you. Use it directly for anything you build or add by hand (e.g. a HookSignalSender re-added after stripping, or a HookUIComponent's own Category/ID).

Worked example

A complete [MenuItem]-driven Editor script that clones a MainMenu screen into a Credits screen, reduces it to a single Back button, adds an OpenCredits button to MainMenu, and wires everything:

using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using JsonProductions.HookUI.Runtime;
using JsonProductions.HookUI.Graph;
using JsonProductions.HookUI.EditorTools.AI;

public static class AddCreditsScreen
{
[MenuItem("Tools/HookUI/AI/Add Credits Screen (example)")]
public static void Run()
{
// 1. Find the template screen and the graph asset - adjust these
// lookups to match the actual project.
var mainMenu = Object.FindObjectsByType<HookView>(FindObjectsSortMode.None)
.FirstOrDefault(v => v.ID == "MainMenu");
var graph = AssetDatabase.LoadAssetAtPath<HookUIFlowGraph>(
"Assets/Resources/MainFlowGraph.asset"); // <- real path varies per project

if (mainMenu == null || graph == null)
{
Debug.LogError("AddCreditsScreen: couldn't find MainMenu view or flow graph.");
return;
}

Undo.SetCurrentGroupName("Add Credits Screen");
int undoGroup = Undo.GetCurrentGroup();

// 2. Clone MainMenu itself as a starting layout for Credits (any
// already-working screen works as a template - it doesn't have
// to be MainMenu).
var credits = HookUIScreenBuilder.CloneScreen(mainMenu, "MainMenu", "Credits");
HookUIScreenBuilder.SetTitle(credits, "CREDITS");

// 3. Reduce it to a single Back button, reusing whichever button
// the template already had.
var buttons = credits.GetComponentsInChildren<HookButton>(true).ToList();
var container = buttons[0].transform.parent;
for (int i = 1; i < buttons.Count; i++) Object.DestroyImmediate(buttons[i].gameObject);

var backButton = buttons[0];
backButton.Category = "Credits";
backButton.ID = "Back"; // A button ID matching HookUIManager's own
// _backButtonID field (default "Back")
// triggers GoBack() automatically - no
// Connect() needed for it.
HookUIScreenBuilder.RegisterID(HookUIIDType.Button, "Credits", "Back");

// 4. Add an OpenCredits button to MainMenu, cloned from one of
// MainMenu's own existing buttons so it matches MainMenu's style
// (not Credits' style - always clone a button from the screen
// it's going to live on).
var mainMenuTemplateBtn = mainMenu.GetComponentInChildren<HookButton>(true);
var openCreditsBtn = HookUIScreenBuilder.CloneButton(
mainMenuTemplateBtn, mainMenuTemplateBtn.transform.parent,
"MainMenu", "OpenCredits", "Credits");
HookUIScreenBuilder.Connect(graph, mainMenu, "OpenCredits", credits);

// 5. Persist everything.
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
Undo.CollapseUndoOperations(undoGroup);
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
EditorSceneManager.SaveScene(EditorSceneManager.GetActiveScene());

Debug.Log("AddCreditsScreen: done.");
}
}

Adapt the template lookups, container-finding, and button count to whatever the target project's hierarchy actually looks like — the shape (clone → strip → re-ID → wire → save) is what stays the same.

Common mistakes

These are real bugs hit while building this API and its worked examples — not hypothetical ones.

  • Forgetting to register IDs. If you build anything by hand instead of through CloneScreen/CloneButton (e.g. a HookSignalSender you added back after stripping it), call HookUIScreenBuilder.RegisterID(...) yourself. An unregistered ID still works at runtime, but won't show up in Inspector dropdowns and won't get duplicate-ID warnings.
  • A View existing without a graph node, or vice versa. Cloning a screen does not automatically add it to the Flow Graph — you still need Connect()/ConnectToQuit()/SetStartScreen() to wire it in, or it's unreachable even though it "exists" in the scene.
  • Reusing a cloned button's OnClick/OnHover events. CloneButton clears these for you. If you build a button any other way, clear them yourself (button.OnClick = new UnityEngine.Events.UnityEvent();) or clicking it will still fire whatever the template button did.
  • A Hide preset with only EnableMove/EnableScale and no EnableFade. HookAnimatorComponent.PlayHide()/InstantHide() force CanvasGroup.alpha to 0 after the animation regardless, so the final state is safe — but if you're authoring a new HookAnimationPreset for a Hide slot, still give it real exit values (fade/scale toward 0), not entrance values copied from a Show preset. The animation itself looks visually wrong mid-transition even though it can no longer get stuck.
  • HookUIComponent.ExecutionMode = Manual + HideOnAwake = true on a panel holding a screen's real nav buttons. This can trap the screen's primary buttons — including whatever button was supposed to reveal the panel — behind a panel with no way to open it. Only use Manual mode on panels holding optional content; see HookUIComponent.
  • Leaving raycastTarget enabled on decorative images (background patterns, glow effects) layered over buttons — they can silently eat clicks meant for the button underneath.

Scope note

This API only covers HookView (Canvas-based) screens. There's no equivalent clone-based helper yet for HookViewToolkit (UI Toolkit) screens, which are authored by hand as UXML/USS — see HookViewToolkit.