건축모드 네트워크 환경 적용

This commit is contained in:
2026-02-14 17:45:31 +09:00
parent 3e1754eb3c
commit e451d95e0e
4 changed files with 195 additions and 32 deletions

View File

@@ -36,7 +36,7 @@ namespace Northbound.Data
[Header("Properties for convenience")] [Header("Properties for convenience")]
public int width => sizeX; public int width => sizeX;
public int length => sizeY; public int length => sizeY;
public float height => sizeY; public float height => sizeZ; // height는 sizeZ로 수정
public int maxHealth => maxHp; public int maxHealth => maxHp;
public float visionRange => atkRange; public float visionRange => atkRange;
public float requiredWorkAmount => manpower; public float requiredWorkAmount => manpower;
@@ -46,7 +46,7 @@ namespace Northbound.Data
bool isRotated = (rotation == 1 || rotation == 3); bool isRotated = (rotation == 1 || rotation == 3);
float w = isRotated ? length : width; float w = isRotated ? length : width;
float l = isRotated ? width : length; float l = isRotated ? width : length;
return new Vector3(w, sizeY, l); return new Vector3(w, sizeZ, l); // 세 번째 차원도 sizeZ 사용
} }
} }
} }

View File

@@ -11,6 +11,7 @@ GameObject:
- component: {fileID: 320439620877427584} - component: {fileID: 320439620877427584}
- component: {fileID: 7934051929434515110} - component: {fileID: 7934051929434515110}
- component: {fileID: 1469866302769820338} - component: {fileID: 1469866302769820338}
- component: {fileID: -4242933244907694767}
m_Layer: 7 m_Layer: 7
m_Name: BuildingFoundation m_Name: BuildingFoundation
m_TagString: Untagged m_TagString: Untagged
@@ -75,6 +76,7 @@ MonoBehaviour:
buildingData: {fileID: 11400000, guid: 23c12a82ea534b34299700b86fffd524, type: 2} buildingData: {fileID: 11400000, guid: 23c12a82ea534b34299700b86fffd524, type: 2}
gridPosition: {x: 0, y: 0, z: 0} gridPosition: {x: 0, y: 0, z: 0}
rotation: 0 rotation: 0
interactionCooldown: 1
constructionAnimationTrigger: Mining constructionAnimationTrigger: Mining
constructionEquipment: constructionEquipment:
socketName: handslot.r socketName: handslot.r
@@ -86,6 +88,27 @@ MonoBehaviour:
detachDelay: 0 detachDelay: 0
foundationVisual: {fileID: 2851644658348875061} foundationVisual: {fileID: 2851644658348875061}
progressBarPrefab: {fileID: 0} progressBarPrefab: {fileID: 0}
--- !u!65 &-4242933244907694767
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1340458267086560577}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!1001 &3121692064363536484 --- !u!1001 &3121692064363536484
PrefabInstance: PrefabInstance:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -27,6 +27,13 @@ namespace Northbound
public GameObject foundationVisual; public GameObject foundationVisual;
public GameObject progressBarPrefab; public GameObject progressBarPrefab;
// 건물 데이터 인덱스 (네트워크 동기화용)
private NetworkVariable<int> _buildingDataIndex = new NetworkVariable<int>(
-1,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
// 현재 건설 진행도 // 현재 건설 진행도
private NetworkVariable<float> _currentProgress = new NetworkVariable<float>( private NetworkVariable<float> _currentProgress = new NetworkVariable<float>(
0f, 0f,
@@ -63,6 +70,12 @@ namespace Northbound
base.OnNetworkSpawn(); base.OnNetworkSpawn();
_currentProgress.OnValueChanged += OnProgressValueChanged; _currentProgress.OnValueChanged += OnProgressValueChanged;
_buildingDataIndex.OnValueChanged += OnBuildingDataIndexChanged;
// 초기값 로드 시도 (Host/Client 모두 동일하게 처리)
// NetworkVariable 초기값은 스폰 시 동기화되지만,
// OnValueChanged는 변경 시만 발생하므로 초기값은 직접 로드해야 함
LoadBuildingDataFromIndex(_buildingDataIndex.Value);
// 진행 UI 생성 // 진행 UI 생성
if (progressBarPrefab != null) if (progressBarPrefab != null)
@@ -75,10 +88,90 @@ namespace Northbound
public override void OnNetworkDespawn() public override void OnNetworkDespawn()
{ {
_currentProgress.OnValueChanged -= OnProgressValueChanged; _currentProgress.OnValueChanged -= OnProgressValueChanged;
_buildingDataIndex.OnValueChanged -= OnBuildingDataIndexChanged;
base.OnNetworkDespawn(); base.OnNetworkDespawn();
} }
private void OnBuildingDataIndexChanged(int oldValue, int newValue)
{
LoadBuildingDataFromIndex(newValue);
UpdateCollider();
}
private void LoadBuildingDataFromIndex(int index)
{
var buildingManager = BuildingManager.Instance;
if (buildingManager == null)
{
return;
}
if (index < 0)
{
return;
}
if (index >= buildingManager.availableBuildings.Count)
{
return;
}
// 이미 로드된 데이터와 동일하면 건너뜀
TowerData newData = buildingManager.availableBuildings[index];
if (buildingData == newData)
{
return;
}
buildingData = newData;
// buildingData 로드 후 업데이트
UpdateCollider();
UpdateVisual();
}
/// <summary>
/// BoxCollider 업데이트 (buildingData 기반)
/// </summary>
private void UpdateCollider()
{
if (buildingData == null)
return;
Vector3 size = buildingData.GetSize(rotation);
// BoxCollider가 없으면 추가
if (_collider == null)
{
_collider = GetComponent<BoxCollider>();
if (_collider == null)
{
_collider = gameObject.AddComponent<BoxCollider>();
}
}
// 상호작용 가능한 크기로 설정 (전체 건물 높이가 아닌 접근 가능한 크기)
_collider.size = new Vector3(size.x, 2f, size.z);
_collider.center = new Vector3(0, 1f, 0);
_collider.isTrigger = false;
}
/// <summary>
/// Visual 스케일 업데이트 (buildingData 기반)
/// </summary>
private void UpdateVisual()
{
if (buildingData == null || foundationVisual == null)
return;
Vector3 size = buildingData.GetSize(rotation);
// 토대 비주얼을 건물 크기에 맞게 조정 (높이는 얇게)
foundationVisual.transform.localScale = new Vector3(size.x, 0.2f, size.z);
foundationVisual.transform.localPosition = new Vector3(0, 0.1f, 0);
}
/// <summary> /// <summary>
/// 토대 초기화 /// 토대 초기화
/// </summary> /// </summary>
@@ -86,7 +179,31 @@ namespace Northbound
{ {
if (!IsServer) return; if (!IsServer) return;
// buildingData null 체크
if (data == null)
{
return;
}
// buildingData 인덱스 찾기
var buildingManager = BuildingManager.Instance;
if (buildingManager == null)
{
return;
}
int dataIndex = buildingManager.availableBuildings.IndexOf(data);
if (dataIndex < 0)
{
return;
}
// 인덱스 설정 (네트워크 동기화됨)
_buildingDataIndex.Value = dataIndex;
// 서버에서도 직접 데이터 로드
buildingData = data; buildingData = data;
gridPosition = pos; gridPosition = pos;
rotation = rot; rotation = rot;
_ownerId.Value = ownerId; _ownerId.Value = ownerId;
@@ -115,8 +232,6 @@ namespace Northbound
_collider.size = new Vector3(size.x, 2f, size.z); // 높이를 2m로 설정하여 상호작용 가능 _collider.size = new Vector3(size.x, 2f, size.z); // 높이를 2m로 설정하여 상호작용 가능
_collider.center = new Vector3(0, 1f, 0); // 중심을 1m 높이에 배치 _collider.center = new Vector3(0, 1f, 0); // 중심을 1m 높이에 배치
_collider.isTrigger = false; // Trigger가 아닌 일반 Collider로 설정 _collider.isTrigger = false; // Trigger가 아닌 일반 Collider로 설정
Debug.Log($"<color=yellow>[BuildingFoundation] 토대 생성: {data.buildingName}, 크기: {size}, 위치: {transform.position}, Collider: {_collider.size}, 소유자: {ownerId}, 팀: {team}</color>");
} }
/// <summary> /// <summary>
@@ -136,10 +251,15 @@ namespace Northbound
public bool CanInteract(ulong playerId) public bool CanInteract(ulong playerId)
{ {
// buildingData가 없으면 상호작용 불가
if (buildingData == null)
{
return false;
}
// 이미 완성됨 // 이미 완성됨
if (_currentProgress.Value >= buildingData.requiredWorkAmount) if (_currentProgress.Value >= buildingData.requiredWorkAmount)
{ {
Debug.Log($"[BuildingFoundation] Already completed");
return false; return false;
} }
@@ -153,7 +273,6 @@ namespace Northbound
TeamType playerTeam = GetPlayerTeam(playerId); TeamType playerTeam = GetPlayerTeam(playerId);
if (playerTeam != _team.Value) if (playerTeam != _team.Value)
{ {
Debug.LogWarning($"[BuildingFoundation] Wrong team: player={playerTeam}, foundation={_team.Value}");
return false; return false;
} }
@@ -162,11 +281,28 @@ namespace Northbound
public void Interact(ulong playerId) public void Interact(ulong playerId)
{ {
if (!IsServer) return; // 네트워크 게임에서는 ServerRpc을 통해서만 서버에서 실행하도록 함
RequestInteractServerRpc(playerId);
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestInteractServerRpc(ulong playerId)
{
// 서버에서만 건설 진행 가능
if (!IsServer)
{
return;
}
if (!CanInteract(playerId)) if (!CanInteract(playerId))
return; return;
// buildingData null 체크
if (buildingData == null)
{
return;
}
_lastInteractionTime = Time.time; _lastInteractionTime = Time.time;
// 플레이어의 작업량 가져오기 // 플레이어의 작업량 가져오기
@@ -175,8 +311,6 @@ namespace Northbound
// 건설 진행 // 건설 진행
_currentProgress.Value += playerWorkPower; _currentProgress.Value += playerWorkPower;
Debug.Log($"<color=green>[BuildingFoundation] 건설 진행: {_currentProgress.Value}/{buildingData.requiredWorkAmount} ({(_currentProgress.Value / buildingData.requiredWorkAmount * 100f):F1}%) - 작업량: {playerWorkPower}</color>");
// 완성 체크 // 완성 체크
if (_currentProgress.Value >= buildingData.requiredWorkAmount) if (_currentProgress.Value >= buildingData.requiredWorkAmount)
{ {
@@ -187,7 +321,8 @@ namespace Northbound
public string GetInteractionPrompt() public string GetInteractionPrompt()
{ {
string buildingName = buildingData != null ? buildingData.buildingName : "건물"; string buildingName = buildingData != null ? buildingData.buildingName : "건물";
float percentage = (_currentProgress.Value / buildingData.requiredWorkAmount) * 100f; float requiredWork = buildingData?.requiredWorkAmount ?? 100f;
float percentage = (_currentProgress.Value / requiredWork) * 100f;
return $"[E] {buildingName} 건설 ({percentage:F0}%)"; return $"[E] {buildingName} 건설 ({percentage:F0}%)";
} }
@@ -270,7 +405,6 @@ namespace Northbound
} }
// 기본값: 10 // 기본값: 10
Debug.LogWarning($"[BuildingFoundation] 플레이어 {playerId}의 workPower를 찾을 수 없어 기본값 10을 사용합니다.");
return 10f; return 10f;
} }
@@ -278,7 +412,10 @@ namespace Northbound
{ {
if (!IsServer) return; if (!IsServer) return;
Debug.Log($"<color=cyan>[BuildingFoundation] 건물 완성! {buildingData.buildingName}</color>"); if (buildingData == null)
{
return;
}
OnConstructionComplete?.Invoke(); OnConstructionComplete?.Invoke();
@@ -324,7 +461,8 @@ namespace Northbound
private void OnProgressValueChanged(float oldValue, float newValue) private void OnProgressValueChanged(float oldValue, float newValue)
{ {
OnProgressChanged?.Invoke(newValue, buildingData.requiredWorkAmount); float requiredWork = buildingData?.requiredWorkAmount ?? 100f;
OnProgressChanged?.Invoke(newValue, requiredWork);
UpdateProgressBar(); UpdateProgressBar();
} }
@@ -337,7 +475,8 @@ namespace Northbound
if (progressBar != null) if (progressBar != null)
{ {
// BuildingHealthBar를 재사용하여 진행도 표시 // BuildingHealthBar를 재사용하여 진행도 표시
progressBar.UpdateHealth((int)_currentProgress.Value, (int)buildingData.requiredWorkAmount); float requiredWork = buildingData?.requiredWorkAmount ?? 100f;
progressBar.UpdateHealth((int)_currentProgress.Value, (int)requiredWork);
} }
} }

View File

@@ -400,34 +400,35 @@ namespace Northbound
GameObject foundationObj = Instantiate(foundationPrefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0)); GameObject foundationObj = Instantiate(foundationPrefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
NetworkObject netObj = foundationObj.GetComponent<NetworkObject>(); NetworkObject netObj = foundationObj.GetComponent<NetworkObject>();
// Add FogOfWarVisibility component to hide foundations in unexplored areas
if (foundationObj.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = foundationObj.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = true; // Foundations remain visible in explored areas
visibility.updateInterval = 0.2f;
}
if (netObj != null) if (netObj != null)
{ {
netObj.SpawnWithOwnership(requestingClientId); // 스폰 먼저 실행 (서버 소유권으로 스폰)
// 소유자 정보는 _ownerId NetworkVariable에 별도로 저장하므로 NetworkObject 소유권은 서버 유지
netObj.Spawn();
// 스폰 후에 초기화 (OnNetworkSpawn 이후에 호출되어 타이밍 문제 해결)
BuildingFoundation foundation = foundationObj.GetComponent<BuildingFoundation>(); BuildingFoundation foundation = foundationObj.GetComponent<BuildingFoundation>();
if (foundation != null) if (foundation == null)
{ {
foundation.Initialize(data, gridPosition, rotation, requestingClientId, playerTeam); Destroy(foundationObj);
placedFoundations.Add(foundation); // 토대 목록에 추가 return;
Debug.Log($"<color=yellow>[BuildingManager] {data.buildingName} 토대 생성 (소유자: {requestingClientId}, 위치: {gridPosition})</color>");
} }
else
// Initialize에서 NetworkVariable을 설정하고 데이터를 로드함
foundation.Initialize(data, gridPosition, rotation, requestingClientId, playerTeam);
// Add FogOfWarVisibility component to hide foundations in unexplored areas
if (foundationObj.GetComponent<FogOfWarVisibility>() == null)
{ {
Debug.LogError("<color=red>[BuildingManager] BuildingFoundation 컴포넌트가 없습니다!</color>"); var visibility = foundationObj.AddComponent<FogOfWarVisibility>();
netObj.Despawn(true); visibility.showInExploredAreas = true; // Foundations remain visible in explored areas
visibility.updateInterval = 0.2f;
} }
placedFoundations.Add(foundation); // 토대 목록에 추가
} }
else else
{ {
Debug.LogError("<color=red>[BuildingManager] NetworkObject 컴포넌트가 없습니다!</color>");
Destroy(foundationObj); Destroy(foundationObj);
} }
} }