건물 체력 시스템 추가
건물 시야 시스템은 기본 건물데이터에 통합
This commit is contained in:
@@ -59,10 +59,10 @@
|
||||
<Compile Include="Assets\Scripts\PlayerVisionProvider.cs" />
|
||||
<Compile Include="Assets\Scripts\PlayerController.cs" />
|
||||
<Compile Include="Assets\FlatKit\Demos\Common\Scripts\Motion\OrbitMotion.cs" />
|
||||
<Compile Include="Assets\Scripts\BuildingDamageTest.cs" />
|
||||
<Compile Include="Assets\Scripts\EquipmentSocket.cs" />
|
||||
<Compile Include="Assets\Scripts\BuildingData.cs" />
|
||||
<Compile Include="Assets\Scripts\Resource.cs" />
|
||||
<Compile Include="Assets\Scripts\BuildingVisionProvider.cs" />
|
||||
<Compile Include="Assets\FlatKit\[Render Pipeline] URP\Water\Scripts\Buoyancy.cs" />
|
||||
<Compile Include="Assets\Scripts\AttackAction.cs" />
|
||||
<Compile Include="Assets\Scripts\IInteractable.cs" />
|
||||
@@ -70,6 +70,7 @@
|
||||
<Compile Include="Assets\Scripts\GlobalTimer.cs" />
|
||||
<Compile Include="Assets\InputSystem_Actions.cs" />
|
||||
<Compile Include="Assets\FlatKit\Demos\[Demo] Desert\Scripts\BillboardLineRendererCircle.cs" />
|
||||
<Compile Include="Assets\Scripts\BuildingHealthBar.cs" />
|
||||
<Compile Include="Assets\Scripts\PlayerSpawnPoint.cs" />
|
||||
<Compile Include="Assets\FlatKit\Demos\Common\Scripts\AutoLoadPipelineAsset.cs" />
|
||||
<Compile Include="Assets\Scripts\AutoHost.cs" />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
public class Building : NetworkBehaviour
|
||||
public class Building : NetworkBehaviour, IDamageable, IVisionProvider
|
||||
{
|
||||
[Header("References")]
|
||||
public BuildingData buildingData;
|
||||
@@ -12,16 +13,386 @@ namespace Northbound
|
||||
public Vector3Int gridPosition;
|
||||
public int rotation; // 0-3 (0=0°, 1=90°, 2=180°, 3=270°)
|
||||
|
||||
[Header("Ownership (for pre-placed buildings)")]
|
||||
[Tooltip("씬에 미리 배치된 건물의 경우 여기서 소유자 설정 (0 = 중립, 1+ = 플레이어 ID)")]
|
||||
public ulong initialOwnerId = 0;
|
||||
[Tooltip("사전 배치 건물인가요? 체크하면 initialOwnerId를 사용합니다")]
|
||||
public bool useInitialOwner = false;
|
||||
|
||||
[Header("Health")]
|
||||
public bool showHealthBar = true;
|
||||
public GameObject healthBarPrefab;
|
||||
|
||||
[Header("Visual Effects")]
|
||||
public GameObject destroyEffectPrefab;
|
||||
public GameObject damageEffectPrefab;
|
||||
public Transform effectSpawnPoint;
|
||||
|
||||
[Header("Debug")]
|
||||
public bool showGridBounds = true;
|
||||
public Color gridBoundsColor = Color.cyan;
|
||||
|
||||
public void Initialize(BuildingData data, Vector3Int gridPos, int rot)
|
||||
// 현재 체력
|
||||
private NetworkVariable<int> _currentHealth = new NetworkVariable<int>(
|
||||
0,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
// 건물 소유자 (사전 배치 건물 또는 동적 건물 모두 지원)
|
||||
private NetworkVariable<ulong> _ownerId = new NetworkVariable<ulong>(
|
||||
0,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
// 이벤트
|
||||
public event Action<int, int> OnHealthChanged; // (current, max)
|
||||
public event Action OnDestroyed;
|
||||
|
||||
private BuildingHealthBar _healthBar;
|
||||
private float _lastRegenTime;
|
||||
private bool _isInitialized = false;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
// 체력 초기화
|
||||
if (_currentHealth.Value == 0)
|
||||
{
|
||||
_currentHealth.Value = buildingData != null ? buildingData.maxHealth : 100;
|
||||
}
|
||||
|
||||
// 소유자 초기화 (사전 배치 건물 체크)
|
||||
if (useInitialOwner && _ownerId.Value == 0)
|
||||
{
|
||||
_ownerId.Value = initialOwnerId;
|
||||
Debug.Log($"<color=cyan>[Building] 사전 배치 건물 '{buildingData?.buildingName ?? gameObject.name}' 소유자: {initialOwnerId}</color>");
|
||||
}
|
||||
else if (!useInitialOwner && _ownerId.Value == 0)
|
||||
{
|
||||
// 동적 생성 건물은 NetworkObject의 Owner 사용
|
||||
_ownerId.Value = OwnerClientId;
|
||||
}
|
||||
|
||||
_lastRegenTime = Time.time;
|
||||
|
||||
// FogOfWar 시스템에 시야 제공자로 등록
|
||||
if (buildingData != null && buildingData.providesVision)
|
||||
{
|
||||
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
|
||||
}
|
||||
}
|
||||
|
||||
// 체력 변경 이벤트 구독
|
||||
_currentHealth.OnValueChanged += OnHealthValueChanged;
|
||||
|
||||
// 체력바 생성
|
||||
if (showHealthBar && healthBarPrefab != null)
|
||||
{
|
||||
CreateHealthBar();
|
||||
}
|
||||
|
||||
// 초기 체력 UI 업데이트
|
||||
UpdateHealthUI();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
_currentHealth.OnValueChanged -= OnHealthValueChanged;
|
||||
|
||||
// FogOfWar 시스템에서 제거
|
||||
if (IsServer && buildingData != null && buildingData.providesVision)
|
||||
{
|
||||
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsServer || buildingData == null)
|
||||
return;
|
||||
|
||||
// 자동 체력 회복
|
||||
if (buildingData.autoRegenerate && _currentHealth.Value < buildingData.maxHealth)
|
||||
{
|
||||
if (Time.time - _lastRegenTime >= 1f)
|
||||
{
|
||||
int regenAmount = Mathf.Min(buildingData.regenPerSecond, buildingData.maxHealth - _currentHealth.Value);
|
||||
_currentHealth.Value += regenAmount;
|
||||
_lastRegenTime = Time.time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 건물 초기화 (BuildingManager가 동적 생성 시 호출)
|
||||
/// </summary>
|
||||
public void Initialize(BuildingData data, Vector3Int gridPos, int rot, ulong ownerId)
|
||||
{
|
||||
buildingData = data;
|
||||
gridPosition = gridPos;
|
||||
rotation = rot;
|
||||
|
||||
// 이미 스폰된 경우
|
||||
if (IsServer && IsSpawned)
|
||||
{
|
||||
_currentHealth.Value = data.maxHealth;
|
||||
_ownerId.Value = ownerId;
|
||||
|
||||
// 시야 제공자 등록
|
||||
if (data.providesVision)
|
||||
{
|
||||
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
|
||||
}
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 건물 소유권 변경 (점령 등)
|
||||
/// </summary>
|
||||
public void SetOwner(ulong newOwnerId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
ulong previousOwner = _ownerId.Value;
|
||||
_ownerId.Value = newOwnerId;
|
||||
|
||||
Debug.Log($"<color=yellow>[Building] {buildingData?.buildingName ?? "건물"} 소유권 변경: {previousOwner} → {newOwnerId}</color>");
|
||||
|
||||
// 시야 제공자 재등록 (소유자가 바뀌었으므로)
|
||||
if (buildingData != null && buildingData.providesVision)
|
||||
{
|
||||
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
|
||||
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
|
||||
}
|
||||
}
|
||||
|
||||
#region IVisionProvider Implementation
|
||||
|
||||
public ulong GetOwnerId() => _ownerId.Value;
|
||||
|
||||
public float GetVisionRange()
|
||||
{
|
||||
return buildingData != null ? buildingData.visionRange : 0f;
|
||||
}
|
||||
|
||||
public Transform GetTransform() => transform;
|
||||
|
||||
public bool IsActive()
|
||||
{
|
||||
// 건물이 스폰되어 있고, 파괴되지 않았으며, 시야 제공 설정이 켜져있어야 함
|
||||
return IsSpawned && !IsDestroyed() && buildingData != null && buildingData.providesVision;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDamageable Implementation
|
||||
|
||||
public void TakeDamage(int damage, ulong attackerId)
|
||||
{
|
||||
if (!IsServer)
|
||||
{
|
||||
// 클라이언트는 서버에 요청
|
||||
TakeDamageServerRpc(damage, attackerId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 무적 건물
|
||||
if (buildingData != null && buildingData.isIndestructible)
|
||||
{
|
||||
Debug.Log($"<color=yellow>[Building] {buildingData.buildingName}은(는) 무적입니다.</color>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 이미 파괴됨
|
||||
if (_currentHealth.Value <= 0)
|
||||
return;
|
||||
|
||||
// 데미지 적용
|
||||
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
||||
_currentHealth.Value -= actualDamage;
|
||||
|
||||
Debug.Log($"<color=red>[Building] {buildingData?.buildingName ?? "건물"}이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{buildingData?.maxHealth ?? 100}</color>");
|
||||
|
||||
// 데미지 이펙트
|
||||
ShowDamageEffectClientRpc();
|
||||
|
||||
// 체력이 0이 되면 파괴
|
||||
if (_currentHealth.Value <= 0)
|
||||
{
|
||||
DestroyBuilding(attackerId);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server)]
|
||||
private void TakeDamageServerRpc(int damage, ulong attackerId)
|
||||
{
|
||||
TakeDamage(damage, attackerId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Health Management
|
||||
|
||||
/// <summary>
|
||||
/// 건물 파괴
|
||||
/// </summary>
|
||||
private void DestroyBuilding(ulong attackerId)
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
Debug.Log($"<color=red>[Building] {buildingData?.buildingName ?? "건물"}이(가) 파괴되었습니다! (공격자: {attackerId})</color>");
|
||||
|
||||
// 파괴 이벤트 발생
|
||||
OnDestroyed?.Invoke();
|
||||
NotifyDestroyedClientRpc();
|
||||
|
||||
// 파괴 이펙트
|
||||
ShowDestroyEffectClientRpc();
|
||||
|
||||
// FogOfWar 시스템에서 제거
|
||||
if (buildingData != null && buildingData.providesVision)
|
||||
{
|
||||
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
|
||||
}
|
||||
|
||||
// BuildingManager에서 제거
|
||||
if (BuildingManager.Instance != null)
|
||||
{
|
||||
BuildingManager.Instance.RemoveBuilding(this);
|
||||
}
|
||||
|
||||
// 네트워크 오브젝트 파괴 (약간의 딜레이)
|
||||
Invoke(nameof(DespawnBuilding), 0.5f);
|
||||
}
|
||||
|
||||
private void DespawnBuilding()
|
||||
{
|
||||
if (IsServer && NetworkObject != null)
|
||||
{
|
||||
NetworkObject.Despawn(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 체력 회복
|
||||
/// </summary>
|
||||
public void Heal(int amount)
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
if (buildingData == null)
|
||||
return;
|
||||
|
||||
int healAmount = Mathf.Min(amount, buildingData.maxHealth - _currentHealth.Value);
|
||||
_currentHealth.Value += healAmount;
|
||||
|
||||
Debug.Log($"<color=green>[Building] {buildingData.buildingName}이(가) {healAmount} 회복되었습니다. 현재 체력: {_currentHealth.Value}/{buildingData.maxHealth}</color>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 체력
|
||||
/// </summary>
|
||||
public int GetCurrentHealth() => _currentHealth.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 최대 체력
|
||||
/// </summary>
|
||||
public int GetMaxHealth() => buildingData != null ? buildingData.maxHealth : 100;
|
||||
|
||||
/// <summary>
|
||||
/// 체력 비율 (0.0 ~ 1.0)
|
||||
/// </summary>
|
||||
public float GetHealthPercentage()
|
||||
{
|
||||
int maxHealth = GetMaxHealth();
|
||||
return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파괴되었는지 여부
|
||||
/// </summary>
|
||||
public bool IsDestroyed() => _currentHealth.Value <= 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Health UI
|
||||
|
||||
private void CreateHealthBar()
|
||||
{
|
||||
if (_healthBar != null)
|
||||
return;
|
||||
|
||||
GameObject healthBarObj = Instantiate(healthBarPrefab, transform);
|
||||
_healthBar = healthBarObj.GetComponent<BuildingHealthBar>();
|
||||
|
||||
if (_healthBar != null)
|
||||
{
|
||||
_healthBar.Initialize(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHealthUI()
|
||||
{
|
||||
if (_healthBar != null)
|
||||
{
|
||||
_healthBar.UpdateHealth(_currentHealth.Value, GetMaxHealth());
|
||||
}
|
||||
|
||||
OnHealthChanged?.Invoke(_currentHealth.Value, GetMaxHealth());
|
||||
}
|
||||
|
||||
private void OnHealthValueChanged(int previousValue, int newValue)
|
||||
{
|
||||
UpdateHealthUI();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Visual Effects
|
||||
|
||||
[ClientRpc]
|
||||
private void ShowDamageEffectClientRpc()
|
||||
{
|
||||
if (damageEffectPrefab != null)
|
||||
{
|
||||
Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
|
||||
GameObject effect = Instantiate(damageEffectPrefab, spawnPoint.position, spawnPoint.rotation);
|
||||
Destroy(effect, 2f);
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void ShowDestroyEffectClientRpc()
|
||||
{
|
||||
if (destroyEffectPrefab != null)
|
||||
{
|
||||
Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
|
||||
GameObject effect = Instantiate(destroyEffectPrefab, spawnPoint.position, spawnPoint.rotation);
|
||||
Destroy(effect, 3f);
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void NotifyDestroyedClientRpc()
|
||||
{
|
||||
if (!IsServer)
|
||||
{
|
||||
OnDestroyed?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Grid Bounds
|
||||
|
||||
/// <summary>
|
||||
/// Gets the grid-based bounds (from BuildingData width/length/height)
|
||||
@@ -47,6 +418,10 @@ namespace Northbound
|
||||
return GetGridBounds();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gizmos
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (!showGridBounds || buildingData == null) return;
|
||||
@@ -71,6 +446,29 @@ namespace Northbound
|
||||
Gizmos.color = Color.magenta;
|
||||
Gizmos.DrawSphere(worldPos, 0.2f);
|
||||
}
|
||||
|
||||
// Draw vision range (if provides vision)
|
||||
if (buildingData.providesVision)
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(transform.position, buildingData.visionRange);
|
||||
}
|
||||
|
||||
// Draw owner ID label
|
||||
#if UNITY_EDITOR
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
|
||||
$"Owner: {_ownerId.Value}");
|
||||
}
|
||||
else if (useInitialOwner)
|
||||
{
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
|
||||
$"Initial Owner: {initialOwnerId}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
69
Assets/Scripts/BuildingDamageTest.cs
Normal file
69
Assets/Scripts/BuildingDamageTest.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 건물에 데미지를 주는 테스트/공격 시스템
|
||||
/// </summary>
|
||||
public class BuildingDamageTest : MonoBehaviour
|
||||
{
|
||||
[Header("Damage Settings")]
|
||||
public int damageAmount = 10;
|
||||
public float damageInterval = 1f;
|
||||
public KeyCode damageKey = KeyCode.F;
|
||||
|
||||
[Header("Target")]
|
||||
public float maxDistance = 10f;
|
||||
public LayerMask buildingLayer;
|
||||
|
||||
private float _lastDamageTime;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(damageKey))
|
||||
{
|
||||
TryDamageBuilding();
|
||||
}
|
||||
|
||||
// 자동 데미지 (테스트용)
|
||||
if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(damageKey))
|
||||
{
|
||||
if (Time.time - _lastDamageTime >= damageInterval)
|
||||
{
|
||||
TryDamageBuilding();
|
||||
_lastDamageTime = Time.time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TryDamageBuilding()
|
||||
{
|
||||
// 카메라에서 레이캐스트
|
||||
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
||||
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, maxDistance, buildingLayer))
|
||||
{
|
||||
Building building = hit.collider.GetComponentInParent<Building>();
|
||||
|
||||
if (building != null)
|
||||
{
|
||||
ulong attackerId = NetworkManager.Singleton != null && NetworkManager.Singleton.IsClient
|
||||
? NetworkManager.Singleton.LocalClientId
|
||||
: 0;
|
||||
|
||||
building.TakeDamage(damageAmount, attackerId);
|
||||
Debug.Log($"<color=yellow>[Test] {building.buildingData?.buildingName ?? "건물"}에게 {damageAmount} 데미지!</color>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(10, 10, 300, 100));
|
||||
GUILayout.Label($"[{damageKey}] 건물에 {damageAmount} 데미지");
|
||||
GUILayout.Label($"[Shift + {damageKey}] 연속 데미지");
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/BuildingDamageTest.cs.meta
Normal file
2
Assets/Scripts/BuildingDamageTest.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62b3bdffc8c849d48886167d316e16b6
|
||||
@@ -23,6 +23,22 @@ namespace Northbound
|
||||
[Tooltip("Can rotate this building?")]
|
||||
public bool allowRotation = true;
|
||||
|
||||
[Header("Health Settings")]
|
||||
[Tooltip("Maximum health of the building")]
|
||||
public int maxHealth = 100;
|
||||
[Tooltip("Can this building be damaged?")]
|
||||
public bool isIndestructible = false;
|
||||
[Tooltip("Auto-regenerate health over time")]
|
||||
public bool autoRegenerate = false;
|
||||
[Tooltip("Health regeneration per second")]
|
||||
public int regenPerSecond = 1;
|
||||
|
||||
[Header("Vision Settings")]
|
||||
[Tooltip("Does this building provide vision?")]
|
||||
public bool providesVision = true;
|
||||
[Tooltip("Vision range in world units")]
|
||||
public float visionRange = 15f;
|
||||
|
||||
public Vector3 GetSize(int rotation)
|
||||
{
|
||||
// Rotation 0,180 = normal, 90,270 = swap width/length
|
||||
|
||||
118
Assets/Scripts/BuildingHealthBar.cs
Normal file
118
Assets/Scripts/BuildingHealthBar.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 건물 위에 표시되는 체력바
|
||||
/// </summary>
|
||||
public class BuildingHealthBar : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
public Image fillImage;
|
||||
public TextMeshProUGUI healthText;
|
||||
public GameObject barContainer;
|
||||
|
||||
[Header("Settings")]
|
||||
public float heightOffset = 3f;
|
||||
public bool hideWhenFull = true;
|
||||
public float hideDelay = 3f;
|
||||
|
||||
[Header("Colors")]
|
||||
public Color fullHealthColor = Color.green;
|
||||
public Color mediumHealthColor = Color.yellow;
|
||||
public Color lowHealthColor = Color.red;
|
||||
public float mediumHealthThreshold = 0.6f;
|
||||
public float lowHealthThreshold = 0.3f;
|
||||
|
||||
private Building _building;
|
||||
private Camera _mainCamera;
|
||||
private float _lastDamageTime;
|
||||
private Canvas _canvas;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Canvas 설정
|
||||
_canvas = GetComponent<Canvas>();
|
||||
if (_canvas == null)
|
||||
{
|
||||
_canvas = gameObject.AddComponent<Canvas>();
|
||||
}
|
||||
_canvas.renderMode = RenderMode.WorldSpace;
|
||||
|
||||
// Canvas Scaler 설정
|
||||
var scaler = GetComponent<CanvasScaler>();
|
||||
if (scaler == null)
|
||||
{
|
||||
scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
}
|
||||
scaler.dynamicPixelsPerUnit = 10f;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
}
|
||||
|
||||
public void Initialize(Building building)
|
||||
{
|
||||
_building = building;
|
||||
transform.localPosition = Vector3.up * heightOffset;
|
||||
}
|
||||
|
||||
public void UpdateHealth(int currentHealth, int maxHealth)
|
||||
{
|
||||
if (fillImage != null)
|
||||
{
|
||||
float healthPercentage = maxHealth > 0 ? (float)currentHealth / maxHealth : 0f;
|
||||
fillImage.fillAmount = healthPercentage;
|
||||
|
||||
// 체력에 따른 색상 변경
|
||||
fillImage.color = GetHealthColor(healthPercentage);
|
||||
}
|
||||
|
||||
if (healthText != null)
|
||||
{
|
||||
healthText.text = $"{currentHealth}/{maxHealth}";
|
||||
}
|
||||
|
||||
// 체력바 표시/숨김
|
||||
if (barContainer != null)
|
||||
{
|
||||
_lastDamageTime = Time.time;
|
||||
barContainer.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 카메라를 향하도록 회전
|
||||
if (_mainCamera != null)
|
||||
{
|
||||
transform.rotation = Quaternion.LookRotation(transform.position - _mainCamera.transform.position);
|
||||
}
|
||||
|
||||
// 체력이 가득 차면 숨김
|
||||
if (hideWhenFull && barContainer != null && _building != null)
|
||||
{
|
||||
float healthPercentage = _building.GetHealthPercentage();
|
||||
|
||||
if (healthPercentage >= 1f && Time.time - _lastDamageTime > hideDelay)
|
||||
{
|
||||
barContainer.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Color GetHealthColor(float healthPercentage)
|
||||
{
|
||||
if (healthPercentage <= lowHealthThreshold)
|
||||
return lowHealthColor;
|
||||
else if (healthPercentage <= mediumHealthThreshold)
|
||||
return mediumHealthColor;
|
||||
else
|
||||
return fullHealthColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/BuildingHealthBar.cs.meta
Normal file
2
Assets/Scripts/BuildingHealthBar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 634b9713ccae67242a54fca1ed979b58
|
||||
@@ -113,41 +113,136 @@ namespace Northbound
|
||||
return false;
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
public void PlaceBuildingServerRpc(int buildingIndex, Vector3 position, int rotation)
|
||||
/// <summary>
|
||||
/// 건물 배치 요청 (클라이언트에서 호출)
|
||||
/// </summary>
|
||||
public void RequestPlaceBuilding(int buildingIndex, Vector3 position, int rotation)
|
||||
{
|
||||
if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count)
|
||||
if (!NetworkManager.Singleton.IsClient)
|
||||
{
|
||||
Debug.LogWarning("[BuildingManager] 클라이언트가 아닙니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
ulong clientId = NetworkManager.Singleton.LocalClientId;
|
||||
PlaceBuildingServerRpc(buildingIndex, position, rotation, clientId);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void PlaceBuildingServerRpc(int buildingIndex, Vector3 position, int rotation, ulong requestingClientId)
|
||||
{
|
||||
// 보안 검증 1: 유효한 클라이언트인지 확인
|
||||
if (!NetworkManager.Singleton.ConnectedClients.ContainsKey(requestingClientId))
|
||||
{
|
||||
Debug.LogWarning($"<color=red>[BuildingManager] 유효하지 않은 클라이언트 ID: {requestingClientId}</color>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 보안 검증 2: 건물 인덱스 유효성 확인
|
||||
if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count)
|
||||
{
|
||||
Debug.LogWarning($"<color=red>[BuildingManager] 유효하지 않은 건물 인덱스: {buildingIndex} (클라이언트: {requestingClientId})</color>");
|
||||
return;
|
||||
}
|
||||
|
||||
BuildingData data = availableBuildings[buildingIndex];
|
||||
|
||||
// IsValidPlacement now returns snapped position in groundPosition
|
||||
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
|
||||
// 보안 검증 3: 건물 데이터 유효성 확인
|
||||
if (data == null || data.prefab == null)
|
||||
{
|
||||
Debug.LogWarning($"<color=red>[BuildingManager] 건물 데이터가 유효하지 않습니다. (클라이언트: {requestingClientId})</color>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 배치 가능 여부 확인
|
||||
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
|
||||
{
|
||||
Debug.LogWarning($"<color=yellow>[BuildingManager] 건물 배치 불가능 위치 (클라이언트: {requestingClientId})</color>");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3Int gridPosition = WorldToGrid(snappedPosition);
|
||||
|
||||
// Spawn building at snapped position
|
||||
// 건물 생성
|
||||
GameObject buildingObj = Instantiate(data.prefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
|
||||
NetworkObject netObj = buildingObj.GetComponent<NetworkObject>();
|
||||
|
||||
if (netObj != null)
|
||||
{
|
||||
netObj.Spawn();
|
||||
// 건물의 소유자를 설정
|
||||
netObj.SpawnWithOwnership(requestingClientId);
|
||||
|
||||
Building building = buildingObj.GetComponent<Building>();
|
||||
if (building == null)
|
||||
{
|
||||
building = buildingObj.AddComponent<Building>();
|
||||
}
|
||||
|
||||
building.Initialize(data, gridPosition, rotation);
|
||||
// 건물 초기화
|
||||
building.Initialize(data, gridPosition, rotation, requestingClientId);
|
||||
placedBuildings.Add(building);
|
||||
|
||||
Debug.Log($"<color=green>[BuildingManager] {data.buildingName} 건설 완료 (소유자: {requestingClientId}, 위치: {gridPosition})</color>");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"<color=red>[BuildingManager] NetworkObject 컴포넌트가 없습니다! (Prefab: {data.prefab.name})</color>");
|
||||
Destroy(buildingObj);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveBuilding(Building building)
|
||||
{
|
||||
if (placedBuildings.Contains(building))
|
||||
{
|
||||
placedBuildings.Remove(building);
|
||||
Debug.Log($"<color=yellow>[BuildingManager] 건물 제거됨: {building.buildingData?.buildingName ?? "Unknown"}</color>");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사전 배치 건물 등록 (씬에 미리 있는 건물용)
|
||||
/// </summary>
|
||||
public void RegisterPrePlacedBuilding(Building building)
|
||||
{
|
||||
if (!placedBuildings.Contains(building))
|
||||
{
|
||||
placedBuildings.Add(building);
|
||||
Debug.Log($"<color=cyan>[BuildingManager] 사전 배치 건물 등록: {building.buildingData?.buildingName ?? building.gameObject.name}</color>");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 위치에 건물이 있는지 확인
|
||||
/// </summary>
|
||||
public Building GetBuildingAtPosition(Vector3Int gridPosition)
|
||||
{
|
||||
foreach (var building in placedBuildings)
|
||||
{
|
||||
if (building != null && building.gridPosition == gridPosition)
|
||||
{
|
||||
return building;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 소유자의 모든 건물 가져오기
|
||||
/// </summary>
|
||||
public List<Building> GetBuildingsByOwner(ulong ownerId)
|
||||
{
|
||||
List<Building> ownedBuildings = new List<Building>();
|
||||
|
||||
foreach (var building in placedBuildings)
|
||||
{
|
||||
if (building != null && building.GetOwnerId() == ownerId)
|
||||
{
|
||||
ownedBuildings.Add(building);
|
||||
}
|
||||
}
|
||||
|
||||
return ownedBuildings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,139 @@ namespace Northbound
|
||||
}
|
||||
}
|
||||
|
||||
private void OnToggleBuildMode(InputAction.CallbackContext context)
|
||||
{
|
||||
isBuildModeActive = !isBuildModeActive;
|
||||
|
||||
if (isBuildModeActive)
|
||||
{
|
||||
CreatePreview();
|
||||
}
|
||||
else
|
||||
{
|
||||
DestroyPreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void CreatePreview()
|
||||
{
|
||||
if (BuildingManager.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[BuildingPlacement] BuildingManager가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedBuildingIndex < 0 || selectedBuildingIndex >= BuildingManager.Instance.availableBuildings.Count)
|
||||
{
|
||||
Debug.LogWarning($"[BuildingPlacement] 유효하지 않은 건물 인덱스: {selectedBuildingIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
if (data == null || data.prefab == null)
|
||||
{
|
||||
Debug.LogWarning("[BuildingPlacement] BuildingData 또는 Prefab이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
previewObject = Instantiate(data.prefab);
|
||||
|
||||
// Remove NetworkObject component from preview
|
||||
NetworkObject netObj = previewObject.GetComponent<NetworkObject>();
|
||||
if (netObj != null)
|
||||
{
|
||||
Destroy(netObj);
|
||||
}
|
||||
|
||||
// Apply ghost materials
|
||||
previewRenderers = previewObject.GetComponentsInChildren<Renderer>();
|
||||
foreach (var renderer in previewRenderers)
|
||||
{
|
||||
Material[] mats = new Material[renderer.materials.Length];
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
mats[i] = validMaterial;
|
||||
}
|
||||
renderer.materials = mats;
|
||||
}
|
||||
|
||||
// Disable colliders in preview
|
||||
foreach (var collider in previewObject.GetComponentsInChildren<Collider>())
|
||||
{
|
||||
collider.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyPreview()
|
||||
{
|
||||
if (previewObject != null)
|
||||
{
|
||||
Destroy(previewObject);
|
||||
previewObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePreviewPosition()
|
||||
{
|
||||
if (previewObject == null || BuildingManager.Instance == null)
|
||||
return;
|
||||
|
||||
Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue());
|
||||
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, maxPlacementDistance, groundLayer))
|
||||
{
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
|
||||
// Check if placement is valid
|
||||
bool isValid = BuildingManager.Instance.IsValidPlacement(data, hit.point, currentRotation, out Vector3 snappedPosition);
|
||||
|
||||
// Update preview position
|
||||
previewObject.transform.position = snappedPosition + data.placementOffset;
|
||||
previewObject.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0);
|
||||
|
||||
// Update material based on validity
|
||||
Material targetMat = isValid ? validMaterial : invalidMaterial;
|
||||
foreach (var renderer in previewRenderers)
|
||||
{
|
||||
Material[] mats = new Material[renderer.materials.Length];
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
mats[i] = targetMat;
|
||||
}
|
||||
renderer.materials = mats;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRotate(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!isBuildModeActive) return;
|
||||
|
||||
currentRotation = (currentRotation + 1) % 4;
|
||||
}
|
||||
|
||||
private void OnBuild(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!isBuildModeActive || previewObject == null || BuildingManager.Instance == null)
|
||||
return;
|
||||
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
Vector3 placementPosition = previewObject.transform.position - data.placementOffset;
|
||||
|
||||
// Validate placement one more time
|
||||
if (BuildingManager.Instance.IsValidPlacement(data, placementPosition, currentRotation, out Vector3 snappedPosition))
|
||||
{
|
||||
// 🔥 변경: PlaceBuildingServerRpc 대신 RequestPlaceBuilding 호출
|
||||
BuildingManager.Instance.RequestPlaceBuilding(selectedBuildingIndex, snappedPosition, currentRotation);
|
||||
|
||||
Debug.Log($"<color=cyan>[BuildingPlacement] 건물 배치 요청: {data.buildingName}</color>");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("<color=yellow>[BuildingPlacement] 건물을 배치할 수 없는 위치입니다.</color>");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (!IsOwner || !isBuildModeActive || !showGridBounds) return;
|
||||
@@ -134,321 +267,6 @@ namespace Northbound
|
||||
|
||||
Gizmos.color = isValid ? new Color(0, 1, 0, 0.5f) : new Color(1, 0, 0, 0.5f);
|
||||
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||||
|
||||
// Draw grid cells
|
||||
Gizmos.color = Color.yellow;
|
||||
float gridSize = BuildingManager.Instance.gridSize;
|
||||
Vector3 snappedPos = BuildingManager.Instance.SnapToGrid(previewObject.transform.position);
|
||||
|
||||
// Draw grid origin point
|
||||
Gizmos.DrawSphere(snappedPos, 0.1f);
|
||||
|
||||
// Draw grid outline for each cell
|
||||
int width = Mathf.RoundToInt(bounds.size.x / gridSize);
|
||||
int length = Mathf.RoundToInt(bounds.size.z / gridSize);
|
||||
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
for (int z = 0; z < length; z++)
|
||||
{
|
||||
Vector3 cellPos = snappedPos + new Vector3(
|
||||
(x - width / 2f + 0.5f) * gridSize,
|
||||
0.01f,
|
||||
(z - length / 2f + 0.5f) * gridSize
|
||||
);
|
||||
Gizmos.DrawWireCube(cellPos, new Vector3(gridSize, 0.01f, gridSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnToggleBuildMode(InputAction.CallbackContext context)
|
||||
{
|
||||
ToggleBuildMode();
|
||||
}
|
||||
|
||||
private void OnRotate(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!isBuildModeActive) return;
|
||||
|
||||
float rotateValue = context.ReadValue<float>();
|
||||
if (rotateValue > 0)
|
||||
{
|
||||
currentRotation = (currentRotation + 1) % 4;
|
||||
}
|
||||
else if (rotateValue < 0)
|
||||
{
|
||||
currentRotation = (currentRotation - 1 + 4) % 4;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBuild(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!isBuildModeActive) return;
|
||||
TryPlaceBuilding();
|
||||
}
|
||||
|
||||
private void ToggleBuildMode()
|
||||
{
|
||||
isBuildModeActive = !isBuildModeActive;
|
||||
|
||||
if (isBuildModeActive)
|
||||
{
|
||||
EnterBuildMode();
|
||||
}
|
||||
else
|
||||
{
|
||||
ExitBuildMode();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnterBuildMode()
|
||||
{
|
||||
currentRotation = 0;
|
||||
CreatePreview();
|
||||
}
|
||||
|
||||
private void ExitBuildMode()
|
||||
{
|
||||
DestroyPreview();
|
||||
}
|
||||
|
||||
private void CreatePreview()
|
||||
{
|
||||
if (BuildingManager.Instance == null)
|
||||
return;
|
||||
|
||||
if (BuildingManager.Instance.availableBuildings.Count == 0)
|
||||
return;
|
||||
|
||||
selectedBuildingIndex = Mathf.Clamp(selectedBuildingIndex, 0,
|
||||
BuildingManager.Instance.availableBuildings.Count - 1);
|
||||
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
if (data == null || data.prefab == null)
|
||||
return;
|
||||
|
||||
previewObject = Instantiate(data.prefab);
|
||||
previewObject.name = "BuildingPreview";
|
||||
|
||||
// Disable colliders on preview
|
||||
foreach (var collider in previewObject.GetComponentsInChildren<Collider>())
|
||||
{
|
||||
collider.enabled = false;
|
||||
}
|
||||
|
||||
// Remove NetworkObject from preview if exists
|
||||
NetworkObject netObj = previewObject.GetComponent<NetworkObject>();
|
||||
if (netObj != null)
|
||||
{
|
||||
Destroy(netObj);
|
||||
}
|
||||
|
||||
// Store original materials and setup preview renderers
|
||||
previewRenderers = previewObject.GetComponentsInChildren<Renderer>();
|
||||
|
||||
if (previewRenderers.Length == 0)
|
||||
{
|
||||
// Add a debug cube so you can at least see something
|
||||
GameObject debugCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
debugCube.transform.SetParent(previewObject.transform);
|
||||
debugCube.transform.localPosition = Vector3.zero;
|
||||
Destroy(debugCube.GetComponent<Collider>());
|
||||
previewRenderers = previewObject.GetComponentsInChildren<Renderer>();
|
||||
}
|
||||
|
||||
originalMaterials = new Material[previewRenderers.Length];
|
||||
|
||||
for (int i = 0; i < previewRenderers.Length; i++)
|
||||
{
|
||||
originalMaterials[i] = previewRenderers[i].material;
|
||||
// Replace all materials with ghost material
|
||||
Material[] mats = new Material[previewRenderers[i].materials.Length];
|
||||
for (int j = 0; j < mats.Length; j++)
|
||||
{
|
||||
mats[j] = validMaterial;
|
||||
}
|
||||
previewRenderers[i].materials = mats;
|
||||
|
||||
// Disable shadow casting
|
||||
previewRenderers[i].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
previewRenderers[i].receiveShadows = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyPreview()
|
||||
{
|
||||
if (previewObject != null)
|
||||
{
|
||||
Destroy(previewObject);
|
||||
previewObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePreviewPosition()
|
||||
{
|
||||
if (BuildingManager.Instance == null) return;
|
||||
|
||||
if (previewObject == null)
|
||||
{
|
||||
CreatePreview();
|
||||
if (previewObject == null) return;
|
||||
}
|
||||
|
||||
if (Mouse.current == null)
|
||||
return;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
Ray ray = Camera.main.ScreenPointToRay(mousePos);
|
||||
|
||||
Vector3 targetPosition = Vector3.zero;
|
||||
bool foundPosition = false;
|
||||
|
||||
// Robust approach: Find where ray intersects ground plane, then raycast down from there
|
||||
// This works even when pointing at empty space between buildings
|
||||
|
||||
// Step 1: Calculate where ray intersects a horizontal plane at Y=0
|
||||
// Using plane intersection math
|
||||
Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
|
||||
float enter;
|
||||
|
||||
if (groundPlane.Raycast(ray, out enter))
|
||||
{
|
||||
// Get the XZ position where ray hits the ground plane
|
||||
Vector3 planeHitPoint = ray.GetPoint(enter);
|
||||
|
||||
// Step 2: Cast down from above this XZ position to find actual ground
|
||||
Vector3 highPoint = new Vector3(planeHitPoint.x, 100f, planeHitPoint.z);
|
||||
|
||||
if (Physics.Raycast(highPoint, Vector3.down, out RaycastHit downHit, 200f, groundLayer))
|
||||
{
|
||||
// Always use the hit point
|
||||
targetPosition = downHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No ground found via raycast, use the plane intersection point directly
|
||||
targetPosition = planeHitPoint;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: Try direct raycast to anything
|
||||
if (Physics.Raycast(ray, out RaycastHit anyHit, maxPlacementDistance))
|
||||
{
|
||||
targetPosition = anyHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundPosition)
|
||||
{
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
|
||||
// IsValidPlacement now returns the snapped position in groundPosition
|
||||
bool isValid = BuildingManager.Instance.IsValidPlacement(data, targetPosition, currentRotation, out Vector3 snappedPosition);
|
||||
|
||||
previewObject.transform.position = snappedPosition + data.placementOffset;
|
||||
previewObject.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0);
|
||||
|
||||
if (!previewObject.activeSelf)
|
||||
{
|
||||
previewObject.SetActive(true);
|
||||
}
|
||||
|
||||
// Update material
|
||||
Material previewMat = isValid ? validMaterial : invalidMaterial;
|
||||
foreach (var renderer in previewRenderers)
|
||||
{
|
||||
Material[] mats = new Material[renderer.materials.Length];
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
mats[i] = previewMat;
|
||||
}
|
||||
renderer.materials = mats;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previewObject.activeSelf)
|
||||
{
|
||||
previewObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TryPlaceBuilding()
|
||||
{
|
||||
if (previewObject == null || !previewObject.activeSelf) return;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
Ray ray = Camera.main.ScreenPointToRay(mousePos);
|
||||
|
||||
Vector3 targetPosition = Vector3.zero;
|
||||
bool foundPosition = false;
|
||||
|
||||
// Use plane intersection to get XZ position, then raycast down to find ground
|
||||
Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
|
||||
float enter;
|
||||
|
||||
if (groundPlane.Raycast(ray, out enter))
|
||||
{
|
||||
Vector3 planeHitPoint = ray.GetPoint(enter);
|
||||
Vector3 highPoint = new Vector3(planeHitPoint.x, 100f, planeHitPoint.z);
|
||||
|
||||
if (Physics.Raycast(highPoint, Vector3.down, out RaycastHit downHit, 200f, groundLayer))
|
||||
{
|
||||
if (downHit.collider.GetComponentInParent<Building>() == null)
|
||||
{
|
||||
targetPosition = downHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetPosition = downHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
targetPosition = planeHitPoint;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: direct raycast
|
||||
if (Physics.Raycast(ray, out RaycastHit anyHit, maxPlacementDistance))
|
||||
{
|
||||
targetPosition = anyHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundPosition)
|
||||
{
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
|
||||
// IsValidPlacement now returns the snapped position
|
||||
if (BuildingManager.Instance.IsValidPlacement(data, targetPosition, currentRotation, out Vector3 snappedPosition))
|
||||
{
|
||||
BuildingManager.Instance.PlaceBuildingServerRpc(selectedBuildingIndex, snappedPosition, currentRotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override public void OnDestroy()
|
||||
{
|
||||
DestroyPreview();
|
||||
|
||||
if (_inputActions != null)
|
||||
{
|
||||
_inputActions.Dispose();
|
||||
}
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 건물의 시야 제공 컴포넌트
|
||||
/// </summary>
|
||||
public class BuildingVisionProvider : NetworkBehaviour, IVisionProvider
|
||||
{
|
||||
[Header("Vision Settings")]
|
||||
public float visionRange = 15f;
|
||||
|
||||
private Building _building;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_building = GetComponent<Building>();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ulong GetOwnerId() => OwnerClientId;
|
||||
public float GetVisionRange() => visionRange;
|
||||
public Transform GetTransform() => transform;
|
||||
public bool IsActive() => IsSpawned;
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(transform.position, visionRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b59ae4328ce49c846b20d7a6d7ce7e47
|
||||
Reference in New Issue
Block a user