using UnityEngine; using WL.Combat; using WL.UI; namespace WL.Player { /// /// 캐릭터 교체기 (PD #711). /// /// 목록의 캐릭터를 **전부 씬에 배치해 두고 활성/비활성으로 교체**한다(생성·파괴하지 않는다 — 교체 지연·GC 회피). /// 교체 시 위치·회전·체력·카메라 모드를 그대로 넘기고, 바깥에서 플레이어를 참조하는 것들을 다시 묶는다. /// · 카메라 추적 대상 (FollowCamera.SetTarget) /// · 몬스터 추적 대상 (EnemyController.RebindPlayer) — 교체된 쪽은 파괴되지 않아 참조가 null 이 되지 않는다 /// · 가상패드 UI (VirtualPadView.RebindPlayer) /// /// 값은 CharacterRoster(ScriptableObject)에서 읽는다(C45). /// public class PlayerSwitcher : MonoBehaviour { [Header("설정 (ScriptableObject)")] [SerializeField] private CharacterRoster roster; [Header("씬 인스턴스 (roster 순서와 1:1)")] [Tooltip("비어 있으면 Awake 에서 roster 의 프리팹을 씬에 만든다")] [SerializeField] private PlayerController[] instances = new PlayerController[0]; [Header("참조 (비우면 씬에서 찾는다)")] [SerializeField] private FollowCamera followCamera; [SerializeField] private VirtualPadView virtualPad; [Header("현재 상태 (읽기 전용)")] [SerializeField] private int currentIndex; /// 현재 조종 중인 캐릭터의 목록 번호. public int CurrentIndex { get { return currentIndex; } } /// 현재 조종 중인 캐릭터. public PlayerController Current { get { return Get(currentIndex); } } /// 목록 길이. public int Count { get { return instances != null ? instances.Length : 0; } } /// 현재 캐릭터의 표시 이름. public string CurrentDisplayName { get { var e = roster != null ? roster.Get(currentIndex) : null; return e != null && !string.IsNullOrEmpty(e.displayName) ? e.displayName : "Player " + (currentIndex + 1); } } /// 현재 캐릭터의 더미 아이콘 색. public Color CurrentIconColor { get { var e = roster != null ? roster.Get(currentIndex) : null; return e != null ? e.iconColor : Color.white; } } /// 교체가 일어날 때마다 알린다(HUD 라벨 갱신용). public event System.Action Switched; public CharacterRoster Roster { get { return roster; } } private PlayerController Get(int i) { if (instances == null || instances.Length == 0) return null; return instances[Mathf.Clamp(i, 0, instances.Length - 1)]; } private void Awake() { if (followCamera == null) followCamera = Object.FindFirstObjectByType(); if (virtualPad == null) virtualPad = Object.FindFirstObjectByType(FindObjectsInactive.Include); EnsureInstances(); } /// 씬에 인스턴스가 없으면 roster 프리팹으로 만든다. 0 번만 켠 채 시작한다. private void EnsureInstances() { if (roster == null || roster.Count == 0) return; if (instances == null || instances.Length != roster.Count) { var grown = new PlayerController[roster.Count]; for (int i = 0; i < grown.Length; i++) grown[i] = (instances != null && i < instances.Length) ? instances[i] : null; instances = grown; } Vector3 spawnPos = Vector3.zero; Quaternion spawnRot = Quaternion.identity; for (int i = 0; i < instances.Length; i++) if (instances[i] != null) { spawnPos = instances[i].transform.position; spawnRot = instances[i].transform.rotation; break; } for (int i = 0; i < instances.Length; i++) { if (instances[i] != null) continue; var e = roster.Get(i); if (e == null || e.prefab == null) continue; var go = Instantiate(e.prefab, spawnPos, spawnRot); go.name = e.prefab.name; instances[i] = go.GetComponent(); } for (int i = 0; i < instances.Length; i++) { if (instances[i] == null) continue; bool on = (i == currentIndex); if (!on) instances[i].SetPlayIntroSequence(false); // 켤 때 시작 시퀀스를 다시 돌리지 않는다 instances[i].gameObject.SetActive(on); } } private void Start() { Rebind(Current); if (Switched != null) Switched(currentIndex); } /// 다음 캐릭터로 순환 교체한다. 교체한 번호를 돌려준다. public int SwitchNext() { if (Count <= 1) return currentIndex; return SwitchTo((currentIndex + 1) % Count); } /// 지정한 번호로 교체한다. public int SwitchTo(int index) { if (Count == 0) return currentIndex; index = Mathf.Clamp(index, 0, Count - 1); var from = Get(currentIndex); var to = Get(index); if (to == null || to == from) return currentIndex; // 위치·회전·체력 인계 Vector3 pos = from != null ? from.transform.position : to.transform.position; Quaternion rot = from != null ? from.transform.rotation : to.transform.rotation; float hp = from != null ? from.CurrentHp : -1f; if (from != null) from.gameObject.SetActive(false); // CharacterController 가 켜져 있으면 위치 대입이 무시된다 — 껐다 켜며 옮긴다 var cc = to.GetComponent(); if (cc != null) cc.enabled = false; to.transform.SetPositionAndRotation(pos, rot); if (roster == null || roster.playIntroOnlyOnce) to.SetPlayIntroSequence(false); to.gameObject.SetActive(true); if (cc != null) cc.enabled = true; if (hp >= 0f) to.SetHp(Mathf.Min(hp, to.MaxHp)); to.LockInput(roster != null ? roster.switchInputLockSeconds : 0.2f); currentIndex = index; Rebind(to); if (Switched != null) Switched(currentIndex); Debug.Log("[PlayerSwitcher] 캐릭터 교체 -> " + (currentIndex + 1) + " " + CurrentDisplayName + " pos=" + pos.ToString("F2") + " hp=" + to.CurrentHp.ToString("F0") + " 카메라모드=" + (followCamera != null ? (followCamera.ModeIndex + 1).ToString() : "-")); return currentIndex; } /// 바깥에서 플레이어를 참조하는 것들을 새 캐릭터로 다시 묶는다. private void Rebind(PlayerController pc) { if (pc == null) return; // 카메라 — 모드는 유지된다(SetTarget 은 모드를 바꾸지 않는다) if (followCamera != null) followCamera.SetTarget(pc.transform); // 몬스터 var enemies = Object.FindObjectsByType(FindObjectsSortMode.None); for (int i = 0; i < enemies.Length; i++) enemies[i].RebindPlayer(pc); // 가상패드 UI if (virtualPad != null) virtualPad.RebindPlayer(pc); } } }