using System.IO; using UnityEditor; using UnityEngine; // 2D 돌진 이펙트 · 몸 뒤덮는 기운(시험) 프리팹 생성 — Unity CLI script eval 로 실행: // unity command --project-path "E:/NerdNavis/ThreeKingdomsSlingshot" script eval --file Tools/build_dash2d.cs // 그림 = Tools/make_dash2d_textures.py → Assets/VFX/TKS2D · 진영색 원본 = Tools/dash2d_camps.csv(그림 · 꼬리 · 초승달 색 모두 이 표에서). // 크기 단위 = 유닛 키 1(표 f_scale 1) · 정면 = +X(표 b_flat = 1). // 이력(PD 2026-11-10): 1차 검정 외곽선 셀 그림 → 2차 「외곽선 짙음 · 밑 이펙트 투명값」 = 외곽선 없는 검기 · 반투명 바람 줄기 → // 3차 「검기는 이전 형태가 더 나음 · 대쉬 이펙트는 몸을 벗어나지 않게 · 첨부 같은 몸 뒤덮는 연출을 스킬용으로 시험」 = // 초승달 Head = 1차 그림(dash2d_crescent · 진영색 곱) · 캐릭터 밑 층 = 유닛 따라 움직이는 Local 공간 · 몸 기준점 반경 0.45 키 안 // (실측 그림 경계 = 좌우 ±0.56 키 · 몸 기준 아래 −0.60 ~ 위 +0.59 키 · 투명 여백 포함) · 기운 = TKS_Fx_Aura2D_<진영>(표 키 test_aura). string dir = "Assets/VFX/TKS2D/"; string fxDir = "Assets/Resources/Fx/"; string dustHex = "#F3EBDD"; var camps = new System.Collections.Generic.List(); foreach (var line in File.ReadAllLines("Tools/dash2d_camps.csv")) { var cells = line.Trim('\uFEFF').Split(','); if (cells.Length < 2 || cells[0] == "s_camp" || cells[0].Length == 0) continue; camps.Add(new[] { cells[0].Trim(), cells[1].Trim() }); } AssetDatabase.Refresh(); var texNames = new System.Collections.Generic.List { "dash2d_crescent", "dash2d_trail", "dash2d_speedline", "dash2d_dot", "dash2d_puff", "aura_wisp" }; foreach (var cp in camps) { texNames.Add("dash2d_slash_" + cp[0]); texNames.Add("aura_comet_" + cp[0]); } foreach (var n in texNames) { var ti = (TextureImporter)AssetImporter.GetAtPath(dir + n + ".png"); ti.textureType = TextureImporterType.Sprite; ti.spriteImportMode = SpriteImportMode.Single; ti.spritePixelsPerUnit = n.StartsWith("dash2d_slash_") || n == "dash2d_crescent" ? 512 : 256; ti.mipmapEnabled = false; ti.alphaIsTransparency = true; ti.wrapMode = TextureWrapMode.Clamp; ti.textureCompression = TextureImporterCompression.Uncompressed; ti.SaveAndReimport(); } Material Mat(string name, string tex) { var path = dir + "Mat_" + name + ".mat"; var m = AssetDatabase.LoadAssetAtPath(path); if (m == null) { m = new Material(Shader.Find("Sprites/Default")); AssetDatabase.CreateAsset(m, path); } m.shader = Shader.Find("Sprites/Default"); m.mainTexture = AssetDatabase.LoadAssetAtPath(dir + tex + ".png"); EditorUtility.SetDirty(m); return m; } var trailMat = Mat("Dash2D_Trail", "dash2d_trail"); var lineMat = Mat("Dash2D_Line", "dash2d_speedline"); var dotMat = Mat("Dash2D_Dot", "dash2d_dot"); var puffMat = Mat("Dash2D_Puff", "dash2d_puff"); var wispMat = Mat("Aura2D_Wisp", "aura_wisp"); Color Hex(string h) { Color c; ColorUtility.TryParseHtmlString(h, out c); return c; } ParticleSystem.MinMaxCurve Two(float a, float b) { return new ParticleSystem.MinMaxCurve(a, b); } ParticleSystem.MinMaxGradient Fade(float a0, float aMid, float midT) { var g = new Gradient(); g.SetKeys(new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(a0, 0f), new GradientAlphaKey(aMid, midT), new GradientAlphaKey(0f, 1f) }); return new ParticleSystem.MinMaxGradient(g); } ParticleSystem NewPs(Transform parent, string name, Vector3 local, Material mat, bool localSpace) { var go = new GameObject(name); go.transform.SetParent(parent, false); go.transform.localPosition = local; var ps = go.AddComponent(); ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); var m = ps.main; m.duration = 1f; m.loop = true; m.playOnAwake = true; m.startSpeed = 0f; m.gravityModifier = 0f; m.simulationSpace = localSpace ? ParticleSystemSimulationSpace.Local : ParticleSystemSimulationSpace.World; m.scalingMode = ParticleSystemScalingMode.Hierarchy; var r = go.GetComponent(); r.sharedMaterial = mat; r.renderMode = ParticleSystemRenderMode.Billboard; r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; r.receiveShadows = false; return ps; } void Backward(ParticleSystem ps, float speed) { var v = ps.velocityOverLifetime; v.enabled = true; v.space = ParticleSystemSimulationSpace.Local; v.x = new ParticleSystem.MinMaxCurve(-speed); v.y = new ParticleSystem.MinMaxCurve(0f); v.z = new ParticleSystem.MinMaxCurve(0f); } void Box(ParticleSystem ps, Vector3 scale) { var s = ps.shape; s.shapeType = ParticleSystemShapeType.Box; s.scale = scale; } SpriteRenderer Sprite(Transform parent, string name, string tex, Vector3 local, Vector3 scale, Color color) { var go = new GameObject(name); go.transform.SetParent(parent, false); go.transform.localPosition = local; go.transform.localScale = scale; var sr = go.AddComponent(); sr.sprite = AssetDatabase.LoadAssetAtPath(dir + tex + ".png"); sr.color = color; return sr; } string Save(GameObject root) { var path = fxDir + root.name + ".prefab"; PrefabUtility.SaveAsPrefabAsset(root, path); Object.DestroyImmediate(root); return path; } var sb = new System.Text.StringBuilder(); foreach (var cp in camps) { var c = Hex(cp[1]); var light = Color.Lerp(c, Color.white, 0.15f); // ── 돌진(기본 공격) ── var root = new GameObject("TKS_Fx_Dash2D_" + cp[0]); // 꼬리(유닛 아래) — 짧게(0.04초 = 보병 속도 기준 몸 뒤 약 0.25 키) · 반투명 진영색 var tgo = new GameObject("Trail"); tgo.transform.SetParent(root.transform, false); var tr = tgo.AddComponent(); tr.sharedMaterial = trailMat; tr.time = 0.04f; tr.minVertexDistance = 0.03f; tr.widthMultiplier = 0.5f; tr.widthCurve = new AnimationCurve(new Keyframe(0f, 1f), new Keyframe(1f, 0.3f)); var g = new Gradient(); g.SetKeys(new[] { new GradientColorKey(light, 0f), new GradientColorKey(light, 1f) }, new[] { new GradientAlphaKey(0.7f, 0f), new GradientAlphaKey(0.35f, 0.5f), new GradientAlphaKey(0f, 1f) }); tr.colorGradient = g; tr.textureMode = LineTextureMode.Stretch; tr.alignment = LineAlignment.View; tr.numCapVertices = 0; tr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; tr.receiveShadows = false; // 바람 줄기(유닛 위 · 표 s_over · 유닛을 따라 움직임) — 몸 안에서 생겨 조금만 뒤로 흐르며 투명해짐(유닛 아래면 몸에 다 가려 안 보임 · 실측) var lines = NewPs(root.transform, "Lines", new Vector3(-0.05f, 0f, 0f), lineMat, true); var lm = lines.main; lm.startLifetime = 0.16f; lm.startSize3D = true; lm.startSizeX = Two(0.35f, 0.55f); lm.startSizeY = Two(0.08f, 0.13f); lm.startSizeZ = 1f; lm.startColor = new Color(1f, 1f, 1f, 0.85f); // 유닛 위 층(표 s_over · PD 「몸의 대쉬 효과가 왜 안 보이나」) — 그림이 비치게 반투명(0.7 · 가는 줄 = 거의 안 보임 · 실측) lm.maxParticles = 30; var le = lines.emission; le.rateOverTime = 50f; Box(lines, new Vector3(0.2f, 0.6f, 0f)); Backward(lines, 0.5f); var lc = lines.colorOverLifetime; lc.enabled = true; lc.color = Fade(1f, 0.7f, 0.5f); lines.GetComponent().alignment = ParticleSystemRenderSpace.Local; // 줄기 길이 = 진행 축(+X) // 바람 알갱이(유닛 위 · 표 s_over) — 작은 흰 점 · 몸 안 var dots = NewPs(root.transform, "Dots", new Vector3(-0.05f, 0f, 0f), dotMat, true); var dm = dots.main; dm.startLifetime = 0.15f; dm.startSize = Two(0.04f, 0.07f); dm.startColor = new Color(1f, 1f, 1f, 0.75f); dm.maxParticles = 16; var de = dots.emission; de.rateOverTime = 18f; Box(dots, new Vector3(0.3f, 0.7f, 0f)); Backward(dots, 0.8f); var dc = dots.colorOverLifetime; dc.enabled = true; dc.color = Fade(1f, 0.8f, 0.5f); // 초승달(유닛 위 · 표 s_over = Head) — 1차 그림(검정 외곽선 · 진영색 2단 · PD 「검기는 이전 형태가 더 나음」) Sprite(root.transform, "Head", "dash2d_crescent", new Vector3(0.32f, 0f, 0f), new Vector3(1f, 1.25f, 1f), c); sb.AppendLine(Save(root)); // ── 몸 뒤덮는 기운(시험 · 스킬용 · 표 키 test_aura · 테스트 씬 「기운 시험」 스위치) ── var aura = new GameObject("TKS_Fx_Aura2D_" + cp[0]); var cometMat = Mat("Aura2D_Comet_" + cp[0], "aura_comet_" + cp[0]); // 몸통 기운(유닛 아래) — 앞머리가 몸 앞 가장자리 · 몸을 감싸고 뒤로 꼬리 · 겹쳐 깜빡이는 혜성 그림(살아 있는 불꽃 느낌) var body = NewPs(aura.transform, "Body", new Vector3(-0.45f, 0f, 0f), cometMat, true); var bm = body.main; bm.startLifetime = 0.2f; bm.startSize3D = true; bm.startSizeX = Two(1.8f, 2.0f); bm.startSizeY = Two(1.15f, 1.3f); bm.startSizeZ = 1f; bm.startColor = new Color(1f, 1f, 1f, 0.6f); bm.maxParticles = 10; var be = body.emission; be.rateOverTime = 30f; Box(body, new Vector3(0.06f, 0.06f, 0f)); var bc = body.colorOverLifetime; bc.enabled = true; var bg = new Gradient(); bg.SetKeys(new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(0f, 0f), new GradientAlphaKey(1f, 0.25f), new GradientAlphaKey(0f, 1f) }); bc.color = new ParticleSystem.MinMaxGradient(bg); body.GetComponent().alignment = ParticleSystemRenderSpace.Local; // 불꽃 혀(유닛 아래) — 밝은 진영색 · 몸에서 뒤로 흘러 나감 var wisps = NewPs(aura.transform, "Wisps", new Vector3(-0.1f, 0f, 0f), wispMat, true); var wm = wisps.main; wm.startLifetime = 0.18f; wm.startSize3D = true; wm.startSizeX = Two(0.4f, 0.8f); wm.startSizeY = Two(0.1f, 0.18f); wm.startSizeZ = 1f; wm.startColor = Color.Lerp(c, Color.white, 0.45f); wm.maxParticles = 30; var we = wisps.emission; we.rateOverTime = 35f; Box(wisps, new Vector3(0.5f, 0.8f, 0f)); Backward(wisps, 3f); var wc = wisps.colorOverLifetime; wc.enabled = true; wc.color = Fade(1f, 0.7f, 0.5f); wisps.GetComponent().alignment = ParticleSystemRenderSpace.Local; // 반짝임(유닛 아래) — 흰 점이 뒤로 var sparks = NewPs(aura.transform, "Sparks", Vector3.zero, dotMat, true); var sm = sparks.main; sm.startLifetime = 0.15f; sm.startSize = Two(0.04f, 0.08f); sm.startColor = Color.white; sm.maxParticles = 20; var se = sparks.emission; se.rateOverTime = 30f; Box(sparks, new Vector3(0.6f, 0.9f, 0f)); Backward(sparks, 4f); var sc = sparks.colorOverLifetime; sc.enabled = true; sc.color = Fade(1f, 0.8f, 0.5f); // 유닛 위(표 s_over = Veil;Rim): 옅은 진영색 막(몸이 기운 속에 있는 느낌) · 앞 가장자리 빛나는 테(2차 검기 그림 재사용) var veilColor = c; veilColor.a = 0.18f; Sprite(aura.transform, "Veil", "dash2d_dot", Vector3.zero, new Vector3(4.4f, 4.4f, 1f), veilColor); // 지름 1.1 키 원(방향마다 같게) Sprite(aura.transform, "Rim", "dash2d_slash_" + cp[0], new Vector3(0.3f, 0f, 0f), new Vector3(0.85f, 1.15f, 1f), new Color(1f, 1f, 1f, 0.9f)); sb.AppendLine(Save(aura)); } // 발밑 흙먼지(모든 진영) — 유닛을 따라 움직임 · 발 둘레에서 피었다 사라짐(몸 밖으로 안 남음) { var root = new GameObject("TKS_Fx_Dust2D"); var ps = NewPs(root.transform, "Puff", Vector3.zero, puffMat, true); var m = ps.main; m.startLifetime = 0.25f; m.startSize = Two(0.22f, 0.32f); m.startRotation = Two(0f, Mathf.PI * 2f); var dust = Hex(dustHex); dust.a = 0.75f; m.startColor = dust; m.maxParticles = 12; var e = ps.emission; e.rateOverTime = 14f; Box(ps, new Vector3(0.5f, 0.05f, 0f)); var v = ps.velocityOverLifetime; v.enabled = true; v.space = ParticleSystemSimulationSpace.World; v.x = Two(-0.1f, 0.1f); v.y = Two(0.2f, 0.4f); v.z = Two(0f, 0f); var sz = ps.sizeOverLifetime; sz.enabled = true; sz.size = new ParticleSystem.MinMaxCurve(1f, new AnimationCurve(new Keyframe(0f, 0.6f), new Keyframe(1f, 1.1f))); var col = ps.colorOverLifetime; col.enabled = true; col.color = Fade(1f, 0.75f, 0.5f); sb.AppendLine(Save(root)); } AssetDatabase.SaveAssets(); return sb.ToString();