WL #947 스킬 테스트 씬 신설 — Assets/WL/SkillTest(WLSkillTest.unity 코드 조립 · WLSkillTestBoot 부트: 매니저 프리팹 직접 배치 → 테이블 → 개발 로그인 → 맵 노드·카메라 리그 → InGameInfo/ProjectileInfo/PCInfo → SceneInfo.CurStageData 선채움(섬 씬 미로드) → PC·HUD → 무적 더미 · WASD/[ ]/R 키 · 상태 파일) · 원본 0줄 · 테이블 0줄
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
d8f6cec099
commit
d51fb12ed1
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fbf99ff76a4410847858e3565d85eea1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fe25a56d1c4ad2845b39d29dcaac17ec
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLSkillTestSceneBuilder.cs — 테스트 씬(Assets/WL/SkillTest/WLSkillTest.unity)을 코드로 조립·저장한다 (PD #947 · 2026-10-19)
|
||||
//
|
||||
// ■ 왜 코드로: 씬에 무엇이 들어가는지가 글로 남고, 깨지면 메뉴 한 번으로 다시 만든다(에디터 손작업 0 · 열린 씬(Title)은 건드리지 않는다).
|
||||
// ■ 들어가는 것 (= Title 이 Addressables 로 올리던 매니저 + InGame 씬 루트 + 원본 카메라 프리팹 + 평지 + 부트)
|
||||
// Directional Light · Ground(Plane · URP Lit 재질 자산 · 부트가 런타임 NavMesh 베이크) · Mgr(AddrResourceMgr·CoroutineMgr·TimeMgr) · EventSystem
|
||||
// · ResWork/UIPrefabs/Title/{Tables, SoundInfo, UIAtlasMgr, SortOrder_5, ActorInfo}.prefab · ResWork/Prefabs/Ingame/Main Camera.prefab(RealCamera)
|
||||
// · InGame 씬 루트 4종(IngameTouchInfo · DungeonInfo · DropItemInfo · Global Volume(GlobalVolumeMgr + 꺼진 Volume · 같은 프로필)) · ProjectileInfo
|
||||
// · WLSkillTestBoot(로그인 뒤 InGameInfo·PCInfo 프리팹 생성)
|
||||
// ■ 메뉴 WL/Skill Test/Build Scene · CLI: unity command eval --code 'return WL.SkillTest.Editor.WLSkillTestSceneBuilder.Build();'
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace WL.SkillTest.Editor
|
||||
{
|
||||
public static class WLSkillTestSceneBuilder
|
||||
{
|
||||
public const string Folder = "Assets/WL/SkillTest";
|
||||
public const string ScenePath = Folder + "/WLSkillTest.unity";
|
||||
public const string GroundMatPath = Folder + "/WLSkillTestGround.mat";
|
||||
|
||||
static readonly string[] TitlePrefabs = { "Tables", "SoundInfo", "UIAtlasMgr", "SortOrder_5", "ActorInfo" };
|
||||
const string TitlePrefabFolder = "Assets/ResWork/UIPrefabs/Title/";
|
||||
const string MainCameraPrefab = "Assets/ResWork/Prefabs/Ingame/Main Camera.prefab";
|
||||
const string InGameInfoPrefab = "Assets/ResWork/Prefabs/Ingame/InGameInfo.prefab";
|
||||
const string PCInfoPrefab = "Assets/ResWork/Prefabs/Ingame/PCInfo.prefab";
|
||||
const string ProjectileInfoPrefab = "Assets/ResWork/Prefabs/Ingame/ProjectileInfo.prefab";
|
||||
const string VolumeProfileGuid = "96e4f5994a1f2d948a293a2d5f2bd1b3"; // InGame.unity 의 Global Volume 프로필(실측)
|
||||
|
||||
[MenuItem("WL/Skill Test/Build Scene (다시 만들기)")]
|
||||
public static void BuildMenu() { Debug.Log(Build()); }
|
||||
|
||||
[MenuItem("WL/Skill Test/Open Scene")]
|
||||
public static void OpenMenu() { Debug.Log(Open()); }
|
||||
|
||||
/// <summary>테스트 씬을 연다(Single). 현재 씬이 저장 안 된 상태면 열지 않고 알린다(K-WL-114).</summary>
|
||||
public static string Open()
|
||||
{
|
||||
var cur = SceneManager.GetActiveScene();
|
||||
if (cur.isDirty) return "현재 씬 " + cur.name + " 이 저장되지 않음 — 저장 없이 다시 열거나 저장한 뒤 다시";
|
||||
if (!System.IO.File.Exists(ScenePath)) return "씬 없음 " + ScenePath + " — Build() 먼저";
|
||||
EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
return "열림 " + ScenePath;
|
||||
}
|
||||
|
||||
static bool IsTestSceneOpen()
|
||||
{
|
||||
for (int i = 0; i < SceneManager.sceneCount; i++) if (SceneManager.GetSceneAt(i).path == ScenePath) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>테스트 씬이 열려 있으면 **저장 없이** 빈 씬으로 바꾼다(다시 만들기 전 · 저장 안 된 변경이 있으면 거절).</summary>
|
||||
public static string CloseTestScene()
|
||||
{
|
||||
if (Application.isPlaying) return "Play 중";
|
||||
if (!IsTestSceneOpen()) return "테스트 씬 안 열림";
|
||||
var a = SceneManager.GetActiveScene();
|
||||
if (a.path == ScenePath && a.isDirty) return "테스트 씬이 저장되지 않음 — 저장 없이 다시 열거나 저장한 뒤";
|
||||
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
return "빈 씬으로 바꿈";
|
||||
}
|
||||
|
||||
public static string Build()
|
||||
{
|
||||
if (Application.isPlaying) return "Play 중에는 만들지 않는다";
|
||||
var sb = new StringBuilder();
|
||||
var prevActive = SceneManager.GetActiveScene();
|
||||
if (!AssetDatabase.IsValidFolder(Folder)) { AssetDatabase.CreateFolder("Assets/WL", "SkillTest"); }
|
||||
|
||||
var cfg = WLSkillTestBoot.Cfg;
|
||||
// 테스트 씬이 열려 있으면 그 경로로 저장할 수 없다(Unity: "Overwriting the same path as another open scene is not allowed")
|
||||
// → 같은 호출 안에서 씬을 바꾸면 파이프라인 실행이 끊긴다(실측) · 먼저 CloseTestScene() 을 따로 부른 뒤 Build()
|
||||
if (IsTestSceneOpen()) return "테스트 씬이 열려 있음 — 먼저 CloseTestScene()(빈 씬으로) 을 부른 뒤 Build()";
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
|
||||
SceneManager.SetActiveScene(scene);
|
||||
|
||||
// 빛
|
||||
var lightGo = new GameObject("Directional Light");
|
||||
var light = lightGo.AddComponent<Light>();
|
||||
light.type = LightType.Directional; light.intensity = 1.1f; light.shadows = LightShadows.Soft; light.color = Color.white;
|
||||
lightGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
|
||||
|
||||
// 평지(콜라이더 = NavMesh 베이크 기준)
|
||||
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
|
||||
ground.name = "Ground";
|
||||
float size = cfg != null && cfg.groundSize > 1f ? cfg.groundSize : 60f;
|
||||
ground.transform.localScale = new Vector3(size / 10f, 1f, size / 10f);
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(GroundMatPath);
|
||||
if (mat == null)
|
||||
{
|
||||
var sh = Shader.Find("Universal Render Pipeline/Lit");
|
||||
mat = new Material(sh != null ? sh : Shader.Find("Standard"));
|
||||
AssetDatabase.CreateAsset(mat, GroundMatPath);
|
||||
}
|
||||
var gc = cfg != null && cfg.groundColor != null && cfg.groundColor.Length >= 3 ? new Color(cfg.groundColor[0], cfg.groundColor[1], cfg.groundColor[2]) : new Color(0.45f, 0.62f, 0.35f);
|
||||
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", gc); else mat.color = gc;
|
||||
EditorUtility.SetDirty(mat);
|
||||
ground.GetComponent<Renderer>().sharedMaterial = mat;
|
||||
GameObjectUtility.SetStaticEditorFlags(ground, StaticEditorFlags.BatchingStatic); // NavMesh 는 부트가 콜라이더 기준 런타임 베이크
|
||||
|
||||
// 매니저(Title 의 Mgr 과 같은 구성)
|
||||
var mgr = new GameObject("Mgr");
|
||||
mgr.AddComponent<AddrResourceMgr>(); mgr.AddComponent<CoroutineMgr>(); mgr.AddComponent<TimeMgr>();
|
||||
var es = new GameObject("EventSystem");
|
||||
es.AddComponent<EventSystem>(); es.AddComponent<StandaloneInputModule>();
|
||||
|
||||
// Title 이 Addressables 로 올리던 프리팹 → 씬에 직접
|
||||
foreach (var n in TitlePrefabs)
|
||||
{
|
||||
var p = AssetDatabase.LoadAssetAtPath<GameObject>(TitlePrefabFolder + n + ".prefab");
|
||||
if (p == null) { sb.Append("🔴 프리팹 없음 ").Append(n).Append('\n'); continue; }
|
||||
var go = PrefabUtility.InstantiatePrefab(p, scene) as GameObject;
|
||||
if (go != null) go.name = n;
|
||||
}
|
||||
// 원본 인게임 카메라(RealCamera · 태그 MainCamera)
|
||||
var camP = AssetDatabase.LoadAssetAtPath<GameObject>(MainCameraPrefab);
|
||||
if (camP != null) { var cam = PrefabUtility.InstantiatePrefab(camP, scene) as GameObject; if (cam != null) cam.name = "Main Camera"; }
|
||||
else sb.Append("🔴 Main Camera 프리팹 없음\n");
|
||||
|
||||
// InGame 씬 루트 4종
|
||||
new GameObject("IngameTouchInfo").AddComponent<InGameTouchInfo>();
|
||||
new GameObject("DungeonInfo").AddComponent<DungeonInfo>();
|
||||
new GameObject("DropItemInfo").AddComponent<DropItemInfo>();
|
||||
var gv = new GameObject("Global Volume");
|
||||
gv.AddComponent<GlobalVolumeMgr>();
|
||||
var vol = gv.AddComponent<Volume>();
|
||||
vol.isGlobal = true; vol.enabled = false; // InGame.unity 실측: Volume 컴포넌트 꺼짐
|
||||
var profPath = AssetDatabase.GUIDToAssetPath(VolumeProfileGuid);
|
||||
if (!string.IsNullOrEmpty(profPath)) vol.sharedProfile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(profPath);
|
||||
// ProjectileInfo 는 씬에 두지 않는다 — 부트가 InGameInfo 뒤에 원본 프리팹으로 만든다(1차 Play: 예열 투사체가 InGameInfo.Ins null 로 NRE 127건)
|
||||
|
||||
// 부트
|
||||
var bootGo = new GameObject("WLSkillTestBoot");
|
||||
var boot = bootGo.AddComponent<WLSkillTestBoot>();
|
||||
boot.inGameInfoPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(InGameInfoPrefab);
|
||||
boot.pcInfoPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(PCInfoPrefab);
|
||||
boot.projectileInfoPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(ProjectileInfoPrefab);
|
||||
if (boot.inGameInfoPrefab == null) sb.Append("🔴 InGameInfo 프리팹 없음\n");
|
||||
if (boot.pcInfoPrefab == null) sb.Append("🔴 PCInfo 프리팹 없음\n");
|
||||
if (boot.projectileInfoPrefab == null) sb.Append("🔴 ProjectileInfo 프리팹 없음\n");
|
||||
|
||||
bool saved = EditorSceneManager.SaveScene(scene, ScenePath);
|
||||
sb.Append(saved ? "저장 " : "🔴 저장 실패 ").Append(ScenePath).Append(" · 루트 ").Append(scene.rootCount).Append('\n');
|
||||
|
||||
// 열려 있던 씬으로 되돌리고 새 씬은 닫는다(Title 무변경)
|
||||
if (prevActive.IsValid() && prevActive.isLoaded) SceneManager.SetActiveScene(prevActive);
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
AssetDatabase.SaveAssets();
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 87ce98ef730cc604bacdad3a67282092
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6c1e9723645442b48a842cdfe5980b98
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a26aaa0b1ac0c404096587f6ba76614c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"note": "WL 스킬 테스트 씬(PD #947 · 2026-10-19) 값. loginId = 개발 로그인 ID(useOptionUserId 1 이면 Title 에서 마지막에 쓴 ID 우선) · mapId 0 = 섬 행(900 · Lobby 모드 · 씬은 로드하지 않음) · dummyMobId 0 = WLIslandMobSettings.hitTestDummyMobId · dummyOffset 전부 0 = WLIslandMobSettings.hitTestDummyOffset · 키 이름 = UnityEngine.KeyCode",
|
||||
"loginId": "TestID",
|
||||
"useOptionUserId": 1,
|
||||
"mapId": 0,
|
||||
"spawnPos": [0, 0, 0],
|
||||
"cameraRig": 1,
|
||||
"dummyMobId": 0,
|
||||
"dummyCount": 1,
|
||||
"dummySpawnerId": 1,
|
||||
"dummyOffset": [0, 0, 0],
|
||||
"dummyGap": 2.5,
|
||||
"waitPcSeconds": 30,
|
||||
"forceSpawn": 1,
|
||||
"probeAtlas": 0,
|
||||
"probeNoLogin": 0,
|
||||
"probeProfiler": 0,
|
||||
"dummyStill": 1,
|
||||
"skyColor": [0.55, 0.75, 0.95],
|
||||
"moveKeys": 1,
|
||||
"weaponPrevKey": "LeftBracket",
|
||||
"weaponNextKey": "RightBracket",
|
||||
"dummyResetKey": "R",
|
||||
"label": 1,
|
||||
"labelFontScale": 0.018,
|
||||
"groundSize": 100,
|
||||
"groundColor": [0.45, 0.62, 0.35]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bb979365e442a1241be2d7b5975bf741
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e7b4ccbbf2b0ad0409dfdd76b3d350cd
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,493 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLSkillTestBoot.cs — 무기 스킬 구현용 가벼운 테스트 씬(WLSkillTest) 부트 (PD #947 · 2026-10-19)
|
||||
//
|
||||
// PD 원문: 「Title 씬에서 무기 스킬 작업을 진행하다보니 쓸데 없이 로딩이 너무 길고 테스트가 힘들어. … 최대한 가벼운 테스트용 씬을 만들고
|
||||
// 이 씬에서 스킬 작업을 할 수 있도록 … (우리 게임의 무기 스킬 구현은 앞으로 테스트 씬을 통해 진행할게)」
|
||||
//
|
||||
// ■ 왜 — Title → 로그인 → Ingame → 섬 흐름은 Play 1회에 약 115 s(도메인 리로드 32 + Title 씬 21 + Title 초기화 47 + 로그인 3.5 + 섬 9 · #943-① 실측).
|
||||
// 스킬 작업은 PC 1 + 무적 더미 1 + 평지면 충분하다 → Title 씬·섬 씬(FarmingIsland)·농장 시스템을 빼고 **원본 InGame 흐름만** 돌린다.
|
||||
// ■ 어떻게 (원본 코드 0줄 · 테이블 0줄 · 빌드 설정 0줄)
|
||||
// ① 씬에 매니저 프리팹을 직접 둔다(Title 이 Addressables 로 올리던 Tables·SoundInfo·UIAtlasMgr·SortOrder_5·ActorInfo + Mgr(AddrResourceMgr·CoroutineMgr·TimeMgr)
|
||||
// + EventSystem + 원본 Main Camera 프리팹(RealCamera) + InGame 씬 루트 4종) → 테이블 로딩 대기. ※ 씬 조립 = Editor/WLSkillTestSceneBuilder.cs
|
||||
// ② 개발 로그인(ServerInfo.Login · TitleInfo 없이 · ID = PlayerPrefs Option_UserID 또는 설정값) → m_ServerData(진짜 계정 데이터 · 장비·스킬 슬롯 그대로).
|
||||
// ③ 섬·던전과 같은 LoadMapMgr 대체 노드(WLIslandMapNodes)에 시작 지점 + 섬 구도 카메라 리그(WLDungeonCameraRig).
|
||||
// ④ InGameInfo·PCInfo 프리팹을 **로그인 뒤** 생성 → InGameInfo 가 NewGameUI(HUD · 조이스틱 · 장비창)를 올리고 NewGameUI.Start 가 Load_Map(섬 900)을 부른다.
|
||||
// 그 전에 SceneInfo.CurStageData 를 900 행으로 미리 채워(리플렉션) 「같은 맵」 분기 = **섬 씬을 로드하지 않고** PCInfo.Make_Actors 만 돈다.
|
||||
// ⑤ PC 가 생기면 StartCam + SetUI_byGameMode(Lobby) + 무적 더미(몬스터 표 hitTestDummyMobId · WLTrailTest 채택 · HP 되채움) → Stage = ready.
|
||||
// ⑥ 키: WASD/방향키 이동(원본 Run_byJoystick) · [ ] 낀 무기 바꾸기(WLInventory 장비 순환) · R 더미 제자리 · Q/E/숫자 = 기존 훅(WLSkillHotkey·WLWeaponGradeCheat).
|
||||
// ■ 값 = Resources/WL/WLSkillTest.json (C45) · 진단 = Describe() · 단계 시각 = Laps(로딩 실측 보고용)
|
||||
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다) · 씬 이름 "WLSkillTest" 는 원본 "StatTest" 분기와 무관(그 분기는 MainToStatTest 를 요구한다).
|
||||
// 🔴 섬 씬 이름(Level01)과 다르므로 WLIslandBridge·WLIslandMobZones 등 섬 전용 훅은 이 씬에서 돌지 않는다(의도).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Unity.AI.Navigation;
|
||||
using UnityEngine;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.SceneManagement;
|
||||
using WL.Combat.Weapon;
|
||||
using WL.Equip;
|
||||
using WL.Island;
|
||||
using WL.IslandMob;
|
||||
|
||||
namespace WL.SkillTest
|
||||
{
|
||||
[System.Serializable]
|
||||
public class WLSkillTestJson
|
||||
{
|
||||
public string note = "";
|
||||
public string loginId = "TestID"; // 개발 로그인 ID 기본값
|
||||
public int useOptionUserId = 1; // 1 = PlayerPrefs Option_UserID(Title 에서 마지막에 쓴 ID)를 우선
|
||||
public int mapId = 0; // 0 = WLIslandSettings.islandMapId(섬 행 900 · Lobby 모드) — 씬은 로드하지 않고 모드·노드만 빌린다
|
||||
public float[] spawnPos = { 0f, 0f, 0f };
|
||||
public int cameraRig = 1; // 1 = 섬 구도 카메라 리그(WLDungeonCameraRig) 생성
|
||||
public int dummyMobId = 0; // 0 = WLIslandMobSettings.hitTestDummyMobId
|
||||
public int dummyCount = 1;
|
||||
public int dummySpawnerId = 1;
|
||||
public float[] dummyOffset = { 0f, 0f, 0f }; // 전부 0 = WLIslandMobSettings.hitTestDummyOffset (PC 기준 오른쪽·위·앞)
|
||||
public float dummyGap = 2.5f; // 더미가 둘 이상일 때 옆 간격 m
|
||||
public float waitPcSeconds = 30f;
|
||||
public int forceSpawn = 1; // 1 = PC 가 생긴 뒤 시작 지점(spawnPos)으로 한 번 더 워프
|
||||
public int probeAtlas = 0; // 진단: 1 = 부트 첫머리에서 UI 아틀라스 로드 시간을 잰다(평소 0)
|
||||
public int probeNoLogin = 0; // 진단: 1 = 로그인 없이 60 s 프레임 멈춤만 기록(평소 0)
|
||||
public int probeProfiler = 0; // 진단: 1 = 프로파일러를 켜고 3 s 넘는 프레임의 무거운 호출 경로를 ProfileDump 에(평소 0 · probeNoLogin 과 함께)
|
||||
public int dummyStill = 1; // 1 = 더미가 제자리(Stop) · 0 = 섬처럼 쫓아옴
|
||||
public float[] skyColor = { 0.55f, 0.75f, 0.95f }; // 스카이박스가 없을 때 카메라 배경색(빈 값이면 손대지 않음)
|
||||
public int moveKeys = 1; // WASD · 방향키 이동
|
||||
public string weaponPrevKey = "LeftBracket";
|
||||
public string weaponNextKey = "RightBracket";
|
||||
public string dummyResetKey = "R";
|
||||
public int label = 1; // 화면 왼쪽 위 상태 글자
|
||||
public float labelFontScale = 0.018f;
|
||||
public float groundSize = 60f; // 평지 한 변 m(빌더가 읽는다)
|
||||
public float[] groundColor = { 0.45f, 0.62f, 0.35f };
|
||||
}
|
||||
|
||||
public sealed class WLSkillTestBoot : MonoBehaviour
|
||||
{
|
||||
public const string SceneName = "WLSkillTest";
|
||||
public const string ResourcesPath = "WL/WLSkillTest";
|
||||
|
||||
[Header("씬 빌더가 채운다 — 로그인 뒤에 생성하는 프리팹(InGameInfo → ProjectileInfo → PCInfo 순 · 투사체 예열이 InGameInfo.Ins 를 쓴다)")]
|
||||
public GameObject inGameInfoPrefab;
|
||||
public GameObject pcInfoPrefab;
|
||||
public GameObject projectileInfoPrefab;
|
||||
|
||||
public static WLSkillTestBoot Ins;
|
||||
public static string Stage = "none";
|
||||
public static string LastLog = "";
|
||||
public static bool Ready;
|
||||
public static Actor Dummy;
|
||||
public static readonly List<string> Laps = new List<string>();
|
||||
/// <summary>PlayFab 호출 시각 기록(에디터 · 로딩 실측용 · 2차 Play 에서 로그인 52 s 의 내역을 보려고)</summary>
|
||||
public static readonly List<string> PfLog = new List<string>();
|
||||
/// <summary>0.5 s 넘게 멈춘 프레임 기록(시각@길이 · 메인 스레드가 무엇에 막혔는지 PfLog·Laps 와 맞춰 본다)</summary>
|
||||
public static readonly List<string> Hangs = new List<string>();
|
||||
public static float T0;
|
||||
float _lastRt;
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
float now = Time.realtimeSinceStartup;
|
||||
if (_lastRt > 0f && now - _lastRt > 0.5f && Hangs.Count < 40)
|
||||
{
|
||||
string flags = "";
|
||||
#if UNITY_EDITOR
|
||||
flags = " L" + (UnityEditor.Lightmapping.isRunning ? 1 : 0) + "S" + (UnityEditor.ShaderUtil.anythingCompiling ? 1 : 0) + "C" + (UnityEditor.EditorApplication.isCompiling ? 1 : 0) + "U" + (UnityEditor.EditorApplication.isUpdating ? 1 : 0);
|
||||
#endif
|
||||
Hangs.Add((now - T0).ToString("F1") + "@" + (now - _lastRt).ToString("F1") + "s" + flags);
|
||||
#if UNITY_EDITOR
|
||||
// 멈춤 직후 그 프레임(과 직전 프레임)의 가장 무거운 호출 경로를 바로 적는다 — 프레임 버퍼가 밀려 사라지기 전에
|
||||
if (Cfg.probeProfiler != 0) { int li = UnityEditorInternal.ProfilerDriver.lastFrameIndex; DumpFrame(li - 1, 1000f); DumpFrame(li, 1000f); }
|
||||
#endif
|
||||
}
|
||||
_lastRt = now;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>진단: 프로파일러로 잡은 긴 프레임의 가장 무거운 호출 경로(멈춤의 정체)</summary>
|
||||
public static string ProfileDump = "";
|
||||
static void ProfilerStart() { s_prof.Length = 0; ProfileDump = ""; UnityEditorInternal.ProfilerDriver.ClearAllFrames(); UnityEditorInternal.ProfilerDriver.enabled = true; }
|
||||
static readonly System.Text.StringBuilder s_prof = new System.Text.StringBuilder();
|
||||
static void DumpFrame(int f, float minMs)
|
||||
{
|
||||
if (f < 0) return;
|
||||
int col = UnityEditor.Profiling.HierarchyFrameDataView.columnTotalTime;
|
||||
var kids = new List<int>();
|
||||
using (var v = UnityEditorInternal.ProfilerDriver.GetHierarchyFrameDataView(f, 0, UnityEditor.Profiling.HierarchyFrameDataView.ViewModes.MergeSamplesWithTheSameName, col, false))
|
||||
{
|
||||
if (v == null || !v.valid || v.frameTimeMs < minMs) return;
|
||||
s_prof.Append(" || frame ").Append(f).Append(' ').Append(v.frameTimeMs.ToString("F0")).Append("ms:");
|
||||
int id = v.GetRootItemID();
|
||||
for (int depth = 0; depth < 12; depth++)
|
||||
{
|
||||
kids.Clear(); v.GetItemChildren(id, kids);
|
||||
if (kids.Count == 0) break;
|
||||
int best = kids[0]; float bestMs = -1f, second = 0f; string secondName = "";
|
||||
foreach (var k in kids) { float ms = v.GetItemColumnDataAsFloat(k, col); if (ms > bestMs) { if (bestMs > second) { second = bestMs; secondName = v.GetItemName(best); } bestMs = ms; best = k; } else if (ms > second) { second = ms; secondName = v.GetItemName(k); } }
|
||||
s_prof.Append(" > ").Append(v.GetItemName(best)).Append(' ').Append(bestMs.ToString("F0"));
|
||||
if (second > 1000f) s_prof.Append(" (+").Append(secondName).Append(' ').Append(second.ToString("F0")).Append(')');
|
||||
id = best;
|
||||
}
|
||||
}
|
||||
ProfileDump = s_prof.ToString();
|
||||
}
|
||||
static void ProfilerDump(float minMs)
|
||||
{
|
||||
int first = UnityEditorInternal.ProfilerDriver.firstFrameIndex, last = UnityEditorInternal.ProfilerDriver.lastFrameIndex;
|
||||
s_prof.Append(" || buffer ").Append(first).Append("~").Append(last);
|
||||
for (int f = first; f <= last && f >= 0; f++) DumpFrame(f, minMs);
|
||||
ProfileDump = s_prof.ToString();
|
||||
UnityEditorInternal.ProfilerDriver.enabled = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
static System.Reflection.FieldInfo s_fAtlas;
|
||||
static bool AtlasReady()
|
||||
{
|
||||
if (!UIAtlasMgr.isIns || UIAtlasMgr.Ins == null) return false;
|
||||
if (s_fAtlas == null) s_fAtlas = typeof(UIAtlasMgr).GetField("UIAtlas", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
return s_fAtlas != null && s_fAtlas.GetValue(UIAtlasMgr.Ins) != null;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
static void OnPf(PlayFab.Internal.ApiProcessingEventArgs e) { PfLog.Add((Time.realtimeSinceStartup - T0).ToString("F1") + " " + e.EventType + " " + e.ApiEndpoint); }
|
||||
static void OnPfErr(PlayFab.SharedModels.PlayFabRequestCommon r, PlayFab.PlayFabError err) { PfLog.Add((Time.realtimeSinceStartup - T0).ToString("F1") + " ERR " + (err != null ? err.Error + " " + err.ErrorMessage : "?")); }
|
||||
#endif
|
||||
|
||||
static WLSkillTestJson s_cfg;
|
||||
bool _moving;
|
||||
KeyCode _kPrev = KeyCode.LeftBracket, _kNext = KeyCode.RightBracket, _kReset = KeyCode.R;
|
||||
string _weaponMsg = "";
|
||||
GUIStyle _style;
|
||||
|
||||
public static WLSkillTestJson Cfg
|
||||
{
|
||||
get
|
||||
{
|
||||
if (s_cfg != null) return s_cfg;
|
||||
s_cfg = new WLSkillTestJson();
|
||||
var ta = Resources.Load<TextAsset>(ResourcesPath);
|
||||
if (ta == null) { LastLog = "설정 없음 " + ResourcesPath; return s_cfg; }
|
||||
try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; }
|
||||
return s_cfg;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Active { get { return SceneManager.GetActiveScene().name == SceneName; } }
|
||||
|
||||
static Vector3 V3(float[] a) { return a != null && a.Length >= 3 ? new Vector3(a[0], a[1], a[2]) : Vector3.zero; }
|
||||
static KeyCode Key(string s, KeyCode d) { KeyCode k; return !string.IsNullOrEmpty(s) && System.Enum.TryParse(s, true, out k) ? k : d; }
|
||||
|
||||
/// <summary>상태 파일(Library/ 아래 · 미추적) — 외부 스크립트는 Play 중 `eval` 대신 이 파일을 읽는다. 실측(8차 Play): Play 뒤 첫 `unity command eval/run_script` 가 EditorLoop 에서 약 35 s 메인 스레드를 멈춘다(스크립트 컴파일 준비).</summary>
|
||||
public static string StatusPath { get { return System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath, "../Library/WLSkillTest_status.txt")); } }
|
||||
|
||||
static void WriteStatus()
|
||||
{
|
||||
try { System.IO.File.WriteAllText(StatusPath, Describe()); } catch { }
|
||||
}
|
||||
|
||||
static void Lap(string name)
|
||||
{
|
||||
Stage = name;
|
||||
Laps.Add(name + " " + (Time.realtimeSinceStartup - T0).ToString("F1") + "s");
|
||||
WriteStatus();
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Ins = this;
|
||||
T0 = Time.realtimeSinceStartup;
|
||||
Laps.Clear(); PfLog.Clear(); Hangs.Clear(); Ready = false; Dummy = null; s_cfg = null;
|
||||
#if UNITY_EDITOR
|
||||
if (Cfg.probeProfiler != 0) ProfilerStart();
|
||||
PlayFab.Internal.PlayFabHttp.ApiProcessingEventHandler -= OnPf; PlayFab.Internal.PlayFabHttp.ApiProcessingEventHandler += OnPf;
|
||||
PlayFab.Internal.PlayFabHttp.ApiProcessingErrorEventHandler -= OnPfErr; PlayFab.Internal.PlayFabHttp.ApiProcessingErrorEventHandler += OnPfErr;
|
||||
#endif
|
||||
var cfg = Cfg;
|
||||
_kPrev = Key(cfg.weaponPrevKey, KeyCode.LeftBracket);
|
||||
_kNext = Key(cfg.weaponNextKey, KeyCode.RightBracket);
|
||||
_kReset = Key(cfg.dummyResetKey, KeyCode.R);
|
||||
Lap("awake");
|
||||
}
|
||||
|
||||
IEnumerator Start()
|
||||
{
|
||||
var cfg = Cfg;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// 진단(probeAtlas 1): UI 스프라이트 아틀라스(.spriteatlasv2)를 에디터 API 로 직접 읽어 걸리는 시간을 잰다 — 4차 Play 실측에서 로그인 중 메인 스레드가 16 s + 34 s 멈췄고 첫 멈춤 끝 = 아틀라스 도착 시각
|
||||
if (cfg.probeAtlas != 0)
|
||||
{
|
||||
float a = Time.realtimeSinceStartup;
|
||||
var atl = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.U2D.SpriteAtlas>("Assets/Res_Addr/MainUI/UIAtlas.spriteatlasv2");
|
||||
float b = Time.realtimeSinceStartup;
|
||||
var sp = atl != null ? atl.GetSprite("grade1") : null;
|
||||
float c = Time.realtimeSinceStartup;
|
||||
Laps.Add("atlasLoad " + (b - a).ToString("F1") + "s getSprite " + (c - b).ToString("F1") + "s spr=" + (sp != null) + " n=" + (atl != null ? atl.spriteCount : -1));
|
||||
}
|
||||
#endif
|
||||
|
||||
// ⓪ 평지 NavMesh — 콜라이더 기준 런타임 베이크(WLDungeonSceneSetup 과 같은 방식 · 임포트 설정 0) + 배경색
|
||||
BakeGround();
|
||||
ApplySky(cfg);
|
||||
Lap("navmesh");
|
||||
|
||||
// ⓪-2 Addressables 초기화를 **따로 재서** 보이게 한다(Use Asset Database 모드의 카탈로그 준비 · 어차피 첫 로드 때 든다)
|
||||
var init = UnityEngine.AddressableAssets.Addressables.InitializeAsync();
|
||||
while (!init.IsDone) yield return null;
|
||||
Lap("addr-init");
|
||||
|
||||
// ① 테이블(Tables 프리팹 = TableChecker + 표 44개)
|
||||
while (!TableChecker.isIns || TableChecker.Ins == null || !TableChecker.Ins.CheckAllLoad()) yield return null;
|
||||
Lap("tables");
|
||||
|
||||
// 진단(probeNoLogin 1): 로그인 없이 60 s 동안 프레임 멈춤만 기록하고 멈춘다 — 멈춤이 PlayFab 때문인지 에디터(조명·셰이더) 때문인지 가른다
|
||||
if (cfg.probeNoLogin != 0)
|
||||
{
|
||||
float w = 0f; while (w < 60f) { w += Time.unscaledDeltaTime; yield return null; }
|
||||
#if UNITY_EDITOR
|
||||
if (cfg.probeProfiler != 0) ProfilerDump(3000f);
|
||||
#endif
|
||||
Stage = "nologin-done"; LastLog = "로그인 없이 60 s 관찰"; WriteStatus(); yield break;
|
||||
}
|
||||
|
||||
// ② 개발 로그인 — TitleInfo.OnClick_DevLogin 과 같은 순서(ID 저장 → ServerInfo.Login)
|
||||
var si = ServerInfo.Ins;
|
||||
string id = cfg.useOptionUserId != 0 ? PlayerPrefs.GetString(MyValue.OptionKey_UserID, cfg.loginId) : cfg.loginId;
|
||||
if (string.IsNullOrEmpty(id)) id = string.IsNullOrEmpty(cfg.loginId) ? "TestID" : cfg.loginId;
|
||||
PlayerPrefs.SetString(MyValue.OptionKey_UserID, id);
|
||||
si.UserID = id;
|
||||
bool done = false, fail = false, atlasLapped = false;
|
||||
si.Login(newuser => { done = true; }, () => { fail = true; });
|
||||
while (!done && !fail)
|
||||
{
|
||||
if (!atlasLapped && AtlasReady()) { atlasLapped = true; Lap("atlas"); Stage = "login"; } // UIAtlasMgr 의 스프라이트 아틀라스(에디터 로드)가 끝난 시각
|
||||
yield return null;
|
||||
}
|
||||
if (!atlasLapped && AtlasReady()) Lap("atlas");
|
||||
if (fail || si.m_ServerData == null) { Stage = "login-fail"; LastLog = "로그인 실패 — ID " + id; WriteStatus(); yield break; }
|
||||
Lap("login " + id);
|
||||
|
||||
// ③ 맵 노드(LoadMapMgr 대체) + 카메라 리그 — PCInfo.Make_Actors 보다 먼저 있어야 한다(같은 프레임)
|
||||
var icfg = WLIslandSettings.Instance;
|
||||
int mapId = cfg.mapId > 0 ? cfg.mapId : (icfg != null ? icfg.islandMapId : 900);
|
||||
MyValue.MyChoiceMapData.n_MapID = mapId;
|
||||
MyValue.MyChoiceMapData.n_PortalIndex = 0;
|
||||
Vector3 spawn = V3(cfg.spawnPos);
|
||||
float yaw = icfg != null ? WLDungeonCameraRig.SolvedYaw(icfg) : 0f;
|
||||
if (icfg != null)
|
||||
{
|
||||
var nodes = WLIslandMapNodes.Ensure(icfg);
|
||||
if (nodes != null) nodes.SetStartPos(spawn, yaw);
|
||||
if (cfg.cameraRig != 0 && Object.FindFirstObjectByType<WLDungeonCameraRig>() == null)
|
||||
WLDungeonCameraRig.Create(icfg, gameObject.scene);
|
||||
}
|
||||
|
||||
// ④ InGame 매니저 — NewGameUI.Start 의 Load_Map(mapId) 이 「같은 맵」 분기를 타도록 SceneInfo.CurStageData 를 미리 채운다
|
||||
if (inGameInfoPrefab != null && InGameInfo.Ins == null) Instantiate(inGameInfoPrefab).name = "InGameInfo";
|
||||
if (projectileInfoPrefab != null && !ProjectileInfo.isIns) Instantiate(projectileInfoPrefab).name = "ProjectileInfo"; // 🔴 InGameInfo 뒤 — 예열 투사체 OnEnable → Off → Show_HitEffect 가 InGameInfo.Ins 를 쓴다(1차 Play 실측 NRE 127건)
|
||||
if (pcInfoPrefab != null && !PCInfo.isIns) Instantiate(pcInfoPrefab).name = "PCInfo";
|
||||
var row = table_battlemapconfig.Ins.Get_Data(mapId);
|
||||
var sinfo = InGameInfo.Ins != null ? InGameInfo.Ins.m_SceneInfo : null;
|
||||
if (row == null) { Stage = "map-row-none"; LastLog = "BattleMapConfig 행 없음 " + mapId; WriteStatus(); yield break; }
|
||||
if (sinfo == null) { Stage = "sceneinfo-none"; LastLog = "InGameInfo.m_SceneInfo 없음"; WriteStatus(); yield break; }
|
||||
var f = typeof(SceneInfo).GetField("CurStageData", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (f == null) { Stage = "reflect-fail"; LastLog = "SceneInfo.CurStageData 필드 없음"; WriteStatus(); yield break; }
|
||||
f.SetValue(sinfo, row);
|
||||
Lap("ingame");
|
||||
|
||||
// ⑤ PC — NewGameUI(HUD) 로드 → Load_Map → Make_Actors(Addressables) → PCActor.Set → MyValue.MyPC
|
||||
float t = 0f;
|
||||
while (t < cfg.waitPcSeconds && !(MyValue.bMyPC && MyValue.MyPC != null)) { t += Time.unscaledDeltaTime; yield return null; }
|
||||
if (!(MyValue.bMyPC && MyValue.MyPC != null)) { Stage = "pc-timeout"; LastLog = "PC 생성 대기 초과 " + cfg.waitPcSeconds + "s · NewGameUI " + NewGameUI.isIns; WriteStatus(); yield break; }
|
||||
Lap("pc");
|
||||
yield return null;
|
||||
// 시작 지점 보정 — 1차 Play 실측: PC 가 평지 모서리(28, -29)에 서 있었다(원인 미확정) → NavMesh 위 시작 지점으로 한 번 더 워프
|
||||
if (cfg.forceSpawn != 0) { var pcA = MyValue.MyPC; pcA.Stop(true); pcA.Set_Warp(NavPos(spawn)); }
|
||||
if (InGameInfo.Ins != null) InGameInfo.Ins.StartCam();
|
||||
t = 0f;
|
||||
while (t < 10f && !NewGameUI.isIns) { t += Time.unscaledDeltaTime; yield return null; }
|
||||
if (NewGameUI.isIns && NewGameUI.Ins != null) NewGameUI.Ins.SetUI_byGameMode(eGameMode.Lobby); // 로딩 화면 닫기 + 섬과 같은 로비 UI
|
||||
Lap("ui");
|
||||
|
||||
// ⑥ 무적 더미
|
||||
yield return StartCoroutine(Co_SpawnDummies(cfg));
|
||||
|
||||
Ready = true;
|
||||
LastLog = "";
|
||||
Lap("ready");
|
||||
}
|
||||
|
||||
void OnDestroy() { try { if (System.IO.File.Exists(StatusPath)) System.IO.File.Delete(StatusPath); } catch { } }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
static Vector3 NavPos(Vector3 want)
|
||||
{
|
||||
UnityEngine.AI.NavMeshHit hit;
|
||||
return UnityEngine.AI.NavMesh.SamplePosition(want, out hit, 5f, UnityEngine.AI.NavMesh.AllAreas) ? hit.position : want;
|
||||
}
|
||||
|
||||
static void ApplySky(WLSkillTestJson cfg)
|
||||
{
|
||||
var cam = Camera.main;
|
||||
if (cam == null || cfg.skyColor == null || cfg.skyColor.Length < 3) return;
|
||||
if (RenderSettings.skybox == null) { cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = new Color(cfg.skyColor[0], cfg.skyColor[1], cfg.skyColor[2]); }
|
||||
}
|
||||
|
||||
void BakeGround()
|
||||
{
|
||||
var surf = Object.FindFirstObjectByType<NavMeshSurface>();
|
||||
if (surf == null)
|
||||
{
|
||||
var ground = GameObject.Find("Ground");
|
||||
if (ground == null) return;
|
||||
surf = ground.AddComponent<NavMeshSurface>();
|
||||
}
|
||||
surf.useGeometry = UnityEngine.AI.NavMeshCollectGeometry.PhysicsColliders;
|
||||
surf.BuildNavMesh();
|
||||
}
|
||||
|
||||
IEnumerator Co_SpawnDummies(WLSkillTestJson cfg)
|
||||
{
|
||||
var ms = WLIslandMobSettings.Instance;
|
||||
int mobId = cfg.dummyMobId > 0 ? cfg.dummyMobId : (ms != null ? ms.hitTestDummyMobId : 101);
|
||||
var data = table_monsterlist.Ins.Get_Data_orNull(mobId);
|
||||
if (data == null) { LastLog = "몬스터 표 없음 " + mobId; yield break; }
|
||||
Vector3 off = V3(cfg.dummyOffset);
|
||||
if (off == Vector3.zero && ms != null) off = ms.hitTestDummyOffset;
|
||||
var pc = MyValue.MyPC;
|
||||
int want = Mathf.Max(1, cfg.dummyCount), made = 0;
|
||||
for (int i = 0; i < want; i++)
|
||||
{
|
||||
bool got = false; int idx = i;
|
||||
AddrResourceMgr.Ins.LoadObject<GameObject>(data.s_MonsterPrefab, h =>
|
||||
{
|
||||
if (h.Status == AsyncOperationStatus.Succeeded && h.Result != null)
|
||||
{
|
||||
var mob = DSUtil.Get_Clone<MobActor>(h.Result);
|
||||
AddrResourceMgr.Ins.Set_AddressableReleaseSelf(mob.gameObject);
|
||||
ActorInfo.Ins.Add_Actor(mob, eRole.Mob, cfg.dummySpawnerId, data.s_MonsterPrefab);
|
||||
mob.Set(true, null, null, cfg.dummySpawnerId, false);
|
||||
Vector3 pos = DummyPos(pc, off, idx, cfg.dummyGap);
|
||||
mob.Set(data, pos, null, 0);
|
||||
if (cfg.dummyStill != 0) mob.Stop(true); // 제자리 더미(쫓아오지 않음) — 0 이면 섬처럼 움직인다
|
||||
if (idx == 0) { WLTrailTest.Adopt(mob); Dummy = mob; }
|
||||
var keep = mob.GetComponent<WLTrailTestDummy>();
|
||||
if (keep == null) mob.gameObject.AddComponent<WLTrailTestDummy>().Begin(mob); // 2마리째부터도 체력 되채움
|
||||
made++;
|
||||
}
|
||||
got = true;
|
||||
});
|
||||
while (!got) yield return null;
|
||||
}
|
||||
Lap("dummy " + made + "(" + mobId + ")");
|
||||
}
|
||||
|
||||
static Vector3 DummyPos(Actor pc, Vector3 off, int idx, float gap)
|
||||
{
|
||||
Vector3 basePos = pc != null ? pc.Get_position() : Vector3.zero;
|
||||
Quaternion rot = pc != null ? Quaternion.Euler(0f, pc.transform.eulerAngles.y, 0f) : Quaternion.identity;
|
||||
Vector3 side = rot * Vector3.right;
|
||||
Vector3 want = basePos + rot * off + side * (gap * ((idx + 1) / 2) * (idx % 2 == 0 ? 1f : -1f)) * (idx == 0 ? 0f : 1f);
|
||||
UnityEngine.AI.NavMeshHit hit;
|
||||
if (UnityEngine.AI.NavMesh.SamplePosition(want, out hit, 3f, UnityEngine.AI.NavMesh.AllAreas)) want = hit.position;
|
||||
return want;
|
||||
}
|
||||
|
||||
/// <summary>더미를 PC 앞 기본 자리로 되돌린다(R 키 · 프로브).</summary>
|
||||
public static string ResetDummy()
|
||||
{
|
||||
var pc = MyValue.MyPC; var d = Dummy;
|
||||
if (pc == null || d == null) return "PC/더미 없음";
|
||||
var ms = WLIslandMobSettings.Instance; var cfg = Cfg;
|
||||
Vector3 off = V3(cfg.dummyOffset); if (off == Vector3.zero && ms != null) off = ms.hitTestDummyOffset;
|
||||
var pos = DummyPos(pc, off, 0, cfg.dummyGap);
|
||||
d.Stop(true); d.Set_Warp(pos);
|
||||
return "더미 " + pos.ToString("F1");
|
||||
}
|
||||
|
||||
/// <summary>낀 무기를 인벤토리 무기 목록에서 앞뒤로 바꾼다(dir ±1 · 결과 한 줄).</summary>
|
||||
public static string CycleWeapon(int dir)
|
||||
{
|
||||
if (!WLInventory.EnsureLoaded()) return "인벤토리 미로드";
|
||||
var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
|
||||
var list = new List<int>(); var names = new List<string>(); int curIdx = -1;
|
||||
foreach (var it in WLInventory.SortedItems())
|
||||
{
|
||||
if (it == null || it.itemJson == null || !it.itemJson.IsEquipment || it.itemJson.EquipmentType != eFREquipmentType.Weapon) continue;
|
||||
if (cur != null && ReferenceEquals(it, cur)) curIdx = list.Count;
|
||||
list.Add(it.rawItem._id); names.Add(it.itemJson.Item_name + "(" + it.itemJson.GroupNo + ")");
|
||||
}
|
||||
if (list.Count == 0) return "무기 없음";
|
||||
int next = curIdx < 0 ? 0 : (curIdx + dir + list.Count) % list.Count;
|
||||
bool ok = WLInventory.Equip(list[next]);
|
||||
return (ok ? "장착 " : "실패 ") + names[next] + " " + (next + 1) + "/" + list.Count + (ok ? "" : " · " + WLInventory.LastMessage);
|
||||
}
|
||||
|
||||
/// <summary>보고·프로브용 한 줄.</summary>
|
||||
public static string Describe()
|
||||
{
|
||||
var pc = MyValue.MyPC;
|
||||
string w = "-";
|
||||
if (WLInventory.Loaded) { var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); if (cur != null && cur.itemJson != null) w = cur.itemJson.Item_name + "(" + cur.itemJson.GroupNo + ")"; }
|
||||
return "stage=" + Stage + " ready=" + Ready + " pc=" + (pc != null ? pc.name + pc.Get_position().ToString("F1") : "-") + " weapon=" + w +
|
||||
" dummy=" + (Dummy != null ? Dummy.name + Dummy.Get_position().ToString("F1") + " hp " + Dummy.Get_HP().ToString("F0") : "-") +
|
||||
" laps=[" + string.Join(" · ", Laps.ToArray()) + "] pf=[" + string.Join(" · ", PfLog.ToArray()) + "] hangs=[" + string.Join(" ", Hangs.ToArray()) + "] " + LastLog
|
||||
#if UNITY_EDITOR
|
||||
+ (string.IsNullOrEmpty(ProfileDump) ? "" : " profile=[" + ProfileDump + "]")
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
void Update()
|
||||
{
|
||||
if (!Ready) return;
|
||||
var pc = MyValue.MyPC as PCActor;
|
||||
if (pc == null) return;
|
||||
var cfg = Cfg;
|
||||
|
||||
if (cfg.moveKeys != 0)
|
||||
{
|
||||
float h = (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow) ? 1f : 0f) - (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow) ? 1f : 0f);
|
||||
float v = (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow) ? 1f : 0f) - (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow) ? 1f : 0f);
|
||||
bool any = h != 0f || v != 0f;
|
||||
if (any)
|
||||
{
|
||||
if (!_moving) { pc.Set_AutoBraking(false); pc.Set_JoystickStatus(1); _moving = true; }
|
||||
var cam = Camera.main;
|
||||
Vector3 fwd = cam != null ? Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up) : Vector3.forward;
|
||||
if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward;
|
||||
fwd.Normalize();
|
||||
Vector3 right = Vector3.Cross(Vector3.up, fwd);
|
||||
pc.Run_byJoystick((fwd * v + right * h).normalized);
|
||||
}
|
||||
else if (_moving) { pc.Set_JoystickStatus(0); pc.Set_AutoBraking(true); _moving = false; }
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(_kPrev)) _weaponMsg = CycleWeapon(-1);
|
||||
if (Input.GetKeyDown(_kNext)) _weaponMsg = CycleWeapon(+1);
|
||||
if (Input.GetKeyDown(_kReset)) _weaponMsg = ResetDummy();
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (Cfg.label == 0) return;
|
||||
if (_style == null) { _style = new GUIStyle(GUI.skin.label); _style.alignment = TextAnchor.UpperLeft; _style.wordWrap = true; }
|
||||
_style.fontSize = Mathf.Max(10, Mathf.RoundToInt(Screen.height * Cfg.labelFontScale));
|
||||
_style.normal.textColor = Ready ? Color.white : new Color(1f, 0.9f, 0.5f);
|
||||
string w = "-";
|
||||
if (WLInventory.Loaded) { var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); if (cur != null && cur.itemJson != null) w = cur.itemJson.Item_name + "(" + cur.itemJson.GroupNo + ")"; }
|
||||
string s = "WL 스킬 테스트 · " + Stage + (Ready ? "" : " …") +
|
||||
"\n무기 " + w + " 더미 " + (Dummy != null ? Dummy.Get_HP().ToString("F0") : "-") +
|
||||
"\nWASD 이동 · [ ] 무기 · R 더미 · Q 주력 · E 각성 · 숫자 등급" +
|
||||
(string.IsNullOrEmpty(_weaponMsg) ? "" : "\n" + _weaponMsg) +
|
||||
(Ready ? "\n" + string.Join(" · ", Laps.ToArray()) : "");
|
||||
var r = new Rect(Screen.width * 0.01f, Screen.height * 0.01f, Screen.width * 0.6f, Screen.height * 0.3f);
|
||||
GUI.color = new Color(0f, 0f, 0f, 0.6f); GUI.Label(new Rect(r.x + 1, r.y + 1, r.width, r.height), s, _style);
|
||||
GUI.color = Color.white; GUI.Label(r, s, _style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d2262201ce81fd5449555b2f0fa8dfed
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-7617188927419010439
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: WLSkillTestGround
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _Smoothness: 0.5
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.45, g: 0.62, b: 0.35, a: 1}
|
||||
- _Color: {r: 0.45, g: 0.62, b: 0.34999996, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bb102d8fd6df38040a18663db0f2504f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue