576 lines
24 KiB
C#
576 lines
24 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace TKS
|
||
{
|
||
/// <summary>
|
||
/// Resources/Units/<art>/meta.json — 조직 스킬 skeletal-anim-pipeline 의 `rig_frames_export.py` 가 쓴다.
|
||
/// 프레임 PNG 는 Resources/Units/<art>/<clip>/f000.png … (임포트 설정 = Editor/UnitArtImporter).
|
||
/// </summary>
|
||
[Serializable]
|
||
public class UnitArtMeta
|
||
{
|
||
[Serializable]
|
||
public class Clip
|
||
{
|
||
public string name;
|
||
public int frames;
|
||
public bool loop;
|
||
/// <summary>이 클립의 초당 프레임(0 = meta fps) — 대기 2~3 프레임 숨 고르기(PD 2026-11-11 · 원화 픽셀을 그대로 눌러 만든 f001·f002).</summary>
|
||
public float fps;
|
||
/// <summary>왕복 재생(0-1-2-1-0 …) — 끝 프레임에서 첫 프레임으로 튀지 않게.</summary>
|
||
public bool pingpong;
|
||
/// <summary>프레임 번호별 유지 초(있으면 fps 대신 · 왕복이면 돌아올 때도 같은 번호 값) · sequence 가 있으면 그 단계별 유지 초.</summary>
|
||
public float[] durations;
|
||
/// <summary>재생 순서(프레임 번호 배열 · 한 방향 반복) — 대기 4 자세 0→1→2→3(Fable 제안 · PD 2026-11-11 「4장 + 깜빡임」) · 없으면 0..n-1(또는 왕복).</summary>
|
||
public int[] sequence;
|
||
/// <summary>가끔 눈 깜빡임(쉼 자세 = sequence 첫 프레임일 때만 · 깜빡임 프레임은 sequence 에 넣지 않음).</summary>
|
||
public Blink blink;
|
||
}
|
||
|
||
[Serializable]
|
||
public class Blink
|
||
{
|
||
public int frame = -1;
|
||
public float every_min = 2.5f;
|
||
public float every_max = 5f;
|
||
public float dur = 0.1f;
|
||
}
|
||
|
||
public float fps = 15f;
|
||
public float ppu = 100f;
|
||
public int[] frame;
|
||
/// <summary>발 위치(리그 원점) — 왼쪽 아래 (0,0) 기준 비율.</summary>
|
||
public float[] pivot;
|
||
/// <summary>대기 첫 프레임의 캐릭터 불투명 세로 픽셀(무기 포함) — bodyHeightPx 가 없을 때의 세로 크기 기준.</summary>
|
||
public float charHeightPx;
|
||
/// <summary>몸(머리 끝~발) 세로 픽셀 · 무기 제외 — 리그 export 는 ppu(= 리그 1단위) · 정지 그림은 굵은 행 범위(2026-11-04 스켈레탈 세션 안내: charHeightPx 로 맞추면 긴 무기를 든 인물이 작아짐).</summary>
|
||
public float bodyHeightPx;
|
||
public Clip[] clips;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 클립 프레임 지연 로드(Fable 검토 2026-11-12 · PD 「모바일 출시 · 리소스 구성 검토 후 진행」) — 사전처럼 쓰되 처음 물을 때 그 클립만 Resources.LoadAll.
|
||
/// 초상만 쓰는 화면(영웅 목록·HUD·교역)이 대기·준비·공격 클립까지 통째로 올리던 것(영웅 72명 ≈ 0.94 GB → 대기 프레임 뒤 ≈ 2 GB)을 막는다.
|
||
/// </summary>
|
||
public class UnitClipStore : IEnumerable<KeyValuePair<string, Sprite[]>>
|
||
{
|
||
readonly string root;
|
||
readonly List<string> names = new List<string>();
|
||
readonly Dictionary<string, Sprite[]> loaded = new Dictionary<string, Sprite[]>();
|
||
|
||
public UnitClipStore(string resourceRoot)
|
||
{
|
||
root = resourceRoot;
|
||
}
|
||
|
||
internal void Declare(string clip)
|
||
{
|
||
if (!string.IsNullOrEmpty(clip) && !names.Contains(clip)) names.Add(clip);
|
||
}
|
||
|
||
/// <summary>meta 에 적힌 클립 수(불러온 수 아님).</summary>
|
||
public int Count
|
||
{
|
||
get { return names.Count; }
|
||
}
|
||
|
||
/// <summary>지금 메모리에 올라온 클립 수(테스트·점검용).</summary>
|
||
public int LoadedCount
|
||
{
|
||
get
|
||
{
|
||
int n = 0;
|
||
foreach (var kv in loaded)
|
||
if (kv.Value != null) n++;
|
||
return n;
|
||
}
|
||
}
|
||
|
||
Sprite[] Get(string clip)
|
||
{
|
||
if (string.IsNullOrEmpty(clip) || !names.Contains(clip)) return null;
|
||
Sprite[] fr;
|
||
if (loaded.TryGetValue(clip, out fr)) return fr;
|
||
fr = Resources.LoadAll<Sprite>(root + clip);
|
||
if (fr != null && fr.Length > 0) Array.Sort(fr, (a, b) => string.CompareOrdinal(a.name, b.name));
|
||
else fr = null;
|
||
loaded[clip] = fr;
|
||
return fr;
|
||
}
|
||
|
||
public bool TryGetValue(string clip, out Sprite[] frames)
|
||
{
|
||
frames = Get(clip);
|
||
return frames != null;
|
||
}
|
||
|
||
public bool ContainsKey(string clip)
|
||
{
|
||
return Get(clip) != null;
|
||
}
|
||
|
||
public Sprite[] this[string clip]
|
||
{
|
||
get
|
||
{
|
||
var fr = Get(clip);
|
||
if (fr == null) throw new KeyNotFoundException(clip);
|
||
return fr;
|
||
}
|
||
}
|
||
|
||
/// <summary>meta 의 모든 클립을 미리 읽음(전투 유닛 = 첫 공격 때 읽느라 한 프레임 멈추지 않게).</summary>
|
||
public void LoadAll()
|
||
{
|
||
foreach (var n in names) Get(n);
|
||
}
|
||
|
||
public IEnumerator<KeyValuePair<string, Sprite[]>> GetEnumerator()
|
||
{
|
||
foreach (var n in names)
|
||
{
|
||
var fr = Get(n);
|
||
if (fr != null) yield return new KeyValuePair<string, Sprite[]>(n, fr);
|
||
}
|
||
}
|
||
|
||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
|
||
{
|
||
return GetEnumerator();
|
||
}
|
||
}
|
||
|
||
public class UnitArtData
|
||
{
|
||
public string Id;
|
||
public UnitArtMeta Meta;
|
||
public UnitClipStore Clips;
|
||
public readonly Dictionary<string, bool> Loop = new Dictionary<string, bool>();
|
||
/// <summary>클립별 초당 프레임(meta 클립 fps · 없으면 meta fps).</summary>
|
||
public readonly Dictionary<string, float> Fps = new Dictionary<string, float>();
|
||
public readonly Dictionary<string, bool> PingPong = new Dictionary<string, bool>();
|
||
/// <summary>클립별 프레임 번호 유지 초(meta 클립 durations · 없으면 fps 고른 간격).</summary>
|
||
public readonly Dictionary<string, float[]> Durations = new Dictionary<string, float[]>();
|
||
/// <summary>클립별 재생 순서(meta 클립 sequence) · 깜빡임.</summary>
|
||
public readonly Dictionary<string, int[]> Sequences = new Dictionary<string, int[]>();
|
||
public readonly Dictionary<string, UnitArtMeta.Blink> Blinks = new Dictionary<string, UnitArtMeta.Blink>();
|
||
|
||
int[] ClipSequence(string clip, int n)
|
||
{
|
||
int[] sq;
|
||
if (clip == null || !Sequences.TryGetValue(clip, out sq) || sq == null || sq.Length == 0) return null;
|
||
foreach (int k in sq)
|
||
if (k < 0 || k >= n)
|
||
return null;
|
||
return sq;
|
||
}
|
||
|
||
/// <summary>이 클립의 깜빡임(프레임이 있으면) · 없으면 null.</summary>
|
||
public UnitArtMeta.Blink ClipBlink(string clip, int n)
|
||
{
|
||
UnitArtMeta.Blink b;
|
||
return clip != null && Blinks.TryGetValue(clip, out b) && b != null && b.frame >= 0 && b.frame < n ? b : null;
|
||
}
|
||
|
||
/// <summary>깜빡임을 끼울 수 있는 프레임(쉼 = 순서의 첫 프레임 · 순서 없으면 0).</summary>
|
||
public int RestFrame(string clip, int n)
|
||
{
|
||
var sq = ClipSequence(clip, n);
|
||
return sq != null ? sq[0] : 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 깜빡임 끼우기 — 쉼 프레임일 때 nextBlink 시각이 지났으면 dur 동안 깜빡임 프레임 · 다음 시각 = 무작위(every_min~max) · 쉼이 아니면 다음 쉼까지 미룸.
|
||
/// now = 재생기의 시계(초) · nextBlink 0 = 아직 안 정함 · 재생기마다 자기 값(유닛마다 엇갈림).
|
||
/// </summary>
|
||
public int ApplyBlink(string clip, int n, int idx, float now, ref float nextBlink, ref float blinkEnd)
|
||
{
|
||
var b = ClipBlink(clip, n);
|
||
if (b == null) return idx;
|
||
int rest = RestFrame(clip, n);
|
||
if (now < blinkEnd) return idx == rest ? b.frame : idx;
|
||
if (nextBlink <= 0f) nextBlink = now + UnityEngine.Random.Range(b.every_min, b.every_max);
|
||
if (now >= nextBlink && idx == rest)
|
||
{
|
||
blinkEnd = now + b.dur;
|
||
nextBlink = blinkEnd + UnityEngine.Random.Range(b.every_min, b.every_max);
|
||
return b.frame;
|
||
}
|
||
return idx;
|
||
}
|
||
|
||
public float ClipFps(string clip)
|
||
{
|
||
float f;
|
||
return clip != null && Fps.TryGetValue(clip, out f) && f > 0.01f ? f : Mathf.Max(0.01f, Meta.fps);
|
||
}
|
||
|
||
bool IsPingPong(string clip)
|
||
{
|
||
bool pp;
|
||
return clip != null && PingPong.TryGetValue(clip, out pp) && pp;
|
||
}
|
||
|
||
float[] ClipDurations(string clip, int n)
|
||
{
|
||
float[] du;
|
||
return clip != null && Durations.TryGetValue(clip, out du) && du != null && du.Length >= n ? du : null;
|
||
}
|
||
|
||
/// <summary>재생 순서 s 번째의 프레임 번호 — 왕복이면 0..n-1 다음 n-2..1.</summary>
|
||
static int SeqIndex(int s, int n, bool pp)
|
||
{
|
||
return !pp || s < n ? s : 2 * n - 2 - s;
|
||
}
|
||
|
||
/// <summary>한 바퀴 초(반복 클립 시작 시점을 흩을 때).</summary>
|
||
public float CycleSeconds(string clip, int n)
|
||
{
|
||
var sq = ClipSequence(clip, n);
|
||
if (sq != null)
|
||
{
|
||
float[] sd;
|
||
bool hasD = Durations.TryGetValue(clip, out sd) && sd != null && sd.Length >= sq.Length;
|
||
float tot = 0f;
|
||
for (int s = 0; s < sq.Length; s++) tot += hasD ? Mathf.Max(0.01f, sd[s]) : 1f / ClipFps(clip);
|
||
return tot;
|
||
}
|
||
bool pp = IsPingPong(clip);
|
||
int steps = pp && n > 1 ? 2 * n - 2 : n;
|
||
var du = ClipDurations(clip, n);
|
||
if (du == null) return steps / ClipFps(clip);
|
||
float total = 0f;
|
||
for (int s = 0; s < steps; s++) total += Mathf.Max(0.01f, du[SeqIndex(s, n, pp)]);
|
||
return total;
|
||
}
|
||
|
||
/// <summary>재생 시간 t(초)의 프레임 번호 — 반복 · 왕복(0..n-1..1) · 프레임별 유지 초 포함.</summary>
|
||
public int FrameAt(string clip, int n, float t)
|
||
{
|
||
if (n <= 1) return 0;
|
||
var sq = ClipSequence(clip, n);
|
||
if (sq != null)
|
||
{
|
||
float[] sd;
|
||
bool hasD = Durations.TryGetValue(clip, out sd) && sd != null && sd.Length >= sq.Length;
|
||
float x = Mathf.Repeat(t, CycleSeconds(clip, n));
|
||
for (int s = 0; s < sq.Length; s++)
|
||
{
|
||
x -= hasD ? Mathf.Max(0.01f, sd[s]) : 1f / ClipFps(clip);
|
||
if (x < 0f) return sq[s];
|
||
}
|
||
return sq[sq.Length - 1];
|
||
}
|
||
bool pp = IsPingPong(clip);
|
||
int steps = pp ? 2 * n - 2 : n;
|
||
var du = ClipDurations(clip, n);
|
||
if (du != null)
|
||
{
|
||
float x = Mathf.Repeat(t, CycleSeconds(clip, n));
|
||
for (int s = 0; s < steps; s++)
|
||
{
|
||
int k = SeqIndex(s, n, pp);
|
||
x -= Mathf.Max(0.01f, du[k]);
|
||
if (x < 0f) return k;
|
||
}
|
||
return SeqIndex(steps - 1, n, pp);
|
||
}
|
||
int i = Mathf.FloorToInt(t * ClipFps(clip)) % steps;
|
||
return SeqIndex(i, n, pp);
|
||
}
|
||
/// <summary>
|
||
/// 크기 기준 px = Resources/Units/art_size.csv 의 size_px(눈 간격 ÷ 기준 비율 = 머리 크기가 표준인 유닛의 키로 환산 · Tools/unit_art_size.py) ·
|
||
/// 0 = 표에 없음 → BodyPx. PD 2026-11-09 「캐릭터 크기 동일 · 말 탄 유닛도 보병·창병과 비슷한 비율」.
|
||
/// </summary>
|
||
public float SizePx;
|
||
/// <summary>말 탄 유닛(크기 표 mounted = 1) — 게임 배치 때 머리 끝 상한(BalanceConfig.mountedArtMaxTop)을 받는다.</summary>
|
||
public bool Mounted;
|
||
/// <summary>대기 그림의 눈 높이(발 위 px · 크기 표 eye_up_px) · 0 = 없음 — 영웅 목록 카드가 눈 줄을 맞춘다(PD 2026-11-10 「전위·태사자처럼 너무 위쪽에 보이는 영웅은 내려」).</summary>
|
||
public float EyeUpPx;
|
||
|
||
/// <summary>그림 속 몸(머리 끝~발) 세로 px — bodyHeightPx(무기 제외) 우선, 없으면 charHeightPx.</summary>
|
||
public float BodyPx
|
||
{
|
||
get { return Meta.bodyHeightPx > 0.5f ? Meta.bodyHeightPx : Meta.charHeightPx; }
|
||
}
|
||
|
||
/// <summary>배율 기준 세로 크기(월드) — 임포터 PPU = meta.ppu 전제 · 크기 표 size_px 우선(모든 유닛의 머리 크기가 같아짐), 없으면 BodyPx.</summary>
|
||
public float CharHeightWorld
|
||
{
|
||
get
|
||
{
|
||
if (Meta.ppu <= 0.01f) return 1f;
|
||
return (SizePx > 0.5f ? SizePx : BodyPx) / Meta.ppu;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>유닛 아트 로더(Resources · 캐시). 아트가 없으면 null → UnitView 가 도형으로 그린다.</summary>
|
||
public static class UnitArt
|
||
{
|
||
public const string Root = "Units/";
|
||
/// <summary>크기 표(TextAsset · utf-8-sig CSV · 열 art · size_px) = Resources/Units/art_size.csv.</summary>
|
||
public const string SizeTable = "art_size";
|
||
static readonly Dictionary<string, UnitArtData> cache = new Dictionary<string, UnitArtData>();
|
||
static Dictionary<string, float> sizes;
|
||
static Dictionary<string, float> eyeUps;
|
||
static HashSet<string> mounted;
|
||
|
||
/// <summary>크기 표의 size_px(없으면 0).</summary>
|
||
public static float SizeOf(string id)
|
||
{
|
||
EnsureSizes();
|
||
float v;
|
||
return id != null && sizes.TryGetValue(id, out v) ? v : 0f;
|
||
}
|
||
|
||
/// <summary>크기 표의 eye_up_px(대기 그림 눈 높이 · 발 위 px · 없으면 0).</summary>
|
||
public static float EyeUpOf(string id)
|
||
{
|
||
EnsureSizes();
|
||
float v;
|
||
return id != null && eyeUps.TryGetValue(id, out v) ? v : 0f;
|
||
}
|
||
|
||
/// <summary>크기 표 mounted = 1(말 탄 유닛).</summary>
|
||
public static bool IsMounted(string id)
|
||
{
|
||
EnsureSizes();
|
||
return id != null && mounted.Contains(id);
|
||
}
|
||
|
||
static void EnsureSizes()
|
||
{
|
||
if (sizes != null) return;
|
||
sizes = new Dictionary<string, float>();
|
||
eyeUps = new Dictionary<string, float>();
|
||
mounted = new HashSet<string>();
|
||
var ta = Resources.Load<TextAsset>(Root + SizeTable);
|
||
if (ta == null) return;
|
||
var lines = ta.text.TrimStart('').Split('\n');
|
||
var head = new List<string>(lines[0].Trim().Split(','));
|
||
int ia = head.IndexOf("art"), isz = head.IndexOf("size_px"), im = head.IndexOf("mounted"), ieu = head.IndexOf("eye_up_px");
|
||
if (ia < 0 || isz < 0 || im < 0)
|
||
{
|
||
Debug.LogWarning("[TKS] 유닛 크기 표 열(art · mounted · size_px) 없음");
|
||
return;
|
||
}
|
||
int need = Mathf.Max(ia, Mathf.Max(isz, im));
|
||
for (int i = 1; i < lines.Length; i++)
|
||
{
|
||
var c = lines[i].Trim().Split(',');
|
||
if (c.Length <= need) continue;
|
||
float v;
|
||
if (float.TryParse(c[isz], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v) && v > 0f)
|
||
sizes[c[ia]] = v;
|
||
if (c[im] == "1") mounted.Add(c[ia]);
|
||
if (ieu >= 0 && ieu < c.Length && float.TryParse(c[ieu], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v) && v > 0f)
|
||
eyeUps[c[ia]] = v;
|
||
}
|
||
}
|
||
|
||
public static UnitArtData Load(string id)
|
||
{
|
||
if (string.IsNullOrEmpty(id)) return null;
|
||
UnitArtData d;
|
||
if (cache.TryGetValue(id, out d)) return d;
|
||
d = LoadUncached(id);
|
||
cache[id] = d;
|
||
return d;
|
||
}
|
||
|
||
public static void ClearCache()
|
||
{
|
||
cache.Clear();
|
||
portraits.Clear();
|
||
sizes = null;
|
||
eyeUps = null;
|
||
mounted = null;
|
||
}
|
||
|
||
static UnitArtData LoadUncached(string id)
|
||
{
|
||
var metaAsset = Resources.Load<TextAsset>(Root + id + "/meta");
|
||
if (metaAsset == null) return null;
|
||
UnitArtMeta meta;
|
||
try
|
||
{
|
||
meta = JsonUtility.FromJson<UnitArtMeta>(metaAsset.text);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning("[TKS] 유닛 아트 meta 해석 실패 " + id + ": " + e.Message);
|
||
return null;
|
||
}
|
||
if (meta == null || meta.clips == null) return null;
|
||
|
||
var d = new UnitArtData { Id = id, Meta = meta, SizePx = SizeOf(id), Mounted = IsMounted(id), EyeUpPx = EyeUpOf(id), Clips = new UnitClipStore(Root + id + "/") };
|
||
foreach (var c in meta.clips)
|
||
{
|
||
if (c == null || string.IsNullOrEmpty(c.name)) continue;
|
||
d.Clips.Declare(c.name); // 프레임은 처음 쓸 때 그 클립만 읽음(UnitClipStore)
|
||
d.Loop[c.name] = c.loop;
|
||
if (c.fps > 0.01f) d.Fps[c.name] = c.fps;
|
||
d.PingPong[c.name] = c.pingpong;
|
||
if (c.durations != null && c.durations.Length > 0) d.Durations[c.name] = c.durations;
|
||
if (c.sequence != null && c.sequence.Length > 0) d.Sequences[c.name] = c.sequence;
|
||
if (c.blink != null && c.blink.frame >= 0) d.Blinks[c.name] = c.blink;
|
||
}
|
||
if (d.Clips.Count == 0) return null;
|
||
// 그림이 실제로 있는지 = 첫 클립 f000 한 장만 확인(없으면 null → 도형 · 옛 동작과 같음 · 이 한 장 = 초상과 같은 텍스처)
|
||
string first = null;
|
||
foreach (var c in meta.clips)
|
||
if (c != null && !string.IsNullOrEmpty(c.name))
|
||
{
|
||
first = c.name;
|
||
break;
|
||
}
|
||
return Resources.Load<Sprite>(Root + id + "/" + first + "/f000") != null ? d : null;
|
||
}
|
||
|
||
static readonly Dictionary<string, Sprite> portraits = new Dictionary<string, Sprite>();
|
||
|
||
/// <summary>
|
||
/// 초상 = front_idle 첫 프레임 한 장만(없으면 다른 클립 첫 프레임 · 없으면 null) — 영웅 목록·HUD·교역은 이것만 쓴다
|
||
/// (클립 전체를 올리지 않음 · Fable 검토 2026-11-12 「초상은 f000 한 장」).
|
||
/// </summary>
|
||
public static Sprite Portrait(string id)
|
||
{
|
||
if (string.IsNullOrEmpty(id)) return null;
|
||
Sprite sp;
|
||
if (portraits.TryGetValue(id, out sp)) return sp;
|
||
sp = Resources.Load<Sprite>(Root + id + "/front_idle/f000");
|
||
if (sp == null)
|
||
{
|
||
var d = Load(id);
|
||
if (d != null)
|
||
foreach (var kv in d.Clips)
|
||
{
|
||
sp = kv.Value[0];
|
||
break;
|
||
}
|
||
}
|
||
portraits[id] = sp;
|
||
return sp;
|
||
}
|
||
}
|
||
|
||
/// <summary>프레임 애니 재생기. 비반복 클립은 끝난 뒤(유지 시간 포함) 대기 클립으로 돌아갈 수 있다.</summary>
|
||
public class FrameAnimator : MonoBehaviour
|
||
{
|
||
SpriteRenderer sr;
|
||
UnitArtData data;
|
||
Sprite[] frames;
|
||
string current;
|
||
string idle;
|
||
bool loop;
|
||
float t;
|
||
float hold;
|
||
bool backToIdle;
|
||
float nextBlink;
|
||
float blinkEnd = -1f;
|
||
|
||
public SpriteRenderer Renderer
|
||
{
|
||
get { return sr; }
|
||
}
|
||
|
||
public string Current
|
||
{
|
||
get { return current; }
|
||
}
|
||
|
||
public string IdleClip
|
||
{
|
||
get { return idle; }
|
||
}
|
||
|
||
public UnitArtData Data
|
||
{
|
||
get { return data; }
|
||
}
|
||
|
||
/// <summary>그려진 머리 끝 높이(월드 · 발 기준) = BodyPx × 배율 — 크기 표로 커진 기마 유닛은 표준 키보다 높다.</summary>
|
||
public float TopHeight { get; private set; }
|
||
|
||
/// <param name="worldHeight">표준 키(월드) — 크기 표 size_px 가 이 높이가 되도록 배율(= 머리 크기 통일).</param>
|
||
/// <param name="mountedMaxTop">말 탄 유닛의 머리 끝 상한 = worldHeight × 이 값(0 = 상한 없음 · 넘으면 배율을 줄임 = 탄 사람은 보병보다 조금 작아짐).</param>
|
||
public void Setup(UnitArtData d, float worldHeight, int sortingOrder, float mountedMaxTop = 0f)
|
||
{
|
||
data = d;
|
||
sr = GetComponent<SpriteRenderer>();
|
||
if (sr == null) sr = gameObject.AddComponent<SpriteRenderer>();
|
||
sr.sortingOrder = sortingOrder;
|
||
float h = d.CharHeightWorld;
|
||
float s = h > 0.001f ? worldHeight / h : 1f;
|
||
float top = d.Meta.ppu > 0.01f ? d.BodyPx / d.Meta.ppu * s : worldHeight;
|
||
if (d.Mounted && mountedMaxTop > 0.01f && top > worldHeight * mountedMaxTop)
|
||
{
|
||
s *= worldHeight * mountedMaxTop / top;
|
||
top = worldHeight * mountedMaxTop;
|
||
}
|
||
transform.localScale = new Vector3(s, s, 1f);
|
||
TopHeight = top;
|
||
}
|
||
|
||
public bool Has(string clip)
|
||
{
|
||
return data != null && clip != null && data.Clips.ContainsKey(clip);
|
||
}
|
||
|
||
public void SetIdle(string clip)
|
||
{
|
||
idle = clip;
|
||
}
|
||
|
||
/// <param name="holdSeconds">비반복 클립을 최소 이만큼 보여준 뒤 대기로(1프레임 자세용).</param>
|
||
public bool Play(string clip, bool loopPlay, float holdSeconds = 0f, bool returnToIdle = false)
|
||
{
|
||
if (!Has(clip)) return false;
|
||
frames = data.Clips[clip];
|
||
current = clip;
|
||
loop = loopPlay;
|
||
hold = holdSeconds;
|
||
backToIdle = returnToIdle;
|
||
t = 0f;
|
||
nextBlink = 0f;
|
||
blinkEnd = -1f;
|
||
// 여러 프레임 반복(대기 숨 고르기) = 유닛마다 시작 시점을 달리(모두 같이 숨 쉬면 어색)
|
||
if (loopPlay && frames.Length > 1) t = UnityEngine.Random.value * data.CycleSeconds(clip, frames.Length);
|
||
sr.sprite = frames[data.FrameAt(clip, frames.Length, t)];
|
||
return true;
|
||
}
|
||
|
||
public void PlayIdle()
|
||
{
|
||
if (idle != null) Play(idle, true);
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
if (frames == null || frames.Length == 0) return;
|
||
t += Time.deltaTime;
|
||
float fps = data.ClipFps(current);
|
||
int n = frames.Length;
|
||
int idx;
|
||
if (loop)
|
||
{
|
||
idx = data.ApplyBlink(current, n, data.FrameAt(current, n, t), t, ref nextBlink, ref blinkEnd);
|
||
}
|
||
else
|
||
{
|
||
idx = Mathf.Min(n - 1, Mathf.FloorToInt(t * fps));
|
||
float dur = n / fps;
|
||
if (t >= dur && t >= hold && backToIdle && idle != null && current != idle)
|
||
{
|
||
PlayIdle();
|
||
return;
|
||
}
|
||
}
|
||
if (sr.sprite != frames[idx]) sr.sprite = frames[idx];
|
||
}
|
||
}
|
||
}
|