Files
lennartandClaude Opus 5 9a0618fd31 replace the NPC scaffold with data-driven prefabs, and retire the builders
NPCStandIn was written to be dropped on a sphere and carried its identity in
per-instance public fields. The cost of that showed up in the scene itself: of
the two spheres, one was the Bartender and the other was named "Bartender (1)"
in the hierarchy while being configured as the Chair — data changed, name never
was. Nothing outside the scene could see what existed, and two copies could
disagree.

Identity now lives in an InteractableDefinition asset and the scene only places
a prefab. The migration matched the old objects by yarn node rather than by
name, which is the only reason the chair ended up as the chair.

InteractableDefinition rather than the planned "character definition": it covers
a character and a talkable object equally, so Bartender.prefab sits under
Prefabs/Characters and Chair.prefab under Prefabs/Interactables.

Three components collapse into one. DialogueInteractable was in no scene at all
— a dead trigger-zone variant of the same idea. ClickInteractableBridge existed
only to adapt NPCStandIn without modifying it, a constraint that died with
NPCStandIn; DialogueInteractor implements IClickInteractable itself. The parts
worth keeping were lifted out rather than discarded: WorldSpaceLabel and
InteractionPrompt are now normal components on prefab children, so a label can
be seen and positioned in the Editor instead of existing only at runtime.
InputDeviceTracker moved into NightclubArcadia.Core and gained a namespace,
leaving no global-namespace runtime types.

Characters get the same pipeline as Skills and Candidates:
Assets/Characters/characters.json is the source of truth, CharacterSetup
regenerates the definition assets on script reload and removes orphans. Adding
an NPC is a JSON entry plus a prefab — no code, and no Unity for the data half.

Phase 5.4 deletes the one-shot builders rather than disabling them. UILayerSetup,
PlayerControlSetup, UILayerSetupAutoRun and YarnDemoSetup assumed the
single-scene layout, had already destroyed a level scene and turned Systems
binary once each, and after NPCStandIn went they no longer compiled. The prefabs
they used to generate are authored and committed; the wiring knowledge is in
those prefabs and in git history. The asset generators — Skill, Candidate,
Character — are untouched and still work.

EditMode 42/42, PlayMode 7/7, YarnCheck 9 files / 35 nodes. All three scenes
still text.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 21:53:06 +02:00

645 lines
29 KiB
C#

using NightclubArcadia.Cinematics;
using NightclubArcadia.Dialogue;
using NightclubArcadia.Skills;
using NightclubArcadia.UI.Dialogue;
using NightclubArcadia.UI.HUD;
using NightclubArcadia.UI.Menu;
using StarterAssets;
using TMPro;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.UI;
using Yarn.Unity;
namespace NightclubArcadia.UI
{
/// <summary>
/// Builds the rudimentary UI layer at runtime, for scenes where the UI
/// prefabs have not been instantiated. It is a fallback only: the authored
/// path is Tools → Nightclub Arcadia → Set Up UI Layer, which produces the
/// prefabs under Assets/Prefabs/UI and wires them into the scene.
///
/// It does NOT auto-spawn. It used to, which meant that once the scene had
/// been wired properly it would still create a second "UI System" object
/// carrying a duplicate UIStateController — logging a Debug.LogError every
/// play — and would reflection-overwrite DialogueCameraFraming's serialised
/// dialogueScreenX, making that value impossible to tune in the Inspector.
/// Add this component to a scene by hand if you want the fallback.
/// </summary>
[DefaultExecutionOrder(-100)]
public sealed class UILayerBootstrap : MonoBehaviour
{
[SerializeField] bool buildIfMissing = true;
void Awake()
{
EnsureFoundation();
if (!buildIfMissing)
{
return;
}
if (FindFirstObjectByType<DialogueStreamPresenter>() == null)
{
BuildDialogueStream();
}
if (FindFirstObjectByType<PortraitHudView>() == null)
{
BuildHud();
}
if (FindFirstObjectByType<PlayerMenuController>() == null)
{
BuildMenu();
}
EnsureCameraFraming();
WireDialogueRunner();
HidePackageDialogueCanvas();
}
void EnsureFoundation()
{
if (UIStateController.Instance == null && FindFirstObjectByType<UIStateController>() == null)
{
var go = GameObject.Find("UI System") ?? new GameObject("UI System");
go.AddComponent<UIStateController>();
}
var player = GameObject.Find("PlayerArmature");
if (player == null)
{
return;
}
if (player.GetComponent<PlayerControlLock>() == null)
{
player.AddComponent<PlayerControlLock>();
}
if (player.GetComponent<PlayerMenuInput>() == null)
{
player.AddComponent<PlayerMenuInput>();
}
var inputs = player.GetComponent<StarterAssetsInputs>();
if (inputs != null)
{
inputs.cursorLocked = false;
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
var transition = FindFirstObjectByType<DialogueTransitionController>();
if (transition != null)
{
var soLock = player.GetComponent<PlayerControlLock>();
// Assign via reflection-free serialized path: public setter not available,
// so use SendMessage-style field via SerializedObject is editor-only.
// Runtime: DialogueTransitionController finds lock from playerArmature in Awake.
var field = typeof(DialogueTransitionController).GetField(
"controlLock",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
field?.SetValue(transition, soLock);
}
}
void BuildDialogueStream()
{
var canvasGo = CreateCanvas("DialogueStreamCanvas", 5);
var canvasGroup = canvasGo.AddComponent<CanvasGroup>();
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
var panel = new GameObject("Panel", typeof(RectTransform), typeof(Image));
panel.transform.SetParent(canvasGo.transform, false);
var panelRt = panel.GetComponent<RectTransform>();
panelRt.anchorMin = new Vector2(0.66f, 0f);
panelRt.anchorMax = Vector2.one;
panelRt.offsetMin = Vector2.zero;
panelRt.offsetMax = Vector2.zero;
panel.GetComponent<Image>().color = new Color(0.05f, 0.05f, 0.08f, 0.92f);
var scrollGo = new GameObject("Scroll", typeof(RectTransform), typeof(ScrollRect), typeof(Image));
scrollGo.transform.SetParent(panel.transform, false);
Stretch(scrollGo.GetComponent<RectTransform>(), 8f);
scrollGo.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.01f);
var viewport = new GameObject("Viewport", typeof(RectTransform), typeof(RectMask2D), typeof(Image));
viewport.transform.SetParent(scrollGo.transform, false);
Stretch(viewport.GetComponent<RectTransform>());
viewport.GetComponent<Image>().color = new Color(1f, 1f, 1f, 0.01f);
var content = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
content.transform.SetParent(viewport.transform, false);
var contentRt = content.GetComponent<RectTransform>();
contentRt.anchorMin = new Vector2(0f, 0f);
contentRt.anchorMax = new Vector2(1f, 0f);
contentRt.pivot = new Vector2(0.5f, 0f);
var vlg = content.GetComponent<VerticalLayoutGroup>();
vlg.childAlignment = TextAnchor.LowerCenter;
vlg.childControlHeight = true;
vlg.childControlWidth = true;
vlg.childForceExpandHeight = false;
vlg.childForceExpandWidth = true;
vlg.spacing = 6f;
content.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var messages = new GameObject("Messages", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
messages.transform.SetParent(content.transform, false);
ConfigureVertical(messages);
var options = new GameObject("Options", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter), typeof(CanvasGroup));
options.transform.SetParent(content.transform, false);
ConfigureVertical(options);
var optionsGroup = options.GetComponent<CanvasGroup>();
optionsGroup.alpha = 0f;
optionsGroup.interactable = false;
optionsGroup.blocksRaycasts = false;
var scroll = scrollGo.GetComponent<ScrollRect>();
scroll.content = contentRt;
scroll.viewport = viewport.GetComponent<RectTransform>();
scroll.horizontal = false;
scroll.vertical = true;
var templates = new GameObject("UITemplates");
templates.transform.SetParent(transform, false);
templates.SetActive(false);
var rowPrefab = BuildMessageRowPrefab(templates.transform);
var stream = canvasGo.AddComponent<DialogueStreamPresenter>();
SetPrivate(stream, "messageRowPrefab", rowPrefab);
SetPrivate(stream, "messageContainer", messages.GetComponent<RectTransform>());
SetPrivate(stream, "scrollRect", scroll);
OptionItem yarnOption = null;
#if UNITY_EDITOR
yarnOption = UnityEditor.AssetDatabase.LoadAssetAtPath<OptionItem>(
"Packages/dev.yarnspinner.unity/Prefabs/Option Item.prefab");
#endif
if (yarnOption == null)
{
yarnOption = BuildFallbackOptionItem(templates.transform);
}
var optionsPresenter = canvasGo.AddComponent<DialogueStreamOptionsPresenter>();
SetPrivate(optionsPresenter, "optionViewPrefab", yarnOption);
SetPrivate(optionsPresenter, "optionsContainer", options.transform);
SetPrivate(optionsPresenter, "canvasGroup", optionsGroup);
var transition = FindFirstObjectByType<DialogueTransitionController>();
if (transition != null)
{
SetPrivate(transition, "dialogueUI", canvasGo);
}
// Interactors used to be pushed this canvas here. DialogueInteractor resolves
// it from the runner at the point of use instead, so there is nothing to push.
}
DialogueMessageRow BuildMessageRowPrefab(Transform parent)
{
var root = new GameObject("DialogueMessageRow", typeof(RectTransform), typeof(DialogueMessageRow), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
root.transform.SetParent(parent, false);
var layout = root.GetComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(12, 12, 8, 8);
layout.spacing = 4f;
layout.childControlHeight = true;
layout.childControlWidth = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
root.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var speaker = CreateTmp("Speaker", root.transform, 22f, FontStyles.Bold);
var body = CreateTmp("Body", root.transform, 28f, FontStyles.Normal);
var row = root.GetComponent<DialogueMessageRow>();
SetPrivate(row, "speakerText", speaker);
SetPrivate(row, "bodyText", body);
SetPrivate(row, "speakerLabel", speaker.gameObject);
return row;
}
OptionItem BuildFallbackOptionItem(Transform parent)
{
var go = new GameObject("OptionItem", typeof(RectTransform), typeof(Image), typeof(OptionItem));
go.transform.SetParent(parent, false);
go.GetComponent<Image>().color = new Color(0.15f, 0.15f, 0.2f, 1f);
var text = CreateTmp("Text", go.transform, 24f, FontStyles.Normal);
Stretch(text.rectTransform);
SetPrivate(go.GetComponent<OptionItem>(), "text", text);
return go.GetComponent<OptionItem>();
}
void BuildHud()
{
var canvasGo = CreateCanvas("HudCanvas", 10);
var panel = new GameObject("PortraitPanel", typeof(RectTransform), typeof(CanvasGroup), typeof(Image), typeof(Button));
panel.transform.SetParent(canvasGo.transform, false);
var rt = panel.GetComponent<RectTransform>();
rt.anchorMin = new Vector2(1f, 0f);
rt.anchorMax = new Vector2(1f, 0f);
rt.pivot = new Vector2(1f, 0f);
rt.sizeDelta = new Vector2(220f, 280f);
rt.anchoredPosition = new Vector2(-24f, 24f);
panel.GetComponent<Image>().color = new Color(0.15f, 0.15f, 0.18f, 0.95f);
// Lives in Assets/Art/UI/Placeholder/Resources so this resolves in
// builds too — the AssetDatabase fallback below is editor-only.
var sprite = Resources.Load<Sprite>("portrait_placeholder");
#if UNITY_EDITOR
if (sprite == null)
{
sprite = UnityEditor.AssetDatabase.LoadAssetAtPath<Sprite>(
"Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png");
}
#endif
if (sprite != null)
{
panel.GetComponent<Image>().sprite = sprite;
panel.GetComponent<Image>().preserveAspect = true;
}
var hint = CreateTmp("TabHint", panel.transform, 18f, FontStyles.Normal);
hint.text = "Tab";
hint.alignment = TextAlignmentOptions.Center;
var hintRt = hint.rectTransform;
hintRt.anchorMin = new Vector2(0f, 0f);
hintRt.anchorMax = new Vector2(1f, 0f);
hintRt.pivot = new Vector2(0.5f, 0f);
hintRt.sizeDelta = new Vector2(0f, 28f);
hintRt.anchoredPosition = new Vector2(0f, 4f);
var view = canvasGo.AddComponent<PortraitHudView>();
SetPrivate(view, "panelRoot", panel);
}
void BuildMenu()
{
var canvasGo = CreateCanvas("PlayerMenuCanvas", 20);
var rootGroup = canvasGo.AddComponent<CanvasGroup>();
rootGroup.alpha = 0f;
rootGroup.interactable = false;
rootGroup.blocksRaycasts = false;
var dimmer = new GameObject("Dimmer", typeof(RectTransform), typeof(Image));
dimmer.transform.SetParent(canvasGo.transform, false);
Stretch(dimmer.GetComponent<RectTransform>());
dimmer.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.6f);
var body = new GameObject("MenuBody", typeof(RectTransform), typeof(Image));
body.transform.SetParent(canvasGo.transform, false);
var bodyRt = body.GetComponent<RectTransform>();
bodyRt.anchorMin = new Vector2(0.15f, 0.1f);
bodyRt.anchorMax = new Vector2(0.85f, 0.9f);
bodyRt.offsetMin = Vector2.zero;
bodyRt.offsetMax = Vector2.zero;
body.GetComponent<Image>().color = new Color(0.08f, 0.08f, 0.1f, 0.98f);
var tabBarGo = new GameObject("TabBar", typeof(RectTransform), typeof(HorizontalLayoutGroup), typeof(MenuTabBar));
tabBarGo.transform.SetParent(body.transform, false);
var tabBarRt = tabBarGo.GetComponent<RectTransform>();
tabBarRt.anchorMin = new Vector2(0f, 1f);
tabBarRt.anchorMax = new Vector2(1f, 1f);
tabBarRt.pivot = new Vector2(0.5f, 1f);
tabBarRt.sizeDelta = new Vector2(0f, 56f);
var tabHlg = tabBarGo.GetComponent<HorizontalLayoutGroup>();
tabHlg.spacing = 8f;
tabHlg.padding = new RectOffset(12, 12, 8, 8);
// childControl* default to false on a script-added layout group.
tabHlg.childControlWidth = true;
tabHlg.childControlHeight = true;
tabHlg.childForceExpandWidth = true;
tabHlg.childForceExpandHeight = true;
var contentArea = new GameObject("ContentArea", typeof(RectTransform));
contentArea.transform.SetParent(body.transform, false);
var contentRt = contentArea.GetComponent<RectTransform>();
contentRt.anchorMin = Vector2.zero;
contentRt.anchorMax = Vector2.one;
contentRt.offsetMin = new Vector2(16f, 16f);
contentRt.offsetMax = new Vector2(-16f, -64f);
var characterPanel = CreateTabPanel("CharacterPanel", contentArea.transform);
var skillsPanel = CreateTabPanel("SkillsPanel", contentArea.transform);
var journalPanel = CreateTabPanel("JournalPanel", contentArea.transform);
// Character
var charVlg = characterPanel.AddComponent<VerticalLayoutGroup>();
charVlg.spacing = 12f;
charVlg.padding = new RectOffset(8, 8, 8, 8);
charVlg.childControlWidth = true;
charVlg.childControlHeight = true;
charVlg.childForceExpandWidth = true;
var charView = characterPanel.AddComponent<CharacterPanelView>();
var nameLabel = CreateTmp("Name", characterPanel.transform, 36f, FontStyles.Bold);
var aliasLabel = CreateTmp("Alias", characterPanel.transform, 28f, FontStyles.Italic);
var invLabel = CreateTmp("Inventory", characterPanel.transform, 24f, FontStyles.Normal);
SetPrivate(charView, "nameLabel", nameLabel);
SetPrivate(charView, "aliasLabel", aliasLabel);
SetPrivate(charView, "inventoryPlaceholder", invLabel);
// Skills
var skillsContent = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
skillsContent.transform.SetParent(skillsPanel.transform, false);
Stretch(skillsContent.GetComponent<RectTransform>());
ConfigureVertical(skillsContent);
skillsContent.GetComponent<RectTransform>().anchorMin = new Vector2(0f, 1f);
skillsContent.GetComponent<RectTransform>().anchorMax = new Vector2(1f, 1f);
skillsContent.GetComponent<RectTransform>().pivot = new Vector2(0.5f, 1f);
var axisHeader = CreateTmp("AxisHeaderTemplate", skillsContent.transform, 20f, FontStyles.Bold);
axisHeader.gameObject.SetActive(false);
var templates = GameObject.Find("UI System")?.transform.Find("UITemplates")
?? new GameObject("UITemplates").transform;
if (templates.parent == null && transform != null)
{
templates.SetParent(transform, false);
templates.gameObject.SetActive(false);
}
var rowPrefab = BuildSkillRowPrefab(templates);
var skillsView = skillsPanel.AddComponent<SkillsPanelView>();
SetPrivate(skillsView, "contentRoot", skillsContent.GetComponent<RectTransform>());
SetPrivate(skillsView, "rowPrefab", rowPrefab);
SetPrivate(skillsView, "axisHeaderPrefab", axisHeader);
SetPrivate(skillsView, "displayMaxRank", 6);
// Journal
var journalView = journalPanel.AddComponent<JournalPanelView>();
var journalLabel = CreateTmp("Placeholder", journalPanel.transform, 28f, FontStyles.Italic);
journalLabel.text = "Nothing recorded yet.";
journalLabel.alignment = TextAlignmentOptions.Center;
Stretch(journalLabel.rectTransform);
SetPrivate(journalView, "placeholderLabel", journalLabel);
var charBtn = CreateTabButton("Character", tabBarGo.transform);
var skillsBtn = CreateTabButton("Skills", tabBarGo.transform);
var journalBtn = CreateTabButton("Journal", tabBarGo.transform);
var tabBar = tabBarGo.GetComponent<MenuTabBar>();
var tabs = new MenuTabBar.TabEntry[3];
tabs[0] = new MenuTabBar.TabEntry
{
button = charBtn,
panel = characterPanel.GetComponent<CanvasGroup>(),
onShown = new UnityEngine.Events.UnityEvent(),
};
// Panels toggle via CanvasGroup, never SetActive, so OnEnable fires
// once at construction and never again — before Yarn has set
// $player_name_full. Refresh on show or the tab stays stale forever.
tabs[0].onShown.AddListener(charView.Refresh);
tabs[1] = new MenuTabBar.TabEntry
{
button = skillsBtn,
panel = skillsPanel.GetComponent<CanvasGroup>(),
onShown = new UnityEngine.Events.UnityEvent(),
};
tabs[1].onShown.AddListener(skillsView.Refresh);
tabs[2] = new MenuTabBar.TabEntry
{
button = journalBtn,
panel = journalPanel.GetComponent<CanvasGroup>(),
onShown = new UnityEngine.Events.UnityEvent(),
};
tabBar.SetTabs(tabs);
var player = GameObject.Find("PlayerArmature");
var menuController = canvasGo.AddComponent<PlayerMenuController>();
SetPrivate(menuController, "menuRoot", canvasGo);
SetPrivate(menuController, "menuInput", player != null ? player.GetComponent<PlayerMenuInput>() : null);
SetPrivate(menuController, "controlLock", player != null ? player.GetComponent<PlayerControlLock>() : null);
SetPrivate(menuController, "tabBar", tabBar);
// Portrait click
var portraitButton = FindFirstObjectByType<PortraitHudView>()?.GetComponentInChildren<Button>(true);
if (portraitButton != null)
{
portraitButton.onClick.AddListener(menuController.Open);
}
}
SkillRowView BuildSkillRowPrefab(Transform parent)
{
var root = new GameObject("SkillRow", typeof(RectTransform), typeof(SkillRowView), typeof(HorizontalLayoutGroup));
root.transform.SetParent(parent, false);
var hlg = root.GetComponent<HorizontalLayoutGroup>();
hlg.padding = new RectOffset(8, 8, 4, 4);
hlg.spacing = 12f;
hlg.childAlignment = TextAnchor.MiddleLeft;
hlg.childControlHeight = true;
hlg.childControlWidth = true;
hlg.childForceExpandHeight = true;
var stripe = new GameObject("Accent", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
stripe.transform.SetParent(root.transform, false);
stripe.GetComponent<LayoutElement>().preferredWidth = 6f;
stripe.GetComponent<Image>().color = Color.white;
var name = CreateTmp("Name", root.transform, 24f, FontStyles.Bold);
name.GetComponent<LayoutElement>().flexibleWidth = 1f;
var bar = new GameObject("Bar", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
bar.transform.SetParent(root.transform, false);
bar.GetComponent<Image>().color = new Color(0.2f, 0.2f, 0.22f);
bar.GetComponent<LayoutElement>().preferredWidth = 160f;
bar.GetComponent<LayoutElement>().preferredHeight = 16f;
var fill = new GameObject("Fill", typeof(RectTransform), typeof(Image));
fill.transform.SetParent(bar.transform, false);
Stretch(fill.GetComponent<RectTransform>());
var fillImage = fill.GetComponent<Image>();
fillImage.type = Image.Type.Filled;
fillImage.fillMethod = Image.FillMethod.Horizontal;
fillImage.color = Color.white;
var rank = CreateTmp("Rank", root.transform, 24f, FontStyles.Normal);
rank.alignment = TextAlignmentOptions.Right;
rank.GetComponent<LayoutElement>().preferredWidth = 40f;
var view = root.GetComponent<SkillRowView>();
SetPrivate(view, "nameLabel", name);
SetPrivate(view, "rankLabel", rank);
SetPrivate(view, "barFill", fillImage);
SetPrivate(view, "accentStripe", stripe.GetComponent<Image>());
return view;
}
void EnsureCameraFraming()
{
var vcam = GameObject.Find("CinemachineCamera");
if (vcam == null)
{
return;
}
var framing = vcam.GetComponent<DialogueCameraFraming>();
if (framing == null)
{
framing = vcam.AddComponent<DialogueCameraFraming>();
}
SetPrivate(framing, "composer", vcam.GetComponent<CinemachinePositionComposer>());
SetPrivate(framing, "dialogueScreenX", -0.17f);
}
void WireDialogueRunner()
{
var runner = FindFirstObjectByType<DialogueRunner>();
var stream = FindFirstObjectByType<DialogueStreamPresenter>();
var options = FindFirstObjectByType<DialogueStreamOptionsPresenter>();
var advancer = FindFirstObjectByType<LineAdvancer>();
if (runner == null || stream == null || options == null)
{
return;
}
var list = new System.Collections.Generic.List<DialoguePresenterBase>();
list.Add(stream);
list.Add(options);
if (advancer != null)
{
list.Add(advancer);
SetPrivate(advancer, "presenter", stream);
}
runner.DialoguePresenters = list;
}
void HidePackageDialogueCanvas()
{
var dialogueSystem = GameObject.Find("Dialogue System");
if (dialogueSystem == null)
{
return;
}
var packageCanvas = dialogueSystem.transform.Find("Canvas");
if (packageCanvas == null)
{
return;
}
var banner = packageCanvas.Find("Skill Check Banner");
var streamCanvas = GameObject.Find("DialogueStreamCanvas");
if (banner != null && streamCanvas != null)
{
banner.SetParent(streamCanvas.transform, false);
var bannerRt = banner.GetComponent<RectTransform>();
if (bannerRt != null)
{
bannerRt.anchorMin = new Vector2(0f, 0.85f);
bannerRt.anchorMax = new Vector2(0.66f, 1f);
bannerRt.offsetMin = new Vector2(16f, 0f);
bannerRt.offsetMax = new Vector2(-16f, -16f);
}
}
packageCanvas.gameObject.SetActive(false);
}
static GameObject CreateCanvas(string name, int sortingOrder)
{
var go = new GameObject(name, typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
var canvas = go.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = sortingOrder;
var scaler = go.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.matchWidthOrHeight = 0.5f;
return go;
}
static GameObject CreateTabPanel(string name, Transform parent)
{
var go = new GameObject(name, typeof(RectTransform), typeof(CanvasGroup));
go.transform.SetParent(parent, false);
Stretch(go.GetComponent<RectTransform>());
var group = go.GetComponent<CanvasGroup>();
group.alpha = 0f;
group.interactable = false;
group.blocksRaycasts = false;
return go;
}
static Button CreateTabButton(string label, Transform parent)
{
var go = new GameObject(label, typeof(RectTransform), typeof(Image), typeof(Button), typeof(LayoutElement));
go.transform.SetParent(parent, false);
go.GetComponent<Image>().color = new Color(0.2f, 0.2f, 0.25f, 1f);
var text = CreateTmp("Label", go.transform, 22f, FontStyles.Bold);
text.text = label;
text.alignment = TextAlignmentOptions.Center;
Stretch(text.rectTransform);
return go.GetComponent<Button>();
}
static TextMeshProUGUI CreateTmp(string name, Transform parent, float size, FontStyles style)
{
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI), typeof(LayoutElement));
go.transform.SetParent(parent, false);
var tmp = go.GetComponent<TextMeshProUGUI>();
tmp.fontSize = size;
tmp.fontStyle = style;
tmp.color = Color.white;
tmp.enableWordWrapping = true;
tmp.text = name;
// No LayoutElement.preferredHeight — it would pin wrapped text to a
// single line's height and make stream rows overlap. See the same
// note in UILayerSetup.CreateTmp.
return tmp;
}
static void Stretch(RectTransform rt, float pad = 0f)
{
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = new Vector2(pad, pad);
rt.offsetMax = new Vector2(-pad, -pad);
}
static void ConfigureVertical(GameObject go)
{
var rt = go.GetComponent<RectTransform>();
rt.anchorMin = new Vector2(0f, 0f);
rt.anchorMax = new Vector2(1f, 0f);
rt.pivot = new Vector2(0.5f, 0f);
// See UILayerSetup.ConfigureVerticalContent: a script-added
// RectTransform starts at sizeDelta (100,100), which overhangs 50px
// off each side under stretch anchors and gets sheared by the mask.
rt.sizeDelta = new Vector2(0f, rt.sizeDelta.y);
rt.anchoredPosition = Vector2.zero;
var vlg = go.GetComponent<VerticalLayoutGroup>();
vlg.childControlHeight = true;
vlg.childControlWidth = true;
vlg.childForceExpandHeight = false;
vlg.childForceExpandWidth = true;
vlg.spacing = 4f;
go.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
}
static void SetPrivate(object target, string fieldName, object value)
{
var type = target.GetType();
while (type != null)
{
var field = type.GetField(fieldName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
if (field != null)
{
field.SetValue(target, value);
return;
}
type = type.BaseType;
}
Debug.LogWarning($"[UILayerBootstrap] Could not set field '{fieldName}' on {target.GetType().Name}");
}
}
}