494 lines
31 KiB
C#
494 lines
31 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 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);
|
|
}
|
|
}
|
|
}
|