Files
Colosseum/Assets/_Game/Scripts/UI/PassiveTreeUI.cs
dal4segno ce883e4fa3 feat: 디버그 패널 스킬 강제 발동 및 UI 모드 토글 시스템 추가
- UIModeController: leftAlt 키로 커서 표시/게임플레이 입력 차단 토글 (공용 싱글톤)
- DebugPanelUI: 보스 스킬 강제 발동 섹션 추가 (드롭다운 + 발동/취소 버튼)
- 에디터에서 Data/Skills의 보스 이름 기반 스킬 검색, 빌드에서 패턴 슬롯 fallback
- BossCombatBehaviorContext.GetAllPatternSkills() 추가 (디버그용 스킬 목록 수집)
- TMP Settings에 한글 폰트(MaruBuri)를 fallback으로 등록
- 젬/패시브/디버그 토글 버튼을 우측 하단에 수직 정렬
- InputSystem에 UIMode 액션(leftAlt) 추가
2026-04-01 23:14:05 +09:00

1454 lines
58 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using Colosseum.Passives;
using Colosseum.Player;
namespace Colosseum.UI
{
/// <summary>
/// 비전투 중 전체 화면으로 여는 패시브 트리 UI입니다.
/// </summary>
public class PassiveTreeUI : MonoBehaviour
{
private sealed class NodeVisual
{
public RectTransform RectTransform;
public Button Button;
public Image Image;
public Image FillImage;
public Image InnerImage;
public Outline Outline;
}
[Header("Toggle")]
[Tooltip("패시브 UI를 여닫는 키")]
[SerializeField] private Key toggleKey = Key.P;
[Tooltip("토글 버튼에 표시할 텍스트")]
[SerializeField] private string toggleButtonLabel = "패시브";
[Tooltip("토글 버튼의 캔버스 기준 위치")]
[SerializeField] private Vector2 toggleButtonAnchoredPosition = new Vector2(-10f, 46f);
[Header("Debug")]
[Tooltip("플레이 모드 시작 시 패널을 자동으로 엽니다.")]
[SerializeField] private bool openOnStartInPlayMode = false;
[Header("Prefabs")]
[Tooltip("패시브 트리 메인 뷰 프리팹")]
[SerializeField] private PassiveTreeViewReferences viewPrefab;
[Tooltip("패시브 노드 프리팹")]
[SerializeField] private PassiveTreeNodeView nodePrefab;
[Tooltip("프로토타입 패시브 카탈로그")]
[SerializeField] private PassivePrototypeCatalogData passivePrototypeCatalog;
[Tooltip("일반 패시브 노드에 사용할 아이콘")]
[SerializeField] private Sprite normalNodeSprite;
[Tooltip("허브/브릿지/완성 패시브 노드에 사용할 아이콘")]
[SerializeField] private Sprite specialNodeSprite;
[Tooltip("노드 안쪽에 겹쳐서 사용할 보조 아이콘")]
[SerializeField] private Sprite innerNodeSprite;
[Header("Style")]
[SerializeField] private Color panelBackgroundColor = new Color(0.07f, 0.07f, 0.1f, 0.98f);
[SerializeField] private Color sectionBackgroundColor = new Color(0.12f, 0.12f, 0.16f, 0.96f);
[SerializeField] private Color sectionOverlayColor = new Color(0f, 0f, 0f, 0.12f);
[SerializeField] private Color attackColor = new Color(0.78f, 0.3f, 0.22f, 1f);
[SerializeField] private Color defenseColor = new Color(0.2f, 0.56f, 0.6f, 1f);
[SerializeField] private Color supportColor = new Color(0.56f, 0.67f, 0.24f, 1f);
[SerializeField] private Color bridgeColor = new Color(0.58f, 0.47f, 0.23f, 1f);
[SerializeField] private Color lockedNodeColor = new Color(0.15f, 0.15f, 0.17f, 0.96f);
[SerializeField] private Color selectedNodeTint = new Color(1f, 0.92f, 0.68f, 1f);
[SerializeField] private Color focusedOutlineColor = new Color(0.95f, 0.92f, 0.82f, 1f);
[SerializeField] private Color normalTextColor = new Color(0.9f, 0.88f, 0.82f, 1f);
[SerializeField] private Color mutedTextColor = new Color(0.72f, 0.71f, 0.66f, 0.95f);
[SerializeField] private Color statusErrorColor = new Color(1f, 0.52f, 0.45f, 1f);
[SerializeField] private Color lineColor = new Color(0.72f, 0.67f, 0.53f, 0.4f);
[SerializeField] private Color activeLineColor = new Color(0.98f, 0.9f, 0.7f, 0.92f);
[SerializeField] private float graphCenterYOffset = -118f;
private const float LeftPanelWidth = 292f;
private const float RightPanelWidth = 308f;
private const float GraphPadding = 24f;
private const float ConnectionThickness = 7f;
private readonly Dictionary<PassiveNodeData, NodeVisual> nodeVisuals = new();
private PlayerNetworkController targetPlayer;
private PlayerMovement playerMovement;
private PlayerSkillInput playerSkillInput;
private Canvas parentCanvas;
private RectTransform canvasRectTransform;
private PassiveTreeViewReferences viewInstance;
private GameObject overlayRoot;
private RectTransform panelRectTransform;
private RectTransform graphRectTransform;
private RectTransform connectionLayer;
private RectTransform nodeLayer;
private RectTransform detailContent;
private TextMeshProUGUI pointsSummaryText;
private TextMeshProUGUI selectionSummaryText;
private TextMeshProUGUI detailText;
private TextMeshProUGUI statusText;
private Button nonePresetButton;
private Button defensePresetButton;
private Button supportPresetButton;
private Button attackPresetButton;
private Button clearButton;
private Button selectNodeButton;
private TextMeshProUGUI selectNodeButtonLabel;
private bool isPanelVisible;
private bool uiInitialized;
private bool previousCursorVisible;
private CursorLockMode previousCursorLockState;
private PassiveNodeData focusedNode;
private string lastStatusMessage = string.Empty;
private bool lastStatusIsError;
private void Awake()
{
EnsureUi();
HidePanelImmediate();
}
private void Start()
{
EnsureUi();
FindLocalPlayer();
RefreshAll();
if (Application.isPlaying && openOnStartInPlayMode)
{
SetPanelVisible(true);
}
}
private void Update()
{
if (targetPlayer == null)
{
FindLocalPlayer();
}
if (Keyboard.current == null)
return;
if (Keyboard.current[toggleKey].wasPressedThisFrame)
{
TogglePanelVisibility();
}
else if (isPanelVisible && Keyboard.current.escapeKey.wasPressedThisFrame)
{
SetPanelVisible(false);
}
if (isPanelVisible)
{
UpdatePanelSize();
}
}
private void OnDestroy()
{
UnsubscribeFromPlayer();
SetGameplayInputBlocked(false);
RestoreCursorState();
}
private void FindLocalPlayer()
{
PlayerNetworkController[] players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
for (int i = 0; i < players.Length; i++)
{
if (!players[i].IsOwner)
continue;
if (targetPlayer == players[i])
return;
SetTarget(players[i]);
return;
}
}
private void SetTarget(PlayerNetworkController target)
{
UnsubscribeFromPlayer();
targetPlayer = target;
playerMovement = targetPlayer != null ? targetPlayer.GetComponent<PlayerMovement>() : null;
playerSkillInput = targetPlayer != null ? targetPlayer.GetComponent<PlayerSkillInput>() : null;
if (targetPlayer != null)
{
targetPlayer.OnPassiveSelectionChanged += HandlePassiveSelectionChanged;
}
if (isPanelVisible)
{
SetGameplayInputBlocked(true);
}
RefreshAll();
}
private void UnsubscribeFromPlayer()
{
if (targetPlayer != null)
{
targetPlayer.OnPassiveSelectionChanged -= HandlePassiveSelectionChanged;
}
}
private void HandlePassiveSelectionChanged()
{
RefreshAll();
}
private void TogglePanelVisibility()
{
SetPanelVisible(!isPanelVisible);
}
private void SetPanelVisible(bool visible)
{
EnsureUi();
isPanelVisible = visible;
UpdatePanelSize();
if (overlayRoot != null)
{
overlayRoot.SetActive(visible);
}
if (visible)
{
RememberCursorState();
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
SetGameplayInputBlocked(true);
RefreshAll();
}
else
{
SetGameplayInputBlocked(false);
RestoreCursorState();
}
}
private void HidePanelImmediate()
{
isPanelVisible = false;
if (overlayRoot != null)
{
overlayRoot.SetActive(false);
}
}
private void RememberCursorState()
{
previousCursorVisible = Cursor.visible;
previousCursorLockState = Cursor.lockState;
}
private void RestoreCursorState()
{
Cursor.visible = previousCursorVisible;
Cursor.lockState = previousCursorLockState;
}
private void SetGameplayInputBlocked(bool blocked)
{
if (playerMovement != null)
{
playerMovement.SetGameplayInputEnabled(!blocked);
}
if (playerSkillInput != null)
{
playerSkillInput.SetGameplayInputEnabled(!blocked);
}
}
private void EnsureUi()
{
if (uiInitialized)
return;
parentCanvas = GetComponentInParent<Canvas>();
canvasRectTransform = parentCanvas != null ? parentCanvas.GetComponent<RectTransform>() : null;
if (parentCanvas == null || canvasRectTransform == null)
{
Debug.LogWarning("[PassiveTreeUI] Canvas를 찾지 못했습니다.");
return;
}
if (!EnsureViewInstance())
return;
BindViewReferences();
UpdatePanelSize();
uiInitialized = overlayRoot != null && panelRectTransform != null && graphRectTransform != null;
}
/// <summary>
/// 런타임에 사용할 패시브 트리 뷰 프리팹을 외부에서 주입합니다.
/// </summary>
public void ConfigurePrefabReferences(PassiveTreeViewReferences configuredViewPrefab, PassiveTreeNodeView configuredNodePrefab)
{
viewPrefab = configuredViewPrefab;
nodePrefab = configuredNodePrefab;
}
private bool EnsureViewInstance()
{
if (viewInstance != null)
return true;
viewInstance = GetComponentInChildren<PassiveTreeViewReferences>(true);
if (viewInstance != null)
return true;
if (viewPrefab == null)
{
Debug.LogWarning("[PassiveTreeUI] 메인 뷰 프리팹이 지정되지 않았습니다.");
return false;
}
viewInstance = Instantiate(viewPrefab, canvasRectTransform);
viewInstance.name = viewPrefab.name;
return viewInstance != null;
}
private void BindViewReferences()
{
if (viewInstance == null)
return;
overlayRoot = viewInstance.OverlayRoot;
panelRectTransform = viewInstance.PanelRect;
graphRectTransform = viewInstance.GraphRect;
connectionLayer = viewInstance.ConnectionLayer;
nodeLayer = viewInstance.NodeLayer;
detailContent = viewInstance.DetailContent;
pointsSummaryText = viewInstance.PointsSummaryText;
selectionSummaryText = viewInstance.SelectionSummaryText;
detailText = viewInstance.DetailText;
statusText = viewInstance.StatusText;
nonePresetButton = viewInstance.NonePresetButton;
defensePresetButton = viewInstance.DefensePresetButton;
supportPresetButton = viewInstance.SupportPresetButton;
attackPresetButton = viewInstance.AttackPresetButton;
clearButton = viewInstance.ClearButton;
selectNodeButton = viewInstance.SelectNodeButton;
selectNodeButtonLabel = viewInstance.SelectNodeButtonLabel;
ApplyStaticViewSettings();
if (statusText != null)
{
statusText.textWrappingMode = TextWrappingModes.NoWrap;
statusText.overflowMode = TextOverflowModes.Ellipsis;
}
BindButton(viewInstance.ToggleButton, TogglePanelVisibility);
BindButton(viewInstance.CloseButton, () => SetPanelVisible(false));
BindButton(nonePresetButton, () => TryApplyPreset(PassivePrototypePresetKind.None));
BindButton(defensePresetButton, () => TryApplyPreset(PassivePrototypePresetKind.Defense));
BindButton(supportPresetButton, () => TryApplyPreset(PassivePrototypePresetKind.Support));
BindButton(attackPresetButton, () => TryApplyPreset(PassivePrototypePresetKind.Attack));
BindButton(clearButton, TryClearSelection);
BindButton(selectNodeButton, TrySelectFocusedNode);
}
private void ApplyStaticViewSettings()
{
if (viewInstance?.ToggleButtonLabel != null)
{
viewInstance.ToggleButtonLabel.text = toggleButtonLabel;
}
if (viewInstance?.ToggleButton != null)
{
RectTransform toggleRect = viewInstance.ToggleButton.GetComponent<RectTransform>();
if (toggleRect != null)
{
toggleRect.anchoredPosition = new Vector2(-10f, 50f);
}
}
}
private static void BindButton(Button button, Action callback)
{
if (button == null || callback == null)
return;
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() => callback());
}
private void UpdatePanelSize()
{
if (panelRectTransform == null || canvasRectTransform == null)
return;
Rect canvasRect = canvasRectTransform.rect;
panelRectTransform.sizeDelta = new Vector2(
Mathf.Max(1160f, canvasRect.width - 24f),
Mathf.Max(720f, canvasRect.height - 24f));
}
private void CreateToggleButton()
{
GameObject buttonObject = CreateUiObject("Button_PassiveTree", canvasRectTransform);
RectTransform buttonRect = buttonObject.AddComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(1f, 0f);
buttonRect.anchorMax = new Vector2(1f, 0f);
buttonRect.pivot = new Vector2(1f, 0f);
buttonRect.anchoredPosition = new Vector2(-10f, 50f);
buttonRect.sizeDelta = new Vector2(110f, 40f);
Image buttonImage = buttonObject.AddComponent<Image>();
buttonImage.color = new Color(0.18f, 0.21f, 0.16f, 0.96f);
Button button = buttonObject.AddComponent<Button>();
button.targetGraphic = buttonImage;
button.onClick.AddListener(TogglePanelVisibility);
TextMeshProUGUI label = CreateFillLabel(buttonRect, "Label", toggleButtonLabel, 20f, TextAlignmentOptions.Center);
label.fontStyle = FontStyles.Bold;
label.textWrappingMode = TextWrappingModes.NoWrap;
}
private void CreateOverlay()
{
overlayRoot = CreateUiObject("Overlay_PassiveTree", canvasRectTransform);
RectTransform overlayRect = overlayRoot.AddComponent<RectTransform>();
StretchToParent(overlayRect);
GameObject backdropObject = CreateUiObject("Backdrop", overlayRect);
RectTransform backdropRect = backdropObject.AddComponent<RectTransform>();
StretchToParent(backdropRect);
Image backdropImage = backdropObject.AddComponent<Image>();
backdropImage.color = new Color(0f, 0f, 0f, 0.52f);
GameObject panelObject = CreateUiObject("Panel", overlayRect);
panelRectTransform = panelObject.AddComponent<RectTransform>();
panelRectTransform.anchorMin = new Vector2(0.5f, 0.5f);
panelRectTransform.anchorMax = new Vector2(0.5f, 0.5f);
panelRectTransform.pivot = new Vector2(0.5f, 0.5f);
panelRectTransform.anchoredPosition = Vector2.zero;
panelRectTransform.sizeDelta = new Vector2(1380f, 820f);
Image panelImage = panelObject.AddComponent<Image>();
panelImage.color = panelBackgroundColor;
VerticalLayoutGroup panelLayout = panelObject.AddComponent<VerticalLayoutGroup>();
panelLayout.padding = new RectOffset(22, 22, 22, 22);
panelLayout.spacing = 16f;
panelLayout.childAlignment = TextAnchor.UpperLeft;
panelLayout.childControlWidth = true;
panelLayout.childControlHeight = true;
panelLayout.childForceExpandWidth = true;
panelLayout.childForceExpandHeight = false;
CreateHeader(panelRectTransform);
CreateBody(panelRectTransform);
CreateFooter(panelRectTransform);
}
private void CreateHeader(RectTransform parent)
{
RectTransform header = CreateContainer(parent, "Header", 54f);
header.gameObject.AddComponent<Image>().color = sectionBackgroundColor;
HorizontalLayoutGroup layout = header.gameObject.AddComponent<HorizontalLayoutGroup>();
layout.padding = new RectOffset(14, 14, 8, 8);
layout.spacing = 10f;
layout.childAlignment = TextAnchor.MiddleLeft;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
RectTransform titleGroup = CreateUiObject("TitleGroup", header).AddComponent<RectTransform>();
titleGroup.gameObject.AddComponent<LayoutElement>().flexibleWidth = 1f;
VerticalLayoutGroup titleLayout = titleGroup.gameObject.AddComponent<VerticalLayoutGroup>();
titleLayout.spacing = 0f;
titleLayout.childAlignment = TextAnchor.MiddleLeft;
titleLayout.childControlWidth = true;
titleLayout.childControlHeight = true;
titleLayout.childForceExpandWidth = true;
titleLayout.childForceExpandHeight = false;
TextMeshProUGUI title = CreateAutoLabel(titleGroup, "Label_Title", "패시브 트리", 24f, TextAlignmentOptions.Left);
title.fontStyle = FontStyles.Bold;
title.textWrappingMode = TextWrappingModes.NoWrap;
pointsSummaryText = CreateAutoLabel(header, "Label_Points", string.Empty, 17f, TextAlignmentOptions.Right);
pointsSummaryText.textWrappingMode = TextWrappingModes.NoWrap;
pointsSummaryText.gameObject.AddComponent<LayoutElement>().preferredWidth = 420f;
Button closeButton = CreateButton(header, "Button_Close", "닫기", new Vector2(84f, 34f), () => SetPanelVisible(false), out _);
closeButton.image.color = new Color(0.34f, 0.2f, 0.2f, 0.96f);
}
private void CreateBody(RectTransform parent)
{
RectTransform body = CreateContainer(parent, "Body", 0f);
LayoutElement bodyLayout = body.GetComponent<LayoutElement>();
bodyLayout.flexibleHeight = 1f;
HorizontalLayoutGroup layout = body.gameObject.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 16f;
layout.childAlignment = TextAnchor.UpperLeft;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = true;
CreateLeftPanel(body);
CreateGraphPanel(body);
CreateRightPanel(body);
}
private void CreateFooter(RectTransform parent)
{
RectTransform footer = CreateContainer(parent, "Footer", 68f);
footer.gameObject.AddComponent<Image>().color = sectionBackgroundColor;
HorizontalLayoutGroup layout = footer.gameObject.AddComponent<HorizontalLayoutGroup>();
layout.padding = new RectOffset(16, 16, 12, 12);
layout.childAlignment = TextAnchor.MiddleLeft;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = true;
statusText = CreateAutoLabel(footer, "Label_Status", "로컬 플레이어를 찾는 중입니다.", 19f, TextAlignmentOptions.MidlineLeft);
}
private void CreateLeftPanel(RectTransform parent)
{
RectTransform section = CreateSectionRoot(parent, "Section_Left", LeftPanelWidth);
LayoutElement layout = section.GetComponent<LayoutElement>();
layout.flexibleWidth = 0f;
layout.minWidth = LeftPanelWidth;
layout.preferredWidth = LeftPanelWidth;
CreateSectionTitle(section, "현재 선택");
selectionSummaryText = CreateInfoText(section, "Label_SelectionSummary", 144f);
CreateSectionTitle(section, "프리셋");
RectTransform presetGrid = CreateContainer(section, "PresetGrid", 0f);
GridLayoutGroup presetLayout = presetGrid.gameObject.AddComponent<GridLayoutGroup>();
presetLayout.cellSize = new Vector2(140f, 58f);
presetLayout.spacing = new Vector2(8f, 8f);
presetLayout.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
presetLayout.constraintCount = 2;
nonePresetButton = CreateButton(presetGrid, "Button_Preset_None", "패시브 없음", Vector2.zero, () => TryApplyPreset(PassivePrototypePresetKind.None), out _);
defensePresetButton = CreateButton(presetGrid, "Button_Preset_Defense", "방어형 패시브", Vector2.zero, () => TryApplyPreset(PassivePrototypePresetKind.Defense), out _);
supportPresetButton = CreateButton(presetGrid, "Button_Preset_Support", "지원형 패시브", Vector2.zero, () => TryApplyPreset(PassivePrototypePresetKind.Support), out _);
attackPresetButton = CreateButton(presetGrid, "Button_Preset_Attack", "공격형 패시브", Vector2.zero, () => TryApplyPreset(PassivePrototypePresetKind.Attack), out _);
clearButton = CreateButton(section, "Button_Clear", "전체 초기화", new Vector2(0f, 52f), TryClearSelection, out _);
}
private void CreateGraphPanel(RectTransform parent)
{
RectTransform section = CreateSectionRoot(parent, "Section_Graph", 0f);
LayoutElement layout = section.GetComponent<LayoutElement>();
layout.flexibleWidth = 1f;
layout.minWidth = 420f;
CreateSectionTitle(section, "트리 그래프");
graphRectTransform = CreateContainer(section, "GraphSurface", 0f);
LayoutElement graphLayout = graphRectTransform.GetComponent<LayoutElement>();
graphLayout.flexibleHeight = 1f;
graphLayout.minHeight = 460f;
graphRectTransform.gameObject.AddComponent<Image>().color = sectionOverlayColor;
connectionLayer = CreateUiObject("Connections", graphRectTransform).AddComponent<RectTransform>();
StretchToParent(connectionLayer);
nodeLayer = CreateUiObject("Nodes", graphRectTransform).AddComponent<RectTransform>();
StretchToParent(nodeLayer);
}
private void CreateRightPanel(RectTransform parent)
{
RectTransform section = CreateSectionRoot(parent, "Section_Right", RightPanelWidth);
LayoutElement layout = section.GetComponent<LayoutElement>();
layout.flexibleWidth = 0f;
layout.minWidth = RightPanelWidth;
layout.preferredWidth = RightPanelWidth;
CreateSectionTitle(section, "노드 상세");
RectTransform detailScroll = CreateScrollView(section, "Scroll_Detail", out detailContent);
LayoutElement detailScrollLayout = detailScroll.gameObject.AddComponent<LayoutElement>();
detailScrollLayout.flexibleHeight = 1f;
detailScrollLayout.minHeight = 360f;
detailText = CreateAutoLabel(detailContent, "Label_Detail", string.Empty, 19f, TextAlignmentOptions.TopLeft);
detailText.textWrappingMode = TextWrappingModes.Normal;
detailText.overflowMode = TextOverflowModes.Overflow;
detailText.margin = new Vector4(18f, 8f, 18f, 14f);
detailText.gameObject.AddComponent<LayoutElement>().minHeight = 360f;
selectNodeButton = CreateButton(section, "Button_SelectNode", "노드 선택", new Vector2(0f, 58f), TrySelectFocusedNode, out selectNodeButtonLabel);
selectNodeButtonLabel.fontSize = 21f;
selectNodeButtonLabel.fontStyle = FontStyles.Bold;
}
private void RefreshAll()
{
if (!uiInitialized)
return;
PassiveTreeData tree = GetCurrentTree();
EnsureFocusedNode(tree);
UpdatePointsSummary(tree);
RefreshSelectionSummary(tree);
RefreshPresetButtons();
RebuildGraph(tree);
RefreshDetail(tree);
if (!string.IsNullOrWhiteSpace(lastStatusMessage))
{
ApplyStatusMessage();
}
else
{
ApplyStatusMessage();
}
}
private PassiveTreeData GetCurrentTree()
{
if (targetPlayer != null && targetPlayer.PassiveTree != null)
return targetPlayer.PassiveTree;
PassivePrototypeCatalogData catalog = targetPlayer != null && targetPlayer.PassivePrototypeCatalogData != null
? targetPlayer.PassivePrototypeCatalogData
: passivePrototypeCatalog;
return PassivePrototypeCatalog.LoadPrototypeTree(catalog);
}
private void EnsureFocusedNode(PassiveTreeData tree)
{
if (tree == null || tree.Nodes == null || tree.Nodes.Count <= 0)
{
focusedNode = null;
return;
}
if (focusedNode != null)
{
PassiveNodeData currentNode = tree.GetNodeById(focusedNode.NodeId);
if (currentNode != null)
{
focusedNode = currentNode;
return;
}
}
IReadOnlyList<string> selectedNodeIds = targetPlayer != null ? targetPlayer.SelectedPassiveNodeIds : Array.Empty<string>();
for (int i = 0; i < selectedNodeIds.Count; i++)
{
PassiveNodeData selectedNode = tree.GetNodeById(selectedNodeIds[i]);
if (selectedNode != null)
{
focusedNode = selectedNode;
return;
}
}
for (int i = 0; i < tree.Nodes.Count; i++)
{
if (tree.Nodes[i] != null && tree.Nodes[i].NodeKind == PassiveNodeKind.Hub)
{
focusedNode = tree.Nodes[i];
return;
}
}
focusedNode = tree.Nodes[0];
}
private void UpdatePointsSummary(PassiveTreeData tree)
{
if (pointsSummaryText == null)
return;
if (targetPlayer == null || tree == null)
{
pointsSummaryText.text = "포인트 정보 없음";
return;
}
pointsSummaryText.text =
$"{targetPlayer.CurrentPassivePresetName} | 포인트 {targetPlayer.UsedPassivePoints}/{tree.InitialPoints} | 남음 {targetPlayer.RemainingPassivePoints}";
}
private void RefreshSelectionSummary(PassiveTreeData tree)
{
if (selectionSummaryText == null)
return;
if (tree == null)
{
selectionSummaryText.text = "패시브 트리 자산을 찾지 못했습니다.";
return;
}
StringBuilder builder = new StringBuilder();
string presetName = targetPlayer != null ? targetPlayer.CurrentPassivePresetName : "미적용";
int usedPoints = targetPlayer != null ? targetPlayer.UsedPassivePoints : 0;
int remainingPoints = targetPlayer != null ? targetPlayer.RemainingPassivePoints : tree.InitialPoints;
IReadOnlyList<string> selectedNodeIds = targetPlayer != null ? targetPlayer.SelectedPassiveNodeIds : Array.Empty<string>();
builder.AppendLine($"현재 프리셋: {presetName}");
builder.AppendLine($"포인트: {usedPoints}/{tree.InitialPoints}");
builder.AppendLine($"남은 포인트: {remainingPoints}");
builder.AppendLine($"선택 노드 수: {selectedNodeIds.Count}개");
selectionSummaryText.text = builder.ToString().TrimEnd();
}
private void RefreshPresetButtons()
{
ApplyPresetButtonState(nonePresetButton, string.Equals(targetPlayer?.CurrentPassivePresetName, "패시브 없음", StringComparison.Ordinal));
ApplyPresetButtonState(defensePresetButton, string.Equals(targetPlayer?.CurrentPassivePresetName, "방어형 패시브", StringComparison.Ordinal));
ApplyPresetButtonState(supportPresetButton, string.Equals(targetPlayer?.CurrentPassivePresetName, "지원형 패시브", StringComparison.Ordinal));
ApplyPresetButtonState(attackPresetButton, string.Equals(targetPlayer?.CurrentPassivePresetName, "공격형 패시브", StringComparison.Ordinal));
if (clearButton != null)
{
clearButton.interactable = targetPlayer != null;
clearButton.image.color = clearButton.interactable
? new Color(0.2f, 0.18f, 0.16f, 0.98f)
: lockedNodeColor;
}
}
private void ApplyPresetButtonState(Button button, bool selected)
{
if (button == null)
return;
bool interactable = targetPlayer != null;
button.interactable = interactable;
button.image.color = !interactable
? lockedNodeColor
: selected ? new Color(0.48f, 0.38f, 0.16f, 0.96f) : new Color(0.2f, 0.2f, 0.24f, 0.96f);
}
private void RebuildGraph(PassiveTreeData tree)
{
if (graphRectTransform == null || connectionLayer == null || nodeLayer == null)
return;
ClearChildren(connectionLayer);
ClearChildren(nodeLayer);
nodeVisuals.Clear();
if (tree == null || tree.Nodes == null || tree.Nodes.Count <= 0)
return;
Canvas.ForceUpdateCanvases();
HashSet<string> drawnConnections = new HashSet<string>();
for (int i = 0; i < tree.Nodes.Count; i++)
{
PassiveNodeData node = tree.Nodes[i];
if (node == null)
continue;
DrawConnectionList(node, node.ConnectedNodes, drawnConnections);
DrawConnectionList(node, node.PrerequisiteNodes, drawnConnections);
}
for (int i = 0; i < tree.Nodes.Count; i++)
{
if (tree.Nodes[i] != null)
{
CreateNodeButton(tree.Nodes[i]);
}
}
}
private void DrawConnectionList(PassiveNodeData fromNode, IReadOnlyList<PassiveNodeData> nodes, HashSet<string> drawnConnections)
{
if (nodes == null)
return;
for (int i = 0; i < nodes.Count; i++)
{
PassiveNodeData targetNode = nodes[i];
if (targetNode == null)
continue;
string key = BuildConnectionKey(fromNode, targetNode);
if (!drawnConnections.Add(key))
continue;
CreateConnectionVisual(fromNode, targetNode);
}
}
private void CreateConnectionVisual(PassiveNodeData fromNode, PassiveNodeData toNode)
{
Vector2 fromPosition = GetGraphAnchoredPosition(fromNode.LayoutPosition);
Vector2 toPosition = GetGraphAnchoredPosition(toNode.LayoutPosition);
Vector2 delta = toPosition - fromPosition;
float distance = delta.magnitude;
if (distance <= 0.01f)
return;
Vector2 direction = delta / distance;
float fromInset = GetConnectionInset(fromNode);
float toInset = GetConnectionInset(toNode);
fromPosition += direction * fromInset;
toPosition -= direction * toInset;
delta = toPosition - fromPosition;
distance = delta.magnitude;
if (distance <= 0.01f)
return;
GameObject lineObject = CreateUiObject($"Line_{fromNode.NodeId}_{toNode.NodeId}", connectionLayer);
RectTransform lineRect = lineObject.AddComponent<RectTransform>();
lineRect.anchorMin = new Vector2(0.5f, 0.5f);
lineRect.anchorMax = new Vector2(0.5f, 0.5f);
lineRect.pivot = new Vector2(0.5f, 0.5f);
lineRect.anchoredPosition = (fromPosition + toPosition) * 0.5f;
lineRect.sizeDelta = new Vector2(distance, ConnectionThickness);
lineRect.localRotation = Quaternion.Euler(0f, 0f, Mathf.Atan2(delta.y, delta.x) * Mathf.Rad2Deg);
lineObject.AddComponent<Image>().color = IsConnectionActive(fromNode, toNode) ? activeLineColor : lineColor;
}
private bool IsConnectionActive(PassiveNodeData leftNode, PassiveNodeData rightNode)
{
bool leftSelected = IsSelected(leftNode);
bool rightSelected = IsSelected(rightNode);
if (leftSelected && rightSelected)
return true;
return leftSelected && CanNodePreviewConnect(rightNode) ||
rightSelected && CanNodePreviewConnect(leftNode);
}
private bool CanNodePreviewConnect(PassiveNodeData node)
{
if (node == null || targetPlayer == null)
return false;
return IsSelected(node) || targetPlayer.CanSelectPassiveNode(node, out _, out _);
}
private void CreateNodeButton(PassiveNodeData node)
{
if (nodePrefab == null)
{
Debug.LogWarning("[PassiveTreeUI] 노드 프리팹이 지정되지 않았습니다.");
return;
}
PassiveTreeNodeView nodeView = Instantiate(nodePrefab, nodeLayer);
nodeView.name = $"Node_{node.NodeId}";
RectTransform rectTransform = nodeView.RootRect != null
? nodeView.RootRect
: nodeView.GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.pivot = new Vector2(0.5f, 0.5f);
rectTransform.anchoredPosition = GetGraphAnchoredPosition(node.LayoutPosition);
rectTransform.sizeDelta = GetNodeSize(node);
Image image = nodeView.BackgroundImage != null
? nodeView.BackgroundImage
: nodeView.GetComponent<Image>();
image.sprite = GetNodeSprite(node);
image.type = Image.Type.Simple;
image.preserveAspect = true;
Image fillImage = nodeView.FillImage;
if (fillImage != null)
{
fillImage.type = Image.Type.Sliced;
}
Image innerImage = nodeView.InnerImage;
if (innerImage != null)
{
innerImage.sprite = GetInnerNodeSprite();
innerImage.type = Image.Type.Simple;
innerImage.preserveAspect = true;
}
Button button = nodeView.Button != null
? nodeView.Button
: nodeView.GetComponent<Button>();
button.targetGraphic = image;
PassiveNodeData capturedNode = node;
button.onClick.AddListener(() => FocusNode(capturedNode));
if (node.NodeKind == PassiveNodeKind.Bridge)
{
rectTransform.localRotation = Quaternion.Euler(0f, 0f, 45f);
}
else
{
rectTransform.localRotation = Quaternion.identity;
}
nodeVisuals[node] = new NodeVisual
{
RectTransform = rectTransform,
Button = button,
Image = image,
FillImage = fillImage,
InnerImage = innerImage,
Outline = nodeView.Outline != null ? nodeView.Outline : button.GetComponent<Outline>(),
};
ApplyNodeVisual(node);
}
private void FocusNode(PassiveNodeData node)
{
focusedNode = node;
RefreshDetail(GetCurrentTree());
foreach (KeyValuePair<PassiveNodeData, NodeVisual> entry in nodeVisuals)
{
ApplyNodeVisual(entry.Key);
}
}
private void RefreshDetail(PassiveTreeData tree)
{
if (detailText == null)
return;
if (tree == null || focusedNode == null)
{
detailText.text = "노드를 선택하면 설명을 표시합니다.";
RefreshSelectButton(null);
return;
}
StringBuilder builder = new StringBuilder();
if (!string.IsNullOrWhiteSpace(focusedNode.DisplayName))
{
builder.AppendLine($"<size=30><b>{focusedNode.DisplayName}</b></size>");
}
builder.AppendLine($"<size=18>{PassivePresentationUtility.GetBranchLabel(focusedNode.Branch)} | {PassivePresentationUtility.GetNodeKindLabel(focusedNode.NodeKind)} | 축 {PassivePresentationUtility.GetAxisSummary(focusedNode.AxisMask)}</size>");
builder.AppendLine($"<size=18>비용 {focusedNode.Cost}</size>");
if (!string.IsNullOrWhiteSpace(focusedNode.Description))
{
builder.AppendLine();
builder.AppendLine("<b>설명</b>");
builder.AppendLine(focusedNode.Description.Trim());
}
if (focusedNode.Effects != null && focusedNode.Effects.Count > 0)
{
builder.AppendLine();
builder.AppendLine("<b>효과</b>");
for (int i = 0; i < focusedNode.Effects.Count; i++)
{
PassiveEffectEntry effect = focusedNode.Effects[i];
if (effect == null)
continue;
builder.Append("• ");
builder.AppendLine(PassivePresentationUtility.GetEffectLabel(effect));
}
}
string stateText = BuildFocusedNodeStateText(tree);
if (!string.IsNullOrWhiteSpace(stateText))
{
builder.AppendLine();
builder.AppendLine("<b>현재 상태</b>");
builder.AppendLine(stateText);
}
detailText.text = builder.ToString().TrimEnd();
RefreshSelectButton(focusedNode);
}
private string BuildFocusedNodeStateText(PassiveTreeData tree)
{
if (focusedNode == null)
return string.Empty;
if (IsSelected(focusedNode))
return "이미 적용된 노드입니다.";
if (targetPlayer == null)
return "로컬 플레이어를 찾으면 선택 가능 여부를 갱신합니다.";
if (targetPlayer.CanSelectPassiveNode(focusedNode, out string reason, out int nextUsedPoints))
{
return $"선택 가능 | 비용 {focusedNode.Cost} | 적용 시 사용 포인트 {nextUsedPoints}/{tree.InitialPoints}";
}
return string.IsNullOrWhiteSpace(reason) ? "선택할 수 없습니다." : reason;
}
private void RefreshSelectButton(PassiveNodeData node)
{
if (selectNodeButton == null || selectNodeButtonLabel == null)
return;
if (node == null)
{
selectNodeButton.interactable = false;
selectNodeButton.image.color = lockedNodeColor;
selectNodeButtonLabel.text = "노드 선택";
return;
}
if (IsSelected(node))
{
selectNodeButton.interactable = false;
selectNodeButton.image.color = new Color(0.28f, 0.3f, 0.18f, 0.96f);
selectNodeButtonLabel.text = "이미 선택됨";
return;
}
bool canSelect = targetPlayer != null && targetPlayer.CanSelectPassiveNode(node, out _, out _);
selectNodeButton.interactable = canSelect;
selectNodeButton.image.color = canSelect ? new Color(0.24f, 0.36f, 0.22f, 0.98f) : lockedNodeColor;
selectNodeButtonLabel.text = canSelect ? "이 노드 선택" : "선택 불가";
}
private void TrySelectFocusedNode()
{
if (focusedNode == null)
{
SetStatusMessage("선택할 노드를 먼저 지정하세요.", true);
return;
}
if (targetPlayer == null)
{
SetStatusMessage("로컬 플레이어를 찾지 못했습니다.", true);
return;
}
bool success = targetPlayer.RequestSelectPassiveNode(focusedNode.NodeId, out string reason);
SetStatusMessage(reason, !success);
RefreshAll();
}
private void TryApplyPreset(PassivePrototypePresetKind presetKind)
{
if (targetPlayer == null)
{
SetStatusMessage("로컬 플레이어를 찾지 못했습니다.", true);
return;
}
bool success = targetPlayer.RequestApplyPrototypePassivePreset(presetKind, out string reason);
SetStatusMessage(reason, !success);
RefreshAll();
}
private void TryClearSelection()
{
if (targetPlayer == null)
{
SetStatusMessage("로컬 플레이어를 찾지 못했습니다.", true);
return;
}
bool success = targetPlayer.RequestClearPassiveSelection(out string reason);
SetStatusMessage(reason, !success);
RefreshAll();
}
private void ApplyNodeVisual(PassiveNodeData node)
{
if (node == null || !nodeVisuals.TryGetValue(node, out NodeVisual visual))
return;
bool selected = IsSelected(node);
bool focused = focusedNode != null && string.Equals(focusedNode.NodeId, node.NodeId, StringComparison.Ordinal);
bool selectable = !selected && targetPlayer != null && targetPlayer.CanSelectPassiveNode(node, out _, out _);
Color baseColor = GetNodeBaseColor(node);
Color selectedFillColor = Color.Lerp(baseColor, Color.white, 0.2f);
Color selectableFillColor = Color.Lerp(baseColor, lockedNodeColor, 0.76f);
Color lockedFillColor = Color.Lerp(baseColor, lockedNodeColor, 0.9f);
Color fillColor = selected
? selectedFillColor
: selectable ? selectableFillColor : lockedFillColor;
visual.Button.interactable = true;
visual.Image.sprite = GetNodeSprite(node);
visual.Image.color = fillColor;
if (visual.FillImage != null)
{
visual.FillImage.color = GetNodeFillColor(selected, selectable, focused);
}
if (visual.InnerImage != null)
{
visual.InnerImage.sprite = GetInnerNodeSprite();
visual.InnerImage.color = GetInnerNodeColor(selected, selectable, focused);
}
visual.RectTransform.localScale = Vector3.one;
Outline outline = visual.Outline != null ? visual.Outline : visual.Button.GetComponent<Outline>();
if (outline == null)
{
outline = visual.Button.gameObject.AddComponent<Outline>();
visual.Outline = outline;
}
outline.effectDistance = selected
? Vector2.zero
: focused ? new Vector2(1.5f, 1.5f)
: selectable ? new Vector2(1f, 1f)
: Vector2.zero;
outline.effectColor = selected
? Color.clear
: focused ? new Color(focusedOutlineColor.r, focusedOutlineColor.g, focusedOutlineColor.b, 0.55f)
: selectable ? new Color(baseColor.r, baseColor.g, baseColor.b, 0.35f)
: Color.clear;
}
private bool IsSelected(PassiveNodeData node)
{
return node != null && targetPlayer != null && targetPlayer.IsPassiveNodeSelected(node.NodeId);
}
private Vector2 GetGraphAnchoredPosition(Vector2 layoutPosition)
{
Rect rect = graphRectTransform != null ? graphRectTransform.rect : new Rect(0f, 0f, 640f, 480f);
float halfExtent = Mathf.Max(0f, Mathf.Min(rect.width, rect.height) * 0.5f - GraphPadding);
return new Vector2(
Mathf.Clamp(layoutPosition.x, -1f, 1f) * halfExtent,
Mathf.Clamp(layoutPosition.y, -1f, 1f) * halfExtent + graphCenterYOffset);
}
private static string BuildConnectionKey(PassiveNodeData leftNode, PassiveNodeData rightNode)
{
return string.Compare(leftNode.NodeId, rightNode.NodeId, StringComparison.Ordinal) <= 0
? $"{leftNode.NodeId}|{rightNode.NodeId}"
: $"{rightNode.NodeId}|{leftNode.NodeId}";
}
private Sprite GetNodeSprite(PassiveNodeData node)
{
bool useSpecialSprite = node != null && (node.NodeKind == PassiveNodeKind.Bridge || node.NodeKind == PassiveNodeKind.Capstone);
Sprite fallbackSprite = useSpecialSprite ? normalNodeSprite : specialNodeSprite;
Sprite preferredSprite = useSpecialSprite ? specialNodeSprite : normalNodeSprite;
return preferredSprite != null ? preferredSprite : fallbackSprite;
}
private Sprite GetInnerNodeSprite()
{
if (innerNodeSprite != null)
return innerNodeSprite;
return normalNodeSprite != null ? normalNodeSprite : specialNodeSprite;
}
private static Color GetInnerNodeColor(bool selected, bool selectable, bool focused)
{
if (selected)
return new Color(0.62f, 0.60f, 0.56f, 0.7f);
if (focused)
return new Color(0.5f, 0.5f, 0.5f, 0.5f);
if (selectable)
return new Color(0.42f, 0.42f, 0.44f, 0.4f);
return new Color(0.3f, 0.3f, 0.32f, 0.34f);
}
private static Color GetNodeFillColor(bool selected, bool selectable, bool focused)
{
if (selected)
return new Color(0.09f, 0.09f, 0.11f, 0.96f);
if (focused)
return new Color(0.08f, 0.08f, 0.10f, 0.94f);
if (selectable)
return new Color(0.07f, 0.07f, 0.09f, 0.92f);
return new Color(0.06f, 0.06f, 0.08f, 0.90f);
}
private Color GetNodeBaseColor(PassiveNodeData node)
{
if (node.Branch == PassiveNodeBranch.Bridge)
return bridgeColor;
if ((node.AxisMask & PassiveAxisMask.Attack) != 0 && (node.AxisMask & PassiveAxisMask.Defense) != 0)
return Color.Lerp(attackColor, defenseColor, 0.5f);
if ((node.AxisMask & PassiveAxisMask.Defense) != 0 && (node.AxisMask & PassiveAxisMask.Support) != 0)
return Color.Lerp(defenseColor, supportColor, 0.5f);
if ((node.AxisMask & PassiveAxisMask.Support) != 0 && (node.AxisMask & PassiveAxisMask.Attack) != 0)
return Color.Lerp(supportColor, attackColor, 0.5f);
if ((node.AxisMask & PassiveAxisMask.Attack) != 0)
return attackColor;
if ((node.AxisMask & PassiveAxisMask.Defense) != 0)
return defenseColor;
if ((node.AxisMask & PassiveAxisMask.Support) != 0)
return supportColor;
return new Color(0.42f, 0.45f, 0.22f, 0.96f);
}
private static Vector2 GetNodeSize(PassiveNodeData node)
{
return node.NodeKind switch
{
PassiveNodeKind.Hub => new Vector2(78f, 78f),
PassiveNodeKind.Capstone => new Vector2(70f, 70f),
PassiveNodeKind.Bridge => new Vector2(56f, 56f),
_ => new Vector2(62f, 62f),
};
}
private static float GetConnectionInset(PassiveNodeData node)
{
Vector2 size = GetNodeSize(node);
float radius = Mathf.Min(size.x, size.y) * 0.5f + ConnectionThickness * 0.5f + 2f;
if (node != null && node.NodeKind == PassiveNodeKind.Bridge)
radius += 4f;
else if (node != null && (node.NodeKind == PassiveNodeKind.Hub || node.NodeKind == PassiveNodeKind.Capstone))
radius += 2f;
return radius;
}
private RectTransform CreateSectionRoot(RectTransform parent, string name, float preferredWidth)
{
RectTransform section = CreateContainer(parent, name, 0f);
LayoutElement layout = section.GetComponent<LayoutElement>();
layout.flexibleHeight = 1f;
layout.preferredWidth = preferredWidth;
section.gameObject.AddComponent<Image>().color = sectionBackgroundColor;
VerticalLayoutGroup verticalLayout = section.gameObject.AddComponent<VerticalLayoutGroup>();
verticalLayout.padding = new RectOffset(16, 16, 16, 16);
verticalLayout.spacing = 12f;
verticalLayout.childAlignment = TextAnchor.UpperLeft;
verticalLayout.childControlWidth = true;
verticalLayout.childControlHeight = true;
verticalLayout.childForceExpandWidth = true;
verticalLayout.childForceExpandHeight = false;
return section;
}
private TextMeshProUGUI CreateSectionTitle(RectTransform parent, string text)
{
TextMeshProUGUI label = CreateAutoLabel(parent, "Label_Title", text, 28f, TextAlignmentOptions.Left);
label.fontStyle = FontStyles.Bold;
return label;
}
private TextMeshProUGUI CreateInfoText(RectTransform parent, string name, float minHeight)
{
RectTransform container = CreateContainer(parent, $"{name}_Container", minHeight);
container.gameObject.AddComponent<Image>().color = sectionOverlayColor;
VerticalLayoutGroup layout = container.gameObject.AddComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(16, 16, 12, 12);
layout.childAlignment = TextAnchor.UpperLeft;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
TextMeshProUGUI label = CreateAutoLabel(container, name, string.Empty, 18f, TextAlignmentOptions.TopLeft);
label.textWrappingMode = TextWrappingModes.Normal;
label.overflowMode = TextOverflowModes.Overflow;
label.extraPadding = true;
label.margin = new Vector4(14f, 6f, 14f, 10f);
return label;
}
private RectTransform CreateScrollView(RectTransform parent, string name, out RectTransform content)
{
GameObject rootObject = CreateUiObject(name, parent);
RectTransform rootRect = rootObject.AddComponent<RectTransform>();
rootObject.AddComponent<Image>().color = sectionOverlayColor;
ScrollRect scrollRect = rootObject.AddComponent<ScrollRect>();
scrollRect.horizontal = false;
scrollRect.vertical = true;
scrollRect.scrollSensitivity = 30f;
RectTransform viewport = CreateUiObject("Viewport", rootRect).AddComponent<RectTransform>();
StretchToParent(viewport);
viewport.gameObject.AddComponent<Image>().color = new Color(1f, 1f, 1f, 0.01f);
Mask mask = viewport.gameObject.AddComponent<Mask>();
mask.showMaskGraphic = false;
content = CreateUiObject("Content", viewport).AddComponent<RectTransform>();
content.anchorMin = new Vector2(0f, 1f);
content.anchorMax = new Vector2(1f, 1f);
content.pivot = new Vector2(0f, 1f);
VerticalLayoutGroup contentLayout = content.gameObject.AddComponent<VerticalLayoutGroup>();
contentLayout.padding = new RectOffset(16, 16, 14, 14);
contentLayout.spacing = 8f;
contentLayout.childAlignment = TextAnchor.UpperLeft;
contentLayout.childControlWidth = true;
contentLayout.childControlHeight = true;
contentLayout.childForceExpandWidth = true;
contentLayout.childForceExpandHeight = false;
ContentSizeFitter fitter = content.gameObject.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
scrollRect.viewport = viewport;
scrollRect.content = content;
return rootRect;
}
private RectTransform CreateContainer(RectTransform parent, string name, float minHeight)
{
GameObject containerObject = CreateUiObject(name, parent);
RectTransform rectTransform = containerObject.AddComponent<RectTransform>();
LayoutElement layout = containerObject.AddComponent<LayoutElement>();
layout.minHeight = minHeight;
return rectTransform;
}
private Button CreateButton(RectTransform parent, string name, string labelText, Vector2 size, Action onClick, out TextMeshProUGUI label)
{
GameObject buttonObject = CreateUiObject(name, parent);
RectTransform rectTransform = buttonObject.AddComponent<RectTransform>();
if (size.sqrMagnitude > 0f)
{
rectTransform.sizeDelta = size;
}
Image image = buttonObject.AddComponent<Image>();
image.color = new Color(0.2f, 0.2f, 0.24f, 0.96f);
Button button = buttonObject.AddComponent<Button>();
button.targetGraphic = image;
if (onClick != null)
{
button.onClick.AddListener(() => onClick());
}
label = CreateFillLabel(rectTransform, "Label", labelText, 18f, TextAlignmentOptions.Center);
label.color = Color.white;
label.textWrappingMode = TextWrappingModes.Normal;
return button;
}
private TextMeshProUGUI CreateAutoLabel(RectTransform parent, string name, string text, float fontSize, TextAlignmentOptions alignment)
{
GameObject labelObject = CreateUiObject(name, parent);
RectTransform rectTransform = labelObject.AddComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0f, 1f);
rectTransform.anchorMax = new Vector2(1f, 1f);
rectTransform.pivot = new Vector2(0f, 1f);
TextMeshProUGUI label = labelObject.AddComponent<TextMeshProUGUI>();
label.font = TMP_Settings.defaultFontAsset;
label.fontSize = fontSize;
label.text = text;
label.alignment = alignment;
label.color = normalTextColor;
label.extraPadding = true;
label.raycastTarget = false;
return label;
}
private TextMeshProUGUI CreateFillLabel(RectTransform parent, string name, string text, float fontSize, TextAlignmentOptions alignment)
{
TextMeshProUGUI label = CreateAutoLabel(parent, name, text, fontSize, alignment);
StretchToParent(label.rectTransform);
return label;
}
private void SetStatusMessage(string message, bool isError)
{
lastStatusMessage = message ?? string.Empty;
lastStatusIsError = isError;
ApplyStatusMessage();
}
private void ApplyStatusMessage()
{
if (statusText == null)
return;
bool hasMessage = !string.IsNullOrWhiteSpace(lastStatusMessage);
Transform footerTransform = statusText.transform.parent;
if (footerTransform != null)
{
Image footerImage = footerTransform.GetComponent<Image>();
if (footerImage != null)
{
Color color = sectionBackgroundColor;
color.a = hasMessage ? sectionBackgroundColor.a : 0f;
footerImage.color = color;
}
}
statusText.text = hasMessage ? lastStatusMessage : string.Empty;
statusText.color = lastStatusIsError ? statusErrorColor : normalTextColor;
}
private static GameObject CreateUiObject(string name, Transform parent)
{
GameObject gameObject = new GameObject(name);
gameObject.transform.SetParent(parent, false);
gameObject.layer = parent.gameObject.layer;
return gameObject;
}
private static void StretchToParent(RectTransform rectTransform)
{
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
rectTransform.anchoredPosition = Vector2.zero;
rectTransform.sizeDelta = Vector2.zero;
}
private static void ClearChildren(RectTransform parent)
{
for (int i = parent.childCount - 1; i >= 0; i--)
{
Destroy(parent.GetChild(i).gameObject);
}
}
}
}