694 lines
28 KiB
C#
694 lines
28 KiB
C#
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Globalization;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.UI;
|
|||
|
|
|
|||
|
|
namespace TKS.OutGame
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 로딩 화면 종류(PD 2026-11-13 「원격 다운로드가 필요한 시점 · 로딩이 필요한 시점 2 경우에 사용」) — 표 s_set 이 모드마다 다른 화면을 고른다.
|
|||
|
|
/// Download = 원격 리소스 받기(배경 + 위 타이틀 로고 + 조운·병졸 + 용량) · Loading = 그 밖의 로딩(검은 화면 + 유비·관우·장비 말 타고 질주 · 심플).
|
|||
|
|
/// </summary>
|
|||
|
|
public enum LoadingMode
|
|||
|
|
{
|
|||
|
|
Download,
|
|||
|
|
Loading
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 로딩·리소스 다운로드 화면(PD 2026-11-13) — 진행 바 위에서 무리가 제자리로 달리고(프레임 반복) 말 뒤로 먼지가 일어 흩어진다.
|
|||
|
|
/// 다운로드 = 풀 덮인 땅 바 위를 조운(아두를 품음)이 백마로 질주 · 뒤를 병졸이 따름 · 위에 타이틀 로고 · 진행률·용량.
|
|||
|
|
/// 로딩 = 배경 없이 검은 화면 · 단순 바 위를 유비·관우·장비가 각자 말을 타고 함께 질주 · 「Loading...」.
|
|||
|
|
/// 값(자리·크기·속도·문구 · 세트별) = 표 Resources/UI/loading_screen.csv(LoadingTable · s_set = download | loading | 빈칸 = 공통 · C45) · 그림 = Resources/UI/Loading/.
|
|||
|
|
/// 캔버스(P41 · 갱신 빈도별): 루트 Static(배경 · 로고 · 바 틀) → Live = Frame(바 채움 · 먼지 · 달리는 무리 · 점 3개) → Info = Event(상태 문구 · 진행률 · 용량).
|
|||
|
|
/// 연결점: Show(cam, mode) → SetProgress(0~1) 또는 SetDownload(받은 바이트, 전체 바이트) → Hide(). 모드를 바꾸면 그 세트로 다시 만든다(같은 객체).
|
|||
|
|
/// 게임에는 아직 리소스 다운로드·씬 비동기 로딩 흐름이 없다(Addressables 미도입 · 2026-11-13 실측) — 부르는 자리는 그 흐름을 만들 때 연결.
|
|||
|
|
/// </summary>
|
|||
|
|
public class LoadingScreen : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
public static LoadingScreen Current { get; private set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 이 화면의 시계(초) — 기본 = Time.unscaledTime(로딩 중 timeScale 과 무관). 확인 캡처만 Time.time + Time.captureDeltaTime 으로 바꿔
|
|||
|
|
/// 원본 크기 화면 읽기가 느려도 캡처 한 장 = 게임 1/16 초(영상 간격 = 실제 재생 · UICaptureDriver.LoadingShots).
|
|||
|
|
/// </summary>
|
|||
|
|
public static System.Func<float> Clock;
|
|||
|
|
|
|||
|
|
public static float Now
|
|||
|
|
{
|
|||
|
|
get { return Clock != null ? Clock() : Time.unscaledTime; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>모드 → 표 세트 이름.</summary>
|
|||
|
|
public static string SetOf(LoadingMode m)
|
|||
|
|
{
|
|||
|
|
return m == LoadingMode.Download ? "download" : "loading";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
float lastNow = -1f;
|
|||
|
|
Camera cam;
|
|||
|
|
|
|||
|
|
public LoadingMode Mode { get; private set; }
|
|||
|
|
/// <summary>지금 화면을 만든 표 세트(download | loading).</summary>
|
|||
|
|
public string Set { get; private set; }
|
|||
|
|
/// <summary>목표 진행률(0~1 · 마지막으로 받은 값).</summary>
|
|||
|
|
public float Target { get; private set; }
|
|||
|
|
/// <summary>화면에 보이는 진행률(목표를 향해 부드럽게 따라감).</summary>
|
|||
|
|
public float Shown { get; private set; }
|
|||
|
|
public long DoneBytes { get; private set; }
|
|||
|
|
public long TotalBytes { get; private set; }
|
|||
|
|
|
|||
|
|
public Canvas RootCanvas { get; private set; }
|
|||
|
|
public RectTransform Live { get; private set; }
|
|||
|
|
public RectTransform Info { get; private set; }
|
|||
|
|
public Image Fill { get; private set; }
|
|||
|
|
public Image Logo { get; private set; }
|
|||
|
|
public Text StatusText { get; private set; }
|
|||
|
|
public Text PercentText { get; private set; }
|
|||
|
|
public Text SizeText { get; private set; }
|
|||
|
|
public Text DotsText { get; private set; }
|
|||
|
|
public readonly List<UiSpriteLoop> Runners = new List<UiSpriteLoop>();
|
|||
|
|
public readonly List<LoadingDust> Dusts = new List<LoadingDust>();
|
|||
|
|
|
|||
|
|
LoadingTable.Row rowStatus;
|
|||
|
|
LoadingTable.Row rowPercent;
|
|||
|
|
LoadingTable.Row rowSize;
|
|||
|
|
LoadingTable.Row rowDots;
|
|||
|
|
float smooth = 1.5f;
|
|||
|
|
float dotStep = 0.4f;
|
|||
|
|
int dotMax = 3;
|
|||
|
|
int shownPct = -1;
|
|||
|
|
int dots = -1;
|
|||
|
|
|
|||
|
|
/// <summary>화면을 띄운다(이미 있으면 그것을 씀 · 모드가 다른 세트면 다시 만듦) · cam = 아웃게임과 같은 카메라(Screen Space - Camera · 캡처에도 찍힘).</summary>
|
|||
|
|
public static LoadingScreen Show(Camera cam, LoadingMode mode)
|
|||
|
|
{
|
|||
|
|
if (Current == null)
|
|||
|
|
{
|
|||
|
|
var go = new GameObject("LoadingScreen");
|
|||
|
|
Current = go.AddComponent<LoadingScreen>();
|
|||
|
|
Current.cam = cam;
|
|||
|
|
}
|
|||
|
|
Current.SetMode(mode);
|
|||
|
|
Current.gameObject.SetActive(true);
|
|||
|
|
return Current;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static void HideCurrent()
|
|||
|
|
{
|
|||
|
|
if (Current != null) Current.Hide();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Hide()
|
|||
|
|
{
|
|||
|
|
if (Current == this) Current = null;
|
|||
|
|
Destroy(gameObject);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void OnDestroy()
|
|||
|
|
{
|
|||
|
|
if (Current == this) Current = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>진행률 0~1 · instant = 부드럽게 따라가지 않고 바로(첫 표시 · 테스트).</summary>
|
|||
|
|
public void SetProgress(float p01, bool instant = false)
|
|||
|
|
{
|
|||
|
|
Target = Mathf.Clamp01(p01);
|
|||
|
|
if (instant) Shown = Target;
|
|||
|
|
Apply();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>받은 바이트 / 전체 바이트 → 용량 글자(그 세트에 size 줄이 있을 때) + 진행률(전체 0 이면 진행률은 그대로).</summary>
|
|||
|
|
public void SetDownload(long doneBytes, long totalBytes, bool instant = false)
|
|||
|
|
{
|
|||
|
|
DoneBytes = System.Math.Max(0L, doneBytes);
|
|||
|
|
TotalBytes = System.Math.Max(0L, totalBytes);
|
|||
|
|
ApplySize();
|
|||
|
|
if (TotalBytes > 0) SetProgress((float)((double)DoneBytes / TotalBytes), instant);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>모드 바꾸기 — 표 세트가 다르면 같은 객체 안에서 화면을 다시 만든다(진행률·용량 값은 유지).</summary>
|
|||
|
|
public void SetMode(LoadingMode m)
|
|||
|
|
{
|
|||
|
|
Mode = m;
|
|||
|
|
string set = SetOf(m);
|
|||
|
|
if (Set != set) Rebuild(set);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>1 MB = 1024² 바이트 · 소수 한 자리.</summary>
|
|||
|
|
public static string Bytes(long b)
|
|||
|
|
{
|
|||
|
|
double mb = b / (1024.0 * 1024.0);
|
|||
|
|
if (mb >= 1024.0) return (mb / 1024.0).ToString("0.0", CultureInfo.InvariantCulture) + " GB";
|
|||
|
|
if (mb >= 0.1) return mb.ToString("0.0", CultureInfo.InvariantCulture) + " MB";
|
|||
|
|
return (b / 1024.0).ToString("0", CultureInfo.InvariantCulture) + " KB";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Rebuild(string set)
|
|||
|
|
{
|
|||
|
|
// 이전 세트의 자식은 지움(Destroy = 이번 프레임 그리기 전에 사라짐) · 루트 캔버스 컴포넌트는 그대로 다시 씀
|
|||
|
|
for (int i = transform.childCount - 1; i >= 0; i--)
|
|||
|
|
{
|
|||
|
|
var c = transform.GetChild(i).gameObject;
|
|||
|
|
c.SetActive(false);
|
|||
|
|
Destroy(c);
|
|||
|
|
}
|
|||
|
|
Runners.Clear();
|
|||
|
|
Dusts.Clear();
|
|||
|
|
Fill = null;
|
|||
|
|
Logo = null;
|
|||
|
|
StatusText = PercentText = SizeText = DotsText = null;
|
|||
|
|
rowStatus = rowPercent = rowSize = rowDots = null;
|
|||
|
|
shownPct = -1;
|
|||
|
|
dots = -1;
|
|||
|
|
Set = set;
|
|||
|
|
Build(set);
|
|||
|
|
ApplySize();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Build(string set)
|
|||
|
|
{
|
|||
|
|
var opt = LoadingTable.First("option", "canvas", set);
|
|||
|
|
int order = opt != null ? (int)opt.a : 900;
|
|||
|
|
float plane = opt != null && opt.b > 0f ? opt.b : 0.3f;
|
|||
|
|
RootCanvas = UiLayer.Root(gameObject, cam, order, plane, UiLayer.Reference, UiRate.Static);
|
|||
|
|
var o = LoadingTable.First("option", "smooth", set);
|
|||
|
|
if (o != null && o.a > 0f) smooth = o.a;
|
|||
|
|
o = LoadingTable.First("option", "dots", set);
|
|||
|
|
if (o != null)
|
|||
|
|
{
|
|||
|
|
if (o.a > 0f) dotStep = o.a;
|
|||
|
|
if (o.size > 0) dotMax = o.size;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var root = (RectTransform)transform;
|
|||
|
|
// 배경(화면을 덮음 · 아래 게임 화면 누름을 막음) · 그림이 없으면 색만(로딩 = 검정)
|
|||
|
|
foreach (var r in LoadingTable.All("bg", set))
|
|||
|
|
{
|
|||
|
|
var img = UiKit.Fill("Bg", root).gameObject.AddComponent<Image>();
|
|||
|
|
img.sprite = LoadingTable.Sprite(r.sprite);
|
|||
|
|
img.color = r.color;
|
|||
|
|
img.raycastTarget = true;
|
|||
|
|
if (img.sprite != null)
|
|||
|
|
{
|
|||
|
|
var fit = img.gameObject.AddComponent<AspectRatioFitter>();
|
|||
|
|
fit.aspectMode = AspectRatioFitter.AspectMode.EnvelopeParent;
|
|||
|
|
fit.aspectRatio = img.sprite.rect.width / img.sprite.rect.height;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 타이틀 로고(다운로드 · 비율 유지 · f_w×f_h 상자 안)
|
|||
|
|
foreach (var r in LoadingTable.All("logo", set))
|
|||
|
|
{
|
|||
|
|
Logo = Pic("Logo", root, r.sprite);
|
|||
|
|
Place(Logo, r);
|
|||
|
|
Logo.preserveAspect = true;
|
|||
|
|
}
|
|||
|
|
foreach (var r in LoadingTable.All("bar", set))
|
|||
|
|
Place(Pic("Bar", root, r.sprite), r);
|
|||
|
|
|
|||
|
|
// 매 프레임: 바 채움 · 먼지 · 달리는 무리 · 점
|
|||
|
|
Live = UiLayer.Group("Live", root, UiRate.Frame);
|
|||
|
|
foreach (var r in LoadingTable.All("fill", set))
|
|||
|
|
{
|
|||
|
|
Fill = Pic("Fill", Live, r.sprite);
|
|||
|
|
Place(Fill, r);
|
|||
|
|
Fill.type = Image.Type.Filled;
|
|||
|
|
Fill.fillMethod = Image.FillMethod.Horizontal;
|
|||
|
|
Fill.fillOrigin = (int)Image.OriginHorizontal.Left;
|
|||
|
|
Fill.fillAmount = 0f;
|
|||
|
|
}
|
|||
|
|
var dustRoot = UiKit.Fill("Dust", Live);
|
|||
|
|
var runnerRoot = UiKit.Fill("Runners", Live);
|
|||
|
|
var runnerByID = new Dictionary<string, UiSpriteLoop>();
|
|||
|
|
foreach (var r in LoadingTable.All("runner", set))
|
|||
|
|
{
|
|||
|
|
var frames = LoadingTable.Frames(r.sprite);
|
|||
|
|
if (frames.Length == 0) continue;
|
|||
|
|
var img = Pic("Runner_" + r.id, runnerRoot, null);
|
|||
|
|
img.sprite = frames[0];
|
|||
|
|
var rt = (RectTransform)img.transform;
|
|||
|
|
rt.pivot = new Vector2(0.5f, 0f); // 발(그림 아래 가운데) = 표 좌표
|
|||
|
|
float aspect = frames[0].rect.width / frames[0].rect.height;
|
|||
|
|
rt.sizeDelta = new Vector2(r.h * aspect * Mathf.Abs(r.scale), r.h * Mathf.Abs(r.scale));
|
|||
|
|
rt.anchoredPosition = Pos(r.x, r.y);
|
|||
|
|
if (r.scale < 0f) rt.localScale = new Vector3(-1f, 1f, 1f);
|
|||
|
|
var loop = UiSpriteLoop.Attach(img, frames, r.fps, r.phase);
|
|||
|
|
loop.name = "Runner_" + r.id;
|
|||
|
|
Runners.Add(loop);
|
|||
|
|
runnerByID[r.id] = loop;
|
|||
|
|
}
|
|||
|
|
foreach (var r in LoadingTable.All("dust", set))
|
|||
|
|
{
|
|||
|
|
UiSpriteLoop host;
|
|||
|
|
runnerByID.TryGetValue(r.reference, out host);
|
|||
|
|
// 먼지 = 달리는 무리 전부의 뒤(앞 말 먼지가 뒤따르는 말 다리를 덮지 않게 · QA r3 M1 · r2 M2 · PD 2026-11-13 「경미한 수정사항 모두 반영」)
|
|||
|
|
var d = LoadingDust.Create(dustRoot, r, host, Pos);
|
|||
|
|
if (d == null) continue;
|
|||
|
|
Dusts.Add(d);
|
|||
|
|
}
|
|||
|
|
// 진행률·상태 글자(값이 바뀔 때만)
|
|||
|
|
Info = UiLayer.Group("Info", root, UiRate.Event);
|
|||
|
|
rowStatus = LoadingTable.First("text", "status", set);
|
|||
|
|
rowPercent = LoadingTable.First("text", "percent", set);
|
|||
|
|
rowSize = LoadingTable.First("text", "size", set);
|
|||
|
|
rowDots = LoadingTable.First("text", "dots", set);
|
|||
|
|
if (rowStatus != null) StatusText = Txt("Status", Info, rowStatus);
|
|||
|
|
if (rowPercent != null) PercentText = Txt("Percent", Info, rowPercent);
|
|||
|
|
if (rowSize != null) SizeText = Txt("Size", Info, rowSize);
|
|||
|
|
if (rowDots != null)
|
|||
|
|
{
|
|||
|
|
DotsText = Txt("Dots", Live, rowDots);
|
|||
|
|
DotsText.alignment = TextAnchor.MiddleLeft;
|
|||
|
|
var drt = (RectTransform)DotsText.transform;
|
|||
|
|
drt.pivot = new Vector2(0f, 0.5f);
|
|||
|
|
DotsText.text = "";
|
|||
|
|
// 점 = 상태 문구 오른쪽 끝 바로 뒤(문구는 가운데 정렬)
|
|||
|
|
if (StatusText != null)
|
|||
|
|
{
|
|||
|
|
var srt = (RectTransform)StatusText.transform;
|
|||
|
|
drt.anchoredPosition = new Vector2(srt.anchoredPosition.x + StatusText.preferredWidth * 0.5f + rowDots.x, srt.anchoredPosition.y + rowDots.y);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Apply();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Update()
|
|||
|
|
{
|
|||
|
|
if (RootCanvas == null) return; // Play 중 재컴파일 = 직렬화 안 된 값이 비면 멈춤
|
|||
|
|
float now = Now;
|
|||
|
|
float dt = lastNow < 0f ? 0f : Mathf.Max(0f, now - lastNow);
|
|||
|
|
lastNow = now;
|
|||
|
|
if (!Mathf.Approximately(Shown, Target))
|
|||
|
|
{
|
|||
|
|
Shown = Mathf.MoveTowards(Shown, Target, smooth * dt);
|
|||
|
|
Apply();
|
|||
|
|
}
|
|||
|
|
if (DotsText != null && rowDots != null)
|
|||
|
|
{
|
|||
|
|
int n = (int)(now / dotStep) % (dotMax + 1);
|
|||
|
|
if (n != dots)
|
|||
|
|
{
|
|||
|
|
dots = n;
|
|||
|
|
DotsText.text = new string(rowDots.text.Length > 0 ? rowDots.text[0] : '.', n);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Apply()
|
|||
|
|
{
|
|||
|
|
if (Fill != null && !Mathf.Approximately(Fill.fillAmount, Shown)) Fill.fillAmount = Shown;
|
|||
|
|
int pct = Mathf.FloorToInt(Shown * 100f + 0.0001f);
|
|||
|
|
if (PercentText != null && rowPercent != null && pct != shownPct)
|
|||
|
|
{
|
|||
|
|
shownPct = pct;
|
|||
|
|
PercentText.text = string.Format(CultureInfo.InvariantCulture, rowPercent.text, pct);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void ApplySize()
|
|||
|
|
{
|
|||
|
|
if (SizeText != null && rowSize != null)
|
|||
|
|
SizeText.text = string.Format(CultureInfo.InvariantCulture, rowSize.text, Bytes(DoneBytes), Bytes(TotalBytes));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 만들기 도움 ----------
|
|||
|
|
|
|||
|
|
/// <summary>표 좌표(기준 화면 1080×1920 px · 왼쪽 위 0,0 · 아래로 +) → 가운데 기준 anchoredPosition.</summary>
|
|||
|
|
static Vector2 Pos(float x, float y)
|
|||
|
|
{
|
|||
|
|
return new Vector2(x - UiLayer.Reference.x * 0.5f, UiLayer.Reference.y * 0.5f - y);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static Image Pic(string name, Transform parent, string sprite)
|
|||
|
|
{
|
|||
|
|
var rt = UiKit.Rect(name, parent, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(100f, 100f));
|
|||
|
|
var img = rt.gameObject.AddComponent<Image>();
|
|||
|
|
if (!string.IsNullOrEmpty(sprite)) img.sprite = LoadingTable.Sprite(sprite);
|
|||
|
|
img.raycastTarget = false;
|
|||
|
|
return img;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void Place(Image img, LoadingTable.Row r)
|
|||
|
|
{
|
|||
|
|
var rt = (RectTransform)img.transform;
|
|||
|
|
rt.anchoredPosition = Pos(r.x, r.y);
|
|||
|
|
rt.sizeDelta = new Vector2(r.w, r.h);
|
|||
|
|
img.color = r.color;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static Text Txt(string name, Transform parent, LoadingTable.Row r)
|
|||
|
|
{
|
|||
|
|
var t = UiKit.Label(parent, r.text, r.size, r.color, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Pos(r.x, r.y), new Vector2(r.w, r.h));
|
|||
|
|
t.name = name;
|
|||
|
|
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|||
|
|
UiKit.Title(t);
|
|||
|
|
if (r.b > 0f)
|
|||
|
|
{
|
|||
|
|
Color sc;
|
|||
|
|
if (!ColorUtility.TryParseHtmlString(r.reference, out sc)) sc = UiKit.Stroke;
|
|||
|
|
UISkin.Stroke(t, sc, r.b);
|
|||
|
|
}
|
|||
|
|
return t;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>UI 이미지 프레임 반복(로딩 화면 달리기 · 고른 간격 fps · 시작 위상 초) — 매 프레임 바뀌므로 Frame 캔버스 안에서만(UiLayer.Isolate).</summary>
|
|||
|
|
public class UiSpriteLoop : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
public Image Img { get; private set; }
|
|||
|
|
public Sprite[] Frames { get; private set; }
|
|||
|
|
public float Fps { get; private set; }
|
|||
|
|
public int Index { get; private set; }
|
|||
|
|
float t0;
|
|||
|
|
|
|||
|
|
public static UiSpriteLoop Attach(Image img, Sprite[] frames, float fps, float phase)
|
|||
|
|
{
|
|||
|
|
if (img == null || frames == null || frames.Length == 0) return null;
|
|||
|
|
var a = img.gameObject.AddComponent<UiSpriteLoop>();
|
|||
|
|
a.Img = img;
|
|||
|
|
a.Frames = frames;
|
|||
|
|
a.Fps = fps > 0f ? fps : 12f;
|
|||
|
|
a.t0 = LoadingScreen.Now - phase;
|
|||
|
|
UiLayer.Isolate(img.gameObject);
|
|||
|
|
return a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>지금 시각의 프레임 번호(0 ~ n-1).</summary>
|
|||
|
|
public int FrameAt(float now)
|
|||
|
|
{
|
|||
|
|
int n = Frames.Length;
|
|||
|
|
int k = Mathf.FloorToInt((now - t0) * Fps) % n;
|
|||
|
|
return k < 0 ? k + n : k;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Update()
|
|||
|
|
{
|
|||
|
|
if (Img == null || Frames == null || Frames.Length == 0) return;
|
|||
|
|
Index = FrameAt(LoadingScreen.Now);
|
|||
|
|
var sp = Frames[Index];
|
|||
|
|
if (Img.sprite != sp) Img.sprite = sp;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 말 뒤 먼지(표 dust 줄) — 달리는 그림의 정해진 프레임(발굽이 땅을 찰 때 · s_text 「on:3,10」)마다 또는 초당 f_fps 개로 먼지 덩이를 내고,
|
|||
|
|
/// 덩이는 뒤(왼쪽)로 밀리며 떠올라 커지고 옅어진다. 덩이 수 = i_size 개 재사용(만들고 지우지 않음).
|
|||
|
|
/// </summary>
|
|||
|
|
public class LoadingDust : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
class Puff
|
|||
|
|
{
|
|||
|
|
public RectTransform rt;
|
|||
|
|
public Image img;
|
|||
|
|
public float t = -1f;
|
|||
|
|
public float x;
|
|||
|
|
public float y;
|
|||
|
|
public float s;
|
|||
|
|
public float rot;
|
|||
|
|
public float vx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readonly List<Puff> pool = new List<Puff>();
|
|||
|
|
LoadingTable.Row row;
|
|||
|
|
UiSpriteLoop host;
|
|||
|
|
Vector2 origin;
|
|||
|
|
Sprite[] sprites;
|
|||
|
|
int[] onFrames;
|
|||
|
|
int lastFrame = -1;
|
|||
|
|
float acc;
|
|||
|
|
int next;
|
|||
|
|
float lastNow = -1f;
|
|||
|
|
System.Random rng;
|
|||
|
|
|
|||
|
|
public int Alive
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
int n = 0;
|
|||
|
|
foreach (var p in pool)
|
|||
|
|
if (p.t >= 0f)
|
|||
|
|
n++;
|
|||
|
|
return n;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static LoadingDust Create(Transform parent, LoadingTable.Row r, UiSpriteLoop host, System.Func<float, float, Vector2> pos)
|
|||
|
|
{
|
|||
|
|
var sprites = LoadingTable.SpriteList(r.sprite);
|
|||
|
|
if (sprites.Length == 0) return null;
|
|||
|
|
var rt = UiKit.Fill("Dust_" + r.id, parent);
|
|||
|
|
var d = rt.gameObject.AddComponent<LoadingDust>();
|
|||
|
|
d.row = r;
|
|||
|
|
d.host = host;
|
|||
|
|
d.sprites = sprites;
|
|||
|
|
d.rng = new System.Random(r.id.GetHashCode());
|
|||
|
|
// 먼지 기준점 = 표 f_x,f_y(기준 화면 px) · 달리는 그림을 따라 붙이지 않고 고정(무리도 제자리)
|
|||
|
|
d.origin = pos(r.x, r.y);
|
|||
|
|
if (r.text.StartsWith("on:"))
|
|||
|
|
{
|
|||
|
|
var parts = r.text.Substring(3).Split(new[] { ',', ';' }, System.StringSplitOptions.RemoveEmptyEntries);
|
|||
|
|
var list = new List<int>();
|
|||
|
|
foreach (var s in parts)
|
|||
|
|
{
|
|||
|
|
int v;
|
|||
|
|
if (int.TryParse(s.Trim(), out v)) list.Add(v);
|
|||
|
|
}
|
|||
|
|
d.onFrames = list.ToArray();
|
|||
|
|
}
|
|||
|
|
int n = Mathf.Max(1, r.size);
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
var prt = UiKit.Rect("Puff", rt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(r.w, r.w));
|
|||
|
|
var img = prt.gameObject.AddComponent<Image>();
|
|||
|
|
img.raycastTarget = false;
|
|||
|
|
img.sprite = sprites[i % sprites.Length];
|
|||
|
|
img.preserveAspect = true;
|
|||
|
|
img.color = new Color(r.color.r, r.color.g, r.color.b, 0f);
|
|||
|
|
prt.gameObject.SetActive(false);
|
|||
|
|
d.pool.Add(new Puff { rt = prt, img = img });
|
|||
|
|
}
|
|||
|
|
UiLayer.Isolate(rt.gameObject);
|
|||
|
|
return d;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Emit()
|
|||
|
|
{
|
|||
|
|
var p = pool[next];
|
|||
|
|
next = (next + 1) % pool.Count;
|
|||
|
|
float j = row.phase; // 흩어짐 px
|
|||
|
|
p.t = 0f;
|
|||
|
|
p.x = origin.x + (float)(rng.NextDouble() * 2 - 1) * j;
|
|||
|
|
p.y = origin.y + (float)(rng.NextDouble() * 2 - 1) * j * 0.4f;
|
|||
|
|
p.s = 0.75f + (float)rng.NextDouble() * 0.5f;
|
|||
|
|
p.rot = (float)(rng.NextDouble() * 2 - 1) * 25f;
|
|||
|
|
p.vx = row.a * (0.8f + (float)rng.NextDouble() * 0.4f);
|
|||
|
|
p.img.sprite = sprites[rng.Next(sprites.Length)];
|
|||
|
|
p.rt.gameObject.SetActive(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Update()
|
|||
|
|
{
|
|||
|
|
if (row == null || pool.Count == 0) return;
|
|||
|
|
float now = LoadingScreen.Now;
|
|||
|
|
float dt = lastNow < 0f ? 0f : Mathf.Max(0f, now - lastNow);
|
|||
|
|
lastNow = now;
|
|||
|
|
if (onFrames != null && onFrames.Length > 0 && host != null)
|
|||
|
|
{
|
|||
|
|
int f = host.Index;
|
|||
|
|
if (f != lastFrame)
|
|||
|
|
{
|
|||
|
|
foreach (var k in onFrames)
|
|||
|
|
if (k == f)
|
|||
|
|
Emit();
|
|||
|
|
lastFrame = f;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else if (row.fps > 0f)
|
|||
|
|
{
|
|||
|
|
acc += dt * row.fps;
|
|||
|
|
while (acc >= 1f)
|
|||
|
|
{
|
|||
|
|
acc -= 1f;
|
|||
|
|
Emit();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
float life = row.b > 0f ? row.b : 0.6f;
|
|||
|
|
foreach (var p in pool)
|
|||
|
|
{
|
|||
|
|
if (p.t < 0f) continue;
|
|||
|
|
p.t += dt;
|
|||
|
|
float u = p.t / life;
|
|||
|
|
if (u >= 1f)
|
|||
|
|
{
|
|||
|
|
p.t = -1f;
|
|||
|
|
p.rt.gameObject.SetActive(false);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
float ease = 1f - (1f - u) * (1f - u);
|
|||
|
|
p.rt.anchoredPosition = new Vector2(p.x + p.vx * p.t, p.y + row.c * ease);
|
|||
|
|
float size = Mathf.Lerp(row.w, row.h, ease) * p.s;
|
|||
|
|
p.rt.sizeDelta = new Vector2(size, size);
|
|||
|
|
p.rt.localEulerAngles = new Vector3(0f, 0f, p.rot);
|
|||
|
|
// 처음 15 % 는 나타나고 그 뒤로 옅어짐
|
|||
|
|
float a = u < 0.15f ? u / 0.15f : 1f - (u - 0.15f) / 0.85f;
|
|||
|
|
p.img.color = new Color(row.color.r, row.color.g, row.color.b, row.color.a * a);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 로딩 화면 표(Resources/UI/loading_screen.csv · utf-8-sig) — 좌표 = 기준 화면 1080×1920 px · 왼쪽 위 0,0 · 아래로 +.
|
|||
|
|
/// s_kind: bg · logo · bar · fill · runner · dust · text · option · s_set = download | loading | 빈칸(공통). 칸 설명 = 표 머리 주석.
|
|||
|
|
/// </summary>
|
|||
|
|
public static class LoadingTable
|
|||
|
|
{
|
|||
|
|
public const string TablePath = "UI/loading_screen";
|
|||
|
|
|
|||
|
|
public class Row
|
|||
|
|
{
|
|||
|
|
public string kind = "";
|
|||
|
|
/// <summary>표 세트(download | loading · 빈칸 = 모든 세트 공통).</summary>
|
|||
|
|
public string set = "";
|
|||
|
|
public string id = "";
|
|||
|
|
public string sprite = "";
|
|||
|
|
public string reference = "";
|
|||
|
|
public string text = "";
|
|||
|
|
public string note = "";
|
|||
|
|
public Color color = Color.white;
|
|||
|
|
public float x;
|
|||
|
|
public float y;
|
|||
|
|
public float w;
|
|||
|
|
public float h;
|
|||
|
|
public float fps;
|
|||
|
|
public float phase;
|
|||
|
|
public float scale = 1f;
|
|||
|
|
public float a;
|
|||
|
|
public float b;
|
|||
|
|
public float c;
|
|||
|
|
public int size;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static List<Row> rows;
|
|||
|
|
|
|||
|
|
public static void Reload()
|
|||
|
|
{
|
|||
|
|
rows = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static List<Row> Rows
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
if (rows == null) rows = Load(TablePath);
|
|||
|
|
return rows;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static bool InSet(Row r, string set)
|
|||
|
|
{
|
|||
|
|
return set == null || r.set.Length == 0 || r.set == set;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>kind(·id) 의 첫 줄 · set = 그 세트 줄 + 공통 줄만(null = 모든 줄).</summary>
|
|||
|
|
public static Row First(string kind, string id = null, string set = null)
|
|||
|
|
{
|
|||
|
|
foreach (var r in Rows)
|
|||
|
|
if (r.kind == kind && (id == null || r.id == id) && InSet(r, set))
|
|||
|
|
return r;
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static List<Row> All(string kind, string set = null)
|
|||
|
|
{
|
|||
|
|
var list = new List<Row>();
|
|||
|
|
foreach (var r in Rows)
|
|||
|
|
if (r.kind == kind && InSet(r, set))
|
|||
|
|
list.Add(r);
|
|||
|
|
return list;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Sprite Sprite(string path)
|
|||
|
|
{
|
|||
|
|
return string.IsNullOrEmpty(path) ? null : Resources.Load<Sprite>(path);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>| 로 나눈 스프라이트 경로들.</summary>
|
|||
|
|
public static Sprite[] SpriteList(string paths)
|
|||
|
|
{
|
|||
|
|
var list = new List<Sprite>();
|
|||
|
|
foreach (var p in paths.Split(new[] { '|' }, System.StringSplitOptions.RemoveEmptyEntries))
|
|||
|
|
{
|
|||
|
|
var s = Sprite(p.Trim());
|
|||
|
|
if (s != null) list.Add(s);
|
|||
|
|
}
|
|||
|
|
return list.ToArray();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>폴더 안 스프라이트 전부(이름 순 = 프레임 순).</summary>
|
|||
|
|
public static Sprite[] Frames(string folder)
|
|||
|
|
{
|
|||
|
|
var all = Resources.LoadAll<Sprite>(folder);
|
|||
|
|
System.Array.Sort(all, (p, q) => string.CompareOrdinal(p.name, q.name));
|
|||
|
|
return all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static List<Row> Load(string path)
|
|||
|
|
{
|
|||
|
|
var list = new List<Row>();
|
|||
|
|
var ta = Resources.Load<TextAsset>(path);
|
|||
|
|
if (ta == null) return list;
|
|||
|
|
string[] head = null;
|
|||
|
|
foreach (var raw in ta.text.Replace("\r", "").Split('\n'))
|
|||
|
|
{
|
|||
|
|
var line = raw.TrimStart('');
|
|||
|
|
if (line.Trim().Length == 0 || line.TrimStart().StartsWith("#")) continue;
|
|||
|
|
var cells = SkillVfx.SplitCsv(line);
|
|||
|
|
if (head == null)
|
|||
|
|
{
|
|||
|
|
head = cells;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
var r = new Row();
|
|||
|
|
for (int i = 0; i < head.Length && i < cells.Length; i++)
|
|||
|
|
{
|
|||
|
|
string k = head[i].Trim(), v = cells[i].Trim();
|
|||
|
|
if (v.Length == 0) continue;
|
|||
|
|
switch (k)
|
|||
|
|
{
|
|||
|
|
case "s_kind": r.kind = v.ToLowerInvariant(); break;
|
|||
|
|
case "s_set": r.set = v.ToLowerInvariant(); break;
|
|||
|
|
case "s_id": r.id = v; break;
|
|||
|
|
case "s_sprite": r.sprite = v; break;
|
|||
|
|
case "s_ref": r.reference = v; break;
|
|||
|
|
case "s_text": r.text = v; break;
|
|||
|
|
case "s_note": r.note = v; break;
|
|||
|
|
case "s_color":
|
|||
|
|
Color c;
|
|||
|
|
if (ColorUtility.TryParseHtmlString(v, out c)) r.color = c;
|
|||
|
|
break;
|
|||
|
|
case "f_x": r.x = F(v); break;
|
|||
|
|
case "f_y": r.y = F(v); break;
|
|||
|
|
case "f_w": r.w = F(v); break;
|
|||
|
|
case "f_h": r.h = F(v); break;
|
|||
|
|
case "f_fps": r.fps = F(v); break;
|
|||
|
|
case "f_phase": r.phase = F(v); break;
|
|||
|
|
case "f_scale": r.scale = F(v); break;
|
|||
|
|
case "f_a": r.a = F(v); break;
|
|||
|
|
case "f_b": r.b = F(v); break;
|
|||
|
|
case "f_c": r.c = F(v); break;
|
|||
|
|
case "i_size": r.size = (int)F(v); break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (r.kind.Length > 0) list.Add(r);
|
|||
|
|
}
|
|||
|
|
return list;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static float F(string v)
|
|||
|
|
{
|
|||
|
|
float f;
|
|||
|
|
return float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out f) ? f : 0f;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|