Project_WL/Assets/WL/Look/Sprite/WLSpriteSet.cs

164 lines
7.7 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLSpriteSet.cs — 「3D 를 도트로 구운」 스프라이트 시트 1장의 메타 (WL-814k · #814)
//
// PD 지시(2026-09-12) 「치비 느낌이 나지 않아도 도트 그림으로 구워 쓰는 방식으로 해봐.」
//
// ■ 이 에셋이 들고 있는 것 — 그림(PNG) 1장 + 그 그림을 「어떻게 읽는지」의 표
// · 시트 = 방향 × 동작 × 프레임을 격자로 붙인 PNG 1장(서브 스프라이트 0 · UV 는 런타임 산술).
// · 동작표 = Animator **상태 이름** → 시트 시작 인덱스 · 프레임 수 · fps.
// 런타임(SpriteBillboard)은 원본 Animator 를 **폴링**만 한다 — 원본 코드 수정 0.
// · worldPerPixel = 구울 때 픽셀 1개가 차지한 월드 크기(m). 런타임 쿼드 크기 = 프레임 px × 이 값.
// 도트 모드의 화면 픽셀 크기와 같게 구우면 **스프라이트 1픽셀 = 화면 1픽셀**이 된다.
// · footPixelX/Y = 월드 원점(캐릭터 발밑)이 프레임 안 어느 픽셀에 찍혔는지(좌하단 원점).
// 런타임은 이 값으로 쿼드를 발밑에 맞춘다 — 동작마다 키가 달라져도 발이 안 뜬다.
//
// ■ 인덱스 규약 (시트 안 위치)
// globalIndex = action.startIndex + dir * action.frameCount + frame
// col = globalIndex % columns · row = globalIndex / columns (row 0 = 시트 **위**)
//
// 🔴 이 파일은 데이터만 든다. 굽는 쪽은 Editor/WLSpriteBaker.cs, 쓰는 쪽은 SpriteBillboard.cs.
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Look.SpriteBake
{
/// <summary>동작 1개(대기·이동·공격·피격·사망 …)가 시트 어디에 있는지.</summary>
[System.Serializable]
public sealed class WLSpriteAction
{
[Tooltip("동작 키 — idle / run / attack / hit / die (SO 의 동작 표와 같은 문자열).")]
public string key = "idle";
[Tooltip("이 동작으로 볼 Animator 상태 이름들(레이어 0 shortNameHash 로 비교).")]
public string[] stateNames = new string[0];
[Tooltip("실제로 구운 AnimationClip 이름(보고·디버그용).")]
public string clipName = "";
[Tooltip("시트 안 시작 인덱스(방향 0 · 프레임 0).")]
public int startIndex;
[Tooltip("방향 1개당 프레임 수.")]
public int frameCount = 1;
[Tooltip("재생 fps(클립 길이 ÷ 프레임 수 로 산출).")]
public float fps = 8f;
[Tooltip("클립이 루프인가(사망은 false → 마지막 프레임에서 멈춘다).")]
public bool loop = true;
[Tooltip("원본 클립 길이(초).")]
public float clipLength = 1f;
// 런타임 캐시(직렬화 안 함 · 첫 사용 때 1회 만든다 → 이후 할당 0)
[System.NonSerialized] public int[] hashes;
}
/// <summary>구운 스프라이트 시트 1장 + 읽는 법. 프리팹 1종당 1개.</summary>
[CreateAssetMenu(fileName = "WLSpriteSet", menuName = "WL/Sprite Set (구운 도트)", order = 36)]
public sealed class WLSpriteSet : ScriptableObject
{
[Header("출처")]
[Tooltip("어느 프리팹을 구웠는가(경로).")]
public string sourcePrefab = "";
[Tooltip("액터 이름이 이 문자열로 시작하면 이 시트를 쓴다(예: Ai01 → 'Ai01(Clone)' 매칭).")]
public string actorNamePrefix = "";
[Tooltip("구운 시각 · 툴 버전(보고용).")]
public string bakeStamp = "";
[Header("시트")]
public Texture2D sheet;
public int frameWidth = 64;
public int frameHeight = 64;
public int columns = 16;
public int rows = 1;
[Header("굽기 파라미터")]
[Tooltip("방향 수(카메라 기준 상대 yaw 를 이 수로 나눈다).")]
public int dirCount = 8;
[Tooltip("구울 때 픽셀 1개가 차지한 월드 크기(m). 런타임 쿼드 크기 = 프레임 px × 이 값.")]
public float worldPerPixel = 0.0283333f;
[Tooltip("구울 때 루트 lossyScale.y. 런타임 액터 스케일이 다르면 이 비율로 쿼드를 키운다.")]
public float bakeScale = 1f;
[Tooltip("월드 원점(발밑)이 프레임 안 어느 픽셀에 찍혔는지 — 좌하단 원점 · 픽셀 단위.")]
public float footPixelX = 32f;
public float footPixelY = 4f;
[Tooltip("구울 때 카메라 내림각(도). 런타임 도트 카메라와 같아야 한다.")]
public float pitchDeg = 43.609f;
[Header("동작 표")]
public WLSpriteAction[] actions = new WLSpriteAction[0];
/// <summary>방향 1개 기준 총 프레임 수(= 시트 총 칸 수 ÷ 방향 수).</summary>
public int TotalFrames
{
get
{
int n = 0;
if (actions != null) for (int i = 0; i < actions.Length; i++) if (actions[i] != null) n += actions[i].frameCount;
return n * Mathf.Max(1, dirCount);
}
}
/// <summary>Animator 상태 해시(레이어 0 shortNameHash)로 동작을 찾는다. 없으면 null.</summary>
public WLSpriteAction FindByStateHash(int shortNameHash)
{
if (actions == null) return null;
for (int i = 0; i < actions.Length; i++)
{
var a = actions[i];
if (a == null) continue;
if (a.hashes == null) BuildHashes(a);
for (int h = 0; h < a.hashes.Length; h++) if (a.hashes[h] == shortNameHash) return a;
}
return null;
}
/// <summary>키로 동작을 찾는다(폴백용 · 예: "idle").</summary>
public WLSpriteAction FindByKey(string key)
{
if (actions == null || string.IsNullOrEmpty(key)) return null;
for (int i = 0; i < actions.Length; i++) if (actions[i] != null && actions[i].key == key) return actions[i];
return null;
}
static void BuildHashes(WLSpriteAction a)
{
var src = a.stateNames ?? new string[0];
a.hashes = new int[src.Length];
for (int i = 0; i < src.Length; i++) a.hashes[i] = Animator.StringToHash(src[i]);
}
/// <summary>시트가 실제로 쓸 수 있는 상태인가.</summary>
public bool IsUsable
{
get
{
return sheet != null && frameWidth > 0 && frameHeight > 0 && columns > 0
&& dirCount > 0 && actions != null && actions.Length > 0;
}
}
/// <summary>globalIndex → 시트 UV 사각형(x, y, w, h · 0~1). 할당 0.</summary>
public Vector4 UvRect(int globalIndex)
{
int cols = Mathf.Max(1, columns);
int col = globalIndex % cols;
int row = globalIndex / cols;
float tw = sheet != null ? sheet.width : 1f;
float th = sheet != null ? sheet.height : 1f;
float u = (col * frameWidth) / tw;
float v = (th - (row + 1) * frameHeight) / th; // row 0 = 시트 위
return new Vector4(frameWidth / tw, frameHeight / th, u, v); // (scaleX, scaleY, offsetX, offsetY) = _ST 규약
}
}
}