Files
Colosseum/Assets/_Game/Scripts/Weapons/WeaponEquipment.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

410 lines
14 KiB
C#

using System;
using UnityEngine;
using Unity.Netcode;
using Colosseum.Stats;
namespace Colosseum.Weapons
{
/// <summary>
/// 무기 장착을 관리하는 컴포넌트.
/// 무기 장착 시 스탯 보너스를 적용하고 배율을 제공하며, 무기 외형을 표시합니다.
/// 메시 이름으로 소켓을 자동 검색합니다.
/// </summary>
public class WeaponEquipment : NetworkBehaviour
{
[Header("References")]
[Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")]
[SerializeField] private CharacterStats characterStats;
[Header("Socket Names (메시 이름)")]
[Tooltip("오른손 메시 이름")]
[SerializeField] private string rightHandName = "Hand_R";
[Tooltip("왼손 메시 이름")]
[SerializeField] private string leftHandName = "Hand_L";
[Tooltip("등 메시 이름")]
[SerializeField] private string backName = "Spine";
[Tooltip("허리 메시 이름")]
[SerializeField] private string hipName = "Hip";
[Tooltip("양손 메시 이름 (기본값: 오른손 사용)")]
[SerializeField] private string twoHandedName = "";
[Header("Starting Weapon")]
[Tooltip("시작 무기 (선택)")]
[SerializeField] private WeaponData startingWeapon;
[Header("네트워크 동기화")]
[Tooltip("이 장착 시스템이 사용하는 모든 WeaponData 목록. 서버→클라이언트 무기 동기화에 사용됩니다.")]
[SerializeField] private System.Collections.Generic.List<WeaponData> registeredWeapons = new();
// 캐싱된 소켓 Transform들
private Transform rightHandSocket;
private Transform leftHandSocket;
private Transform backSocket;
private Transform hipSocket;
private Transform twoHandedSocket;
// 현재 장착 중인 무기
private WeaponData currentWeapon;
// 현재 생성된 무기 인스턴스
private GameObject currentWeaponInstance;
// 현재 적용된 스탯 수정자들 (해제 시 제거용)
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;
public bool HasWeaponEquipped => currentWeapon != null;
public GameObject CurrentWeaponInstance => currentWeaponInstance;
// 배율 프로퍼티 (무기 없으면 기본값 1.0)
public float DamageMultiplier => currentWeapon != null ? currentWeapon.DamageMultiplier : 1f;
public float RangeMultiplier => currentWeapon != null ? currentWeapon.RangeMultiplier : 1f;
public float ManaCostMultiplier => currentWeapon != null ? currentWeapon.ManaCostMultiplier : 1f;
// 이벤트
public event Action<WeaponData> OnWeaponEquipped;
public event Action<WeaponData> OnWeaponUnequipped;
private void Awake()
{
// CharacterStats 참조 확인
if (characterStats == null)
{
characterStats = GetComponent<CharacterStats>();
}
// 소켓 자동 검색
CacheSockets();
}
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()
{
equippedWeaponId.OnValueChanged -= HandleEquippedWeaponChanged;
}
/// <summary>
/// 메시 이름으로 소켓 Transform 캐싱
/// </summary>
private void CacheSockets()
{
rightHandSocket = FindDeepChild(rightHandName);
leftHandSocket = FindDeepChild(leftHandName);
backSocket = FindDeepChild(backName);
hipSocket = FindDeepChild(hipName);
// 양손은 별도 이름 없으면 오른손 사용
if (!string.IsNullOrEmpty(twoHandedName))
{
twoHandedSocket = FindDeepChild(twoHandedName);
}
else
{
twoHandedSocket = rightHandSocket;
}
Debug.Log($"[WeaponEquipment] Sockets cached - R:{rightHandSocket != null}, L:{leftHandSocket != null}, Back:{backSocket != null}, Hip:{hipSocket != null}");
}
/// <summary>
/// 이름으로 자식 Transform 재귀 검색
/// </summary>
private Transform FindDeepChild(string name)
{
if (string.IsNullOrEmpty(name)) return null;
// BFS로 검색
var queue = new System.Collections.Generic.Queue<Transform>();
queue.Enqueue(transform);
while (queue.Count > 0)
{
Transform current = queue.Dequeue();
if (current.name == name)
{
return current;
}
foreach (Transform child in current)
{
queue.Enqueue(child);
}
}
return null;
}
private void HandleEquippedWeaponChanged(int oldValue, int newValue)
{
if (IsServer) return; // 서버는 EquipWeapon/UnequipWeapon에서 직접 처리
if (newValue == -1)
UnequipWeaponInternal();
else
SpawnWeaponVisualsLocal(newValue);
}
/// <summary>
/// 무기 장착 (서버에서만 호출)
/// </summary>
public void EquipWeapon(WeaponData weapon)
{
if (weapon == null)
{
Debug.LogWarning("[WeaponEquipment] EquipWeapon called with null weapon");
return;
}
if (!IsServer)
{
Debug.LogWarning("[WeaponEquipment] EquipWeapon can only be called on server");
return;
}
// 기존 무기 해제
if (currentWeapon != null)
{
UnequipWeapon();
}
currentWeapon = weapon;
// 스탯 보너스 적용
ApplyStatBonuses(weapon);
// 무기 외형 생성 및 부착
SpawnWeaponVisuals(weapon);
// 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);
Debug.Log($"[WeaponEquipment] Equipped: {weapon.WeaponName} at {weapon.WeaponSlot}");
}
/// <summary>
/// 무기 해제 (서버에서만 호출)
/// </summary>
public void UnequipWeapon()
{
if (currentWeapon == null) return;
if (!IsServer)
{
Debug.LogWarning("[WeaponEquipment] UnequipWeapon can only be called on server");
return;
}
WeaponData previousWeapon = currentWeapon;
// 스탯 보너스 제거
RemoveStatBonuses();
// 무기 외형 제거
DespawnWeaponVisuals();
currentWeapon = null;
equippedWeaponId.Value = -1;
// 이벤트 발생
OnWeaponUnequipped?.Invoke(previousWeapon);
Debug.Log($"[WeaponEquipment] Unequipped: {previousWeapon.WeaponName}");
}
/// <summary>
/// 내부 해제 로직 (클라이언트 동기화용)
/// </summary>
private void UnequipWeaponInternal()
{
if (currentWeaponInstance == null && currentWeapon == null) return;
WeaponData previousWeapon = currentWeapon;
DespawnWeaponVisualsLocal();
OnWeaponUnequipped?.Invoke(previousWeapon);
}
/// <summary>
/// 무기의 스탯 보너스 적용
/// </summary>
private void ApplyStatBonuses(WeaponData weapon)
{
if (characterStats == null) return;
// 모든 스탯 타입에 대해 보너스 적용
foreach (StatType statType in System.Enum.GetValues(typeof(StatType)))
{
int bonus = weapon.GetStatBonus(statType);
if (bonus != 0)
{
var stat = characterStats.GetStat(statType);
if (stat != null)
{
var modifier = new StatModifier(bonus, StatModType.Flat, weapon);
stat.AddModifier(modifier);
activeModifiers[statType] = modifier;
Debug.Log($"[WeaponEquipment] Applied {statType} +{bonus}");
}
}
}
}
/// <summary>
/// 무기의 스탯 보너스 제거
/// </summary>
private void RemoveStatBonuses()
{
if (characterStats == null) return;
// 각 스탯에서 무기로부터 추가된 수정자 제거
foreach (StatType statType in System.Enum.GetValues(typeof(StatType)))
{
var stat = characterStats.GetStat(statType);
if (stat != null && activeModifiers.TryGetValue(statType, out StatModifier modifier))
{
stat.RemoveModifier(modifier);
Debug.Log($"[WeaponEquipment] Removed {statType} modifier");
}
}
activeModifiers.Clear();
}
/// <summary>
/// 무기 외형 생성 및 부착 (서버)
/// </summary>
private void SpawnWeaponVisuals(WeaponData weapon)
{
if (weapon == null || weapon.WeaponPrefab == null) return;
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;
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;
Destroy(currentWeaponInstance);
currentWeaponInstance = null;
}
/// <summary>
/// 무기 외형 제거 (클라이언트)
/// </summary>
private void DespawnWeaponVisualsLocal()
{
if (currentWeaponInstance == null) return;
Destroy(currentWeaponInstance);
currentWeaponInstance = null;
currentWeapon = null;
}
/// <summary>
/// 슬롯 타입에 맞는 소켓 Transform 반환
/// </summary>
private Transform GetSocketForSlot(WeaponSlot slot)
{
return slot switch
{
WeaponSlot.RightHand => rightHandSocket,
WeaponSlot.LeftHand => leftHandSocket,
WeaponSlot.Back => backSocket,
WeaponSlot.Hip => hipSocket,
WeaponSlot.TwoHanded => twoHandedSocket != null ? twoHandedSocket : rightHandSocket,
_ => rightHandSocket,
};
}
/// <summary>
/// 서버에 무기 장착 요청
/// </summary>
[Rpc(SendTo.Server)]
public void RequestEquipWeaponRpc(int weaponInstanceId)
{
// TODO: WeaponDatabase에서 ID로 WeaponData 조회 후 EquipWeapon 호출
Debug.Log($"[WeaponEquipment] Client requested weapon equip: {weaponInstanceId}");
}
/// <summary>
/// 서버에 무기 해제 요청
/// </summary>
[Rpc(SendTo.Server)]
public void RequestUnequipWeaponRpc()
{
UnequipWeapon();
}
}
}