using UnityEngine; /// /// 채팅 UI 의 색과 글꼴, 그리고 런타임에 만들어 쓰는 둥근 사각형 스프라이트. /// /// 에셋을 하나도 만들지 않고 코드에서 전부 생성한다. 씬이나 프리팹에 UI 를 넣어두면 /// 캐릭터 로딩과 마찬가지로 병합 사고가 나기 쉽고, 무엇보다 이 프로젝트에는 /// TextMeshPro Essentials 가 임포트돼 있지 않다. TMP 는 기본 폰트 에셋이 없으면 /// 글자가 아예 안 나오므로, 한글이 확실히 나오는 레거시 Text + OS 동적 폰트로 간다. /// public static class ChatUiTheme { // 데스크톱 위에 반투명하게 얹히므로 알파를 남긴다. 완전 불투명이면 위젯처럼 보이지 않는다. public static readonly Color PanelBackground = new Color(0.09f, 0.10f, 0.13f, 0.94f); public static readonly Color HeaderBackground = new Color(0.13f, 0.15f, 0.19f, 0.98f); public static readonly Color UserBubble = new Color(0.20f, 0.38f, 0.68f, 0.95f); public static readonly Color AssistantBubble = new Color(0.19f, 0.21f, 0.26f, 0.95f); public static readonly Color ErrorBubble = new Color(0.48f, 0.18f, 0.20f, 0.95f); public static readonly Color InputBackground = new Color(0.16f, 0.18f, 0.22f, 0.98f); public static readonly Color AccentButton = new Color(0.24f, 0.45f, 0.78f, 1f); public static readonly Color PrimaryText = new Color(0.94f, 0.95f, 0.97f, 1f); public static readonly Color DimText = new Color(0.62f, 0.66f, 0.72f, 1f); /// 글꼴 후보. 앞에서부터 설치돼 있는 것을 쓴다. 한글이 나오는 것이 최우선. static readonly string[] FontCandidates = { "Malgun Gothic", // Windows 기본 한글 글꼴 "맑은 고딕", "Noto Sans KR", "Segoe UI", "Arial", }; static Font cachedFont; static Sprite cachedRounded; static Sprite cachedRoundedSmall; /// /// 한글이 나오는 동적 글꼴. OS 에 설치된 글꼴에서 만들기 때문에 프로젝트에 /// 폰트 에셋을 넣지 않아도 되고, 글리프를 필요할 때 아틀라스에 채워 넣는다. /// public static Font Font { get { if (cachedFont != null) return cachedFont; cachedFont = UnityEngine.Font.CreateDynamicFontFromOSFont(FontCandidates, 16); if (cachedFont == null) { // 이 경로로 오면 한글은 네모로 보인다. 그래도 앱이 죽지는 않게 한다. cachedFont = Resources.GetBuiltinResource("LegacyRuntime.ttf"); Debug.LogWarning("[ChatUiTheme] OS 글꼴을 만들지 못했습니다. 한글이 깨질 수 있습니다."); } return cachedFont; } } /// 말풍선과 패널에 쓰는 둥근 사각형(9-슬라이스). public static Sprite RoundedPanel => cachedRounded != null ? cachedRounded : (cachedRounded = BuildRoundedRect(12)); /// 입력칸과 버튼처럼 작은 요소에 쓰는 덜 둥근 사각형. public static Sprite RoundedSmall => cachedRoundedSmall != null ? cachedRoundedSmall : (cachedRoundedSmall = BuildRoundedRect(6)); /// /// 모서리가 둥근 흰색 사각형 텍스처를 만들고 9-슬라이스 스프라이트로 감싼다. /// 흰색으로 만들어 두면 Image.color 로 아무 색이나 입힐 수 있어 스프라이트 하나면 충분하다. /// /// 크기는 (2r+2)². 테두리를 r 로 잡으면 가운데 2px 만 늘어나므로 어떤 크기로 /// 늘려도 모서리 곡률이 유지된다. /// static Sprite BuildRoundedRect(int radius) { int size = radius * 2 + 2; var tex = new Texture2D(size, size, TextureFormat.RGBA32, false) { name = "ChatRounded" + radius, filterMode = FilterMode.Bilinear, wrapMode = TextureWrapMode.Clamp, hideFlags = HideFlags.HideAndDontSave, }; var pixels = new Color32[size * size]; for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { float px = x + 0.5f; float py = y + 0.5f; // 모서리 원의 중심들이 이루는 안쪽 사각형 위의 가장 가까운 점. // 그 점까지의 거리가 반지름을 넘어가면 바깥이다. float nx = Mathf.Clamp(px, radius, size - radius); float ny = Mathf.Clamp(py, radius, size - radius); float dist = Mathf.Sqrt((px - nx) * (px - nx) + (py - ny) * (py - ny)); // +0.5 는 1픽셀 폭의 계단 완화. 이게 없으면 모서리가 거칠다. float alpha = Mathf.Clamp01(radius - dist + 0.5f); pixels[y * size + x] = new Color32(255, 255, 255, (byte)(alpha * 255f)); } } tex.SetPixels32(pixels); tex.Apply(false, true); var sprite = Sprite.Create( tex, new Rect(0f, 0f, size, size), new Vector2(0.5f, 0.5f), 100f, 0, SpriteMeshType.FullRect, new Vector4(radius, radius, radius, radius)); sprite.name = "ChatRoundedSprite" + radius; sprite.hideFlags = HideFlags.HideAndDontSave; return sprite; } }