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

164 lines
7.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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 규약
}
}
}