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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user