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:
- The
HookView'sID— the component in the scene, with aCategory/IDpair. - An entry in
HookUIDatabase— otherwise the ID won't show up in Inspector dropdowns. - A
ScreenNodeDatain 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 theViewIDstring 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);
}
}
CloneScreenduplicates an existingHookView, re-IDs it, stripsHookSignalSender/HookSignalListenerfrom the clone (their Signal IDs would otherwise still point at the template's signal), and registers the new View ID inHookUIDatabase.SetTitlefinds the biggestTextMeshProUGUIunder 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.CloneButtonduplicates an existingHookButtonunder a container, re-IDs it, clearsOnClick/OnHover, sets its label text, and registers the new Button ID.GetOrCreateScreenNodefinds or creates theScreenNodeDatafor a view, placing new nodes at a free position to the right of every existing node. Safe to call repeatedly.Connectwires a button on one screen to navigate to another: adds the button ID to the source node'sOutgoingButtonsand points aNodeConnectionat the target node, creating either node if needed. Idempotent — re-running with the same arguments just re-points the same connection.ConnectToQuitwires a button to quit the app, reusing a single sharedQuitNodeDataif one already exists in the graph.SetStartScreenpoints the graph's start node at a screen — moves the existing start connection rather than adding a second one.RegisterIDis the low-level database registrationCloneScreen/CloneButtoncall for you. Use it directly for anything you build or add by hand (e.g. aHookSignalSenderre-added after stripping, or aHookUIComponent'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. aHookSignalSenderyou added back after stripping it), callHookUIScreenBuilder.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/OnHoverevents.CloneButtonclears 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/EnableScaleand noEnableFade.HookAnimatorComponent.PlayHide()/InstantHide()forceCanvasGroup.alphato0after the animation regardless, so the final state is safe — but if you're authoring a newHookAnimationPresetfor 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 = trueon 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 useManualmode on panels holding optional content; seeHookUIComponent.- Leaving
raycastTargetenabled 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.