176 lines
7.7 KiB
C#
176 lines
7.7 KiB
C#
using UnityEngine;
|
|
using WL.Combat;
|
|
using WL.UI;
|
|
|
|
namespace WL.Player
|
|
{
|
|
/// <summary>
|
|
/// 캐릭터 교체기 (PD #711).
|
|
///
|
|
/// 목록의 캐릭터를 **전부 씬에 배치해 두고 활성/비활성으로 교체**한다(생성·파괴하지 않는다 — 교체 지연·GC 회피).
|
|
/// 교체 시 위치·회전·체력·카메라 모드를 그대로 넘기고, 바깥에서 플레이어를 참조하는 것들을 다시 묶는다.
|
|
/// · 카메라 추적 대상 (FollowCamera.SetTarget)
|
|
/// · 몬스터 추적 대상 (EnemyController.RebindPlayer) — 교체된 쪽은 파괴되지 않아 참조가 null 이 되지 않는다
|
|
/// · 가상패드 UI (VirtualPadView.RebindPlayer)
|
|
///
|
|
/// 값은 CharacterRoster(ScriptableObject)에서 읽는다(C45).
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>현재 조종 중인 캐릭터의 목록 번호.</summary>
|
|
public int CurrentIndex { get { return currentIndex; } }
|
|
/// <summary>현재 조종 중인 캐릭터.</summary>
|
|
public PlayerController Current { get { return Get(currentIndex); } }
|
|
/// <summary>목록 길이.</summary>
|
|
public int Count { get { return instances != null ? instances.Length : 0; } }
|
|
/// <summary>현재 캐릭터의 표시 이름.</summary>
|
|
public string CurrentDisplayName
|
|
{
|
|
get
|
|
{
|
|
var e = roster != null ? roster.Get(currentIndex) : null;
|
|
return e != null && !string.IsNullOrEmpty(e.displayName) ? e.displayName : "Player " + (currentIndex + 1);
|
|
}
|
|
}
|
|
/// <summary>현재 캐릭터의 더미 아이콘 색.</summary>
|
|
public Color CurrentIconColor
|
|
{
|
|
get { var e = roster != null ? roster.Get(currentIndex) : null; return e != null ? e.iconColor : Color.white; }
|
|
}
|
|
/// <summary>교체가 일어날 때마다 알린다(HUD 라벨 갱신용).</summary>
|
|
public event System.Action<int> 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<FollowCamera>();
|
|
if (virtualPad == null) virtualPad = Object.FindFirstObjectByType<VirtualPadView>(FindObjectsInactive.Include);
|
|
EnsureInstances();
|
|
}
|
|
|
|
/// <summary>씬에 인스턴스가 없으면 roster 프리팹으로 만든다. 0 번만 켠 채 시작한다.</summary>
|
|
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<PlayerController>();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>다음 캐릭터로 순환 교체한다. 교체한 번호를 돌려준다.</summary>
|
|
public int SwitchNext()
|
|
{
|
|
if (Count <= 1) return currentIndex;
|
|
return SwitchTo((currentIndex + 1) % Count);
|
|
}
|
|
|
|
/// <summary>지정한 번호로 교체한다.</summary>
|
|
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<CharacterController>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>바깥에서 플레이어를 참조하는 것들을 새 캐릭터로 다시 묶는다.</summary>
|
|
private void Rebind(PlayerController pc)
|
|
{
|
|
if (pc == null) return;
|
|
|
|
// 카메라 — 모드는 유지된다(SetTarget 은 모드를 바꾸지 않는다)
|
|
if (followCamera != null) followCamera.SetTarget(pc.transform);
|
|
|
|
// 몬스터
|
|
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
|
|
for (int i = 0; i < enemies.Length; i++) enemies[i].RebindPlayer(pc);
|
|
|
|
// 가상패드 UI
|
|
if (virtualPad != null) virtualPad.RebindPlayer(pc);
|
|
}
|
|
}
|
|
}
|