Files
MyCharacterAgent/Assets/02_Scripts/Chat/ChatUiTheme.cs
2026-09-01 14:40:02 +09:00

124 lines
5.4 KiB
C#

using UnityEngine;
/// <summary>
/// 채팅 UI 의 색과 글꼴, 그리고 런타임에 만들어 쓰는 둥근 사각형 스프라이트.
///
/// 에셋을 하나도 만들지 않고 코드에서 전부 생성한다. 씬이나 프리팹에 UI 를 넣어두면
/// 캐릭터 로딩과 마찬가지로 병합 사고가 나기 쉽고, 무엇보다 이 프로젝트에는
/// TextMeshPro Essentials 가 임포트돼 있지 않다. TMP 는 기본 폰트 에셋이 없으면
/// 글자가 아예 안 나오므로, 한글이 확실히 나오는 레거시 Text + OS 동적 폰트로 간다.
/// </summary>
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);
/// <summary>글꼴 후보. 앞에서부터 설치돼 있는 것을 쓴다. 한글이 나오는 것이 최우선.</summary>
static readonly string[] FontCandidates =
{
"Malgun Gothic", // Windows 기본 한글 글꼴
"맑은 고딕",
"Noto Sans KR",
"Segoe UI",
"Arial",
};
static Font cachedFont;
static Sprite cachedRounded;
static Sprite cachedRoundedSmall;
/// <summary>
/// 한글이 나오는 동적 글꼴. OS 에 설치된 글꼴에서 만들기 때문에 프로젝트에
/// 폰트 에셋을 넣지 않아도 되고, 글리프를 필요할 때 아틀라스에 채워 넣는다.
/// </summary>
public static Font Font
{
get
{
if (cachedFont != null) return cachedFont;
cachedFont = UnityEngine.Font.CreateDynamicFontFromOSFont(FontCandidates, 16);
if (cachedFont == null)
{
// 이 경로로 오면 한글은 네모로 보인다. 그래도 앱이 죽지는 않게 한다.
cachedFont = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
Debug.LogWarning("[ChatUiTheme] OS 글꼴을 만들지 못했습니다. 한글이 깨질 수 있습니다.");
}
return cachedFont;
}
}
/// <summary>말풍선과 패널에 쓰는 둥근 사각형(9-슬라이스).</summary>
public static Sprite RoundedPanel => cachedRounded != null
? cachedRounded
: (cachedRounded = BuildRoundedRect(12));
/// <summary>입력칸과 버튼처럼 작은 요소에 쓰는 덜 둥근 사각형.</summary>
public static Sprite RoundedSmall => cachedRoundedSmall != null
? cachedRoundedSmall
: (cachedRoundedSmall = BuildRoundedRect(6));
/// <summary>
/// 모서리가 둥근 흰색 사각형 텍스처를 만들고 9-슬라이스 스프라이트로 감싼다.
/// 흰색으로 만들어 두면 Image.color 로 아무 색이나 입힐 수 있어 스프라이트 하나면 충분하다.
///
/// 크기는 (2r+2)². 테두리를 r 로 잡으면 가운데 2px 만 늘어나므로 어떤 크기로
/// 늘려도 모서리 곡률이 유지된다.
/// </summary>
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;
}
}