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:
2026-03-17 20:46:45 +09:00
parent b470aa4f8a
commit e5ef94da85
24 changed files with 5150 additions and 116 deletions

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using System.Collections.Generic;
using Unity.Netcode;
namespace Colosseum.Skills
{
@@ -7,7 +8,7 @@ namespace Colosseum.Skills
/// 스킬 실행을 관리하는 컴포넌트.
/// 애니메이션 이벤트 기반으로 효과가 발동됩니다.
/// </summary>
public class SkillController : MonoBehaviour
public class SkillController : NetworkBehaviour
{
private const string SKILL_STATE_NAME = "Skill";
private const string END_STATE_NAME = "SkillEnd";
@@ -19,6 +20,10 @@ namespace Colosseum.Skills
[Tooltip("Skill 상태에 연결된 기본 클립 (Override용)")]
[SerializeField] private AnimationClip baseSkillClip;
[Header("네트워크 동기화")]
[Tooltip("이 SkillController가 사용하는 모든 스킬/엔드 클립 (순서대로 인덱스 부여). 서버→클라이언트 클립 동기화에 사용됩니다.")]
[SerializeField] private List<AnimationClip> registeredClips = new();
[Header("설정")]
[SerializeField] private bool debugMode = false;
[Tooltip("공격 범위 시각화 (Scene 뷰에서 확인)")]
@@ -34,6 +39,7 @@ namespace Colosseum.Skills
// 쿨타임 추적
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
public bool IsExecutingSkill => currentSkill != null && !skillEndRequested;
public bool IsPlayingAnimation => currentSkill != null;
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
@@ -55,6 +61,7 @@ namespace Colosseum.Skills
}
}
private void Update()
{
if (currentSkill == null || animator == null) return;
@@ -166,6 +173,10 @@ namespace Colosseum.Skills
animator.Rebind();
animator.Update(0f);
animator.Play(SKILL_STATE_NAME, 0, 0f);
// 클라이언트에 클립 동기화
if (IsServer && IsSpawned)
PlaySkillClipClientRpc(registeredClips.IndexOf(clip));
}
/// <summary>
@@ -187,6 +198,10 @@ namespace Colosseum.Skills
animator.Rebind();
animator.Update(0f);
animator.Play(SKILL_STATE_NAME, 0, 0f);
// 클라이언트에 클립 동기화
if (IsServer && IsSpawned)
PlaySkillClipClientRpc(registeredClips.IndexOf(clip));
}
/// <summary>
@@ -198,6 +213,41 @@ namespace Colosseum.Skills
{
animator.runtimeAnimatorController = baseController;
}
// 클라이언트에 복원 동기화
if (IsServer && IsSpawned)
RestoreBaseControllerClientRpc();
}
/// <summary>
/// 클라이언트: override 컨트롤러 적용 + 스킬 상태 재생 (원자적 실행으로 타이밍 문제 해결)
/// </summary>
[Rpc(SendTo.NotServer)]
private void PlaySkillClipClientRpc(int clipIndex)
{
if (baseSkillClip == null || animator == null || baseController == null) return;
if (clipIndex < 0 || clipIndex >= registeredClips.Count || registeredClips[clipIndex] == null)
{
if (debugMode) Debug.LogWarning($"[SkillController] Clip index {clipIndex} not found in registeredClips. Add it to sync to clients.");
return;
}
var overrideController = new AnimatorOverrideController(baseController);
overrideController[baseSkillClip] = registeredClips[clipIndex];
animator.runtimeAnimatorController = overrideController;
animator.Rebind();
animator.Update(0f);
animator.Play(SKILL_STATE_NAME, 0, 0f);
}
/// <summary>
/// 클라이언트: 기본 컨트롤러 복원
/// </summary>
[Rpc(SendTo.NotServer)]
private void RestoreBaseControllerClientRpc()
{
if (animator != null && baseController != null)
animator.runtimeAnimatorController = baseController;
}
/// <summary>
@@ -206,6 +256,8 @@ namespace Colosseum.Skills
/// </summary>
public void OnEffect(int index)
{
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return;
if (currentSkill == null)
{
if (debugMode) Debug.LogWarning("[Effect] No skill executing");
@@ -246,6 +298,8 @@ namespace Colosseum.Skills
/// </summary>
public void OnSkillEnd()
{
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return;
if (currentSkill == null)
{
if (debugMode) Debug.LogWarning("[SkillEnd] No skill executing");