- 수성바탕체와 마루 부리 기반 UI 폰트 규칙, TMP 에셋 생성, 일괄 적용용 에디터 도구를 추가하고 로비 빌더도 같은 규칙을 따르도록 정리 - 기존 UI 프리팹과 Lobby/Test 씬의 TMP 폰트를 역할별로 교체하고 강조 텍스트와 HUD 계층에 맞는 자간을 반영 - 넥슨 Lv.2 고딕 검토 후 제외하고 액션바, HP/MP, 보스 체력바 숫자와 라벨을 마루 부리 기준으로 재조정 - Assets/_Game/Fonts 경로에 원본 폰트와 TMP 에셋을 정리하고 공유 Obsidian Vault 경로를 AGENTS에 기록 - Unity 리프레시와 HUD 보정 적용 후 콘솔 경고/에러 없는 상태를 확인
421 lines
20 KiB
C#
421 lines
20 KiB
C#
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using TMPro;
|
|
using Unity.Netcode;
|
|
using Unity.Netcode.Transports.UTP;
|
|
|
|
namespace Colosseum.Editor
|
|
{
|
|
public static class LobbySceneBuilder
|
|
{
|
|
[MenuItem("Colosseum/Build Lobby Scene")]
|
|
public static void Build()
|
|
{
|
|
// ── 씬 생성 ──────────────────────────────────────────
|
|
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
|
|
// ── NetworkManager ───────────────────────────────────
|
|
var nmGO = new GameObject("NetworkManager");
|
|
var nm = nmGO.AddComponent<NetworkManager>();
|
|
var transport = nmGO.AddComponent<UnityTransport>();
|
|
nm.NetworkConfig = new NetworkConfig
|
|
{
|
|
NetworkTransport = transport
|
|
};
|
|
|
|
// ── Network Prefabs 등록 ──────────────────────────────
|
|
var playerPrefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/_Game/Prefabs/Player/Prefab_Player_Default.prefab");
|
|
if (playerPrefabAsset != null)
|
|
AddNetworkPrefab(nm, playerPrefabAsset);
|
|
else
|
|
Debug.LogWarning("[LobbySceneBuilder] Player prefab not found at expected path.");
|
|
|
|
// ── LobbyManager ─────────────────────────────────────
|
|
var lmGO = new GameObject("LobbyManager");
|
|
lmGO.AddComponent<Unity.Netcode.NetworkObject>();
|
|
lmGO.AddComponent<Colosseum.Network.LobbyManager>();
|
|
|
|
// ── Canvas ───────────────────────────────────────────
|
|
var canvasGO = new GameObject("Canvas");
|
|
var canvas = canvasGO.AddComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvasGO.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
canvasGO.AddComponent<GraphicRaycaster>();
|
|
|
|
// EventSystem — New Input System 사용 중이므로 InputSystemUIInputModule 사용
|
|
var esGO = new GameObject("EventSystem");
|
|
esGO.AddComponent<UnityEngine.EventSystems.EventSystem>();
|
|
esGO.AddComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
|
|
|
|
// ── ConnectPanel ──────────────────────────────────────
|
|
var connectPanel = CreatePanel(canvasGO.transform, "ConnectPanel");
|
|
var connectRect = connectPanel.GetComponent<RectTransform>();
|
|
connectRect.anchorMin = Vector2.zero;
|
|
connectRect.anchorMax = Vector2.one;
|
|
connectRect.offsetMin = Vector2.zero;
|
|
connectRect.offsetMax = Vector2.zero;
|
|
|
|
var vLayout = connectPanel.AddComponent<VerticalLayoutGroup>();
|
|
vLayout.childAlignment = TextAnchor.MiddleCenter;
|
|
vLayout.spacing = 12;
|
|
vLayout.childForceExpandWidth = false;
|
|
vLayout.childForceExpandHeight = false;
|
|
connectPanel.AddComponent<ContentSizeFitter>();
|
|
|
|
CreateLabel(connectPanel.transform, "TitleLabel", "Colosseum Lobby", 36, UIFontRole.Emphasis);
|
|
var ipInput = CreateInputField(connectPanel.transform, "IpInput", "Host IP (127.0.0.1)", 300, 50);
|
|
var portInput = CreateInputField(connectPanel.transform, "PortInput", "Port (7777)", 300, 50);
|
|
var hostBtn = CreateButton(connectPanel.transform, "HostButton", "Host", 200, 50);
|
|
var joinBtn = CreateButton(connectPanel.transform, "JoinButton", "Join", 200, 50);
|
|
var statusText = CreateLabel(connectPanel.transform, "StatusText", "", 18, UIFontRole.Body);
|
|
statusText.color = Color.yellow;
|
|
|
|
// ── LobbyPanel ────────────────────────────────────────
|
|
var lobbyPanel = CreatePanel(canvasGO.transform, "LobbyPanel");
|
|
var lobbyRect = lobbyPanel.GetComponent<RectTransform>();
|
|
lobbyRect.anchorMin = Vector2.zero;
|
|
lobbyRect.anchorMax = Vector2.one;
|
|
lobbyRect.offsetMin = Vector2.zero;
|
|
lobbyRect.offsetMax = Vector2.zero;
|
|
lobbyPanel.SetActive(false);
|
|
|
|
var vLayout2 = lobbyPanel.AddComponent<VerticalLayoutGroup>();
|
|
vLayout2.childAlignment = TextAnchor.MiddleCenter;
|
|
vLayout2.spacing = 12;
|
|
vLayout2.childForceExpandWidth = false;
|
|
vLayout2.childForceExpandHeight = false;
|
|
|
|
CreateLabel(lobbyPanel.transform, "LobbyTitle", "Waiting Room", 32, UIFontRole.Emphasis);
|
|
|
|
// PlayerList: ScrollView 역할을 하는 VerticalLayout 컨테이너
|
|
var playerListGO = new GameObject("PlayerList");
|
|
playerListGO.transform.SetParent(lobbyPanel.transform, false);
|
|
var plRect = playerListGO.AddComponent<RectTransform>();
|
|
plRect.sizeDelta = new Vector2(400, 200);
|
|
var plLayout = playerListGO.AddComponent<VerticalLayoutGroup>();
|
|
plLayout.childAlignment = TextAnchor.UpperCenter;
|
|
plLayout.spacing = 8;
|
|
plLayout.childForceExpandWidth = true;
|
|
plLayout.childForceExpandHeight = false;
|
|
playerListGO.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
var readyBtn = CreateButton(lobbyPanel.transform, "ReadyButton", "준비", 200, 50);
|
|
var startBtn = CreateButton(lobbyPanel.transform, "StartButton", "게임 시작", 200, 50);
|
|
var discBtn = CreateButton(lobbyPanel.transform, "DisconnectButton", "나가기", 200, 50);
|
|
|
|
// ── PlayerSlot 프리팹 ─────────────────────────────────
|
|
var slotGO = new GameObject("PlayerSlot");
|
|
var slotRect = slotGO.AddComponent<RectTransform>();
|
|
slotRect.sizeDelta = new Vector2(380, 40);
|
|
var bg = slotGO.AddComponent<Image>();
|
|
bg.color = new Color(0.2f, 0.2f, 0.2f, 0.8f);
|
|
|
|
var slotLabel = new GameObject("Label");
|
|
slotLabel.transform.SetParent(slotGO.transform, false);
|
|
var slotLabelRect = slotLabel.AddComponent<RectTransform>();
|
|
slotLabelRect.anchorMin = Vector2.zero;
|
|
slotLabelRect.anchorMax = Vector2.one;
|
|
slotLabelRect.offsetMin = new Vector2(8, 0);
|
|
slotLabelRect.offsetMax = new Vector2(-8, 0);
|
|
var slotTmp = slotLabel.AddComponent<TextMeshProUGUI>();
|
|
slotTmp.text = "Player";
|
|
slotTmp.fontSize = 20;
|
|
slotTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
|
UIFontSetupTool.ApplyRole(slotTmp, UIFontRole.Body);
|
|
|
|
System.IO.Directory.CreateDirectory(Application.dataPath + "/_Game/Prefabs/UI");
|
|
var slotPrefab = PrefabUtility.SaveAsPrefabAsset(slotGO, "Assets/_Game/Prefabs/UI/UI_PlayerSlot.prefab");
|
|
Object.DestroyImmediate(slotGO);
|
|
|
|
// ── LobbyUI 연결 ──────────────────────────────────────
|
|
var uiGO = new GameObject("UIController");
|
|
var lobbyUI = uiGO.AddComponent<Colosseum.UI.LobbyUI>();
|
|
|
|
SetPrivateField(lobbyUI, "connectPanel", connectPanel);
|
|
SetPrivateField(lobbyUI, "ipInput", ipInput.GetComponent<TMP_InputField>());
|
|
SetPrivateField(lobbyUI, "portInput", portInput.GetComponent<TMP_InputField>());
|
|
SetPrivateField(lobbyUI, "hostButton", hostBtn.GetComponent<Button>());
|
|
SetPrivateField(lobbyUI, "joinButton", joinBtn.GetComponent<Button>());
|
|
SetPrivateField(lobbyUI, "connectStatusText", statusText);
|
|
SetPrivateField(lobbyUI, "lobbyPanel", lobbyPanel);
|
|
SetPrivateField(lobbyUI, "playerListParent", playerListGO.transform);
|
|
SetPrivateField(lobbyUI, "playerSlotPrefab", slotPrefab);
|
|
SetPrivateField(lobbyUI, "readyButton", readyBtn.GetComponent<Button>());
|
|
SetPrivateField(lobbyUI, "startButton", startBtn.GetComponent<Button>());
|
|
SetPrivateField(lobbyUI, "disconnectButton", discBtn.GetComponent<Button>());
|
|
|
|
// ── 씬 저장 ───────────────────────────────────────────
|
|
EditorSceneManager.SaveScene(scene, "Assets/Scenes/Lobby.unity");
|
|
|
|
// ── Build Settings 등록 ───────────────────────────────
|
|
AddSceneToBuildSettings("Assets/Scenes/Lobby.unity", 0);
|
|
AddSceneToBuildSettings("Assets/Scenes/Test.unity", 1);
|
|
|
|
Debug.Log("[LobbySceneBuilder] Lobby scene built successfully.");
|
|
}
|
|
|
|
[MenuItem("Colosseum/Register Network Prefabs")]
|
|
public static void RegisterNetworkPrefabs()
|
|
{
|
|
var nm = Object.FindFirstObjectByType<NetworkManager>();
|
|
if (nm == null)
|
|
{
|
|
Debug.LogError("[LobbySceneBuilder] NetworkManager not found in current scene.");
|
|
return;
|
|
}
|
|
|
|
var playerPrefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/_Game/Prefabs/Player/Prefab_Player_Default.prefab");
|
|
if (playerPrefabAsset == null)
|
|
{
|
|
Debug.LogError("[LobbySceneBuilder] Player prefab not found.");
|
|
return;
|
|
}
|
|
|
|
// NetworkPrefabsList ScriptableObject 생성 또는 로드
|
|
const string listPath = "Assets/_Game/NetworkPrefabsList.asset";
|
|
var prefabList = AssetDatabase.LoadAssetAtPath<NetworkPrefabsList>(listPath);
|
|
if (prefabList == null)
|
|
{
|
|
prefabList = ScriptableObject.CreateInstance<NetworkPrefabsList>();
|
|
AssetDatabase.CreateAsset(prefabList, listPath);
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
Debug.Log("[LobbySceneBuilder] Created NetworkPrefabsList asset.");
|
|
}
|
|
|
|
// 플레이어 프리팹을 리스트에 추가 (내부 필드명은 "List")
|
|
if (!prefabList.Contains(playerPrefabAsset))
|
|
{
|
|
prefabList.Add(new NetworkPrefab { Prefab = playerPrefabAsset });
|
|
EditorUtility.SetDirty(prefabList);
|
|
AssetDatabase.SaveAssets();
|
|
Debug.Log("[LobbySceneBuilder] Added player prefab to NetworkPrefabsList.");
|
|
}
|
|
|
|
// NetworkManager에 리스트 등록 & PlayerPrefab은 null로 (자동 스폰 방지)
|
|
var so = new SerializedObject(nm);
|
|
so.Update();
|
|
|
|
var playerPrefabProp = so.FindProperty("NetworkConfig.PlayerPrefab");
|
|
if (playerPrefabProp != null)
|
|
playerPrefabProp.objectReferenceValue = null;
|
|
|
|
var nmListsProp = so.FindProperty("NetworkConfig.Prefabs.NetworkPrefabsLists");
|
|
if (nmListsProp != null)
|
|
{
|
|
bool alreadyAdded = false;
|
|
for (int i = 0; i < nmListsProp.arraySize; i++)
|
|
{
|
|
if (nmListsProp.GetArrayElementAtIndex(i).objectReferenceValue == prefabList)
|
|
{ alreadyAdded = true; break; }
|
|
}
|
|
if (!alreadyAdded)
|
|
{
|
|
// null 항목 제거 후 추가
|
|
for (int i = nmListsProp.arraySize - 1; i >= 0; i--)
|
|
if (nmListsProp.GetArrayElementAtIndex(i).objectReferenceValue == null)
|
|
nmListsProp.DeleteArrayElementAtIndex(i);
|
|
|
|
int idx = nmListsProp.arraySize;
|
|
nmListsProp.InsertArrayElementAtIndex(idx);
|
|
nmListsProp.GetArrayElementAtIndex(idx).objectReferenceValue = prefabList;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("[LobbySceneBuilder] Could not find NetworkConfig.Prefabs.NetworkPrefabsLists property.");
|
|
}
|
|
|
|
so.ApplyModifiedProperties();
|
|
EditorSceneManager.MarkSceneDirty(nm.gameObject.scene);
|
|
EditorSceneManager.SaveScene(nm.gameObject.scene);
|
|
Debug.Log("[LobbySceneBuilder] NetworkPrefabsList registered. PlayerPrefab cleared. Scene saved.");
|
|
}
|
|
|
|
[MenuItem("Colosseum/Open Lobby Scene")]
|
|
public static void OpenLobbyScene()
|
|
{
|
|
if (EditorApplication.isPlaying)
|
|
EditorApplication.isPlaying = false;
|
|
EditorSceneManager.OpenScene("Assets/Scenes/Lobby.unity");
|
|
}
|
|
|
|
[MenuItem("Colosseum/Play Lobby Scene")]
|
|
public static void PlayLobbyScene()
|
|
{
|
|
if (!EditorApplication.isPlaying)
|
|
{
|
|
EditorSceneManager.OpenScene("Assets/Scenes/Lobby.unity");
|
|
EditorApplication.isPlaying = true;
|
|
}
|
|
}
|
|
|
|
[MenuItem("Colosseum/Stop Play")]
|
|
public static void StopPlay()
|
|
{
|
|
EditorApplication.isPlaying = false;
|
|
}
|
|
|
|
// ── 헬퍼 메서드 ──────────────────────────────────────────
|
|
|
|
private static GameObject CreatePanel(Transform parent, string name)
|
|
{
|
|
var go = new GameObject(name);
|
|
go.transform.SetParent(parent, false);
|
|
go.AddComponent<RectTransform>();
|
|
var img = go.AddComponent<Image>();
|
|
img.color = new Color(0, 0, 0, 0.6f);
|
|
return go;
|
|
}
|
|
|
|
private static GameObject CreateInputField(Transform parent, string name, string placeholder, float w, float h)
|
|
{
|
|
var go = new GameObject(name);
|
|
go.transform.SetParent(parent, false);
|
|
var rect = go.AddComponent<RectTransform>();
|
|
rect.sizeDelta = new Vector2(w, h);
|
|
go.AddComponent<Image>().color = new Color(0.15f, 0.15f, 0.15f, 1f);
|
|
var input = go.AddComponent<TMP_InputField>();
|
|
|
|
var textArea = new GameObject("Text Area");
|
|
textArea.transform.SetParent(go.transform, false);
|
|
var taRect = textArea.AddComponent<RectTransform>();
|
|
taRect.anchorMin = Vector2.zero;
|
|
taRect.anchorMax = Vector2.one;
|
|
taRect.offsetMin = new Vector2(8, 2);
|
|
taRect.offsetMax = new Vector2(-8, -2);
|
|
textArea.AddComponent<RectMask2D>();
|
|
|
|
var phGO = new GameObject("Placeholder");
|
|
phGO.transform.SetParent(textArea.transform, false);
|
|
var phRect = phGO.AddComponent<RectTransform>();
|
|
phRect.anchorMin = Vector2.zero;
|
|
phRect.anchorMax = Vector2.one;
|
|
phRect.offsetMin = phRect.offsetMax = Vector2.zero;
|
|
var phTmp = phGO.AddComponent<TextMeshProUGUI>();
|
|
phTmp.text = placeholder;
|
|
phTmp.fontSize = 18;
|
|
phTmp.color = new Color(0.5f, 0.5f, 0.5f);
|
|
UIFontSetupTool.ApplyRole(phTmp, UIFontRole.Combat);
|
|
|
|
var txtGO = new GameObject("Text");
|
|
txtGO.transform.SetParent(textArea.transform, false);
|
|
var txtRect = txtGO.AddComponent<RectTransform>();
|
|
txtRect.anchorMin = Vector2.zero;
|
|
txtRect.anchorMax = Vector2.one;
|
|
txtRect.offsetMin = txtRect.offsetMax = Vector2.zero;
|
|
var txt = txtGO.AddComponent<TextMeshProUGUI>();
|
|
txt.fontSize = 18;
|
|
txt.color = Color.white;
|
|
UIFontSetupTool.ApplyRole(txt, UIFontRole.Combat);
|
|
|
|
input.textViewport = taRect;
|
|
input.placeholder = phTmp;
|
|
input.textComponent = txt;
|
|
|
|
return go;
|
|
}
|
|
|
|
private static GameObject CreateButton(Transform parent, string name, string label, float w, float h)
|
|
{
|
|
var go = new GameObject(name);
|
|
go.transform.SetParent(parent, false);
|
|
var rect = go.AddComponent<RectTransform>();
|
|
rect.sizeDelta = new Vector2(w, h);
|
|
go.AddComponent<Image>().color = new Color(0.2f, 0.5f, 0.8f, 1f);
|
|
go.AddComponent<Button>();
|
|
|
|
var txtGO = new GameObject("Text");
|
|
txtGO.transform.SetParent(go.transform, false);
|
|
var txtRect = txtGO.AddComponent<RectTransform>();
|
|
txtRect.anchorMin = Vector2.zero;
|
|
txtRect.anchorMax = Vector2.one;
|
|
txtRect.offsetMin = txtRect.offsetMax = Vector2.zero;
|
|
var tmp = txtGO.AddComponent<TextMeshProUGUI>();
|
|
tmp.text = label;
|
|
tmp.fontSize = 20;
|
|
tmp.alignment = TextAlignmentOptions.Center;
|
|
tmp.color = Color.white;
|
|
UIFontSetupTool.ApplyRole(tmp, UIFontRole.Combat);
|
|
|
|
return go;
|
|
}
|
|
|
|
private static TextMeshProUGUI CreateLabel(Transform parent, string name, string text, int size, UIFontRole role = UIFontRole.Body)
|
|
{
|
|
var go = new GameObject(name);
|
|
go.transform.SetParent(parent, false);
|
|
var rect = go.AddComponent<RectTransform>();
|
|
rect.sizeDelta = new Vector2(400, size * 1.6f);
|
|
var tmp = go.AddComponent<TextMeshProUGUI>();
|
|
tmp.text = text;
|
|
tmp.fontSize = size;
|
|
tmp.alignment = TextAlignmentOptions.Center;
|
|
tmp.color = Color.white;
|
|
UIFontSetupTool.ApplyRole(tmp, role);
|
|
return tmp;
|
|
}
|
|
|
|
private static void SetPrivateField(object obj, string fieldName, object value)
|
|
{
|
|
var field = obj.GetType().GetField(fieldName,
|
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
if (field != null)
|
|
field.SetValue(obj, value);
|
|
else
|
|
Debug.LogWarning($"[LobbySceneBuilder] Field not found: {fieldName}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// NetworkPrefabHandler의 내부 List<NetworkPrefab>에 리플렉션으로 프리팹을 추가합니다.
|
|
/// (Prefabs 프로퍼티가 IReadOnlyList이므로 직접 Add 불가)
|
|
/// </summary>
|
|
private static void AddNetworkPrefab(NetworkManager nm, GameObject prefab)
|
|
{
|
|
var handler = nm.NetworkConfig.Prefabs;
|
|
var bindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
|
|
|
|
// 타입으로 직접 탐색 (필드명은 NGO 버전마다 다를 수 있음)
|
|
System.Collections.Generic.List<NetworkPrefab> list = null;
|
|
foreach (var field in handler.GetType().GetFields(bindingFlags))
|
|
{
|
|
if (field.FieldType == typeof(System.Collections.Generic.List<NetworkPrefab>))
|
|
{
|
|
list = field.GetValue(handler) as System.Collections.Generic.List<NetworkPrefab>;
|
|
if (list != null) break;
|
|
}
|
|
}
|
|
|
|
if (list == null)
|
|
{
|
|
Debug.LogError("[LobbySceneBuilder] Could not find List<NetworkPrefab> on NetworkPrefabHandler via reflection.");
|
|
return;
|
|
}
|
|
|
|
list.Add(new NetworkPrefab { Prefab = prefab });
|
|
EditorUtility.SetDirty(nm);
|
|
}
|
|
|
|
private static void AddSceneToBuildSettings(string scenePath, int index)
|
|
{
|
|
var scenes = EditorBuildSettings.scenes;
|
|
foreach (var s in scenes)
|
|
if (s.path == scenePath) return; // 이미 등록됨
|
|
|
|
var newScenes = new EditorBuildSettingsScene[scenes.Length + 1];
|
|
|
|
// index 위치에 삽입
|
|
int insertAt = Mathf.Clamp(index, 0, scenes.Length);
|
|
for (int i = 0; i < insertAt; i++)
|
|
newScenes[i] = scenes[i];
|
|
newScenes[insertAt] = new EditorBuildSettingsScene(scenePath, true);
|
|
for (int i = insertAt; i < scenes.Length; i++)
|
|
newScenes[i + 1] = scenes[i];
|
|
|
|
EditorBuildSettings.scenes = newScenes;
|
|
}
|
|
}
|
|
}
|