- UIModeController: leftAlt 키로 커서 표시/게임플레이 입력 차단 토글 (공용 싱글톤) - DebugPanelUI: 보스 스킬 강제 발동 섹션 추가 (드롭다운 + 발동/취소 버튼) - 에디터에서 Data/Skills의 보스 이름 기반 스킬 검색, 빌드에서 패턴 슬롯 fallback - BossCombatBehaviorContext.GetAllPatternSkills() 추가 (디버그용 스킬 목록 수집) - TMP Settings에 한글 폰트(MaruBuri)를 fallback으로 등록 - 젬/패시브/디버그 토글 버튼을 우측 하단에 수직 정렬 - InputSystem에 UIMode 액션(leftAlt) 추가
938 lines
37 KiB
C#
938 lines
37 KiB
C#
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using TMPro;
|
|
|
|
using Unity.Netcode;
|
|
|
|
using Colosseum.Enemy;
|
|
using Colosseum.Abnormalities;
|
|
using Colosseum.AI;
|
|
using Colosseum.Skills;
|
|
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
namespace Colosseum.UI
|
|
{
|
|
/// <summary>
|
|
/// 런타임 디버그 패널 UI.
|
|
/// 호스트(서버)에서 보스 HP, 페이즈, 보호막 등을 조작할 수 있는 접이식 HUD 패널입니다.
|
|
/// </summary>
|
|
public class DebugPanelUI : MonoBehaviour
|
|
{
|
|
[Header("Abnormality Data (Runtime)")]
|
|
[Tooltip("기절 이상상태 데이터")]
|
|
[SerializeField] private AbnormalityData stunAbnormalityData;
|
|
[Tooltip("침묵 이상상태 데이터")]
|
|
[SerializeField] private AbnormalityData silenceAbnormalityData;
|
|
|
|
[Header("Settings")]
|
|
[Tooltip("패널 너비")]
|
|
[SerializeField] private float panelWidth = 280f;
|
|
[Tooltip("패널 최대 높이")]
|
|
[SerializeField] private float panelMaxHeight = 500f;
|
|
|
|
// 보스 캐시
|
|
private BossEnemy cachedBoss;
|
|
private bool suppressSliderCallback;
|
|
|
|
// 스킬 강제 발동
|
|
private TMP_Dropdown skillDropdown;
|
|
private List<SkillData> debugSkillList;
|
|
private SkillController debugSkillController;
|
|
private BossEnemy cachedBossForSkillDropdown;
|
|
|
|
// UI 참조
|
|
private GameObject toggleButtonObject;
|
|
private GameObject panelRoot;
|
|
private bool isPanelOpen;
|
|
|
|
// 보스 HP UI
|
|
private TMP_Text hpInfoText;
|
|
private Slider hpSlider;
|
|
private Image hpSliderFillImage;
|
|
private TMP_InputField hpInputField;
|
|
|
|
// 보스 제어 UI
|
|
private TMP_InputField phaseInputField;
|
|
|
|
// 보호막 UI
|
|
private TMP_InputField shieldAmountField;
|
|
|
|
private void Awake()
|
|
{
|
|
BuildUI();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
if (panelRoot != null)
|
|
panelRoot.SetActive(false);
|
|
|
|
if (UIModeController.Instance != null)
|
|
UIModeController.Instance.OnUIModeChanged += OnUIModeChanged;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (UIModeController.Instance != null)
|
|
UIModeController.Instance.OnUIModeChanged -= OnUIModeChanged;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
RefreshBoss();
|
|
if (cachedBoss == null)
|
|
return;
|
|
|
|
UpdateHPDisplay();
|
|
RefreshSkillDropdownIfNeeded();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 보스 참조 새로고침
|
|
/// </summary>
|
|
private void RefreshBoss()
|
|
{
|
|
if (cachedBoss != null && cachedBoss.gameObject.activeInHierarchy)
|
|
return;
|
|
|
|
cachedBoss = BossEnemy.ActiveBoss;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 보스 없음 여부 반환
|
|
/// </summary>
|
|
private bool NoBoss => cachedBoss == null;
|
|
|
|
/// <summary>
|
|
/// 서버 권한 확인
|
|
/// </summary>
|
|
private bool IsHost => NetworkManager.Singleton != null && NetworkManager.Singleton.IsServer;
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// UI 빌드
|
|
// ──────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 전체 UI 트리 생성
|
|
/// </summary>
|
|
private void BuildUI()
|
|
{
|
|
BuildToggleButton();
|
|
BuildPanel();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 토글 버튼 생성 (우측 하단 고정)
|
|
/// </summary>
|
|
private void BuildToggleButton()
|
|
{
|
|
toggleButtonObject = new GameObject("DebugToggle",
|
|
typeof(RectTransform), typeof(Image), typeof(Button));
|
|
toggleButtonObject.transform.SetParent(transform, false);
|
|
|
|
RectTransform r = toggleButtonObject.GetComponent<RectTransform>();
|
|
r.anchorMin = new Vector2(1f, 0f);
|
|
r.anchorMax = new Vector2(1f, 0f);
|
|
r.pivot = new Vector2(1f, 0f);
|
|
r.anchoredPosition = new Vector2(-10f, 10f);
|
|
r.sizeDelta = new Vector2(80f, 36f);
|
|
|
|
toggleButtonObject.GetComponent<Image>().color = new Color(0.15f, 0.15f, 0.15f, 0.92f);
|
|
|
|
TMP_Text label = MakeTextChild("Label", toggleButtonObject.transform);
|
|
label.text = "Debug";
|
|
label.fontSize = 16f;
|
|
label.fontStyle = FontStyles.Bold;
|
|
label.alignment = TextAlignmentOptions.Center;
|
|
label.color = new Color(0.8f, 0.8f, 0.8f);
|
|
|
|
toggleButtonObject.GetComponent<Button>().onClick.AddListener(TogglePanel);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 메인 패널 생성
|
|
/// </summary>
|
|
private void BuildPanel()
|
|
{
|
|
panelRoot = new GameObject("Panel",
|
|
typeof(RectTransform), typeof(Image), typeof(Mask), typeof(ScrollRect));
|
|
panelRoot.transform.SetParent(transform, false);
|
|
|
|
RectTransform pr = panelRoot.GetComponent<RectTransform>();
|
|
pr.anchorMin = new Vector2(1f, 0f);
|
|
pr.anchorMax = new Vector2(1f, 0f);
|
|
pr.pivot = new Vector2(1f, 0f);
|
|
pr.anchoredPosition = new Vector2(-10f, 56f);
|
|
pr.sizeDelta = new Vector2(panelWidth, panelMaxHeight);
|
|
|
|
panelRoot.GetComponent<Image>().color = new Color(0.08f, 0.08f, 0.08f, 0.92f);
|
|
panelRoot.GetComponent<Mask>().showMaskGraphic = true;
|
|
|
|
ScrollRect scroll = panelRoot.GetComponent<ScrollRect>();
|
|
scroll.horizontal = false;
|
|
scroll.movementType = ScrollRect.MovementType.Clamped;
|
|
scroll.scrollSensitivity = 30f;
|
|
|
|
// 뷰포트
|
|
GameObject viewport = new GameObject("Viewport",
|
|
typeof(RectTransform), typeof(Image));
|
|
viewport.transform.SetParent(panelRoot.transform, false);
|
|
|
|
RectTransform vr = viewport.GetComponent<RectTransform>();
|
|
vr.anchorMin = Vector2.zero;
|
|
vr.anchorMax = Vector2.one;
|
|
vr.sizeDelta = Vector2.zero;
|
|
viewport.GetComponent<Image>().color = Color.clear;
|
|
scroll.viewport = vr;
|
|
|
|
// 콘텐츠
|
|
GameObject content = new GameObject("Content",
|
|
typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
|
|
content.transform.SetParent(viewport.transform, false);
|
|
|
|
RectTransform cr = content.GetComponent<RectTransform>();
|
|
cr.anchorMin = new Vector2(0f, 1f);
|
|
cr.anchorMax = new Vector2(1f, 1f);
|
|
cr.pivot = new Vector2(0.5f, 1f);
|
|
cr.sizeDelta = Vector2.zero;
|
|
scroll.content = cr;
|
|
|
|
VerticalLayoutGroup vlg = content.GetComponent<VerticalLayoutGroup>();
|
|
vlg.childAlignment = TextAnchor.UpperLeft;
|
|
vlg.childControlWidth = true;
|
|
vlg.childControlHeight = true;
|
|
vlg.childForceExpandWidth = true;
|
|
vlg.childForceExpandHeight = false;
|
|
vlg.padding = new RectOffset(8, 8, 8, 8);
|
|
vlg.spacing = 6f;
|
|
|
|
content.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
// 섹션들
|
|
BuildBossHPSection(content.transform);
|
|
BuildBossControlSection(content.transform);
|
|
BuildShieldSection(content.transform);
|
|
BuildAbnormalitySection(content.transform);
|
|
BuildSkillForceSection(content.transform);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 보스 HP 섹션
|
|
/// </summary>
|
|
private void BuildBossHPSection(Transform parent)
|
|
{
|
|
MakeSectionHeader("보스 HP", parent);
|
|
|
|
hpInfoText = MakeLabel("HP: ---", parent, 15f);
|
|
|
|
hpSlider = MakeSlider("HPSlider", parent, out hpSliderFillImage);
|
|
hpSlider.onValueChanged.AddListener(OnHPSliderChanged);
|
|
|
|
// 직접 HP 입력
|
|
GameObject hpRow = MakeRow(parent);
|
|
hpInputField = MakeInputField("HPInput", hpRow.transform, "0", 100f);
|
|
MakeButton("적용", hpRow.transform, ApplyDirectHP, 60f);
|
|
|
|
// 프리셋
|
|
GameObject presetRow = MakeRow(parent);
|
|
MakeButton("10%", presetRow.transform, () => SetBossHP(0.1f), 55f);
|
|
MakeButton("30%", presetRow.transform, () => SetBossHP(0.3f), 55f);
|
|
MakeButton("50%", presetRow.transform, () => SetBossHP(0.5f), 55f);
|
|
MakeButton("100%", presetRow.transform, () => SetBossHP(1f), 55f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 보스 제어 섹션
|
|
/// </summary>
|
|
private void BuildBossControlSection(Transform parent)
|
|
{
|
|
MakeSectionHeader("보스 제어", parent);
|
|
|
|
GameObject phaseRow = MakeRow(parent);
|
|
MakeLabel("페이즈:", phaseRow.transform, 14f, 50f);
|
|
phaseInputField = MakeInputField("PhaseInput", phaseRow.transform, "0", 40f);
|
|
MakeButton("전환", phaseRow.transform, OnForcePhase, 60f);
|
|
|
|
MakeButton("현재 페이즈 재시작", parent, OnRestartPhase);
|
|
MakeButton("리스폰", parent, OnRespawn);
|
|
MakeButton("HP 풀회복", parent, OnFullHeal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 보호막 섹션
|
|
/// </summary>
|
|
private void BuildShieldSection(Transform parent)
|
|
{
|
|
MakeSectionHeader("보호막", parent);
|
|
|
|
GameObject row = MakeRow(parent);
|
|
MakeLabel("량:", row.transform, 14f, 30f);
|
|
shieldAmountField = MakeInputField("ShieldAmt", row.transform, "1000", 60f);
|
|
MakeButton("적용", row.transform, OnApplyShield, 60f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이상상태 섹션
|
|
/// </summary>
|
|
private void BuildAbnormalitySection(Transform parent)
|
|
{
|
|
MakeSectionHeader("이상상태", parent);
|
|
|
|
MakeButton("기절", parent, OnApplyStun);
|
|
MakeButton("침묵", parent, OnApplySilence);
|
|
MakeButton("위협 초기화", parent, OnClearThreat);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스킬 강제 발동 섹션
|
|
/// </summary>
|
|
private void BuildSkillForceSection(Transform parent)
|
|
{
|
|
MakeSectionHeader("스킬 강제 발동", parent);
|
|
|
|
skillDropdown = MakeDropdown("SkillDropdown", parent);
|
|
|
|
GameObject row = MakeRow(parent);
|
|
MakeButton("발동", row.transform, OnForceSkill, 80f);
|
|
MakeButton("취소", row.transform, OnCancelSkill, 80f);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// UI 업데이트
|
|
// ──────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// HP 디스플레이 갱신
|
|
/// </summary>
|
|
private void UpdateHPDisplay()
|
|
{
|
|
if (hpInfoText == null || hpSlider == null)
|
|
return;
|
|
|
|
float current = cachedBoss.CurrentHealth;
|
|
float max = cachedBoss.MaxHealth;
|
|
float pct = max > 0f ? current / max : 0f;
|
|
|
|
hpInfoText.text = $"HP: {Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)} ({pct * 100f:F1}%)";
|
|
|
|
suppressSliderCallback = true;
|
|
hpSlider.value = pct;
|
|
suppressSliderCallback = false;
|
|
|
|
if (hpSliderFillImage != null)
|
|
hpSliderFillImage.color = GetHealthColor(pct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// HP 퍼센트에 따른 색상
|
|
/// </summary>
|
|
private static Color GetHealthColor(float pct)
|
|
{
|
|
if (pct > 0.6f) return new Color(0.2f, 0.8f, 0.2f);
|
|
if (pct > 0.3f) return new Color(0.9f, 0.7f, 0.1f);
|
|
return new Color(0.9f, 0.2f, 0.2f);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// 보스 조작
|
|
// ──────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 보스 HP를 퍼센트로 설정
|
|
/// </summary>
|
|
private void SetBossHP(float percent)
|
|
{
|
|
if (!IsHost || NoBoss) return;
|
|
|
|
float targetHP = cachedBoss.MaxHealth * percent;
|
|
float diff = cachedBoss.CurrentHealth - targetHP;
|
|
|
|
if (diff > 0f)
|
|
cachedBoss.TakeDamage(diff);
|
|
else if (diff < 0f)
|
|
cachedBoss.Heal(-diff);
|
|
}
|
|
|
|
private void OnHPSliderChanged(float value)
|
|
{
|
|
if (suppressSliderCallback) return;
|
|
SetBossHP(value);
|
|
}
|
|
|
|
private void ApplyDirectHP()
|
|
{
|
|
if (NoBoss || !IsHost) return;
|
|
if (float.TryParse(hpInputField.text, out float hp))
|
|
{
|
|
float clamped = Mathf.Clamp(hp, 0f, cachedBoss.MaxHealth);
|
|
float pct = cachedBoss.MaxHealth > 0f ? clamped / cachedBoss.MaxHealth : 0f;
|
|
SetBossHP(pct);
|
|
}
|
|
}
|
|
|
|
private void OnForcePhase()
|
|
{
|
|
if (NoBoss || !IsHost) return;
|
|
if (int.TryParse(phaseInputField.text, out int phase))
|
|
{
|
|
phase = Mathf.Clamp(phase, 0, Mathf.Max(0, cachedBoss.TotalPhases - 1));
|
|
cachedBoss.ForcePhaseTransition(phase);
|
|
}
|
|
}
|
|
|
|
private void OnRestartPhase()
|
|
{
|
|
if (NoBoss || !IsHost) return;
|
|
cachedBoss.RestartCurrentPhase();
|
|
}
|
|
|
|
private void OnRespawn()
|
|
{
|
|
if (NoBoss || !IsHost) return;
|
|
cachedBoss.Respawn();
|
|
}
|
|
|
|
private void OnFullHeal()
|
|
{
|
|
if (NoBoss || !IsHost) return;
|
|
cachedBoss.Heal(cachedBoss.MaxHealth);
|
|
}
|
|
|
|
private void OnApplyShield()
|
|
{
|
|
if (NoBoss || !IsHost) return;
|
|
if (float.TryParse(shieldAmountField.text, out float amt) && amt > 0f)
|
|
cachedBoss.ApplyShield(amt, 30f);
|
|
}
|
|
|
|
private void OnApplyStun()
|
|
{
|
|
if (NoBoss || !IsHost || stunAbnormalityData == null) return;
|
|
AbnormalityManager am = cachedBoss.GetComponent<AbnormalityManager>();
|
|
if (am != null) am.ApplyAbnormality(stunAbnormalityData, cachedBoss.gameObject);
|
|
}
|
|
|
|
private void OnApplySilence()
|
|
{
|
|
if (NoBoss || !IsHost || silenceAbnormalityData == null) return;
|
|
AbnormalityManager am = cachedBoss.GetComponent<AbnormalityManager>();
|
|
if (am != null) am.ApplyAbnormality(silenceAbnormalityData, cachedBoss.gameObject);
|
|
}
|
|
|
|
private void OnClearThreat()
|
|
{
|
|
if (NoBoss || !IsHost) return;
|
|
cachedBoss.ClearAllThreat();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// 스킬 강제 발동
|
|
// ──────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 보스가 변경되었으면 스킬 드롭다운을 갱신합니다.
|
|
/// </summary>
|
|
private void RefreshSkillDropdownIfNeeded()
|
|
{
|
|
if (skillDropdown == null)
|
|
return;
|
|
|
|
if (cachedBoss != cachedBossForSkillDropdown)
|
|
{
|
|
cachedBossForSkillDropdown = cachedBoss;
|
|
RebuildSkillDropdown();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 드롭다운을 갱신합니다.
|
|
/// 에디터에서는 Data/Skills에서 보스 이름이 포함된 스킬을 모두 검색하고,
|
|
/// 빌드에서는 패턴 슬롯의 스킬만 표시합니다.
|
|
/// </summary>
|
|
private void RebuildSkillDropdown()
|
|
{
|
|
debugSkillController = cachedBoss != null
|
|
? cachedBoss.GetComponent<SkillController>()
|
|
: null;
|
|
|
|
if (cachedBoss == null || debugSkillController == null)
|
|
{
|
|
skillDropdown.ClearOptions();
|
|
skillDropdown.options.Add(new TMP_Dropdown.OptionData("보스 없음"));
|
|
skillDropdown.value = 0;
|
|
debugSkillList = null;
|
|
return;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
debugSkillList = LoadSkillsFromAssetFolder();
|
|
#else
|
|
debugSkillList = LoadSkillsFromPatternSlots();
|
|
#endif
|
|
|
|
if (debugSkillList == null || debugSkillList.Count == 0)
|
|
{
|
|
skillDropdown.ClearOptions();
|
|
skillDropdown.options.Add(new TMP_Dropdown.OptionData("스킬 없음"));
|
|
skillDropdown.value = 0;
|
|
return;
|
|
}
|
|
|
|
List<TMP_Dropdown.OptionData> options = new List<TMP_Dropdown.OptionData>();
|
|
for (int i = 0; i < debugSkillList.Count; i++)
|
|
{
|
|
string name = debugSkillList[i].SkillName;
|
|
options.Add(new TMP_Dropdown.OptionData(string.IsNullOrEmpty(name) ? $"Skill {i}" : name));
|
|
}
|
|
|
|
skillDropdown.ClearOptions();
|
|
skillDropdown.AddOptions(options);
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// 에디터 전용: Data/Skills에서 보스 이름이 포함된 SkillData를 모두 검색합니다.
|
|
/// </summary>
|
|
private List<SkillData> LoadSkillsFromAssetFolder()
|
|
{
|
|
string bossName = cachedBoss.gameObject.name;
|
|
string[] guids = AssetDatabase.FindAssets($"t:SkillData", new[] { "Assets/_Game/Data/Skills" });
|
|
|
|
List<SkillData> result = new List<SkillData>();
|
|
for (int i = 0; i < guids.Length; i++)
|
|
{
|
|
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
|
// 파일명에 보스 이름이 포함된 스킬만 필터링
|
|
if (path.Contains(bossName))
|
|
{
|
|
SkillData skill = AssetDatabase.LoadAssetAtPath<SkillData>(path);
|
|
if (skill != null)
|
|
result.Add(skill);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// 패턴 슬롯에서 고유 스킬을 수집합니다 (빌드용 fallback).
|
|
/// </summary>
|
|
private List<SkillData> LoadSkillsFromPatternSlots()
|
|
{
|
|
BossCombatBehaviorContext context = cachedBoss.GetComponent<BossCombatBehaviorContext>();
|
|
if (context == null)
|
|
return null;
|
|
|
|
return context.GetAllPatternSkills();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 드롭다운에서 선택한 스킬을 강제 발동합니다.
|
|
/// </summary>
|
|
private void OnForceSkill()
|
|
{
|
|
if (!IsHost || NoBoss || debugSkillController == null || debugSkillList == null)
|
|
return;
|
|
|
|
int index = skillDropdown.value;
|
|
if (index < 0 || index >= debugSkillList.Count)
|
|
return;
|
|
|
|
if (debugSkillController.IsExecutingSkill)
|
|
debugSkillController.CancelSkill();
|
|
|
|
debugSkillController.ResetAllCooldowns();
|
|
debugSkillController.ExecuteSkill(debugSkillList[index]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 실행 중인 스킬을 취소합니다.
|
|
/// </summary>
|
|
private void OnCancelSkill()
|
|
{
|
|
if (!IsHost || NoBoss || debugSkillController == null)
|
|
return;
|
|
|
|
if (debugSkillController.IsExecutingSkill)
|
|
debugSkillController.CancelSkill();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// 토글
|
|
// ──────────────────────────────────────────────────
|
|
|
|
private void TogglePanel()
|
|
{
|
|
if (UIModeController.Instance != null)
|
|
UIModeController.Instance.SetUIModeActive(!UIModeController.Instance.IsUIModeActive);
|
|
}
|
|
|
|
private void OnUIModeChanged(bool uiModeActive)
|
|
{
|
|
if (panelRoot != null)
|
|
panelRoot.SetActive(uiModeActive);
|
|
isPanelOpen = uiModeActive;
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// UI 유틸리티
|
|
// ──────────────────────────────────────────────────
|
|
|
|
private static TMP_FontAsset DefaultFont => TMP_Settings.defaultFontAsset;
|
|
|
|
private static void MakeSectionHeader(string text, Transform parent)
|
|
{
|
|
TMP_Text h = MakeLabel(text, parent, 16f);
|
|
h.fontStyle = FontStyles.Bold;
|
|
h.color = new Color(0.9f, 0.9f, 0.9f);
|
|
}
|
|
|
|
private static TMP_Text MakeLabel(string text, Transform parent, float fontSize, float width = 0f)
|
|
{
|
|
GameObject go = new GameObject("Label", typeof(RectTransform), typeof(TextMeshProUGUI));
|
|
go.transform.SetParent(parent, false);
|
|
|
|
if (width > 0f)
|
|
{
|
|
LayoutElement le = go.AddComponent<LayoutElement>();
|
|
le.preferredWidth = width;
|
|
le.minWidth = width;
|
|
}
|
|
|
|
TMP_Text t = go.GetComponent<TextMeshProUGUI>();
|
|
t.text = text;
|
|
t.fontSize = fontSize;
|
|
t.color = new Color(0.75f, 0.75f, 0.75f);
|
|
t.alignment = TextAlignmentOptions.Left;
|
|
t.textWrappingMode = TextWrappingModes.NoWrap;
|
|
if (DefaultFont != null) t.font = DefaultFont;
|
|
|
|
return t;
|
|
}
|
|
|
|
private static Button MakeButton(string text, Transform parent, Action onClick, float width = 0f)
|
|
{
|
|
GameObject go = new GameObject("Btn_" + text,
|
|
typeof(RectTransform), typeof(Image), typeof(Button));
|
|
go.transform.SetParent(parent, false);
|
|
|
|
LayoutElement le = go.AddComponent<LayoutElement>();
|
|
le.preferredHeight = 28f;
|
|
if (width > 0f) { le.preferredWidth = width; le.minWidth = width; }
|
|
|
|
go.GetComponent<Image>().color = new Color(0.18f, 0.18f, 0.18f, 1f);
|
|
go.GetComponent<Button>().onClick.AddListener(new UnityEngine.Events.UnityAction(onClick));
|
|
|
|
// 버튼 내 텍스트
|
|
TMP_Text lbl = new GameObject("Text", typeof(RectTransform), typeof(TextMeshProUGUI))
|
|
.GetComponent<TextMeshProUGUI>();
|
|
lbl.transform.SetParent(go.transform, false);
|
|
RectTransform lr = lbl.GetComponent<RectTransform>();
|
|
lr.anchorMin = Vector2.zero;
|
|
lr.anchorMax = Vector2.one;
|
|
lr.sizeDelta = Vector2.zero;
|
|
lbl.text = text;
|
|
lbl.fontSize = 14f;
|
|
lbl.alignment = TextAlignmentOptions.Center;
|
|
lbl.color = new Color(0.85f, 0.85f, 0.85f);
|
|
lbl.textWrappingMode = TextWrappingModes.NoWrap;
|
|
if (DefaultFont != null) lbl.font = DefaultFont;
|
|
|
|
return go.GetComponent<Button>();
|
|
}
|
|
|
|
private static GameObject MakeRow(Transform parent)
|
|
{
|
|
GameObject go = new GameObject("Row", typeof(RectTransform), typeof(HorizontalLayoutGroup));
|
|
go.transform.SetParent(parent, false);
|
|
|
|
HorizontalLayoutGroup hlg = go.GetComponent<HorizontalLayoutGroup>();
|
|
hlg.childAlignment = TextAnchor.MiddleLeft;
|
|
hlg.childControlWidth = true;
|
|
hlg.childControlHeight = true;
|
|
hlg.childForceExpandWidth = false;
|
|
hlg.childForceExpandHeight = false;
|
|
hlg.spacing = 4f;
|
|
|
|
go.AddComponent<LayoutElement>().preferredHeight = 28f;
|
|
|
|
return go;
|
|
}
|
|
|
|
private static Slider MakeSlider(string name, Transform parent, out Image fillImage)
|
|
{
|
|
GameObject go = new GameObject(name, typeof(RectTransform), typeof(Slider));
|
|
go.transform.SetParent(parent, false);
|
|
|
|
go.AddComponent<LayoutElement>().preferredHeight = 24f;
|
|
|
|
Slider slider = go.GetComponent<Slider>();
|
|
slider.minValue = 0f;
|
|
slider.maxValue = 1f;
|
|
slider.value = 1f;
|
|
|
|
// 배경
|
|
GameObject bg = new GameObject("BG", typeof(RectTransform), typeof(Image));
|
|
bg.transform.SetParent(go.transform, false);
|
|
RectTransform bgr = bg.GetComponent<RectTransform>();
|
|
bgr.anchorMin = new Vector2(0f, 0.25f);
|
|
bgr.anchorMax = new Vector2(1f, 0.75f);
|
|
bgr.sizeDelta = Vector2.zero;
|
|
bg.GetComponent<Image>().color = new Color(0.2f, 0.2f, 0.2f, 1f);
|
|
slider.targetGraphic = bg.GetComponent<Image>();
|
|
|
|
// Fill Area + Fill
|
|
GameObject fa = new GameObject("Fill Area", typeof(RectTransform));
|
|
fa.transform.SetParent(go.transform, false);
|
|
RectTransform far = fa.GetComponent<RectTransform>();
|
|
far.anchorMin = Vector2.zero;
|
|
far.anchorMax = Vector2.one;
|
|
far.sizeDelta = Vector2.zero;
|
|
|
|
GameObject fill = new GameObject("Fill", typeof(RectTransform), typeof(Image));
|
|
fill.transform.SetParent(fa.transform, false);
|
|
RectTransform fr = fill.GetComponent<RectTransform>();
|
|
fr.anchorMin = Vector2.zero;
|
|
fr.anchorMax = Vector2.one;
|
|
fr.sizeDelta = Vector2.zero;
|
|
fillImage = fill.GetComponent<Image>();
|
|
fillImage.color = new Color(0.2f, 0.8f, 0.2f);
|
|
slider.fillRect = fr;
|
|
|
|
// Handle Slide Area + Handle
|
|
GameObject hsa = new GameObject("Handle Slide Area", typeof(RectTransform));
|
|
hsa.transform.SetParent(go.transform, false);
|
|
RectTransform hsar = hsa.GetComponent<RectTransform>();
|
|
hsar.anchorMin = Vector2.zero;
|
|
hsar.anchorMax = Vector2.one;
|
|
hsar.offsetMin = new Vector2(10f, 0f);
|
|
hsar.offsetMax = new Vector2(-10f, 0f);
|
|
|
|
GameObject handle = new GameObject("Handle", typeof(RectTransform), typeof(Image));
|
|
handle.transform.SetParent(hsa.transform, false);
|
|
handle.GetComponent<RectTransform>().sizeDelta = new Vector2(16f, 0f);
|
|
handle.GetComponent<Image>().color = Color.white;
|
|
slider.handleRect = handle.GetComponent<RectTransform>();
|
|
|
|
return slider;
|
|
}
|
|
|
|
private static TMP_InputField MakeInputField(string name, Transform parent,
|
|
string placeholder, float width)
|
|
{
|
|
GameObject go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(TMP_InputField));
|
|
go.transform.SetParent(parent, false);
|
|
|
|
LayoutElement le = go.AddComponent<LayoutElement>();
|
|
le.preferredWidth = width;
|
|
le.minWidth = width;
|
|
le.preferredHeight = 28f;
|
|
|
|
Image bg = go.GetComponent<Image>();
|
|
bg.color = new Color(0.12f, 0.12f, 0.12f, 1f);
|
|
|
|
TMP_InputField input = go.GetComponent<TMP_InputField>();
|
|
input.targetGraphic = bg;
|
|
|
|
// Text
|
|
TMP_Text txt = new GameObject("Text", typeof(RectTransform), typeof(TextMeshProUGUI))
|
|
.GetComponent<TextMeshProUGUI>();
|
|
txt.transform.SetParent(go.transform, false);
|
|
txt.GetComponent<RectTransform>().anchorMin = Vector2.zero;
|
|
txt.GetComponent<RectTransform>().anchorMax = Vector2.one;
|
|
txt.GetComponent<RectTransform>().offsetMin = new Vector2(4f, 2f);
|
|
txt.GetComponent<RectTransform>().offsetMax = new Vector2(-4f, -2f);
|
|
txt.fontSize = 14f;
|
|
txt.alignment = TextAlignmentOptions.MidlineLeft;
|
|
txt.color = Color.white;
|
|
txt.textWrappingMode = TextWrappingModes.NoWrap;
|
|
if (DefaultFont != null) txt.font = DefaultFont;
|
|
input.textComponent = txt;
|
|
|
|
// Placeholder
|
|
TMP_Text ph = new GameObject("Placeholder", typeof(RectTransform), typeof(TextMeshProUGUI))
|
|
.GetComponent<TextMeshProUGUI>();
|
|
ph.transform.SetParent(go.transform, false);
|
|
ph.GetComponent<RectTransform>().anchorMin = Vector2.zero;
|
|
ph.GetComponent<RectTransform>().anchorMax = Vector2.one;
|
|
ph.GetComponent<RectTransform>().offsetMin = new Vector2(4f, 2f);
|
|
ph.GetComponent<RectTransform>().offsetMax = new Vector2(-4f, -2f);
|
|
ph.fontSize = 14f;
|
|
ph.alignment = TextAlignmentOptions.MidlineLeft;
|
|
ph.color = new Color(0.5f, 0.5f, 0.5f);
|
|
ph.text = placeholder;
|
|
ph.textWrappingMode = TextWrappingModes.NoWrap;
|
|
if (DefaultFont != null) ph.font = DefaultFont;
|
|
input.placeholder = ph;
|
|
|
|
return input;
|
|
}
|
|
|
|
private static TMP_Text MakeTextChild(string name, Transform parent)
|
|
{
|
|
TMP_Text t = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI))
|
|
.GetComponent<TextMeshProUGUI>();
|
|
t.transform.SetParent(parent, false);
|
|
t.GetComponent<RectTransform>().anchorMin = Vector2.zero;
|
|
t.GetComponent<RectTransform>().anchorMax = Vector2.one;
|
|
t.GetComponent<RectTransform>().sizeDelta = Vector2.zero;
|
|
if (DefaultFont != null) t.font = DefaultFont;
|
|
return t;
|
|
}
|
|
|
|
private static TMP_Dropdown MakeDropdown(string name, Transform parent)
|
|
{
|
|
// 메인 드롭다운 오브젝트
|
|
GameObject dropdownGo = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(TMP_Dropdown));
|
|
dropdownGo.transform.SetParent(parent, false);
|
|
dropdownGo.AddComponent<LayoutElement>().preferredHeight = 30f;
|
|
|
|
// 캡션 라벨
|
|
GameObject captionGo = new GameObject("Caption", typeof(RectTransform), typeof(TextMeshProUGUI));
|
|
captionGo.transform.SetParent(dropdownGo.transform, false);
|
|
RectTransform captionRt = captionGo.GetComponent<RectTransform>();
|
|
captionRt.anchorMin = Vector2.zero;
|
|
captionRt.anchorMax = Vector2.one;
|
|
captionRt.offsetMin = new Vector2(10f, 1f);
|
|
captionRt.offsetMax = new Vector2(-25f, -2f);
|
|
TMP_Text captionText = captionGo.GetComponent<TextMeshProUGUI>();
|
|
captionText.text = "스킬 선택";
|
|
captionText.fontSize = 14f;
|
|
captionText.alignment = TextAlignmentOptions.MidlineLeft;
|
|
captionText.color = Color.white;
|
|
captionText.textWrappingMode = TextWrappingModes.NoWrap;
|
|
if (DefaultFont != null) captionText.font = DefaultFont;
|
|
|
|
// 화살표
|
|
GameObject arrowGo = new GameObject("Arrow", typeof(RectTransform), typeof(Image));
|
|
arrowGo.transform.SetParent(dropdownGo.transform, false);
|
|
RectTransform arrowRt = arrowGo.GetComponent<RectTransform>();
|
|
arrowRt.anchorMin = new Vector2(1f, 0.5f);
|
|
arrowRt.anchorMax = new Vector2(1f, 0.5f);
|
|
arrowRt.sizeDelta = new Vector2(20f, 20f);
|
|
arrowRt.pivot = new Vector2(0.5f, 0.5f);
|
|
arrowRt.anchoredPosition = new Vector2(-15f, 0f);
|
|
arrowGo.GetComponent<Image>().color = new Color(0.6f, 0.6f, 0.6f);
|
|
|
|
// 템플릿 (팝업 목록)
|
|
GameObject templateGo = new GameObject("Template", typeof(RectTransform), typeof(Image), typeof(ScrollRect));
|
|
templateGo.transform.SetParent(dropdownGo.transform, false);
|
|
templateGo.SetActive(false);
|
|
RectTransform templateRt = templateGo.GetComponent<RectTransform>();
|
|
templateRt.anchorMin = new Vector2(0f, -1f);
|
|
templateRt.anchorMax = new Vector2(1f, 0f);
|
|
templateRt.sizeDelta = new Vector2(0f, 150f);
|
|
templateRt.pivot = new Vector2(0.5f, 1f);
|
|
templateGo.GetComponent<Image>().color = new Color(0.15f, 0.15f, 0.15f, 0.95f);
|
|
|
|
ScrollRect scroll = templateGo.GetComponent<ScrollRect>();
|
|
scroll.horizontal = false;
|
|
scroll.movementType = ScrollRect.MovementType.Clamped;
|
|
scroll.scrollSensitivity = 20f;
|
|
|
|
// 뷰포트
|
|
GameObject viewportGo = new GameObject("Viewport", typeof(RectTransform), typeof(Image));
|
|
viewportGo.transform.SetParent(templateGo.transform, false);
|
|
RectTransform viewportRt = viewportGo.GetComponent<RectTransform>();
|
|
viewportRt.anchorMin = Vector2.zero;
|
|
viewportRt.anchorMax = Vector2.one;
|
|
viewportRt.sizeDelta = Vector2.zero;
|
|
viewportRt.pivot = new Vector2(0f, 1f);
|
|
viewportGo.GetComponent<Image>().color = Color.clear;
|
|
scroll.viewport = viewportRt;
|
|
|
|
// 콘텐츠
|
|
GameObject contentGo = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
|
|
contentGo.transform.SetParent(viewportGo.transform, false);
|
|
RectTransform contentRt = contentGo.GetComponent<RectTransform>();
|
|
contentRt.anchorMin = new Vector2(0f, 1f);
|
|
contentRt.anchorMax = new Vector2(1f, 1f);
|
|
contentRt.pivot = new Vector2(0.5f, 1f);
|
|
contentRt.sizeDelta = Vector2.zero;
|
|
contentRt.anchoredPosition = Vector2.zero;
|
|
|
|
VerticalLayoutGroup vlg = contentGo.GetComponent<VerticalLayoutGroup>();
|
|
vlg.childAlignment = TextAnchor.MiddleLeft;
|
|
vlg.childControlWidth = true;
|
|
vlg.childControlHeight = true;
|
|
vlg.childForceExpandWidth = true;
|
|
vlg.childForceExpandHeight = false;
|
|
contentGo.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
scroll.content = contentRt;
|
|
|
|
// 아이템 템플릿
|
|
GameObject itemGo = new GameObject("Item", typeof(RectTransform), typeof(Toggle));
|
|
itemGo.transform.SetParent(contentGo.transform, false);
|
|
RectTransform itemRt = itemGo.GetComponent<RectTransform>();
|
|
itemRt.anchorMin = new Vector2(0f, 0.5f);
|
|
itemRt.anchorMax = new Vector2(1f, 0.5f);
|
|
itemRt.sizeDelta = new Vector2(0f, 20f);
|
|
|
|
LayoutElement itemLe = itemGo.AddComponent<LayoutElement>();
|
|
itemLe.preferredHeight = 20f;
|
|
itemLe.minHeight = 20f;
|
|
|
|
// 아이템 배경
|
|
GameObject itemBgGo = new GameObject("Item Background", typeof(RectTransform), typeof(Image));
|
|
itemBgGo.transform.SetParent(itemGo.transform, false);
|
|
RectTransform itemBgRt = itemBgGo.GetComponent<RectTransform>();
|
|
itemBgRt.anchorMin = Vector2.zero;
|
|
itemBgRt.anchorMax = Vector2.one;
|
|
itemBgRt.sizeDelta = Vector2.zero;
|
|
itemBgGo.GetComponent<Image>().color = new Color(0.25f, 0.25f, 0.25f, 1f);
|
|
|
|
// 아이템 체크마크
|
|
GameObject checkGo = new GameObject("Item Checkmark", typeof(RectTransform), typeof(Image));
|
|
checkGo.transform.SetParent(itemGo.transform, false);
|
|
RectTransform checkRt = checkGo.GetComponent<RectTransform>();
|
|
checkRt.anchorMin = new Vector2(0f, 0.5f);
|
|
checkRt.anchorMax = new Vector2(0f, 0.5f);
|
|
checkRt.sizeDelta = new Vector2(20f, 20f);
|
|
checkRt.pivot = new Vector2(0.5f, 0.5f);
|
|
checkRt.anchoredPosition = new Vector2(10f, 0f);
|
|
|
|
// 아이템 라벨
|
|
GameObject itemLabelGo = new GameObject("Item Label", typeof(RectTransform), typeof(TextMeshProUGUI));
|
|
itemLabelGo.transform.SetParent(itemGo.transform, false);
|
|
RectTransform itemLabelRt = itemLabelGo.GetComponent<RectTransform>();
|
|
itemLabelRt.anchorMin = Vector2.zero;
|
|
itemLabelRt.anchorMax = Vector2.one;
|
|
itemLabelRt.offsetMin = new Vector2(20f, 1f);
|
|
itemLabelRt.offsetMax = new Vector2(-5f, -2f);
|
|
TMP_Text itemLabelText = itemLabelGo.GetComponent<TextMeshProUGUI>();
|
|
itemLabelText.fontSize = 14f;
|
|
itemLabelText.alignment = TextAlignmentOptions.MidlineLeft;
|
|
itemLabelText.color = Color.white;
|
|
itemLabelText.textWrappingMode = TextWrappingModes.NoWrap;
|
|
if (DefaultFont != null) itemLabelText.font = DefaultFont;
|
|
|
|
// 토글 연결
|
|
Toggle toggle = itemGo.GetComponent<Toggle>();
|
|
toggle.targetGraphic = itemBgGo.GetComponent<Image>();
|
|
toggle.graphic = checkGo.GetComponent<Image>();
|
|
toggle.isOn = false;
|
|
|
|
// 드롭다운 연결
|
|
TMP_Dropdown dropdown = dropdownGo.GetComponent<TMP_Dropdown>();
|
|
dropdown.targetGraphic = dropdownGo.GetComponent<Image>();
|
|
dropdown.captionText = captionText;
|
|
dropdown.itemText = itemLabelText;
|
|
dropdown.template = templateRt;
|
|
|
|
return dropdown;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|