507 lines
16 KiB
C#
507 lines
16 KiB
C#
using System;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
using Northbound.Data;
|
|
|
|
namespace Northbound
|
|
{
|
|
/// <summary>
|
|
/// 건물 토대 - 플레이어가 상호작용하여 건물을 완성시킴
|
|
/// </summary>
|
|
public class BuildingFoundation : NetworkBehaviour, IInteractable, ITeamMember
|
|
{
|
|
[Header("Target Building")]
|
|
public TowerData buildingData;
|
|
public Vector3Int gridPosition;
|
|
public int rotation;
|
|
|
|
[Header("Construction Settings")]
|
|
[Tooltip("상호작용 쿨다운 (초)")]
|
|
public float interactionCooldown = 1f;
|
|
[Tooltip("건설 시 플레이어가 재생할 애니메이션 트리거")]
|
|
public string constructionAnimationTrigger = "Build";
|
|
[Tooltip("건설 시 사용할 도구 (선택사항)")]
|
|
public EquipmentData constructionEquipment;
|
|
|
|
[Header("Visual")]
|
|
public GameObject foundationVisual;
|
|
public GameObject progressBarPrefab;
|
|
|
|
// 건물 데이터 인덱스 (네트워크 동기화용)
|
|
private NetworkVariable<int> _buildingDataIndex = new NetworkVariable<int>(
|
|
-1,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
// 현재 건설 진행도
|
|
private NetworkVariable<float> _currentProgress = new NetworkVariable<float>(
|
|
0f,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
// 건물 소유자
|
|
private NetworkVariable<ulong> _ownerId = new NetworkVariable<ulong>(
|
|
0,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
// 팀
|
|
private NetworkVariable<TeamType> _team = new NetworkVariable<TeamType>(
|
|
TeamType.Neutral,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
// 이벤트
|
|
public event Action<float, float> OnProgressChanged; // (current, max)
|
|
public event Action OnConstructionComplete;
|
|
|
|
private GameObject _progressBarInstance;
|
|
private float _lastInteractionTime;
|
|
private BoxCollider _collider;
|
|
|
|
public ulong OwnerId => _ownerId.Value;
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
base.OnNetworkSpawn();
|
|
|
|
_currentProgress.OnValueChanged += OnProgressValueChanged;
|
|
_buildingDataIndex.OnValueChanged += OnBuildingDataIndexChanged;
|
|
|
|
// 초기값 로드 시도 (Host/Client 모두 동일하게 처리)
|
|
// NetworkVariable 초기값은 스폰 시 동기화되지만,
|
|
// OnValueChanged는 변경 시만 발생하므로 초기값은 직접 로드해야 함
|
|
LoadBuildingDataFromIndex(_buildingDataIndex.Value);
|
|
|
|
// 진행 UI 생성
|
|
if (progressBarPrefab != null)
|
|
{
|
|
_progressBarInstance = Instantiate(progressBarPrefab, transform);
|
|
UpdateProgressBar();
|
|
}
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
_currentProgress.OnValueChanged -= OnProgressValueChanged;
|
|
_buildingDataIndex.OnValueChanged -= OnBuildingDataIndexChanged;
|
|
|
|
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>
|
|
public void Initialize(TowerData data, Vector3Int pos, int rot, ulong ownerId, TeamType team)
|
|
{
|
|
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;
|
|
|
|
gridPosition = pos;
|
|
rotation = rot;
|
|
_ownerId.Value = ownerId;
|
|
_team.Value = team;
|
|
_currentProgress.Value = 0f;
|
|
|
|
// TowerData의 크기를 기반으로 스케일 설정
|
|
Vector3 size = data.GetSize(rot);
|
|
|
|
// foundationVisual의 스케일만 조정 (토대 자체의 pivot은 중앙에 유지)
|
|
if (foundationVisual != null)
|
|
{
|
|
// 토대 비주얼을 건물 크기에 맞게 조정 (높이는 얇게)
|
|
foundationVisual.transform.localScale = new Vector3(size.x, 0.2f, size.z);
|
|
foundationVisual.transform.localPosition = new Vector3(0, 0.1f, 0); // 바닥에서 약간 위
|
|
}
|
|
|
|
// BoxCollider 추가 및 크기 설정 (상호작용용)
|
|
_collider = GetComponent<BoxCollider>();
|
|
if (_collider == null)
|
|
{
|
|
_collider = gameObject.AddComponent<BoxCollider>();
|
|
}
|
|
|
|
// 상호작용 가능한 크기로 설정 (전체 건물 높이가 아닌 접근 가능한 크기)
|
|
_collider.size = new Vector3(size.x, 2f, size.z); // 높이를 2m로 설정하여 상호작용 가능
|
|
_collider.center = new Vector3(0, 1f, 0); // 중심을 1m 높이에 배치
|
|
_collider.isTrigger = false; // Trigger가 아닌 일반 Collider로 설정
|
|
}
|
|
|
|
/// <summary>
|
|
/// 토대의 그리드 경계 가져오기 (BuildingManager의 충돌 체크용)
|
|
/// </summary>
|
|
public Bounds GetGridBounds()
|
|
{
|
|
if (buildingData == null)
|
|
return new Bounds(transform.position, Vector3.one);
|
|
|
|
Vector3 size = buildingData.GetSize(rotation);
|
|
// 토대의 위치를 중심으로 건물이 차지할 공간 반환
|
|
return new Bounds(transform.position + Vector3.up * size.y * 0.5f, size);
|
|
}
|
|
|
|
#region IInteractable Implementation
|
|
|
|
public bool CanInteract(ulong playerId)
|
|
{
|
|
// buildingData가 없으면 상호작용 불가
|
|
if (buildingData == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 이미 완성됨
|
|
if (_currentProgress.Value >= buildingData.requiredWorkAmount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 쿨다운 확인
|
|
if (Time.time - _lastInteractionTime < interactionCooldown)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 같은 팀만 건설 가능 - 플레이어의 팀을 가져와서 비교
|
|
TeamType playerTeam = GetPlayerTeam(playerId);
|
|
if (playerTeam != _team.Value)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void Interact(ulong playerId)
|
|
{
|
|
// 네트워크 게임에서는 ServerRpc을 통해서만 서버에서 실행하도록 함
|
|
RequestInteractServerRpc(playerId);
|
|
}
|
|
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
private void RequestInteractServerRpc(ulong playerId)
|
|
{
|
|
// 서버에서만 건설 진행 가능
|
|
if (!IsServer)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanInteract(playerId))
|
|
return;
|
|
|
|
// buildingData null 체크
|
|
if (buildingData == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_lastInteractionTime = Time.time;
|
|
|
|
// 플레이어의 작업량 가져오기
|
|
float playerWorkPower = GetPlayerWorkPower(playerId);
|
|
|
|
// 건설 진행
|
|
_currentProgress.Value += playerWorkPower;
|
|
|
|
// 완성 체크
|
|
if (_currentProgress.Value >= buildingData.requiredWorkAmount)
|
|
{
|
|
CompleteConstruction();
|
|
}
|
|
}
|
|
|
|
public string GetInteractionPrompt()
|
|
{
|
|
string buildingName = buildingData != null ? buildingData.buildingName : "건물";
|
|
float requiredWork = buildingData?.requiredWorkAmount ?? 100f;
|
|
float percentage = (_currentProgress.Value / requiredWork) * 100f;
|
|
return $"[E] {buildingName} 건설 ({percentage:F0}%)";
|
|
}
|
|
|
|
public string GetInteractionAnimation()
|
|
{
|
|
return constructionAnimationTrigger;
|
|
}
|
|
|
|
public EquipmentData GetEquipmentData()
|
|
{
|
|
return constructionEquipment;
|
|
}
|
|
|
|
public Transform GetTransform()
|
|
{
|
|
return transform;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ITeamMember Implementation
|
|
|
|
public TeamType GetTeam()
|
|
{
|
|
return _team.Value;
|
|
}
|
|
|
|
public void SetTeam(TeamType team)
|
|
{
|
|
if (!IsServer) return;
|
|
_team.Value = team;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// 플레이어의 팀 가져오기
|
|
/// </summary>
|
|
private TeamType GetPlayerTeam(ulong playerId)
|
|
{
|
|
// 플레이어의 NetworkObject 찾기
|
|
if (NetworkManager.Singleton != null && NetworkManager.Singleton.SpawnManager != null)
|
|
{
|
|
if (NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client))
|
|
{
|
|
if (client.PlayerObject != null)
|
|
{
|
|
var teamMember = client.PlayerObject.GetComponent<ITeamMember>();
|
|
if (teamMember != null)
|
|
{
|
|
return teamMember.GetTeam();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 기본값: 플레이어 팀
|
|
return TeamType.Player;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 플레이어의 작업량 가져오기 (PlayerData.manpower)
|
|
/// </summary>
|
|
private float GetPlayerWorkPower(ulong playerId)
|
|
{
|
|
// PlayerInteraction 컴포넌트에서 workPower 가져오기
|
|
if (NetworkManager.Singleton != null && NetworkManager.Singleton.SpawnManager != null)
|
|
{
|
|
if (NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client))
|
|
{
|
|
if (client.PlayerObject != null)
|
|
{
|
|
var playerInteraction = client.PlayerObject.GetComponent<PlayerInteraction>();
|
|
if (playerInteraction != null)
|
|
{
|
|
return playerInteraction.WorkPower;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 기본값: 10
|
|
return 10f;
|
|
}
|
|
|
|
private void CompleteConstruction()
|
|
{
|
|
if (!IsServer) return;
|
|
|
|
if (buildingData == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnConstructionComplete?.Invoke();
|
|
|
|
// BuildingManager에서 토대 제거
|
|
var buildingManager = BuildingManager.Instance;
|
|
if (buildingManager != null)
|
|
{
|
|
buildingManager.RemoveFoundation(this);
|
|
}
|
|
|
|
// 완성된 건물 생성
|
|
SpawnCompletedBuilding();
|
|
|
|
// 토대 제거
|
|
if (NetworkObject != null)
|
|
{
|
|
NetworkObject.Despawn(true);
|
|
}
|
|
}
|
|
|
|
private void SpawnCompletedBuilding()
|
|
{
|
|
if (!IsServer || buildingData == null || buildingData.prefab == null)
|
|
return;
|
|
|
|
// BuildingManager를 통해 건물 생성
|
|
var buildingManager = BuildingManager.Instance;
|
|
if (buildingManager != null)
|
|
{
|
|
buildingManager.SpawnCompletedBuildingServerRpc(
|
|
buildingData.name,
|
|
gridPosition,
|
|
rotation,
|
|
_ownerId.Value,
|
|
_team.Value
|
|
);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("[BuildingFoundation] BuildingManager를 찾을 수 없습니다!");
|
|
}
|
|
}
|
|
|
|
private void OnProgressValueChanged(float oldValue, float newValue)
|
|
{
|
|
float requiredWork = buildingData?.requiredWorkAmount ?? 100f;
|
|
OnProgressChanged?.Invoke(newValue, requiredWork);
|
|
UpdateProgressBar();
|
|
}
|
|
|
|
private void UpdateProgressBar()
|
|
{
|
|
if (_progressBarInstance == null) return;
|
|
|
|
// 진행바 UI 업데이트 (BuildingHealthBar와 유사한 구조 사용 가능)
|
|
var progressBar = _progressBarInstance.GetComponent<BuildingHealthBar>();
|
|
if (progressBar != null)
|
|
{
|
|
// BuildingHealthBar를 재사용하여 진행도 표시
|
|
float requiredWork = buildingData?.requiredWorkAmount ?? 100f;
|
|
progressBar.UpdateHealth((int)_currentProgress.Value, (int)requiredWork);
|
|
}
|
|
}
|
|
|
|
private void OnDrawGizmos()
|
|
{
|
|
if (buildingData == null) return;
|
|
|
|
// 건물 경계 표시 (노란색)
|
|
Gizmos.color = Color.yellow;
|
|
Vector3 size = buildingData.GetSize(rotation);
|
|
Gizmos.DrawWireCube(transform.position + Vector3.up * size.y * 0.5f, size);
|
|
|
|
// Collider 경계 표시 (초록색)
|
|
if (_collider != null)
|
|
{
|
|
Gizmos.color = Color.green;
|
|
Gizmos.DrawWireCube(transform.position + _collider.center, _collider.size);
|
|
}
|
|
|
|
// 상호작용 가능 여부 표시
|
|
if (_currentProgress.Value < (buildingData?.requiredWorkAmount ?? 100f))
|
|
{
|
|
Gizmos.color = Color.cyan;
|
|
Gizmos.DrawSphere(transform.position + Vector3.up, 0.3f);
|
|
}
|
|
}
|
|
}
|
|
} |