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>
This commit is contained in:
231
Assets/_Game/Scripts/UI/LobbyUI.cs
Normal file
231
Assets/_Game/Scripts/UI/LobbyUI.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/LobbyUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/LobbyUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa0ac7df4e465a4458e7d7dcc073d648
|
||||
Reference in New Issue
Block a user