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:
@@ -34,6 +34,10 @@ namespace Colosseum.Weapons
|
||||
[Tooltip("시작 무기 (선택)")]
|
||||
[SerializeField] private WeaponData startingWeapon;
|
||||
|
||||
[Header("네트워크 동기화")]
|
||||
[Tooltip("이 장착 시스템이 사용하는 모든 WeaponData 목록. 서버→클라이언트 무기 동기화에 사용됩니다.")]
|
||||
[SerializeField] private System.Collections.Generic.List<WeaponData> registeredWeapons = new();
|
||||
|
||||
// 캐싱된 소켓 Transform들
|
||||
private Transform rightHandSocket;
|
||||
private Transform leftHandSocket;
|
||||
@@ -51,7 +55,7 @@ namespace Colosseum.Weapons
|
||||
private readonly System.Collections.Generic.Dictionary<StatType, StatModifier> activeModifiers
|
||||
= new System.Collections.Generic.Dictionary<StatType, StatModifier>();
|
||||
|
||||
// 무기 장착 상태 동기화
|
||||
// 무기 장착 상태 동기화 (registeredWeapons 인덱스, -1 = 없음)
|
||||
private NetworkVariable<int> equippedWeaponId = new NetworkVariable<int>(-1);
|
||||
|
||||
public WeaponData CurrentWeapon => currentWeapon;
|
||||
@@ -81,14 +85,17 @@ namespace Colosseum.Weapons
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// 네트워크 변수 변경 콜백
|
||||
equippedWeaponId.OnValueChanged += HandleEquippedWeaponChanged;
|
||||
|
||||
// 서버에서 시작 무기 장착
|
||||
if (IsServer && startingWeapon != null)
|
||||
{
|
||||
EquipWeapon(startingWeapon);
|
||||
}
|
||||
else if (!IsServer && equippedWeaponId.Value >= 0)
|
||||
{
|
||||
// 늦게 접속한 클라이언트: 현재 장착된 무기 시각화
|
||||
SpawnWeaponVisualsLocal(equippedWeaponId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
@@ -150,14 +157,12 @@ namespace Colosseum.Weapons
|
||||
|
||||
private void HandleEquippedWeaponChanged(int oldValue, int newValue)
|
||||
{
|
||||
// -1이면 무기 해제, 그 외에는 무기 장착됨
|
||||
// (GetInstanceID()는 음수를 반환할 수 있으므로 >= 0 체크 사용 불가)
|
||||
if (IsServer) return; // 서버는 EquipWeapon/UnequipWeapon에서 직접 처리
|
||||
|
||||
if (newValue == -1)
|
||||
{
|
||||
UnequipWeaponInternal();
|
||||
}
|
||||
// 클라이언트에서는 서버에서 이미 장착된 무기 정보를 받아야 함
|
||||
// TODO: WeaponDatabase에서 ID로 WeaponData 조회
|
||||
else
|
||||
SpawnWeaponVisualsLocal(newValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -192,8 +197,10 @@ namespace Colosseum.Weapons
|
||||
// 무기 외형 생성 및 부착
|
||||
SpawnWeaponVisuals(weapon);
|
||||
|
||||
// 네트워크 동기화 (간단한 ID 사용, 실제로는 WeaponDatabase 필요)
|
||||
equippedWeaponId.Value = weapon.GetInstanceID();
|
||||
// registeredWeapons 인덱스로 동기화
|
||||
equippedWeaponId.Value = registeredWeapons.IndexOf(weapon);
|
||||
if (equippedWeaponId.Value < 0)
|
||||
Debug.LogWarning($"[WeaponEquipment] '{weapon.WeaponName}' is not in registeredWeapons. Add it to sync to clients.");
|
||||
|
||||
// 이벤트 발생
|
||||
OnWeaponEquipped?.Invoke(weapon);
|
||||
@@ -236,12 +243,10 @@ namespace Colosseum.Weapons
|
||||
/// </summary>
|
||||
private void UnequipWeaponInternal()
|
||||
{
|
||||
if (currentWeapon == null) return;
|
||||
if (currentWeaponInstance == null && currentWeapon == null) return;
|
||||
|
||||
WeaponData previousWeapon = currentWeapon;
|
||||
RemoveStatBonuses();
|
||||
DespawnWeaponVisuals();
|
||||
currentWeapon = null;
|
||||
DespawnWeaponVisualsLocal();
|
||||
|
||||
OnWeaponUnequipped?.Invoke(previousWeapon);
|
||||
}
|
||||
@@ -294,13 +299,12 @@ namespace Colosseum.Weapons
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 무기 외형 생성 및 부착
|
||||
/// 무기 외형 생성 및 부착 (서버)
|
||||
/// </summary>
|
||||
private void SpawnWeaponVisuals(WeaponData weapon)
|
||||
{
|
||||
if (weapon == null || weapon.WeaponPrefab == null) return;
|
||||
|
||||
// 적절한 소켓 찾기
|
||||
Transform socket = GetSocketForSlot(weapon.WeaponSlot);
|
||||
if (socket == null)
|
||||
{
|
||||
@@ -308,59 +312,63 @@ namespace Colosseum.Weapons
|
||||
return;
|
||||
}
|
||||
|
||||
// 무기 인스턴스 생성
|
||||
currentWeaponInstance = Instantiate(weapon.WeaponPrefab, socket);
|
||||
currentWeaponInstance.transform.localPosition = weapon.PositionOffset;
|
||||
currentWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset);
|
||||
|
||||
// 소켓 스케일 보정 (부모 스케일이 작은 경우 무기도 작아지는 문제 해결)
|
||||
Vector3 scaleCompensation = new Vector3(
|
||||
socket.lossyScale.x != 0 ? 1f / socket.lossyScale.x : 1f,
|
||||
socket.lossyScale.y != 0 ? 1f / socket.lossyScale.y : 1f,
|
||||
socket.lossyScale.z != 0 ? 1f / socket.lossyScale.z : 1f
|
||||
);
|
||||
currentWeaponInstance.transform.localScale = Vector3.Scale(weapon.Scale, scaleCompensation);
|
||||
currentWeaponInstance = Instantiate(weapon.WeaponPrefab, socket);
|
||||
currentWeaponInstance.transform.localPosition = weapon.PositionOffset;
|
||||
currentWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset);
|
||||
currentWeaponInstance.transform.localScale = weapon.Scale;
|
||||
|
||||
// 디버그: 스케일 정보 출력
|
||||
Debug.Log($"[WeaponEquipment] Weapon instantiated - LocalScale: {currentWeaponInstance.transform.localScale}, LossyScale: {currentWeaponInstance.transform.lossyScale}");
|
||||
Debug.Log($"[WeaponEquipment] Socket: {socket.name}, Socket scale: {socket.lossyScale}");
|
||||
Debug.Log($"[WeaponEquipment] Position offset: {weapon.PositionOffset}, Rotation offset: {weapon.RotationOffset}");
|
||||
|
||||
// 네트워크 동기화를 위해 Spawn (서버에서만)
|
||||
if (IsServer && currentWeaponInstance.TryGetComponent<NetworkObject>(out var networkObject))
|
||||
{
|
||||
networkObject.Spawn(true);
|
||||
}
|
||||
|
||||
Debug.Log($"[WeaponEquipment] Spawned weapon visual: {weapon.WeaponName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 무기 외형 제거
|
||||
/// 클라이언트: registeredWeapons 인덱스로 무기 외형 생성
|
||||
/// </summary>
|
||||
private void SpawnWeaponVisualsLocal(int weaponIndex)
|
||||
{
|
||||
if (weaponIndex < 0 || weaponIndex >= registeredWeapons.Count || registeredWeapons[weaponIndex] == null)
|
||||
{
|
||||
Debug.LogWarning($"[WeaponEquipment] Weapon index {weaponIndex} not found in registeredWeapons.");
|
||||
return;
|
||||
}
|
||||
|
||||
var weapon = registeredWeapons[weaponIndex];
|
||||
if (weapon.WeaponPrefab == null) return;
|
||||
|
||||
DespawnWeaponVisualsLocal();
|
||||
|
||||
Transform socket = GetSocketForSlot(weapon.WeaponSlot);
|
||||
if (socket == null)
|
||||
{
|
||||
Debug.LogWarning($"[WeaponEquipment] No socket found for slot: {weapon.WeaponSlot}");
|
||||
return;
|
||||
}
|
||||
|
||||
currentWeaponInstance = Instantiate(weapon.WeaponPrefab, socket);
|
||||
currentWeaponInstance.transform.localPosition = weapon.PositionOffset;
|
||||
currentWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset);
|
||||
currentWeaponInstance.transform.localScale = weapon.Scale;
|
||||
currentWeapon = weapon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 무기 외형 제거 (서버)
|
||||
/// </summary>
|
||||
private void DespawnWeaponVisuals()
|
||||
{
|
||||
if (currentWeaponInstance == null) return;
|
||||
|
||||
// 네트워크 Object면 Despawn
|
||||
if (currentWeaponInstance.TryGetComponent<NetworkObject>(out var networkObject) && networkObject.IsSpawned)
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
networkObject.Despawn(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(currentWeaponInstance);
|
||||
}
|
||||
|
||||
Destroy(currentWeaponInstance);
|
||||
currentWeaponInstance = null;
|
||||
Debug.Log("[WeaponEquipment] Despawned weapon visual");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 무기 외형 제거 (클라이언트)
|
||||
/// </summary>
|
||||
private void DespawnWeaponVisualsLocal()
|
||||
{
|
||||
if (currentWeaponInstance == null) return;
|
||||
Destroy(currentWeaponInstance);
|
||||
currentWeaponInstance = null;
|
||||
currentWeapon = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user