VSClone/Assets/Scripts/View/Art.cs

360 lines
24 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using UnityEngine;
// 코드로 그리는 그림(원작 추출 아트 미사용) — 아이콘 · 방 · 소품 · 스트리머
namespace VS.View
{
/// 간단한 안티에일리어싱 래스터 (거리 기반 커버리지)
public class Raster
{
public readonly int W, H; public readonly Color[] Px;
public Raster(int w, int h) { W = w; H = h; Px = new Color[w * h]; }
public void Blend(int x, int y, Color c, float a)
{
if (x < 0 || y < 0 || x >= W || y >= H || a <= 0f) return;
a = Mathf.Clamp01(a) * c.a;
int i = y * W + x;
var d = Px[i];
float oa = a + d.a * (1 - a);
if (oa <= 0.0001f) return;
Px[i] = new Color((c.r * a + d.r * d.a * (1 - a)) / oa, (c.g * a + d.g * d.a * (1 - a)) / oa, (c.b * a + d.b * d.a * (1 - a)) / oa, oa);
}
// sdf: 음수 = 안쪽
public void Shape(Func<float, float, float> sdf, Color c, float x0, float y0, float x1, float y1)
{
int ax = Mathf.Max(0, (int)x0 - 2), ay = Mathf.Max(0, (int)y0 - 2), bx = Mathf.Min(W - 1, (int)x1 + 2), by = Mathf.Min(H - 1, (int)y1 + 2);
for (int y = ay; y <= by; y++)
for (int x = ax; x <= bx; x++)
{
float d = sdf(x + 0.5f, y + 0.5f);
if (d < 0.75f) Blend(x, y, c, Mathf.Clamp01(0.5f - d));
}
}
public void Circle(float cx, float cy, float r, Color c) => Shape((x, y) => Mathf.Sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy)) - r, c, cx - r, cy - r, cx + r, cy + r);
public void Ring(float cx, float cy, float r, float w, Color c) => Shape((x, y) => Mathf.Abs(Mathf.Sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy)) - r) - w * 0.5f, c, cx - r - w, cy - r - w, cx + r + w, cy + r + w);
public void Ellipse(float cx, float cy, float rx, float ry, Color c) => Shape((x, y) => { float dx = (x - cx) / rx, dy = (y - cy) / ry; return (Mathf.Sqrt(dx * dx + dy * dy) - 1f) * Mathf.Min(rx, ry); }, c, cx - rx, cy - ry, cx + rx, cy + ry);
public void RRect(float x0, float y0, float x1, float y1, float r, Color c)
{
float cx = (x0 + x1) * 0.5f, cy = (y0 + y1) * 0.5f, hx = (x1 - x0) * 0.5f, hy = (y1 - y0) * 0.5f;
r = Mathf.Min(r, Mathf.Min(hx, hy));
Shape((x, y) =>
{
float qx = Mathf.Abs(x - cx) - hx + r, qy = Mathf.Abs(y - cy) - hy + r;
float ox = Mathf.Max(qx, 0), oy = Mathf.Max(qy, 0);
return Mathf.Sqrt(ox * ox + oy * oy) + Mathf.Min(Mathf.Max(qx, qy), 0) - r;
}, c, x0, y0, x1, y1);
}
public void Poly(Vector2[] p, Color c)
{
float minx = float.MaxValue, miny = float.MaxValue, maxx = float.MinValue, maxy = float.MinValue;
foreach (var v in p) { minx = Mathf.Min(minx, v.x); miny = Mathf.Min(miny, v.y); maxx = Mathf.Max(maxx, v.x); maxy = Mathf.Max(maxy, v.y); }
Shape((x, y) =>
{
float d = float.MaxValue; bool inside = false;
for (int i = 0, j = p.Length - 1; i < p.Length; j = i++)
{
var a = p[i]; var b = p[j];
var e = b - a; var w = new Vector2(x, y) - a;
var proj = w - e * Mathf.Clamp01(Vector2.Dot(w, e) / Vector2.Dot(e, e));
d = Mathf.Min(d, proj.sqrMagnitude);
if ((a.y > y) != (b.y > y) && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) inside = !inside;
}
return (inside ? -1f : 1f) * Mathf.Sqrt(d);
}, c, minx, miny, maxx, maxy);
}
public void Line(Vector2 a, Vector2 b, float w, Color c)
{
Shape((x, y) =>
{
var e = b - a; var p = new Vector2(x, y) - a;
float h = Mathf.Clamp01(Vector2.Dot(p, e) / Mathf.Max(0.0001f, Vector2.Dot(e, e)));
return (p - e * h).magnitude - w * 0.5f;
}, c, Mathf.Min(a.x, b.x) - w, Mathf.Min(a.y, b.y) - w, Mathf.Max(a.x, b.x) + w, Mathf.Max(a.y, b.y) + w);
}
public void Star(float cx, float cy, float r, Color c)
{
var p = new Vector2[10];
for (int i = 0; i < 10; i++) { float ang = Mathf.PI / 2 + i * Mathf.PI / 5; float rr = i % 2 == 0 ? r : r * 0.45f; p[i] = new Vector2(cx + Mathf.Cos(ang) * rr, cy + Mathf.Sin(ang) * rr); }
Poly(p, c);
}
public void Heart(float cx, float cy, float s, Color c)
{
Circle(cx - s * 0.5f, cy + s * 0.25f, s * 0.55f, c);
Circle(cx + s * 0.5f, cy + s * 0.25f, s * 0.55f, c);
Poly(new[] { new Vector2(cx - s * 1.02f, cy + s * 0.1f), new Vector2(cx + s * 1.02f, cy + s * 0.1f), new Vector2(cx, cy - s * 1.05f) }, c);
}
public Texture2D Tex()
{
var t = new Texture2D(W, H, TextureFormat.RGBA32, false) { wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear };
t.SetPixels(Px); t.Apply(false, false);
return t;
}
public Sprite Sprite(Vector4 border = default, float ppu = 100f) => UnityEngine.Sprite.Create(Tex(), new Rect(0, 0, W, H), new Vector2(0.5f, 0.5f), ppu, 0, SpriteMeshType.FullRect, border);
}
public static class Art
{
static Sprite round, pill, square, circle, softCircle;
public static Sprite Round { get { if (round == null) { var r = new Raster(64, 64); r.RRect(0, 0, 64, 64, 22, Color.white); round = r.Sprite(new Vector4(26, 26, 26, 26)); } return round; } }
public static Sprite Pill { get { if (pill == null) { var r = new Raster(64, 32); r.RRect(0, 0, 64, 32, 16, Color.white); pill = r.Sprite(new Vector4(17, 15, 17, 15)); } return pill; } }
public static Sprite Square { get { if (square == null) { var r = new Raster(4, 4); for (int i = 0; i < 16; i++) r.Px[i] = Color.white; square = r.Sprite(); } return square; } }
public static Sprite Circle { get { if (circle == null) { var r = new Raster(128, 128); r.Circle(64, 64, 62, Color.white); circle = r.Sprite(); } return circle; } }
public static Sprite SoftCircle
{
get
{
if (softCircle == null)
{
var r = new Raster(128, 128);
for (int y = 0; y < 128; y++) for (int x = 0; x < 128; x++) { float d = Vector2.Distance(new Vector2(x, y), new Vector2(64, 64)) / 64f; r.Px[y * 128 + x] = new Color(1, 1, 1, Mathf.Clamp01(1 - d) * Mathf.Clamp01(1 - d)); }
softCircle = r.Sprite();
}
return softCircle;
}
}
static readonly Dictionary<string, Sprite> icons = new Dictionary<string, Sprite>();
/// 아이콘(128×128) — 이름별 코드 그림
public static Sprite Icon(string name)
{
if (icons.TryGetValue(name, out var s)) return s;
var r = new Raster(128, 128);
Color ink = Pal.Ink;
switch (name)
{
case "coin":
r.Circle(64, 64, 56, new Color32(214, 150, 20, 255)); r.Circle(64, 66, 50, Pal.Yellow); r.Ring(64, 66, 38, 5, new Color32(240, 170, 30, 255));
r.RRect(56, 42, 72, 90, 6, new Color32(232, 160, 24, 255)); break;
case "ticket":
r.RRect(10, 30, 118, 98, 14, new Color32(255, 120, 160, 255)); r.Circle(10, 64, 14, Color.clear); r.RRect(16, 36, 112, 92, 10, new Color32(255, 170, 200, 255));
r.Star(64, 64, 22, Color.white); break;
case "heart": r.Heart(64, 70, 30, Pal.Pink); r.Circle(48, 82, 8, new Color(1, 1, 1, 0.6f)); break;
case "follower":
r.Circle(64, 84, 22, Pal.Purple); r.Ellipse(64, 34, 40, 26, Pal.Purple); break;
case "view":
r.Ellipse(64, 64, 56, 32, Color.white); r.Circle(64, 64, 22, Pal.Purple); r.Circle(64, 64, 10, ink); break;
case "house":
r.Poly(new[] { new Vector2(10, 64), new Vector2(64, 116), new Vector2(118, 64) }, Pal.Orange); r.RRect(24, 14, 104, 70, 6, new Color32(255, 220, 150, 255)); r.RRect(54, 14, 76, 48, 4, new Color32(170, 100, 60, 255)); break;
case "closet":
r.Line(new Vector2(64, 104), new Vector2(64, 92), 6, ink); r.Ring(64, 108, 8, 5, ink);
r.Poly(new[] { new Vector2(64, 92), new Vector2(118, 50), new Vector2(10, 50) }, Pal.Pink); r.RRect(10, 40, 118, 54, 6, Pal.PinkDark); break;
case "skill":
r.RRect(48, 50, 80, 112, 16, new Color32(90, 90, 110, 255)); r.RRect(54, 58, 74, 104, 10, new Color32(150, 150, 175, 255)); r.Line(new Vector2(64, 50), new Vector2(64, 22), 8, ink); r.RRect(40, 14, 88, 26, 6, ink); break;
case "x2":
r.Heart(64, 70, 30, Pal.Pink); break;
case "auto":
r.Poly(new[] { new Vector2(70, 120), new Vector2(30, 56), new Vector2(62, 56), new Vector2(54, 8), new Vector2(98, 74), new Vector2(66, 74) }, Pal.Yellow); break;
case "gear":
for (int i = 0; i < 8; i++) { float a = i * Mathf.PI / 4; r.RRect(64 + Mathf.Cos(a) * 44 - 12, 64 + Mathf.Sin(a) * 44 - 12, 64 + Mathf.Cos(a) * 44 + 12, 64 + Mathf.Sin(a) * 44 + 12, 4, new Color32(150, 150, 170, 255)); }
r.Circle(64, 64, 42, new Color32(150, 150, 170, 255)); r.Circle(64, 64, 16, Color.white); break;
case "gift":
r.RRect(18, 14, 110, 80, 8, Pal.Purple); r.RRect(12, 72, 116, 96, 8, Pal.PurpleDark); r.RRect(56, 14, 72, 96, 2, Pal.Yellow);
r.Ellipse(48, 104, 16, 10, Pal.Yellow); r.Ellipse(80, 104, 16, 10, Pal.Yellow); break;
case "task":
r.RRect(22, 10, 106, 116, 12, Color.white); r.RRect(30, 18, 98, 108, 8, new Color32(240, 240, 250, 255));
for (int i = 0; i < 3; i++) { r.Circle(42, 88 - i * 26, 6, Pal.Green); r.RRect(54, 84 - i * 26, 92, 92 - i * 26, 3, Pal.Gray); } break;
case "afk":
r.Circle(64, 64, 52, Color.white); r.Ring(64, 64, 46, 8, Pal.Blue); r.Line(new Vector2(64, 64), new Vector2(64, 96), 8, ink); r.Line(new Vector2(64, 64), new Vector2(88, 54), 8, ink); break;
case "lock":
r.Ring(64, 76, 22, 10, new Color32(120, 120, 140, 255)); r.RRect(30, 14, 98, 74, 10, new Color32(160, 160, 180, 255)); r.Circle(64, 46, 8, ink); break;
case "up":
r.Poly(new[] { new Vector2(64, 118), new Vector2(118, 60), new Vector2(84, 60), new Vector2(84, 10), new Vector2(44, 10), new Vector2(44, 60), new Vector2(10, 60) }, Pal.Green); break;
case "star": r.Star(64, 64, 56, Pal.Yellow); break;
case "fire":
r.Ellipse(64, 46, 36, 38, Pal.Orange); r.Poly(new[] { new Vector2(30, 52), new Vector2(64, 124), new Vector2(98, 52) }, Pal.Orange); r.Ellipse(64, 42, 20, 22, Pal.Yellow); break;
case "chat":
r.RRect(10, 34, 118, 110, 22, Color.white); r.Poly(new[] { new Vector2(30, 40), new Vector2(22, 10), new Vector2(56, 36) }, Color.white);
for (int i = 0; i < 3; i++) r.Circle(38 + i * 26, 72, 8, Pal.Purple); break;
case "shop":
r.RRect(14, 12, 114, 84, 10, Pal.Blue); r.Poly(new[] { new Vector2(8, 84), new Vector2(120, 84), new Vector2(104, 116), new Vector2(24, 116) }, new Color32(255, 110, 120, 255)); break;
case "pk":
r.Line(new Vector2(20, 20), new Vector2(108, 108), 14, new Color32(200, 200, 215, 255)); r.Line(new Vector2(108, 20), new Vector2(20, 108), 14, new Color32(200, 200, 215, 255)); r.Circle(64, 64, 14, Pal.Red); break;
case "calendar":
r.RRect(14, 10, 114, 104, 12, Color.white); r.RRect(14, 78, 114, 104, 12, Pal.Red); r.RRect(30, 24, 98, 70, 6, new Color32(235, 235, 245, 255)); break;
case "hand":
r.RRect(46, 8, 86, 76, 18, new Color32(255, 214, 180, 255)); r.RRect(54, 50, 74, 122, 10, new Color32(255, 214, 180, 255)); break;
default: r.Circle(64, 64, 50, Pal.Gray); break;
}
s = r.Sprite();
icons[name] = s;
return s;
}
// ---------- 방 ----------
static readonly Color[][] HouseTheme =
{
new Color[] { new Color32(255, 214, 230, 255), new Color32(214, 160, 196, 255), new Color32(255, 240, 248, 255) }, // 침실
new Color[] { new Color32(196, 226, 210, 255), new Color32(150, 120, 96, 255), new Color32(236, 250, 240, 255) }, // 교실
new Color[] { new Color32(150, 214, 255, 255), new Color32(244, 220, 160, 255), new Color32(220, 244, 255, 255) }, // 해변
new Color[] { new Color32(255, 236, 196, 255), new Color32(200, 170, 140, 255), new Color32(255, 250, 236, 255) }, // 부엌
new Color[] { new Color32(220, 206, 255, 255), new Color32(170, 160, 200, 255), new Color32(246, 240, 255, 255) }, // 가게
new Color[] { new Color32(196, 206, 255, 255), new Color32(140, 120, 180, 255), new Color32(236, 240, 255, 255) }, // 새 침실
};
static readonly Dictionary<string, Sprite> cache = new Dictionary<string, Sprite>();
/// 방 배경 (house · 방 아이템 시각 단계 0~3 · -1 = 기본)
public static Sprite RoomBg(int house, int stage)
{
string key = $"bg{house}_{stage}";
if (cache.TryGetValue(key, out var s)) return s;
const int W = 540, H = 720;
var r = new Raster(W, H);
var th = HouseTheme[Mathf.Clamp(house, 0, HouseTheme.Length - 1)];
Color wall = th[0], floor = th[1], hi = th[2];
float floorY = H * 0.36f;
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++)
{
Color c = y < floorY ? Color.Lerp(floor * 0.85f, floor, y / floorY) : Color.Lerp(wall, hi, (y - floorY) / (H - floorY));
c.a = 1; r.Px[y * W + x] = c;
}
if (house == 2)
{
r.RRect(-10, -10, W + 10, floorY, 0, new Color32(244, 214, 150, 255));
r.Ellipse(W * 0.5f, floorY + 6, W * 0.7f, 26, new Color32(80, 170, 240, 255));
r.Circle(W * 0.82f, H * 0.84f, 42, new Color32(255, 240, 140, 255));
}
else
{
// 벽지 무늬 (단계가 오를수록 화려)
int st = Mathf.Max(0, stage);
for (int i = 0; i < 9 + st * 3; i++)
{
float x = (i * 97 + 31) % W, y = floorY + 40 + (i * 173 % (int)(H - floorY - 60));
if (st >= 2) r.Star(x, y, 8 + st * 2, new Color(1, 1, 1, 0.5f)); else r.Circle(x, y, 6 + st * 2, new Color(1, 1, 1, 0.45f));
}
r.RRect(0, floorY - 6, W, floorY + 8, 4, Color.Lerp(floor, Color.white, 0.35f));
if (st >= 1) { r.RRect(W * 0.08f, H * 0.62f, W * 0.34f, H * 0.9f, 10, new Color32(255, 255, 255, 200)); r.RRect(W * 0.1f, H * 0.64f, W * 0.32f, H * 0.88f, 8, new Color32(160, 214, 255, 255)); }
if (st >= 3) for (int i = 0; i < 12; i++) r.Circle(20 + i * 46, H - 26, 7, i % 2 == 0 ? Pal.Yellow : Pal.Pink);
}
s = r.Sprite();
s.texture.filterMode = FilterMode.Bilinear;
cache[key] = s;
return s;
}
static int Kind(string n)
{
n = (n ?? "").ToLowerInvariant();
if (n.Contains("sofa") || n.Contains("bed") || n.Contains("chair") && !n.Contains("beach")) return 0;
if (n.Contains("table") || n.Contains("desk") || n.Contains("counter")) return 1;
if (n.Contains("shelf") || n.Contains("rack") || n.Contains("fridge") || n.Contains("chest") || n.Contains("case") || n == "pc" || n.Contains("cooler")) return 2;
if (n.Contains("monitor") || n.Contains("laptop") || n.Contains("picture") || n.Contains("board") || n.Contains("window") || n.Contains("clock") || n.Contains("sign")) return 3;
if (n.Contains("light") || n.Contains("lamp")) return 4;
if (n.Contains("carpet") || n.Contains("mat") || n.Contains("ring")) return 5;
if (n.Contains("curtain")) return 6;
if (n.Contains("tripod") || n.Contains("camera") || n.Contains("micro") || n.Contains("headphone")) return 7;
return 8; // 둥근 소품 (곰인형·화분·트로피·케이크·컵·모래성·마네킹 …)
}
static readonly Color[] PropColor =
{
new Color32(255, 130, 170, 255), new Color32(120, 190, 255, 255), new Color32(255, 196, 90, 255), new Color32(150, 120, 230, 255),
new Color32(110, 210, 170, 255), new Color32(255, 150, 110, 255), new Color32(200, 140, 255, 255), new Color32(90, 200, 230, 255),
new Color32(250, 120, 120, 255), new Color32(170, 200, 90, 255), new Color32(240, 170, 220, 255),
};
/// 소품 그림 (아이템 이름 → 모양 · 단계 0~3 → 장식)
public static Sprite Prop(int house, int index, string name, int stage)
{
string key = $"p{house}_{index}_{stage}";
if (cache.TryGetValue(key, out var s)) return s;
var r = new Raster(160, 160);
Color c = PropColor[(index + house * 3) % PropColor.Length];
Color dark = Color.Lerp(c, Pal.Ink, 0.35f); Color light = Color.Lerp(c, Color.white, 0.45f);
if (stage >= 2) dark = Color.Lerp(dark, Pal.Yellow, 0.5f);
switch (Kind(name))
{
case 0: r.RRect(14, 60, 146, 110, 18, dark); r.RRect(10, 30, 150, 74, 16, c); r.RRect(4, 26, 30, 96, 12, dark); r.RRect(130, 26, 156, 96, 12, dark); r.RRect(34, 54, 126, 70, 8, light); break;
case 1: r.RRect(10, 86, 150, 106, 8, c); r.RRect(20, 20, 34, 90, 6, dark); r.RRect(126, 20, 140, 90, 6, dark); r.RRect(16, 100, 144, 112, 6, light); break;
case 2: r.RRect(36, 8, 124, 150, 10, dark); r.RRect(44, 16, 116, 142, 6, c); for (int i = 0; i < 3; i++) r.RRect(44, 46 + i * 34, 116, 52 + i * 34, 2, dark); break;
case 3: r.RRect(16, 40, 144, 136, 12, dark); r.RRect(24, 48, 136, 128, 8, light); r.RRect(70, 12, 90, 42, 4, dark); r.RRect(46, 8, 114, 18, 4, dark); break;
case 4: r.Poly(new[] { new Vector2(40, 96), new Vector2(120, 96), new Vector2(100, 146), new Vector2(60, 146) }, c); r.RRect(74, 20, 86, 98, 4, dark); r.Ellipse(80, 16, 32, 10, dark); r.Circle(80, 90, 16, new Color(1, 1, 0.7f, 0.8f)); break;
case 5: r.Ellipse(80, 60, 74, 34, c); r.Ellipse(80, 60, 58, 24, light); r.Ellipse(80, 60, 34, 12, c); break;
case 6: r.RRect(8, 140, 152, 152, 4, dark); r.Poly(new[] { new Vector2(10, 146), new Vector2(64, 146), new Vector2(48, 12), new Vector2(14, 12) }, c); r.Poly(new[] { new Vector2(96, 146), new Vector2(150, 146), new Vector2(146, 12), new Vector2(112, 12) }, c); break;
case 7: r.Line(new Vector2(80, 70), new Vector2(40, 8), 7, dark); r.Line(new Vector2(80, 70), new Vector2(120, 8), 7, dark); r.Line(new Vector2(80, 70), new Vector2(80, 8), 7, dark); r.RRect(48, 70, 112, 120, 14, c); r.Circle(80, 95, 16, light); break;
default: r.Ellipse(80, 64, 48, 52, c); r.Circle(80, 122, 32, c); r.Circle(70, 128, 6, Pal.Ink); r.Circle(90, 128, 6, Pal.Ink); r.Ellipse(80, 60, 30, 34, light); break;
}
if (stage >= 1) r.Star(140, 140, 14, Pal.Yellow);
if (stage >= 2) r.Star(118, 146, 10, Pal.Yellow);
if (stage >= 3) { r.Star(20, 146, 12, Pal.Yellow); r.Ring(80, 80, 74, 3, new Color(1f, 0.9f, 0.4f, 0.6f)); }
s = r.Sprite();
cache[key] = s;
return s;
}
// ---------- 스트리머 ----------
public static readonly Color32[] Hair =
{
new Color32(255, 150, 200, 255), new Color32(120, 80, 60, 255), new Color32(255, 220, 120, 255), new Color32(60, 50, 80, 255),
new Color32(160, 120, 255, 255), new Color32(120, 220, 255, 255), new Color32(255, 120, 90, 255), new Color32(240, 240, 250, 255),
};
public static readonly Color32[] Cloth =
{
new Color32(255, 110, 170, 255), new Color32(110, 170, 255, 255), new Color32(255, 200, 80, 255), new Color32(130, 220, 160, 255),
new Color32(180, 120, 255, 255), new Color32(255, 140, 110, 255), new Color32(80, 80, 110, 255), new Color32(250, 250, 255, 255),
new Color32(255, 90, 110, 255), new Color32(90, 210, 220, 255),
};
/// 프레임: 0 대기 · 1 눈 감기 · 2 탭 포즈 · 3~6 하이프 (원작 SpriteData 순서와 같은 뜻)
public static Sprite Idol(int outfit, int frame)
{
string key = $"idol{outfit}_{frame}";
if (cache.TryGetValue(key, out var s)) return s;
const int W = 300, H = 420;
var r = new Raster(W, H);
int o = outfit < 0 || outfit >= 1000 ? 0 : outfit + 1;
Color hair = Hair[o % Hair.Length], cloth = Cloth[(o * 3 + 1) % Cloth.Length], cloth2 = Color.Lerp(Cloth[(o * 7 + 4) % Cloth.Length], Color.white, 0.2f);
Color skin = new Color32(255, 224, 205, 255), ink = Pal.Ink;
float bob = frame >= 3 ? ((frame % 2 == 0) ? 10 : -4) : (frame == 2 ? 6 : 0);
float cx = W * 0.5f, headY = 300 + bob;
// 뒷머리
r.Ellipse(cx, headY - 20, 92, 120, hair);
// 몸
r.RRect(cx - 70, 40 + bob, cx + 70, 200 + bob, 40, cloth);
r.Poly(new[] { new Vector2(cx - 92, 20 + bob), new Vector2(cx + 92, 20 + bob), new Vector2(cx + 64, 120 + bob), new Vector2(cx - 64, 120 + bob) }, cloth2);
r.RRect(cx - 18, 186 + bob, cx + 18, 216 + bob, 8, skin);
// 팔
Vector2 sl = new Vector2(cx - 64, 170 + bob), sr = new Vector2(cx + 64, 170 + bob);
Vector2 hl, hr;
if (frame == 2) { hl = new Vector2(cx - 120, 250 + bob); hr = new Vector2(cx + 96, 110 + bob); }
else if (frame >= 3) { bool up = frame % 2 == 1; hl = new Vector2(cx - 118, up ? 290 : 220 + bob); hr = new Vector2(cx + 118, up ? 220 : 290 + bob); }
else { hl = new Vector2(cx - 88, 90 + bob); hr = new Vector2(cx + 88, 90 + bob); }
r.Line(sl, hl, 30, cloth); r.Line(sr, hr, 30, cloth); r.Circle(hl.x, hl.y, 17, skin); r.Circle(hr.x, hr.y, 17, skin);
// 머리
r.Circle(cx, headY, 82, skin);
r.Ellipse(cx, headY + 52, 90, 44, hair);
r.Ellipse(cx - 70, headY + 6, 24, 60, hair); r.Ellipse(cx + 70, headY + 6, 24, 60, hair);
// 눈 · 볼 · 입
if (frame == 1) { r.Line(new Vector2(cx - 42, headY - 4), new Vector2(cx - 18, headY - 4), 6, ink); r.Line(new Vector2(cx + 18, headY - 4), new Vector2(cx + 42, headY - 4), 6, ink); }
else if (frame >= 3) { r.Star(cx - 30, headY - 2, 14, ink); r.Star(cx + 30, headY - 2, 14, ink); }
else { r.Ellipse(cx - 30, headY - 2, 10, 15, ink); r.Ellipse(cx + 30, headY - 2, 10, 15, ink); r.Circle(cx - 27, headY + 4, 4, Color.white); r.Circle(cx + 33, headY + 4, 4, Color.white); }
r.Ellipse(cx - 50, headY - 26, 14, 8, new Color(1f, 0.5f, 0.6f, 0.55f)); r.Ellipse(cx + 50, headY - 26, 14, 8, new Color(1f, 0.5f, 0.6f, 0.55f));
if (frame >= 2) r.Ellipse(cx, headY - 40, 16, 12, new Color32(200, 60, 90, 255)); else r.Line(new Vector2(cx - 12, headY - 40), new Vector2(cx + 12, headY - 40), 5, new Color32(200, 60, 90, 255));
// 의상 장식(의상마다 다름)
int deco = o % 4;
if (deco == 1) r.Star(cx + 56, headY + 70, 22, Pal.Yellow);
else if (deco == 2) { r.Ellipse(cx + 60, headY + 66, 26, 14, Pal.Pink); r.Ellipse(cx + 84, headY + 66, 26, 14, Pal.Pink); r.Circle(cx + 72, headY + 66, 9, Pal.PinkDark); }
else if (deco == 3) r.Poly(new[] { new Vector2(cx - 50, headY + 92), new Vector2(cx - 30, headY + 140), new Vector2(cx, headY + 100), new Vector2(cx + 30, headY + 140), new Vector2(cx + 50, headY + 92) }, Pal.Yellow);
// 마이크
if (frame != 2) { r.Line(new Vector2(hr.x, hr.y), new Vector2(hr.x + 6, hr.y + 40), 8, new Color32(70, 70, 90, 255)); r.Circle(hr.x + 6, hr.y + 46, 14, new Color32(110, 110, 130, 255)); }
s = r.Sprite();
cache[key] = s;
return s;
}
}
}