Project_WL/Assets/WL/Start/StartWakeSequence.cs

233 lines
12 KiB
C#

// PD 지시 #750 — 시작 맵 기상 연출 (쓰러진 상태 → 일어나기 → 조작 개방)
// 2단계 배치 경로: Assets/WL/Start/StartWakeSequence.cs
// 배치 방법: WL_Start.prefab 루트(MapData 와 같은 오브젝트)에 컴포넌트로 추가하고
// settings 에 Assets/WL/Settings/StartWakeSettings.asset 을 연결한다.
// 컴포넌트가 붙어 있다는 사실 자체가 「이 맵에서 기상 연출을 한다」는 플래그다.
// (MapData 에 bool 을 추가하지 않으므로 힘민지 기존 코드 수정이 필요 없다.)
//
// ── 힘민지 코드 수정량: 0줄 ───────────────────────────────────────────
// 이 컴포넌트는 자체 코루틴으로 MyValue.bMyPC 를 폴링하므로 훅이 필요 없다.
// 더 완벽한 프레임 단위 시작을 원하면 아래 「선택적 훅」을 참고. (§주석 하단)
//
// ── 실측 근거 (2026-09-05) ────────────────────────────────────────────
// · 시작 맵은 e_MapType == Lobby → PCActor.Set_Obj (PCActor.cs:270-273) 가
// "Assets/Res_Addr/Animations/pcanim_lobby.controller" 만 로드한다.
// 따라서 상태 추가는 컨트롤러 13개가 아니라 pcanim_lobby 1개면 충분하다.
// · 컨트롤러 할당은 비동기다 (PCActor.cs:283 콜백). Actor.Set 의 Play_Idle()(Actor.cs:297) 은
// 컨트롤러 할당 이전에 실행되므로, 기상 포즈는 컨트롤러가 붙은 뒤에 걸어야 한다.
// · 조이스틱 이동을 실제로 막는 유일한 레버는 OptionInfo.Set_PCMove 다.
// PCActor.Run_byJoystick (PCActor.cs:351-392) 은 IsStop 을 검사하지 않는다.
// 선례: FieldBossData.Co_Cam (FieldBossData.cs:42-52) 가 동일한 잠금→연출→해제 패턴.
// · 애니메이션 재생 API 는 전부 문자열 상태명이다 — Actor.AnimationPlay (Actor.cs:2020).
using System.Collections;
using UnityEngine;
namespace WL.Start
{
public class StartWakeSequence : MonoBehaviour
{
[SerializeField] private StartWakeSettings settings;
/// <summary>연출이 진행 중인가. 다른 시스템이 참조할 수 있게 공개한다.</summary>
public static bool IsPlaying { get; private set; }
/// <summary>이 맵에서 기상 연출을 쓰는가 (컴포넌트 존재 = true).</summary>
public static bool IsArmed { get; private set; }
private void Awake()
{
IsArmed = true;
if (settings == null)
{
Debug.LogError("[StartWake] settings 가 비어 있다. StartWakeSettings.asset 을 연결할 것. 연출을 건너뛴다.");
IsArmed = false;
return;
}
StartCoroutine(Co_Sequence());
}
private void OnDestroy()
{
if (IsPlaying) Unlock(null);
IsArmed = false;
}
private void Log(string s)
{
if (settings != null && settings.verboseLog) Debug.Log("[StartWake] " + s);
}
private IEnumerator Co_Sequence()
{
IsPlaying = true;
float t0 = Time.realtimeSinceStartup;
// 1) 플레이어 등장 대기 — PCActor.ReSet 에서 MyValue.MyPC 가 채워진다 (PCActor.cs:151)
while (!MyValue.bMyPC)
{
if (Timeout(t0)) { Log("플레이어 대기 타임아웃 — 연출 취소"); Unlock(null); yield break; }
yield return null;
}
var pc = MyValue.MyPC;
Log("플레이어 인식");
// #756 (2026-09-06): 로비 모드는 PCActor.Set_Obj 가 Change_Weapon 을 건너뛴다 → MapData.showWeaponInLobby 가 켜진 맵에서는 여기서 장착한다.
var mapData = GetComponent<MapData>();
if (mapData != null && mapData.showWeaponInLobby) { pc.Change_Weapon(); Log("무기 장착 (showWeaponInLobby)"); }
// 2) 즉시 잠근다 (조이스틱이 살아나기 전에)
Lock(pc);
// 3+4) 애니메이터 컨트롤러가 비동기로 붙기를 기다린다 (PCActor.Set_Obj 의 LoadObject 콜백).
//
// 🔴 실측(2026-09-05 Play 검증): 클래스 컨트롤러(pcanim_mage)가 먼저 붙어 있고
// 로비용 pcanim_lobby 는 그 뒤에 콜백으로 "교체"된다.
// 따라서 "컨트롤러가 null 이 아님"만 기다리면 아직 pcanim_mage 인 시점에
// 상태 검사를 해 버려 연출이 통째로 건너뛰어졌다.
// → lying/getup 상태를 실제로 가진 컨트롤러가 붙을 때까지 기다린다.
int lyingHash = Animator.StringToHash(settings.lyingStateName);
int getUpHash = Animator.StringToHash(settings.getUpStateName);
var anim = pc.m_animation;
while (anim == null || anim.runtimeAnimatorController == null
|| !anim.HasState(0, lyingHash) || !anim.HasState(0, getUpHash))
{
if (Timeout(t0))
{
Debug.LogWarning("[StartWake] '" + settings.lyingStateName + "' / '" + settings.getUpStateName
+ "' 상태를 가진 컨트롤러가 " + settings.readyTimeout + "초 안에 붙지 않았다 (현재: "
+ (anim == null || anim.runtimeAnimatorController == null ? "없음" : anim.runtimeAnimatorController.name)
+ "). WL_StartWakeAnimSetup 실행 여부를 확인할 것. 연출을 건너뛴다.");
Unlock(pc);
yield break;
}
anim = pc.m_animation;
yield return null;
}
Log("애니메이터 준비 완료 — " + anim.runtimeAnimatorController.name);
// 5) 쓰러진 자세 — 컨트롤러가 idle 로 진입한 직후를 덮어쓴다
pc.AnimationPlay(settings.lyingStateName);
Log("쓰러진 자세 진입");
// 6) 인트로 카메라 도착 대기 (RealCamera.CamStatus 0 → 1)
if (settings.waitForIntroCamera)
{
float camT0 = Time.realtimeSinceStartup;
while (!IntroCameraArrived())
{
if (Time.realtimeSinceStartup - camT0 > settings.introCameraTimeout)
{ Log("인트로 카메라 대기 타임아웃 — 그대로 진행"); break; }
// 카메라가 움직이는 동안 idle 로 되돌아가지 않도록 자세를 유지한다
KeepLying(anim, lyingHash);
yield return null;
}
Log("인트로 카메라 도착");
}
// 7) 뜸 들이기
float wait = settings.delayBeforeGetUp;
while (wait > 0f) { KeepLying(anim, lyingHash); wait -= Time.deltaTime; yield return null; }
// 8) 기상
pc.AnimationPlay(settings.getUpStateName);
Log("기상 시작");
yield return null; // 상태 전이가 반영되는 1프레임
float dur = settings.getUpDuration;
if (dur <= 0f)
{
var info = anim.GetCurrentAnimatorStateInfo(0);
dur = info.shortNameHash == getUpHash ? info.length : 1.5f;
Log("기상 클립 길이 실측 = " + dur.ToString("F3") + "s");
}
yield return new WaitForSeconds(dur);
if (settings.delayAfterGetUp > 0f) yield return new WaitForSeconds(settings.delayAfterGetUp);
// 9) 개방
pc.Play_Idle();
Unlock(pc);
Log("조작 개방 — 연출 종료");
}
private bool Timeout(float t0)
{
return Time.realtimeSinceStartup - t0 > settings.readyTimeout;
}
// 애니메이터가 다른 상태로 새면 되돌린다 (Actor 쪽에서 Play_Idle 이 불릴 수 있다)
private void KeepLying(Animator anim, int lyingHash)
{
if (anim == null) return;
var st = anim.GetCurrentAnimatorStateInfo(0);
if (st.shortNameHash != lyingHash) anim.Play(settings.lyingStateName);
}
// RealCamera 의 인트로 이동 완료 판정.
// CamStatus 의 접근성이 미확인이라 리플렉션으로 읽고, 못 읽으면 false 를 돌려
// introCameraTimeout 으로 넘어가게 한다.
private static System.Reflection.FieldInfo _camStatusField;
private static bool _camStatusResolved;
private bool IntroCameraArrived()
{
var cam = MyValue.m_RealCamera;
if (cam == null) return false;
if (!_camStatusResolved)
{
_camStatusResolved = true;
_camStatusField = cam.GetType().GetField("CamStatus",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
if (_camStatusField == null)
Debug.LogWarning("[StartWake] RealCamera.CamStatus 를 못 읽었다 — introCameraTimeout 으로 진행한다.");
}
if (_camStatusField == null) return false;
object v = _camStatusField.GetValue(cam);
return v is int && (int)v != 0;
}
private void Lock(PCActor pc)
{
if (pc == null) return;
if (settings.lockMove && OptionInfo.Ins != null)
{
OptionInfo.Ins.Set_PCMove(pc, false);
Log("이동 잠금 (Set_PCMove false)");
}
if (settings.lockSkillsViaStun) Log("주의: lockSkillsViaStun 은 Actor 의 dic_ccData 접근이 필요하다 — 2단계 구현 전 PD 결정 필요");
}
private void Unlock(PCActor pc)
{
IsPlaying = false;
if (pc == null) pc = MyValue.bMyPC ? MyValue.MyPC : null;
if (pc == null) return;
if (settings != null && settings.lockMove && OptionInfo.Ins != null)
{
OptionInfo.Ins.Set_PCMove(pc, true);
Log("이동 잠금 해제 (Set_PCMove true)");
}
}
}
}
// ─────────────────────────────────────────────────────────────────────
// 선택적 훅 (프레임 단위로 완벽한 시작을 원할 때만 — 힘민지 코드 2줄)
//
// 1) Assets/Script/Character/PCActor.cs — Set_Obj() 의 컨트롤러 로드 콜백 안,
// m_animation.runtimeAnimatorController = handle.Result; (283행) 바로 다음 줄에:
//
// if (WL.Start.StartWakeSequence.IsArmed) AnimationPlay("lying");
//
// 효과: 컨트롤러가 붙는 그 프레임에 즉시 엎드린 자세가 된다.
// 이 훅이 없으면 컨트롤러 기본 상태(idle)가 1~2프레임 보일 수 있다.
// 단, 그 시점의 카메라는 인트로 이동 중이라 실사용상 거의 드러나지 않는다.
//
// 2) Assets/Script/Util/RealCamera.cs — Start_Game() (244행) 첫 줄에:
//
// if (WL.Start.StartWakeSequence.IsPlaying) { /* 연출 중 — 자동 개방을 늦춘다 */ }
//
// 효과: All_Stop(false) 시점을 연출 종료까지 미루고 싶을 때만 필요.
// 현재 구현은 Set_PCMove 로 이동을 막으므로 이 훅 없이도 동작한다.
// ─────────────────────────────────────────────────────────────────────