Files
Colosseum/Assets/_Game/Scripts/UI/LobbyUI.cs
dal4segno e5ef94da85 feat: 멀티플레이어 네트워크 동기화 구현
- 로비 씬 추가 및 LobbyManager/LobbyUI/LobbySceneBuilder 구현
- NetworkPrefabsList로 플레이어 프리팹 등록 (PlayerPrefab 자동스폰 비활성화)
- PlayerMovement 서버 권한 이동 아키텍처로 전환
  - NetworkVariable<Vector2>로 클라이언트 입력 → 서버 전달
  - 점프 JumpRequestRpc로 서버 검증 후 실행
- 보스 프리팹에 NetworkTransform/NetworkAnimator 추가 (서버 권한)
- SkillController를 NetworkBehaviour로 전환
  - PlaySkillClipClientRpc로 클립 override + 재생 원자적 동기화
  - OnEffect/OnSkillEnd 클라이언트 실행 차단
- WeaponEquipment 클라이언트 무기 시각화 동기화 수정
  - registeredWeapons 인덱스 기반 NetworkVariable 동기화
  - SpawnWeaponVisualsLocal로 클라이언트 무기 생성
  - 중복 Instantiate 버그 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 20:46:45 +09:00

232 lines
8.9 KiB
C#

using System.Collections.Generic;
using Colosseum.Network;
using TMPro;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;
namespace Colosseum.UI
{
/// <summary>
/// 로비 씬 UI 제어.
/// 두 패널 구조:
/// connectPanel — IP/포트 입력 후 Host 또는 Join
/// lobbyPanel — 대기실 (플레이어 목록, 준비, 게임 시작)
/// </summary>
public class LobbyUI : MonoBehaviour
{
// ─── Connect Panel ────────────────────────────────────────
[Header("Connect Panel")]
[SerializeField] private GameObject connectPanel;
[SerializeField] private TMP_InputField ipInput;
[SerializeField] private TMP_InputField portInput;
[SerializeField] private Button hostButton;
[SerializeField] private Button joinButton;
[SerializeField] private TextMeshProUGUI connectStatusText;
// ─── Lobby Panel ──────────────────────────────────────────
[Header("Lobby Panel")]
[SerializeField] private GameObject lobbyPanel;
[SerializeField] private Transform playerListParent;
[SerializeField] private GameObject playerSlotPrefab; // TextMeshProUGUI 하나 포함
[SerializeField] private Button readyButton;
[SerializeField] private Button startButton; // 호스트만 표시
[SerializeField] private Button disconnectButton;
// ─── 내부 ─────────────────────────────────────────────────
private readonly List<GameObject> _slots = new();
private bool _isReady;
// ─── 초기화 ───────────────────────────────────────────────
private void Start()
{
ipInput.text = "127.0.0.1";
portInput.text = "7777";
hostButton.onClick.AddListener(OnHostClicked);
joinButton.onClick.AddListener(OnJoinClicked);
readyButton.onClick.AddListener(OnReadyClicked);
startButton.onClick.AddListener(OnStartClicked);
disconnectButton.onClick.AddListener(OnDisconnectClicked);
ShowConnectPanel();
}
private void OnEnable()
{
if (NetworkManager.Singleton == null) return;
NetworkManager.Singleton.OnClientConnectedCallback += OnConnectionEvent;
NetworkManager.Singleton.OnClientDisconnectCallback += OnConnectionEvent;
}
private void OnDisable()
{
if (NetworkManager.Singleton != null)
{
NetworkManager.Singleton.OnClientConnectedCallback -= OnConnectionEvent;
NetworkManager.Singleton.OnClientDisconnectCallback -= OnConnectionEvent;
}
}
// ─── Connect Panel 버튼 ───────────────────────────────────
private void OnHostClicked()
{
if (!TryGetAddress(out string ip, out ushort port)) return;
LobbyManager.Instance.StartHost(ip, port);
LobbyManager.Instance.OnPlayersChanged += RefreshPlayerList;
ShowLobbyPanel();
}
private void OnJoinClicked()
{
if (!TryGetAddress(out string ip, out ushort port)) return;
SetConnectStatus("Connecting...");
joinButton.interactable = false;
hostButton.interactable = false;
LobbyManager.Instance.StartClient(ip, port);
// 연결 성공/실패 대기
NetworkManager.Singleton.OnClientConnectedCallback += OnJoinSuccess;
NetworkManager.Singleton.OnTransportFailure += OnJoinFailed;
}
private void OnJoinSuccess(ulong clientId)
{
if (clientId != NetworkManager.Singleton.LocalClientId) return;
NetworkManager.Singleton.OnClientConnectedCallback -= OnJoinSuccess;
NetworkManager.Singleton.OnTransportFailure -= OnJoinFailed;
LobbyManager.Instance.OnPlayersChanged += RefreshPlayerList;
ShowLobbyPanel();
}
private void OnJoinFailed()
{
NetworkManager.Singleton.OnClientConnectedCallback -= OnJoinSuccess;
NetworkManager.Singleton.OnTransportFailure -= OnJoinFailed;
SetConnectStatus("Connection failed. Check IP/Port.");
joinButton.interactable = true;
hostButton.interactable = true;
}
// ─── Lobby Panel 버튼 ─────────────────────────────────────
private void OnReadyClicked()
{
_isReady = !_isReady;
LobbyManager.Instance.SetReadyRpc(_isReady);
readyButton.GetComponentInChildren<TextMeshProUGUI>().text =
_isReady ? "Ready!" : "Ready";
}
private void OnStartClicked()
{
LobbyManager.Instance.StartGame();
}
private void OnDisconnectClicked()
{
LobbyManager.Instance.OnPlayersChanged -= RefreshPlayerList;
LobbyManager.Instance.Disconnect();
_isReady = false;
ShowConnectPanel();
}
// ─── 패널 전환 ────────────────────────────────────────────
private void ShowConnectPanel()
{
connectPanel.SetActive(true);
lobbyPanel.SetActive(false);
SetConnectStatus("");
hostButton.interactable = true;
joinButton.interactable = true;
}
private void ShowLobbyPanel()
{
connectPanel.SetActive(false);
lobbyPanel.SetActive(true);
bool isHost = NetworkManager.Singleton.IsHost;
startButton.gameObject.SetActive(isHost);
readyButton.gameObject.SetActive(!isHost);
RefreshPlayerList();
}
// ─── 플레이어 목록 갱신 ───────────────────────────────────
private void OnConnectionEvent(ulong _) => RefreshPlayerList();
private void RefreshPlayerList()
{
if (LobbyManager.Instance == null) return;
// 슬롯 수 맞추기
int count = LobbyManager.Instance.PlayerCount;
while (_slots.Count < count)
{
var slot = Instantiate(playerSlotPrefab, playerListParent);
_slots.Add(slot);
}
while (_slots.Count > count)
{
Destroy(_slots[^1]);
_slots.RemoveAt(_slots.Count - 1);
}
for (int i = 0; i < count; i++)
{
var data = LobbyManager.Instance.GetPlayer(i);
var label = _slots[i].GetComponentInChildren<TextMeshProUGUI>();
if (label != null)
label.text = $"{data.PlayerName} {(data.IsReady ? "[Ready]" : "")}";
}
// 호스트: 모두 준비됐을 때만 시작 활성화
if (NetworkManager.Singleton.IsHost)
{
bool allReady = AllPlayersReady();
startButton.interactable = allReady && count >= 1;
}
}
private bool AllPlayersReady()
{
if (LobbyManager.Instance == null) return false;
int count = LobbyManager.Instance.PlayerCount;
for (int i = 0; i < count; i++)
{
// 호스트 본인은 준비 체크 면제
if (LobbyManager.Instance.GetPlayer(i).ClientId ==
NetworkManager.Singleton.LocalClientId) continue;
if (!LobbyManager.Instance.GetPlayer(i).IsReady) return false;
}
return count > 0;
}
// ─── 유틸 ─────────────────────────────────────────────────
private bool TryGetAddress(out string ip, out ushort port)
{
ip = ipInput.text.Trim();
if (string.IsNullOrEmpty(ip)) ip = "127.0.0.1";
if (!ushort.TryParse(portInput.text.Trim(), out port))
{
port = 7777;
SetConnectStatus("Invalid port number.");
return false;
}
return true;
}
private void SetConnectStatus(string msg)
{
if (connectStatusText != null)
connectStatusText.text = msg;
}
}
}