294 lines
12 KiB
C#
294 lines
12 KiB
C#
|
|
using System.Collections.Generic;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.UI;
|
|||
|
|
|
|||
|
|
namespace TKS.OutGame
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 로비 배경 연출(PD 2026-11-11 「펄럭이는 깃발 · 환하게 비쳐진 관청의 불빛이나 등불 · 가끔 날아다니는 새」) — 값 = 표 lobby_scene.csv(LobbyScene · C45).
|
|||
|
|
/// 한 Frame 캔버스(「Ambient」 · 배경 그림 위 · 영웅 아래)에 모두 담는다 — 매 프레임 바뀌는 것끼리만 다시 묶임(P41 · 화면 캔버스는 그대로).
|
|||
|
|
/// 좌표 = 배경 그림 px(그림 크기 artW × artH) → 이 층(그림을 꽉 채움)의 단위로 매 프레임 환산(그림이 화면 폭에 맞춰 커져도 같은 자리).
|
|||
|
|
/// </summary>
|
|||
|
|
public class LobbyAmbient : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
class Glow
|
|||
|
|
{
|
|||
|
|
public Graphic g;
|
|||
|
|
public LobbyScene.Row row;
|
|||
|
|
public float baseA;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class Mover
|
|||
|
|
{
|
|||
|
|
public RectTransform rt;
|
|||
|
|
public Image img;
|
|||
|
|
public Sprite[] frames;
|
|||
|
|
public float x;
|
|||
|
|
public float y;
|
|||
|
|
public float vx;
|
|||
|
|
public float vy;
|
|||
|
|
public float size;
|
|||
|
|
public float t;
|
|||
|
|
public float life;
|
|||
|
|
public float ph;
|
|||
|
|
public float rot;
|
|||
|
|
public float vrot;
|
|||
|
|
public bool on;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
RectTransform area;
|
|||
|
|
RectTransform back;
|
|||
|
|
RectTransform front;
|
|||
|
|
float artW;
|
|||
|
|
float artH;
|
|||
|
|
readonly List<Glow> glows = new List<Glow>();
|
|||
|
|
readonly List<Mover> birds = new List<Mover>();
|
|||
|
|
readonly List<Mover> petals = new List<Mover>();
|
|||
|
|
LobbyScene.Row birdRow;
|
|||
|
|
LobbyScene.Row petalRow;
|
|||
|
|
float nextFlock;
|
|||
|
|
float nextPetal;
|
|||
|
|
float openedAt;
|
|||
|
|
|
|||
|
|
/// <summary>영웅 자리 층(그림을 꽉 채움 · 그림 좌표 = 정규화 앵커) — 뒤 연출(깃발·불빛)과 앞 연출(새·꽃잎) 사이.</summary>
|
|||
|
|
public RectTransform Lineup { get; private set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 그림(art) 위에 장면 층(Frame 캔버스 1장)을 만든다 — 순서 = Back(깃발·불빛) → Lineup(그림자·영웅 · 대기 프레임 재생) → Front(새·꽃잎 · PD 2026-11-11 「벚꽃잎은 유닛보다 앞」).
|
|||
|
|
/// 영웅 대기 프레임이 매 프레임 바뀌므로 영웅도 이 Frame 층에 둔다(화면 캔버스 = Event 그대로 · P41).
|
|||
|
|
/// </summary>
|
|||
|
|
public static LobbyAmbient Build(RectTransform art, float artW, float artH)
|
|||
|
|
{
|
|||
|
|
var layer = UiLayer.Group("Ambient", art, UiRate.Frame);
|
|||
|
|
var a = layer.gameObject.AddComponent<LobbyAmbient>();
|
|||
|
|
a.area = layer;
|
|||
|
|
a.back = UiKit.Fill("Back", layer);
|
|||
|
|
a.Lineup = UiKit.Fill("Lineup", layer);
|
|||
|
|
a.front = UiKit.Fill("Front", layer);
|
|||
|
|
a.artW = artW;
|
|||
|
|
a.artH = artH;
|
|||
|
|
a.Setup();
|
|||
|
|
return a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>그림 px 사각형(왼쪽 위 x,y · 폭 w · 높이 h) = 정규화 앵커(그림과 함께 늘어남) · 뒤 층.</summary>
|
|||
|
|
RectTransform ArtRect(string name, float x, float y, float w, float h)
|
|||
|
|
{
|
|||
|
|
var rt = UiKit.Rect(name, back, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero);
|
|||
|
|
rt.anchorMin = new Vector2(x / artW, 1f - (y + h) / artH);
|
|||
|
|
rt.anchorMax = new Vector2((x + w) / artW, 1f - y / artH);
|
|||
|
|
rt.offsetMin = rt.offsetMax = Vector2.zero;
|
|||
|
|
return rt;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static Sprite[] Load(LobbyScene.Row r)
|
|||
|
|
{
|
|||
|
|
var list = new List<Sprite>();
|
|||
|
|
foreach (var id in r.Sprites)
|
|||
|
|
{
|
|||
|
|
var sp = UISkin.Icon(id);
|
|||
|
|
if (sp != null) list.Add(sp);
|
|||
|
|
}
|
|||
|
|
return list.ToArray();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Setup()
|
|||
|
|
{
|
|||
|
|
// 깃발(원래 깃발 위에 같은 자리 · UiWaveImage)
|
|||
|
|
foreach (var r in LobbyScene.All("flag"))
|
|||
|
|
{
|
|||
|
|
var sp = Load(r);
|
|||
|
|
if (sp.Length == 0) continue;
|
|||
|
|
var rt = ArtRect("Flag_" + sp[0].name, r.x, r.y, r.w, r.h);
|
|||
|
|
var wv = rt.gameObject.AddComponent<UiWaveImage>();
|
|||
|
|
wv.sprite = sp[0];
|
|||
|
|
wv.raycastTarget = false;
|
|||
|
|
wv.amp = r.amp;
|
|||
|
|
wv.period = r.period;
|
|||
|
|
wv.waves = r.waves;
|
|||
|
|
wv.phase = r.phase;
|
|||
|
|
wv.billow = Mathf.Max(0f, r.billow);
|
|||
|
|
if (r.shade >= 0f) wv.shade = r.shade;
|
|||
|
|
wv.color = r.color;
|
|||
|
|
}
|
|||
|
|
// 불빛(관청 창 · 문 앞 번짐 · 석등) = 알파 깜박임
|
|||
|
|
foreach (var r in LobbyScene.All("light"))
|
|||
|
|
{
|
|||
|
|
var sp = Load(r);
|
|||
|
|
if (sp.Length == 0) continue;
|
|||
|
|
var img = ArtRect("Light_" + sp[0].name, r.x, r.y, r.w, r.h).gameObject.AddComponent<Image>();
|
|||
|
|
img.sprite = sp[0];
|
|||
|
|
img.raycastTarget = false;
|
|||
|
|
img.color = r.color;
|
|||
|
|
glows.Add(new Glow { g = img, row = r, baseA = r.color.a });
|
|||
|
|
}
|
|||
|
|
birdRow = LobbyScene.First("bird");
|
|||
|
|
petalRow = LobbyScene.First("petal");
|
|||
|
|
openedAt = Time.unscaledTime;
|
|||
|
|
if (birdRow != null)
|
|||
|
|
{
|
|||
|
|
var fr = Load(birdRow);
|
|||
|
|
for (int i = 0; i < Mathf.Max(0, birdRow.count) && fr.Length > 0; i++) birds.Add(NewMover("Bird" + i, fr));
|
|||
|
|
nextFlock = openedAt + birdRow.first;
|
|||
|
|
}
|
|||
|
|
if (petalRow != null)
|
|||
|
|
{
|
|||
|
|
var fr = Load(petalRow);
|
|||
|
|
for (int i = 0; i < Mathf.Max(0, petalRow.count) && fr.Length > 0; i++) petals.Add(NewMover("Petal" + i, fr));
|
|||
|
|
nextPetal = openedAt + petalRow.first;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Mover NewMover(string name, Sprite[] frames)
|
|||
|
|
{
|
|||
|
|
var rt = UiKit.Rect(name, front, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero);
|
|||
|
|
var img = rt.gameObject.AddComponent<Image>();
|
|||
|
|
img.raycastTarget = false;
|
|||
|
|
img.sprite = frames[0];
|
|||
|
|
rt.gameObject.SetActive(false);
|
|||
|
|
return new Mover { rt = rt, img = img, frames = frames };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void OnEnable()
|
|||
|
|
{
|
|||
|
|
// 화면을 다시 열면 새 떼 · 꽃잎 시계를 다시 시작(열자마자 하나는 보이게 = 표 f_first)
|
|||
|
|
float now = Time.unscaledTime;
|
|||
|
|
if (birdRow != null) nextFlock = now + birdRow.first;
|
|||
|
|
if (petalRow != null) nextPetal = now + petalRow.first;
|
|||
|
|
foreach (var m in birds) Hide(m);
|
|||
|
|
foreach (var m in petals) Hide(m);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void Hide(Mover m)
|
|||
|
|
{
|
|||
|
|
m.on = false;
|
|||
|
|
if (m.rt != null) m.rt.gameObject.SetActive(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static float Flicker(LobbyScene.Row r, float t)
|
|||
|
|
{
|
|||
|
|
// 촛불 = 느린 물결 + 빠른 떨림(두 사인의 합 · 0~1)
|
|||
|
|
float p = Mathf.Max(0.05f, r.period);
|
|||
|
|
float s = 0.62f * Mathf.Sin((t / p + r.phase) * Mathf.PI * 2f) + 0.38f * Mathf.Sin((t / p * 2.71f + r.phase * 1.7f) * Mathf.PI * 2f);
|
|||
|
|
return Mathf.Lerp(r.min, r.max, 0.5f + 0.5f * s);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Update()
|
|||
|
|
{
|
|||
|
|
float t = Time.unscaledTime;
|
|||
|
|
float dt = Mathf.Min(0.1f, Time.unscaledDeltaTime);
|
|||
|
|
float unit = area.rect.width / Mathf.Max(1f, artW);
|
|||
|
|
if (unit <= 0f) return;
|
|||
|
|
foreach (var g in glows)
|
|||
|
|
{
|
|||
|
|
var c = g.g.color;
|
|||
|
|
c.a = g.baseA * Flicker(g.row, t);
|
|||
|
|
g.g.color = c;
|
|||
|
|
}
|
|||
|
|
UpdateBirds(t, dt, unit);
|
|||
|
|
UpdatePetals(t, dt, unit);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void UpdateBirds(float t, float dt, float unit)
|
|||
|
|
{
|
|||
|
|
if (birdRow == null || birds.Count == 0) return;
|
|||
|
|
bool any = false;
|
|||
|
|
foreach (var b in birds) any |= b.on;
|
|||
|
|
if (!any && t >= nextFlock)
|
|||
|
|
{
|
|||
|
|
// 새 떼 1번 — 왼→오 또는 오→왼 · 앞장선 새 뒤로 비스듬히
|
|||
|
|
int n = Random.Range(1, birds.Count + 1);
|
|||
|
|
float dir = Random.value < 0.5f ? 1f : -1f;
|
|||
|
|
float y0 = birdRow.y + Random.value * birdRow.h;
|
|||
|
|
float sp = birdRow.speed * Random.Range(0.85f, 1.15f);
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
var b = birds[i];
|
|||
|
|
b.on = true;
|
|||
|
|
b.size = birdRow.w * Random.Range(0.85f, 1.05f);
|
|||
|
|
b.x = dir > 0f ? -b.size - i * b.size * 1.3f : artW + b.size + i * b.size * 1.3f;
|
|||
|
|
// 뒤따르는 새 = 위·아래 번갈아 조금씩(떼가 세로로 퍼지면 좌우 버튼 열 사이 틈에 걸림 · QA r1 M5)
|
|||
|
|
b.y = y0 + (i % 2 == 1 ? -1f : 1f) * ((i + 1) / 2) * b.size * 0.3f;
|
|||
|
|
b.vx = dir * sp;
|
|||
|
|
b.ph = Random.value;
|
|||
|
|
b.rt.gameObject.SetActive(true);
|
|||
|
|
b.rt.localScale = new Vector3(dir, 1f, 1f); // 그림 = 오른쪽을 봄
|
|||
|
|
}
|
|||
|
|
nextFlock = t + Random.Range(birdRow.gapMin, birdRow.gapMax);
|
|||
|
|
}
|
|||
|
|
foreach (var b in birds)
|
|||
|
|
{
|
|||
|
|
if (!b.on) continue;
|
|||
|
|
b.x += b.vx * dt;
|
|||
|
|
float bob = birdRow.amp * Mathf.Sin((t / Mathf.Max(0.05f, birdRow.period) * 0.5f + b.ph) * Mathf.PI * 2f);
|
|||
|
|
int f = (int)((t / Mathf.Max(0.05f, birdRow.period) + b.ph) * b.frames.Length) % b.frames.Length;
|
|||
|
|
b.img.sprite = b.frames[f];
|
|||
|
|
var sz = b.frames[f].rect.size;
|
|||
|
|
float k = b.size / Mathf.Max(1f, sz.x);
|
|||
|
|
b.rt.sizeDelta = sz * k * unit;
|
|||
|
|
b.rt.anchoredPosition = new Vector2(b.x * unit, -(b.y + bob) * unit);
|
|||
|
|
if ((b.vx > 0f && b.x > artW + b.size * 2f) || (b.vx < 0f && b.x < -b.size * 2f))
|
|||
|
|
{
|
|||
|
|
Hide(b);
|
|||
|
|
nextFlock = Mathf.Max(nextFlock, t + Random.Range(birdRow.gapMin, birdRow.gapMax));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void UpdatePetals(float t, float dt, float unit)
|
|||
|
|
{
|
|||
|
|
if (petalRow == null || petals.Count == 0) return;
|
|||
|
|
if (t >= nextPetal)
|
|||
|
|
{
|
|||
|
|
foreach (var p in petals)
|
|||
|
|
{
|
|||
|
|
if (p.on) continue;
|
|||
|
|
p.on = true;
|
|||
|
|
p.x = petalRow.x + Random.value * petalRow.w;
|
|||
|
|
p.y = petalRow.y + Random.value * petalRow.h;
|
|||
|
|
p.vy = petalRow.speed * Random.Range(0.8f, 1.2f);
|
|||
|
|
p.vx = petalRow.speed * Random.Range(0.35f, 0.7f);
|
|||
|
|
p.size = petalRow.scale * Random.Range(0.8f, 1.15f);
|
|||
|
|
p.ph = Random.value;
|
|||
|
|
p.t = 0f;
|
|||
|
|
p.life = Mathf.Max(0.5f, petalRow.period * Random.Range(0.8f, 1.2f));
|
|||
|
|
p.rot = Random.value * 360f;
|
|||
|
|
p.vrot = Random.Range(-90f, 90f);
|
|||
|
|
p.img.sprite = p.frames[Random.Range(0, p.frames.Length)];
|
|||
|
|
p.rt.gameObject.SetActive(true);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
nextPetal = t + Random.Range(petalRow.gapMin, petalRow.gapMax);
|
|||
|
|
}
|
|||
|
|
foreach (var p in petals)
|
|||
|
|
{
|
|||
|
|
if (!p.on) continue;
|
|||
|
|
p.t += dt;
|
|||
|
|
if (p.t >= p.life)
|
|||
|
|
{
|
|||
|
|
Hide(p);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
p.x += p.vx * dt;
|
|||
|
|
p.y += p.vy * dt;
|
|||
|
|
p.rot += p.vrot * dt;
|
|||
|
|
float sway = petalRow.amp * Mathf.Sin((p.t * 1.3f + p.ph) * Mathf.PI * 2f);
|
|||
|
|
var sz = p.img.sprite.rect.size;
|
|||
|
|
float k = p.size / Mathf.Max(1f, Mathf.Max(sz.x, sz.y));
|
|||
|
|
p.rt.sizeDelta = sz * k * unit;
|
|||
|
|
p.rt.anchoredPosition = new Vector2((p.x + sway) * unit, -p.y * unit);
|
|||
|
|
p.rt.localEulerAngles = new Vector3(0f, 0f, p.rot);
|
|||
|
|
// 나타남(f_min 초) · 사라짐(f_max 초) — PD 2026-11-11 「짧게 떨어지는 느낌으로 보이다 투명하게 없어지도록」
|
|||
|
|
float fin = Mathf.Max(0.05f, petalRow.min), fout = Mathf.Max(0.05f, petalRow.max);
|
|||
|
|
float a = Mathf.Clamp01(Mathf.Min(p.t / fin, (p.life - p.t) / fout));
|
|||
|
|
var c = p.img.color;
|
|||
|
|
if (!Mathf.Approximately(c.a, a))
|
|||
|
|
{
|
|||
|
|
c.a = a;
|
|||
|
|
p.img.color = c;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|