557 lines
24 KiB
C#
557 lines
24 KiB
C#
|
|
using System;
|
|||
|
|
using System.Collections;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Globalization;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.UI;
|
|||
|
|
|
|||
|
|
namespace TKS.OutGame
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 타이틀 화면(PD 2026-11-13 「타이틀 화면이 등장할 때 캐릭터들이 타이틀 화면의 현재 위치로 모이는 연출과 함께 시작 · 이후 대기 애니메이션처럼 움직이는 연출 ·
|
|||
|
|
/// 꾸미기 요소도 가벼운 애니메이션 · "Press Any Key" 깜박임 · 터치하면 다음 조작으로」).
|
|||
|
|
/// 시작 흐름 = BootFlow.TitleProvider(스플래시 → (받기) → 타이틀) → 누르면 검정 페이드 → 본영 로비.
|
|||
|
|
/// 값 = 표 Resources/UI/title_screen.csv(LoadingTable 형식) · 꾸미기 = title_ambient.csv(LobbyAmbient · 로비·내정과 같은 규칙) · C45.
|
|||
|
|
/// 층(P41): 루트 = Static(배경) → 그림 공간 Ambient(Frame · 뒤 연출 → 햇살·장수·착지 먼지 → 앞 불씨) → Ui(Frame · 로고·반짝이·글자 · 화면 공간).
|
|||
|
|
/// </summary>
|
|||
|
|
public class TitleScreen : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
public const string TablePath = "UI/title_screen";
|
|||
|
|
public const string AmbientPath = "UI/title_ambient";
|
|||
|
|
public const float ArtW = 1080f;
|
|||
|
|
public const float ArtH = 1920f;
|
|||
|
|
|
|||
|
|
/// <summary>테스트·검증 캡처가 메모리 세이브에서도 타이틀을 띄울 때(표 skip_memory 무시).</summary>
|
|||
|
|
public static bool ForceShow;
|
|||
|
|
|
|||
|
|
public static TitleScreen Current { get; private set; }
|
|||
|
|
|
|||
|
|
public class Hero
|
|||
|
|
{
|
|||
|
|
public string id;
|
|||
|
|
public RectTransform rt;
|
|||
|
|
public Image img;
|
|||
|
|
public Sprite[] frames;
|
|||
|
|
public LoadingTable.Row row;
|
|||
|
|
public float[] dur;
|
|||
|
|
public int blinkFrame = -1;
|
|||
|
|
public float blinkMin;
|
|||
|
|
public float blinkMax;
|
|||
|
|
public float blinkDur;
|
|||
|
|
public bool landed;
|
|||
|
|
public float idleT0;
|
|||
|
|
public float nextBlink;
|
|||
|
|
public float blinkEnd = -1f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class Spark
|
|||
|
|
{
|
|||
|
|
public RectTransform rt;
|
|||
|
|
public Image img;
|
|||
|
|
public LoadingTable.Row row;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class Puff
|
|||
|
|
{
|
|||
|
|
public RectTransform rt;
|
|||
|
|
public Image img;
|
|||
|
|
public float t0;
|
|||
|
|
public float baseW;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public Canvas RootCanvas { get; private set; }
|
|||
|
|
/// <summary>그림 공간(1080×1920 · 화면을 꽉 채우게 크기만 바꿈 · 배경·연출·장수의 좌표 = 그림 px).</summary>
|
|||
|
|
public RectTransform Art { get; private set; }
|
|||
|
|
public LobbyAmbient Ambient { get; private set; }
|
|||
|
|
/// <summary>무대 층(Frame · 장수·로고·글자) — 안의 StageArt = 그림 좌표 1080×1920 · 화면 안에 다 들어오는 배율(contain).</summary>
|
|||
|
|
public RectTransform Ui { get; private set; }
|
|||
|
|
public RectTransform Stage { get; private set; }
|
|||
|
|
public Image Logo { get; private set; }
|
|||
|
|
public Image Rays { get; private set; }
|
|||
|
|
public Text PressText { get; private set; }
|
|||
|
|
public readonly List<Hero> Heroes = new List<Hero>();
|
|||
|
|
/// <summary>등장 연출이 끝났는가(로고까지 내려앉음).</summary>
|
|||
|
|
public bool IntroDone { get; private set; }
|
|||
|
|
/// <summary>「Press Any Key」가 떠서 누르면 다음으로 가는가.</summary>
|
|||
|
|
public bool PressReady { get; private set; }
|
|||
|
|
public bool Leaving { get; private set; }
|
|||
|
|
/// <summary>등장 연출 시계(초 · 건너뛰면 끝 시각으로).</summary>
|
|||
|
|
public float Elapsed { get; private set; }
|
|||
|
|
public float IntroEnd { get; private set; }
|
|||
|
|
|
|||
|
|
Camera cam;
|
|||
|
|
Action onStart;
|
|||
|
|
List<LoadingTable.Row> rows;
|
|||
|
|
LoadingTable.Row rowRays;
|
|||
|
|
LoadingTable.Row rowLogo;
|
|||
|
|
LoadingTable.Row rowPress;
|
|||
|
|
LoadingTable.Row rowPuff;
|
|||
|
|
readonly List<Spark> sparks = new List<Spark>();
|
|||
|
|
readonly List<Puff> puffs = new List<Puff>();
|
|||
|
|
Sprite puffSprite;
|
|||
|
|
float last;
|
|||
|
|
Vector2 lastRoot;
|
|||
|
|
|
|||
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
|||
|
|
static void Register()
|
|||
|
|
{
|
|||
|
|
BootFlow.TitleProvider = Provide;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>BootFlow 훅 — 표가 있고(메모리 세이브면 표 skip_memory 에 따라 건너뜀) 타이틀을 띄우면 true.</summary>
|
|||
|
|
public static bool Provide(Camera cam, Action toLobby)
|
|||
|
|
{
|
|||
|
|
var list = LoadingTable.Load(TablePath);
|
|||
|
|
if (list.Count == 0) return false;
|
|||
|
|
var skip = list.Find(x => x.kind == "option" && x.id == "skip_memory");
|
|||
|
|
if (!ForceShow && skip != null && skip.a > 0f && SaveStore.MemoryOnly) return false;
|
|||
|
|
Show(cam, toLobby);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>타이틀을 띄운다(이미 있으면 닫고 새로) · onStart = 누름 → 페이드 뒤 부름(로비).</summary>
|
|||
|
|
public static TitleScreen Show(Camera cam, Action onStart)
|
|||
|
|
{
|
|||
|
|
if (Current != null) Current.Close();
|
|||
|
|
var go = new GameObject("TitleScreen");
|
|||
|
|
var t = go.AddComponent<TitleScreen>();
|
|||
|
|
Current = t;
|
|||
|
|
t.cam = cam;
|
|||
|
|
t.onStart = onStart;
|
|||
|
|
t.Build();
|
|||
|
|
return t;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
LoadingTable.Row Opt(string id)
|
|||
|
|
{
|
|||
|
|
return rows.Find(x => x.kind == "option" && x.id == id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
LoadingTable.Row First(string kind)
|
|||
|
|
{
|
|||
|
|
return rows.Find(x => x.kind == kind);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Build()
|
|||
|
|
{
|
|||
|
|
rows = LoadingTable.Load(TablePath);
|
|||
|
|
var oc = Opt("canvas");
|
|||
|
|
RootCanvas = UiLayer.Root(gameObject, cam, oc != null ? (int)oc.a : 850, oc != null && oc.b > 0f ? oc.b : 0.3f, UiLayer.Reference, UiRate.Static);
|
|||
|
|
var root = (RectTransform)transform;
|
|||
|
|
HideBelow(RootCanvas.sortingOrder);
|
|||
|
|
|
|||
|
|
// 그림 공간(배경 크기 그대로 · 화면을 덮는 배율만 Update 에서)
|
|||
|
|
var bgRow = First("bg");
|
|||
|
|
Art = UiKit.Rect("Art", root, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(ArtW, ArtH));
|
|||
|
|
var bg = Art.gameObject.AddComponent<Image>();
|
|||
|
|
bg.sprite = bgRow != null ? LoadingTable.Sprite(bgRow.sprite) : null;
|
|||
|
|
bg.color = bgRow != null ? bgRow.color : Color.black;
|
|||
|
|
bg.raycastTarget = true; // 아래 화면 누름을 막음
|
|||
|
|
FitArt();
|
|||
|
|
|
|||
|
|
// 꾸미기(로비·내정과 같은 LobbyAmbient · Frame 캔버스 1장 · 배경 그림에 붙음 = 배경과 같은 배율) — Back(빛·흙먼지) → Lineup(햇살) → Front(불씨)
|
|||
|
|
Ambient = LobbyAmbient.Build(Art, ArtW, ArtH, LobbyScene.Load(AmbientPath));
|
|||
|
|
var lineup = Ambient.Lineup;
|
|||
|
|
|
|||
|
|
// 무대(장수·착지 먼지·로고·반짝이·글자) = 그림 좌표 그대로 · 화면 안에 다 들어오는 배율(QA r1 B1~B4: 배경 배율(cover)로 키우면 20:9 = 로고·황충 활 잘림 · 4:3 = 로고가 얼굴을 가리고 관우 하체 잘림)
|
|||
|
|
Ui = UiLayer.Group("Stage", root, UiRate.Frame);
|
|||
|
|
Stage = UiKit.Rect("StageArt", Ui, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(ArtW, ArtH));
|
|||
|
|
lastRoot = Vector2.zero;
|
|||
|
|
FitArt();
|
|||
|
|
var stage = Stage;
|
|||
|
|
|
|||
|
|
rowRays = First("rays");
|
|||
|
|
if (rowRays != null)
|
|||
|
|
{
|
|||
|
|
Rays = ArtImage("Rays", lineup, rowRays.sprite, rowRays.x, rowRays.y, rowRays.w, rowRays.w);
|
|||
|
|
Rays.color = WithA(rowRays.color, 0f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var heroRows = rows.FindAll(x => x.kind == "hero");
|
|||
|
|
heroRows.Sort((p, q) => p.size.CompareTo(q.size));
|
|||
|
|
foreach (var r in heroRows)
|
|||
|
|
{
|
|||
|
|
var fr = LoadingTable.Frames(r.sprite);
|
|||
|
|
if (fr.Length == 0) continue;
|
|||
|
|
var h = new Hero { id = r.id, row = r, frames = fr };
|
|||
|
|
h.img = ArtImage("Hero_" + r.id, stage, null, r.x, r.y, r.w, r.h);
|
|||
|
|
h.img.sprite = fr[0];
|
|||
|
|
h.rt = (RectTransform)h.img.transform;
|
|||
|
|
h.dur = ParseList(r.text);
|
|||
|
|
var b = ParseList(r.reference);
|
|||
|
|
if (b.Length >= 4 && (int)b[0] < fr.Length)
|
|||
|
|
{
|
|||
|
|
h.blinkFrame = (int)b[0];
|
|||
|
|
h.blinkMin = b[1];
|
|||
|
|
h.blinkMax = b[2];
|
|||
|
|
h.blinkDur = b[3];
|
|||
|
|
}
|
|||
|
|
Heroes.Add(h);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
rowPuff = First("puff");
|
|||
|
|
if (rowPuff != null) puffSprite = LoadingTable.Sprite(rowPuff.sprite);
|
|||
|
|
|
|||
|
|
// 로고·글자 = 무대 위(장수와 같은 배율 · 로고 f_y = 그림 위에서 · 글자 f_y = 그림 아래에서)
|
|||
|
|
rowLogo = First("logo");
|
|||
|
|
if (rowLogo != null)
|
|||
|
|
{
|
|||
|
|
var sp = LoadingTable.Sprite(rowLogo.sprite);
|
|||
|
|
float h = sp != null ? rowLogo.w * sp.rect.height / sp.rect.width : rowLogo.w * 0.6f;
|
|||
|
|
var rt = UiKit.Rect("Logo", stage, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(rowLogo.x, -rowLogo.y), new Vector2(rowLogo.w, h));
|
|||
|
|
Logo = rt.gameObject.AddComponent<Image>();
|
|||
|
|
Logo.sprite = sp;
|
|||
|
|
Logo.raycastTarget = false;
|
|||
|
|
Logo.color = WithA(rowLogo.color, 0f);
|
|||
|
|
foreach (var r in rows.FindAll(x => x.kind == "sparkle"))
|
|||
|
|
{
|
|||
|
|
var s = UiKit.Rect("Spark_" + r.id, rt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(r.x, r.y), new Vector2(r.w, r.w));
|
|||
|
|
var im = s.gameObject.AddComponent<Image>();
|
|||
|
|
im.sprite = LoadingTable.Sprite(r.sprite);
|
|||
|
|
im.raycastTarget = false;
|
|||
|
|
im.color = WithA(r.color, 0f);
|
|||
|
|
sparks.Add(new Spark { rt = s, img = im, row = r });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
rowPress = First("press");
|
|||
|
|
if (rowPress != null)
|
|||
|
|
{
|
|||
|
|
PressText = UiKit.Label(stage, rowPress.text, rowPress.size, rowPress.color, TextAnchor.MiddleCenter,
|
|||
|
|
new Vector2(0.5f, 0f), new Vector2(0.5f, 0.5f), new Vector2(rowPress.x, rowPress.y), new Vector2(rowPress.w, rowPress.h));
|
|||
|
|
if (UISkin.TitleFont != null) PressText.font = UISkin.TitleFont;
|
|||
|
|
var o = PressText.gameObject.AddComponent<Outline>();
|
|||
|
|
o.effectColor = new Color(0.12f, 0.06f, 0.03f, 0.95f);
|
|||
|
|
o.effectDistance = new Vector2(3f, -3f);
|
|||
|
|
var sh = PressText.gameObject.AddComponent<Shadow>();
|
|||
|
|
sh.effectColor = new Color(0f, 0f, 0f, 0.55f);
|
|||
|
|
sh.effectDistance = new Vector2(0f, -6f);
|
|||
|
|
PressText.color = WithA(rowPress.color, 0f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 등장 연출이 끝나는 시각 = 가장 늦은 장수 착지 · 로고 내려앉음
|
|||
|
|
IntroEnd = 0f;
|
|||
|
|
foreach (var h in Heroes) IntroEnd = Mathf.Max(IntroEnd, h.row.phase + h.row.fps);
|
|||
|
|
if (rowLogo != null) IntroEnd = Mathf.Max(IntroEnd, rowLogo.phase + rowLogo.fps);
|
|||
|
|
Elapsed = 0f;
|
|||
|
|
last = Time.unscaledTime;
|
|||
|
|
Apply();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Image ArtImage(string name, Transform parent, string sprite, float x, float y, float w, float h)
|
|||
|
|
{
|
|||
|
|
// 그림 px(왼쪽 위 0,0 · 아래 +) → 그림 공간(부모 = 그림 크기 그대로 1080×1920)
|
|||
|
|
var rt = UiKit.Rect(name, parent, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(x, -y), new Vector2(w, h));
|
|||
|
|
var img = rt.gameObject.AddComponent<Image>();
|
|||
|
|
if (!string.IsNullOrEmpty(sprite)) img.sprite = LoadingTable.Sprite(sprite);
|
|||
|
|
img.raycastTarget = false;
|
|||
|
|
return img;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static float[] ParseList(string s)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(s)) return new float[0];
|
|||
|
|
var parts = s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
|||
|
|
var list = new List<float>();
|
|||
|
|
foreach (var p in parts)
|
|||
|
|
{
|
|||
|
|
float f;
|
|||
|
|
if (float.TryParse(p.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out f)) list.Add(f);
|
|||
|
|
}
|
|||
|
|
return list.ToArray();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static Color WithA(Color c, float a)
|
|||
|
|
{
|
|||
|
|
c.a = a;
|
|||
|
|
return c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void SetA(Graphic g, Color baseC, float a01)
|
|||
|
|
{
|
|||
|
|
var c = g.color;
|
|||
|
|
float a = baseC.a * Mathf.Clamp01(a01);
|
|||
|
|
if (!Mathf.Approximately(c.a, a))
|
|||
|
|
{
|
|||
|
|
c.a = a;
|
|||
|
|
g.color = c;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static float EaseOutBack(float p, float k)
|
|||
|
|
{
|
|||
|
|
p = Mathf.Clamp01(p) - 1f;
|
|||
|
|
return 1f + p * p * ((k + 1f) * p + k);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>그림 공간이 화면을 덮게(가로·세로 중 큰 배율 · 화면 밖으로 나가는 쪽은 잘림).</summary>
|
|||
|
|
void FitArt()
|
|||
|
|
{
|
|||
|
|
var root = (RectTransform)transform;
|
|||
|
|
var sz = root.rect.size;
|
|||
|
|
if (sz == lastRoot || sz.x <= 0f || sz.y <= 0f) return;
|
|||
|
|
lastRoot = sz;
|
|||
|
|
float s = Mathf.Max(sz.x / ArtW, sz.y / ArtH); // 배경 = 화면을 덮음(cover)
|
|||
|
|
Art.localScale = new Vector3(s, s, 1f);
|
|||
|
|
if (Stage != null)
|
|||
|
|
{
|
|||
|
|
float c = Mathf.Min(sz.x / ArtW, sz.y / ArtH); // 무대 = 화면 안에 다 들어옴(contain · 9:16 이면 배경과 같음)
|
|||
|
|
Stage.localScale = new Vector3(c, c, 1f);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>연출 시계를 정함(검증 캡처 · 테스트 · 착지 먼지는 그 순간 착지한 장수만).</summary>
|
|||
|
|
public void SetTime(float t)
|
|||
|
|
{
|
|||
|
|
Elapsed = Mathf.Max(0f, t);
|
|||
|
|
Apply();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>등장 연출을 끝 모습으로(누름 · 테스트).</summary>
|
|||
|
|
public void SkipIntro()
|
|||
|
|
{
|
|||
|
|
if (Elapsed < IntroEnd) Elapsed = IntroEnd;
|
|||
|
|
Apply();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>누름 — 등장 중이면 건너뜀(표 skip) · 글자가 떴으면 페이드 뒤 onStart.</summary>
|
|||
|
|
public void Press()
|
|||
|
|
{
|
|||
|
|
if (Leaving) return;
|
|||
|
|
if (!PressReady)
|
|||
|
|
{
|
|||
|
|
var sk = Opt("skip");
|
|||
|
|
if (sk == null || sk.a > 0f) SkipIntro();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
Leaving = true;
|
|||
|
|
var lv = Opt("leave");
|
|||
|
|
float fOut = lv != null ? lv.a : 0.3f, fHold = lv != null ? lv.b : 0.1f, fIn = lv != null ? lv.c : 0.3f;
|
|||
|
|
int order = lv != null && lv.size > 0 ? lv.size : 960;
|
|||
|
|
var fader = ScreenFader.Create(cam, order, lv != null ? lv.color : Color.black);
|
|||
|
|
fader.StartCoroutine(LeaveRoutine(fader, fOut, fHold, fIn));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
IEnumerator LeaveRoutine(ScreenFader fader, float fOut, float fHold, float fIn)
|
|||
|
|
{
|
|||
|
|
yield return fader.FadeTo(1f, fOut);
|
|||
|
|
var cb = onStart;
|
|||
|
|
Close();
|
|||
|
|
if (cb != null) cb();
|
|||
|
|
for (float w = 0f; w < fHold; w += Time.unscaledDeltaTime) yield return null;
|
|||
|
|
yield return fader.FadeTo(0f, fIn);
|
|||
|
|
Destroy(fader.gameObject);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Close()
|
|||
|
|
{
|
|||
|
|
RestoreBelow();
|
|||
|
|
if (Current == this) Current = null;
|
|||
|
|
Destroy(gameObject);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void OnDestroy()
|
|||
|
|
{
|
|||
|
|
RestoreBelow();
|
|||
|
|
if (Current == this) Current = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readonly List<Canvas> hidden = new List<Canvas>();
|
|||
|
|
|
|||
|
|
/// <summary>타이틀이 화면을 다 덮는 동안 그 아래 루트 캔버스(로비·재화 바 등)를 끔(QA r1 M5 · 저사양 · 안 보이는 화면을 계속 그림) · 페이드처럼 위 순서는 그대로.</summary>
|
|||
|
|
void HideBelow(int order)
|
|||
|
|
{
|
|||
|
|
foreach (var c in FindObjectsByType<Canvas>(FindObjectsSortMode.None))
|
|||
|
|
{
|
|||
|
|
if (c == null || !c.isRootCanvas || !c.enabled || c.gameObject == gameObject || c.sortingOrder >= order) continue;
|
|||
|
|
c.enabled = false;
|
|||
|
|
hidden.Add(c);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void RestoreBelow()
|
|||
|
|
{
|
|||
|
|
foreach (var c in hidden)
|
|||
|
|
if (c != null)
|
|||
|
|
c.enabled = true;
|
|||
|
|
hidden.Clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>끈 아래 캔버스 수(테스트).</summary>
|
|||
|
|
public int HiddenCount
|
|||
|
|
{
|
|||
|
|
get { return hidden.Count; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static bool AnyInput()
|
|||
|
|
{
|
|||
|
|
if (Input.anyKeyDown) return true; // 키보드 · 마우스 단추
|
|||
|
|
for (int i = 0; i < Input.touchCount; i++)
|
|||
|
|
if (Input.GetTouch(i).phase == TouchPhase.Began)
|
|||
|
|
return true;
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Update()
|
|||
|
|
{
|
|||
|
|
if (rows == null) return; // Play 중 재컴파일 = 직렬화 안 된 값이 비면 멈춤
|
|||
|
|
float now = Time.unscaledTime;
|
|||
|
|
float dt = Mathf.Min(0.1f, Mathf.Max(0f, now - last));
|
|||
|
|
last = now;
|
|||
|
|
Elapsed += dt;
|
|||
|
|
FitArt();
|
|||
|
|
if (!Leaving && AnyInput()) Press();
|
|||
|
|
Apply();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Apply()
|
|||
|
|
{
|
|||
|
|
float t = Elapsed;
|
|||
|
|
float now = Time.unscaledTime;
|
|||
|
|
// 햇살 = 천천히 돌며 숨
|
|||
|
|
if (Rays != null && rowRays != null)
|
|||
|
|
{
|
|||
|
|
Rays.rectTransform.localEulerAngles = new Vector3(0f, 0f, -rowRays.a * now);
|
|||
|
|
float breath = 0.5f + 0.5f * Mathf.Sin(now / Mathf.Max(0.1f, rowRays.fps) * Mathf.PI * 2f);
|
|||
|
|
float alpha = Mathf.Lerp(rowRays.b, rowRays.c, breath) * Mathf.Clamp01(t / Mathf.Max(0.05f, rowRays.phase + 0.5f));
|
|||
|
|
SetA(Rays, new Color(1f, 1f, 1f, 1f), alpha);
|
|||
|
|
}
|
|||
|
|
// 장수 = 화면 밖 → 제자리(EaseOutBack) → 착지 먼지 → 대기 프레임
|
|||
|
|
foreach (var h in Heroes)
|
|||
|
|
{
|
|||
|
|
var r = h.row;
|
|||
|
|
float p = r.fps > 0f ? (t - r.phase) / r.fps : (t >= r.phase ? 1f : 0f);
|
|||
|
|
if (p <= 0f)
|
|||
|
|
{
|
|||
|
|
SetA(h.img, r.color, 0f);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
float e = EaseOutBack(p, r.c);
|
|||
|
|
var target = Target(r);
|
|||
|
|
var from = target + new Vector2(r.a, -r.b);
|
|||
|
|
var pos = Vector2.LerpUnclamped(from, target, e);
|
|||
|
|
if (h.rt.anchoredPosition != pos) h.rt.anchoredPosition = pos;
|
|||
|
|
float sc = Mathf.LerpUnclamped(r.scale, 1f, e);
|
|||
|
|
var sv = new Vector3(sc, sc, 1f);
|
|||
|
|
if (h.rt.localScale != sv) h.rt.localScale = sv;
|
|||
|
|
SetA(h.img, r.color, Mathf.Clamp01(p * 4f));
|
|||
|
|
if (p >= 1f && !h.landed)
|
|||
|
|
{
|
|||
|
|
h.landed = true;
|
|||
|
|
h.idleT0 = now - UnityEngine.Random.value * Cycle(h);
|
|||
|
|
h.nextBlink = now + UnityEngine.Random.Range(h.blinkMin, h.blinkMax);
|
|||
|
|
if (t - r.phase - r.fps < 0.2f) SpawnPuff(h); // 건너뛰기로 한꺼번에 착지하면 먼지 없음
|
|||
|
|
}
|
|||
|
|
if (h.landed) IdleFrame(h, now);
|
|||
|
|
}
|
|||
|
|
UpdatePuffs(now);
|
|||
|
|
// 로고 = 크게 → 제자리(EaseOutBack) · 그 뒤 둥실 · 반짝이
|
|||
|
|
if (Logo != null && rowLogo != null)
|
|||
|
|
{
|
|||
|
|
float p = rowLogo.fps > 0f ? (t - rowLogo.phase) / rowLogo.fps : 1f;
|
|||
|
|
float e = p <= 0f ? 0f : EaseOutBack(p, 1.7f);
|
|||
|
|
float sc = p <= 0f ? rowLogo.scale : Mathf.LerpUnclamped(rowLogo.scale, 1f, e);
|
|||
|
|
var sv = new Vector3(sc, sc, 1f);
|
|||
|
|
if (Logo.rectTransform.localScale != sv) Logo.rectTransform.localScale = sv;
|
|||
|
|
SetA(Logo, rowLogo.color, Mathf.Clamp01(p * 3f));
|
|||
|
|
float fl = p >= 1f ? rowLogo.a * Mathf.Sin((t - rowLogo.phase - rowLogo.fps) / Mathf.Max(0.1f, rowLogo.b) * Mathf.PI * 2f) : 0f;
|
|||
|
|
var lp = new Vector2(rowLogo.x, -rowLogo.y + fl);
|
|||
|
|
if (Logo.rectTransform.anchoredPosition != lp) Logo.rectTransform.anchoredPosition = lp;
|
|||
|
|
foreach (var s in sparks)
|
|||
|
|
{
|
|||
|
|
float k = p >= 1f ? Mathf.Repeat((t - rowLogo.phase - rowLogo.fps) / Mathf.Max(0.1f, s.row.a) + s.row.phase, 1f) : 0f;
|
|||
|
|
float tw = p >= 1f ? Mathf.Sin(k * Mathf.PI) : 0f; // 0 → 1 → 0
|
|||
|
|
tw = tw * tw;
|
|||
|
|
SetA(s.img, s.row.color, tw);
|
|||
|
|
var ss = new Vector3(0.4f + 0.6f * tw, 0.4f + 0.6f * tw, 1f);
|
|||
|
|
if (s.rt.localScale != ss) s.rt.localScale = ss;
|
|||
|
|
s.rt.localEulerAngles = new Vector3(0f, 0f, k * 90f);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
IntroDone = t >= IntroEnd;
|
|||
|
|
// 「Press Any Key」 = 등장 연출 끝 + f_phase 초 뒤부터 깜박임 · 그때부터 누르면 다음으로
|
|||
|
|
if (PressText != null && rowPress != null)
|
|||
|
|
{
|
|||
|
|
float since = t - IntroEnd - rowPress.phase;
|
|||
|
|
PressReady = since >= 0f;
|
|||
|
|
float blink = 0.5f + 0.5f * Mathf.Cos(Mathf.Max(0f, since) / Mathf.Max(0.1f, rowPress.a) * Mathf.PI * 2f);
|
|||
|
|
SetA(PressText, rowPress.color, since < 0f ? 0f : Mathf.Lerp(rowPress.b, 1f, blink) * Mathf.Clamp01(since / 0.25f));
|
|||
|
|
}
|
|||
|
|
else PressReady = IntroDone;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>장수 제자리(그림 공간) — 폭·높이가 홀수면 반 칸 밀어 모서리가 정수 픽셀에(QA r1 M6 · 선 흐림 방지).</summary>
|
|||
|
|
public static Vector2 Target(LoadingTable.Row r)
|
|||
|
|
{
|
|||
|
|
float ox = Mathf.RoundToInt(r.w) % 2 == 1 ? 0.5f : 0f;
|
|||
|
|
float oy = Mathf.RoundToInt(r.h) % 2 == 1 ? 0.5f : 0f;
|
|||
|
|
return new Vector2(r.x + ox, -(r.y + oy));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
float Cycle(Hero h)
|
|||
|
|
{
|
|||
|
|
float c = 0f;
|
|||
|
|
foreach (var d in h.dur) c += d;
|
|||
|
|
return Mathf.Max(0.1f, c);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>대기 = 자세 0→1→2→3 한 방향 반복(R22 · 표 유지 초) · 깜빡임 = 쉼 자세(0)일 때만.</summary>
|
|||
|
|
void IdleFrame(Hero h, float now)
|
|||
|
|
{
|
|||
|
|
int n = Mathf.Min(h.dur.Length, h.blinkFrame >= 0 ? h.blinkFrame : h.frames.Length);
|
|||
|
|
int k = 0;
|
|||
|
|
if (n > 1)
|
|||
|
|
{
|
|||
|
|
float c = Cycle(h);
|
|||
|
|
float u = Mathf.Repeat(now - h.idleT0, c);
|
|||
|
|
for (k = 0; k < n - 1; k++)
|
|||
|
|
{
|
|||
|
|
if (u < h.dur[k]) break;
|
|||
|
|
u -= h.dur[k];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (h.blinkFrame >= 0)
|
|||
|
|
{
|
|||
|
|
if (now >= h.blinkEnd && now >= h.nextBlink && k == 0)
|
|||
|
|
{
|
|||
|
|
h.blinkEnd = now + h.blinkDur;
|
|||
|
|
h.nextBlink = now + UnityEngine.Random.Range(h.blinkMin, h.blinkMax);
|
|||
|
|
}
|
|||
|
|
if (now < h.blinkEnd && k == 0) k = h.blinkFrame;
|
|||
|
|
}
|
|||
|
|
var sp = h.frames[Mathf.Clamp(k, 0, h.frames.Length - 1)];
|
|||
|
|
if (h.img.sprite != sp) h.img.sprite = sp;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void SpawnPuff(Hero h)
|
|||
|
|
{
|
|||
|
|
if (puffSprite == null || rowPuff == null) return;
|
|||
|
|
var r = h.row;
|
|||
|
|
float w = r.w * rowPuff.w;
|
|||
|
|
float hh = w * puffSprite.rect.height / puffSprite.rect.width;
|
|||
|
|
var img = ArtImage("Puff_" + h.id, Stage, null, r.x, r.y + r.h * rowPuff.c, w, hh);
|
|||
|
|
img.sprite = puffSprite;
|
|||
|
|
img.color = WithA(rowPuff.color, 0f);
|
|||
|
|
img.transform.SetSiblingIndex(h.rt.GetSiblingIndex() + 1); // 그 장수 바로 앞(발을 덮는 먼지 · QA r1 U4 = 뒤에 두면 바닥과 같은 색이라 안 보임)
|
|||
|
|
puffs.Add(new Puff { rt = img.rectTransform, img = img, t0 = Time.unscaledTime, baseW = w });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void UpdatePuffs(float now)
|
|||
|
|
{
|
|||
|
|
for (int i = puffs.Count - 1; i >= 0; i--)
|
|||
|
|
{
|
|||
|
|
var p = puffs[i];
|
|||
|
|
float u = (now - p.t0) / Mathf.Max(0.05f, rowPuff.a);
|
|||
|
|
if (u >= 1f || p.img == null)
|
|||
|
|
{
|
|||
|
|
if (p.img != null) Destroy(p.img.gameObject);
|
|||
|
|
puffs.RemoveAt(i);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
float s = Mathf.Lerp(0.6f, 1.25f, 1f - (1f - u) * (1f - u));
|
|||
|
|
p.rt.localScale = new Vector3(s, s, 1f);
|
|||
|
|
SetA(p.img, rowPuff.color, rowPuff.b * (1f - u));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|