지하 생성 및 터널 구조 개선
This commit is contained in:
@@ -12,21 +12,23 @@ public class BuildManager : NetworkBehaviour
|
||||
public struct TurretData
|
||||
{
|
||||
public string turretName;
|
||||
public GameObject finalPrefab; // NetworkObject 필수
|
||||
public GameObject ghostPrefab; // 로컬 프리뷰용
|
||||
public GameObject finalPrefab;
|
||||
public GameObject ghostPrefab;
|
||||
public float buildTime;
|
||||
}
|
||||
|
||||
[Header("Grid Settings")]
|
||||
[Header("Uniform Grid Settings")]
|
||||
public float cellSize = 1f;
|
||||
public float tunnelHeight = 3f;
|
||||
[SerializeField] private float yOffset = 0.5f; // 터널 중심점 오프셋
|
||||
[SerializeField] private float pivotOffset = 0.5f; // 블록/터널 에셋의 중심점
|
||||
|
||||
[Header("Tunnel Settings")]
|
||||
public int tunnelLengthInBlocks = 3;
|
||||
|
||||
[Header("Prefabs & Layers")]
|
||||
[SerializeField] private GameObject constructionSitePrefab;
|
||||
[SerializeField] private List<TurretData> turretLibrary = new List<TurretData>();
|
||||
[SerializeField] private LayerMask groundLayer;
|
||||
[SerializeField] private LayerMask playerLayer;
|
||||
[SerializeField] private LayerMask tunnelLayer;
|
||||
|
||||
private int _selectedTurretIndex = 0;
|
||||
private bool _isBuildMode = false;
|
||||
@@ -34,7 +36,6 @@ public class BuildManager : NetworkBehaviour
|
||||
private Vector3Int _currentGridPos;
|
||||
private PlayerInputActions _inputActions;
|
||||
|
||||
// 데이터 레지스트리 (다른 코드에서 참조)
|
||||
private Dictionary<Vector3Int, TunnelNode> _tunnelRegistry = new Dictionary<Vector3Int, TunnelNode>();
|
||||
private HashSet<Vector3Int> _occupiedNodes = new HashSet<Vector3Int>();
|
||||
|
||||
@@ -42,26 +43,20 @@ public class BuildManager : NetworkBehaviour
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
|
||||
_inputActions = new PlayerInputActions();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// 최신 Action-based 이벤트 바인딩
|
||||
_inputActions.Player.ToggleBuild.performed += ctx => ToggleBuildMode();
|
||||
_inputActions.Player.Build.performed += ctx => OnBuildRequested();
|
||||
_inputActions.Player.Cancel.performed += ctx => ExitBuildMode();
|
||||
|
||||
// 숫자키 슬롯 선택 (Input Action 에셋 설정에 따라 Select1, Select2... 등 사용)
|
||||
_inputActions.Player.Select1.performed += ctx => SelectTurret(0);
|
||||
_inputActions.Player.Select2.performed += ctx => SelectTurret(1);
|
||||
_inputActions.Player.Select3.performed += ctx => SelectTurret(2);
|
||||
|
||||
_inputActions.Enable();
|
||||
|
||||
// 시작 시 씬에 배치된 터널 자동 등록
|
||||
RegisterAllExistingTunnels();
|
||||
}
|
||||
|
||||
void Update()
|
||||
@@ -70,144 +65,115 @@ public class BuildManager : NetworkBehaviour
|
||||
UpdateGhostPosition();
|
||||
}
|
||||
|
||||
#region Selection & Build Logic
|
||||
|
||||
public void SelectTurret(int index)
|
||||
{
|
||||
if (index < 0 || index >= turretLibrary.Count) return;
|
||||
|
||||
_selectedTurretIndex = index;
|
||||
Debug.Log($"타워 선택: {turretLibrary[_selectedTurretIndex].turretName}");
|
||||
|
||||
if (_isBuildMode)
|
||||
{
|
||||
if (_ghostInstance != null) Destroy(_ghostInstance);
|
||||
_ghostInstance = Instantiate(turretLibrary[_selectedTurretIndex].ghostPrefab);
|
||||
_ghostInstance.transform.position = GridToWorld(_currentGridPos);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGhostPosition()
|
||||
{
|
||||
Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue());
|
||||
int tunnelMask = LayerMask.GetMask("Tunnel");
|
||||
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, groundLayer | tunnelMask))
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, groundLayer | tunnelLayer))
|
||||
{
|
||||
Vector3Int gridPos = WorldToGrid3D(hit.point);
|
||||
Vector3 targetPos;
|
||||
|
||||
// 터널 조준 시 해당 터널의 한 칸 아래로 스냅
|
||||
if (((1 << hit.collider.gameObject.layer) & tunnelMask) != 0)
|
||||
// 터널 조준 시: 복잡한 계산 없이 X, Z는 유지, Y만 3 낮춤
|
||||
if (((1 << hit.collider.gameObject.layer) & tunnelLayer) != 0)
|
||||
{
|
||||
gridPos = WorldToGrid3D(hit.collider.transform.position) + Vector3Int.down;
|
||||
Vector3 parentPos = hit.collider.transform.position;
|
||||
targetPos = new Vector3(parentPos.x, parentPos.y - (tunnelLengthInBlocks * cellSize), parentPos.z);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 지형 조준 시: 격자에 맞춤
|
||||
_currentGridPos = WorldToGrid3D(hit.point + hit.normal * 0.01f);
|
||||
targetPos = GridToWorld(_currentGridPos);
|
||||
}
|
||||
|
||||
_currentGridPos = gridPos;
|
||||
_ghostInstance.transform.position = GridToWorld(gridPos);
|
||||
_currentGridPos = WorldToGrid3D(targetPos);
|
||||
_ghostInstance.transform.position = targetPos;
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 좌표를 반올림하여 0.5 단위 오차를 제거
|
||||
public Vector3Int WorldToGrid3D(Vector3 worldPos)
|
||||
{
|
||||
return new Vector3Int(
|
||||
Mathf.RoundToInt((worldPos.x - pivotOffset) / cellSize),
|
||||
Mathf.RoundToInt((worldPos.y - pivotOffset) / cellSize),
|
||||
Mathf.RoundToInt((worldPos.z - pivotOffset) / cellSize)
|
||||
);
|
||||
}
|
||||
|
||||
public Vector3 GridToWorld(Vector3Int gridPos)
|
||||
{
|
||||
return new Vector3(
|
||||
(gridPos.x * cellSize) + pivotOffset,
|
||||
(gridPos.y * cellSize) + pivotOffset,
|
||||
(gridPos.z * cellSize) + pivotOffset
|
||||
);
|
||||
}
|
||||
|
||||
// [에러 해결] ConstructionSite.cs 참조용
|
||||
public TurretData GetTurretData(int index)
|
||||
{
|
||||
if (index < 0 || index >= turretLibrary.Count) return default;
|
||||
return turretLibrary[index];
|
||||
}
|
||||
|
||||
// BuildManager.cs 내 핵심 수정 부분
|
||||
// [수정] 서버와 클라이언트 모두에서 터널 위치를 기록하도록 함
|
||||
// BuildManager.cs 내부
|
||||
public void RegisterTunnel(Vector3Int topPos, TunnelNode node)
|
||||
{
|
||||
// 터널이 점유하는 Y, Y-1, Y-2 세 칸 모두에 노드를 등록합니다.
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
Vector3Int cellPos = topPos + (Vector3Int.down * i);
|
||||
if (!_tunnelRegistry.ContainsKey(cellPos))
|
||||
{
|
||||
_tunnelRegistry.Add(cellPos, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [수정] 특정 좌표의 터널을 찾는 공용 함수
|
||||
public TunnelNode GetTunnelAt(Vector3Int pos)
|
||||
{
|
||||
return _tunnelRegistry.GetValueOrDefault(pos);
|
||||
}
|
||||
|
||||
// 1. 건설 요청 시 실제 계산된 worldPos를 넘겨줍니다.
|
||||
private void OnBuildRequested()
|
||||
{
|
||||
if (!_isBuildMode || EventSystem.current.IsPointerOverGameObject()) return;
|
||||
|
||||
// 서버에 건설 가능 여부 확인 및 생성 요청
|
||||
RequestBuildRpc(_selectedTurretIndex, _currentGridPos);
|
||||
|
||||
// 고스트가 현재 위치한 '그 좌표'를 그대로 보냅니다.
|
||||
RequestBuildRpc(_selectedTurretIndex, _currentGridPos, _ghostInstance.transform.position);
|
||||
ExitBuildMode();
|
||||
}
|
||||
|
||||
// 2. RPC에서 넘겨받은 worldPos를 사용하여 토대를 생성합니다.
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestBuildRpc(int index, Vector3Int gridPos)
|
||||
private void RequestBuildRpc(int index, Vector3Int gridPos, Vector3 worldPos)
|
||||
{
|
||||
// 서버 측 중복 점유 확인
|
||||
if (_occupiedNodes.Contains(gridPos)) return;
|
||||
|
||||
Vector3 spawnPos = GridToWorld(gridPos);
|
||||
|
||||
// 1. 토대 생성 및 네트워크 스폰
|
||||
GameObject siteObj = Instantiate(constructionSitePrefab, spawnPos, Quaternion.identity);
|
||||
// GridToWorld를 다시 계산하지 않고 전달받은 worldPos를 그대로 사용합니다.
|
||||
GameObject siteObj = Instantiate(constructionSitePrefab, worldPos, Quaternion.identity);
|
||||
siteObj.GetComponent<NetworkObject>().Spawn();
|
||||
|
||||
// 2. 토대 데이터 초기화
|
||||
ConstructionSite site = siteObj.GetComponent<ConstructionSite>();
|
||||
if (site != null)
|
||||
{
|
||||
site.Initialize(index, gridPos);
|
||||
}
|
||||
|
||||
_occupiedNodes.Add(gridPos);
|
||||
if (site != null) site.Initialize(index, gridPos);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility Functions (External References)
|
||||
|
||||
// ConstructionSite에서 프리팹 정보를 가져갈 때 사용
|
||||
public TurretData GetTurretData(int index) => turretLibrary[index];
|
||||
|
||||
// TunnelNode가 스폰될 때 자신을 등록하기 위해 사용
|
||||
public void RegisterTunnel(Vector3Int pos, TunnelNode node)
|
||||
public void SelectTurret(int index)
|
||||
{
|
||||
if (!_tunnelRegistry.ContainsKey(pos))
|
||||
if (index < 0 || index >= turretLibrary.Count) return;
|
||||
_selectedTurretIndex = index;
|
||||
if (_isBuildMode && _ghostInstance != null)
|
||||
{
|
||||
_tunnelRegistry.Add(pos, node);
|
||||
_occupiedNodes.Add(pos); // 건설된 구역으로 마킹
|
||||
Destroy(_ghostInstance);
|
||||
_ghostInstance = Instantiate(turretLibrary[_selectedTurretIndex].ghostPrefab);
|
||||
}
|
||||
}
|
||||
|
||||
// TunnelNode가 위아래 노드를 찾기 위해 사용
|
||||
public TunnelNode GetTunnelAt(Vector3Int pos) => _tunnelRegistry.GetValueOrDefault(pos);
|
||||
|
||||
// 좌표 변환: 월드 -> 격자 인덱스 (0.5 오프셋 반영)
|
||||
public Vector3Int WorldToGrid3D(Vector3 worldPos)
|
||||
{
|
||||
return new Vector3Int(
|
||||
Mathf.FloorToInt(worldPos.x / cellSize),
|
||||
Mathf.RoundToInt((worldPos.y - yOffset) / tunnelHeight),
|
||||
Mathf.FloorToInt(worldPos.z / cellSize)
|
||||
);
|
||||
}
|
||||
|
||||
// 좌표 변환: 격자 인덱스 -> 월드 (0.5 오프셋 반영)
|
||||
public Vector3 GridToWorld(Vector3Int gridPos)
|
||||
{
|
||||
return new Vector3(
|
||||
gridPos.x * cellSize,
|
||||
(gridPos.y * tunnelHeight) + yOffset,
|
||||
gridPos.z * cellSize
|
||||
);
|
||||
}
|
||||
|
||||
// 씬에 미리 배치된 터널들을 한꺼번에 등록
|
||||
public void RegisterAllExistingTunnels()
|
||||
{
|
||||
TunnelNode[] nodes = FindObjectsByType<TunnelNode>(FindObjectsSortMode.None);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
Vector3Int pos = WorldToGrid3D(node.transform.position);
|
||||
RegisterTunnel(pos, node);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mode Switching
|
||||
|
||||
public void ToggleBuildMode() { if (_isBuildMode) ExitBuildMode(); else EnterBuildMode(); }
|
||||
private void EnterBuildMode()
|
||||
{
|
||||
if (turretLibrary.Count == 0) return;
|
||||
_isBuildMode = true;
|
||||
_ghostInstance = Instantiate(turretLibrary[_selectedTurretIndex].ghostPrefab);
|
||||
}
|
||||
private void ExitBuildMode()
|
||||
{
|
||||
_isBuildMode = false;
|
||||
if (_ghostInstance) Destroy(_ghostInstance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void ToggleBuildMode() { if (_isBuildMode) ExitBuildMode(); else EnterBuildMode(); }
|
||||
private void EnterBuildMode() { _isBuildMode = true; if (turretLibrary.Count > 0) _ghostInstance = Instantiate(turretLibrary[_selectedTurretIndex].ghostPrefab); }
|
||||
private void ExitBuildMode() { _isBuildMode = false; if (_ghostInstance) Destroy(_ghostInstance); }
|
||||
public override void OnNetworkDespawn() => _inputActions.Disable();
|
||||
}
|
||||
120
Assets/Scripts/GameBase/UndergroundGenerator.cs
Normal file
120
Assets/Scripts/GameBase/UndergroundGenerator.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
|
||||
public class UndergroundGenerator : NetworkBehaviour
|
||||
{
|
||||
[Header("Generation Range")]
|
||||
[SerializeField] private Vector3Int generationRange = new Vector3Int(20, 30, 10);
|
||||
[SerializeField] private float noiseScale = 0.12f;
|
||||
|
||||
[Header("Thresholds (0 to 1)")]
|
||||
[SerializeField, Range(0, 1)] private float hollowThreshold = 0.35f;
|
||||
[SerializeField, Range(0, 1)] private float baseResourceThreshold = 0.8f;
|
||||
|
||||
[Header("Depth Settings")]
|
||||
[SerializeField] private bool increaseResourceWithDepth = true;
|
||||
[SerializeField] private float depthFactor = 0.005f; // 깊어질수록 임계값 감소 (자원 증가)
|
||||
|
||||
[Header("Prefabs")]
|
||||
[SerializeField] private GameObject normalBlockPrefab;
|
||||
[SerializeField] private GameObject resourceBlockPrefab;
|
||||
|
||||
[Header("Organization")]
|
||||
[SerializeField] private string containerName = "UndergroundBlocks";
|
||||
private Transform _blockContainer; // 블록들을 담을 부모 오브젝트
|
||||
|
||||
private float _seedX, _seedY, _seedZ;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_seedX = Random.Range(0f, 99999f);
|
||||
_seedY = Random.Range(0f, 99999f);
|
||||
_seedZ = Random.Range(0f, 99999f);
|
||||
|
||||
// Hierarchy 정리를 위한 컨테이너 생성
|
||||
_blockContainer = new GameObject(containerName).transform;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (IsServer) GenerateFromGeneratorPivot();
|
||||
}
|
||||
|
||||
private void GenerateFromGeneratorPivot()
|
||||
{
|
||||
Vector3Int originGrid = BuildManager.Instance.WorldToGrid3D(transform.position);
|
||||
|
||||
for (int x = 0; x < generationRange.x; x++)
|
||||
{
|
||||
for (int y = 0; y > -generationRange.y; y--)
|
||||
{
|
||||
for (int z = 0; z < generationRange.z; z++)
|
||||
{
|
||||
Vector3Int targetGridPos = originGrid + new Vector3Int(x, y, z);
|
||||
float noise = Get3DNoise(targetGridPos.x, targetGridPos.y, targetGridPos.z);
|
||||
|
||||
if (noise < hollowThreshold) continue;
|
||||
|
||||
float currentThreshold = baseResourceThreshold + (y * depthFactor);
|
||||
GameObject prefab = (noise > currentThreshold) ? resourceBlockPrefab : normalBlockPrefab;
|
||||
|
||||
SpawnBlock(prefab, targetGridPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnBlock(GameObject prefab, Vector3Int gridPos)
|
||||
{
|
||||
if (prefab == null) return;
|
||||
|
||||
Vector3 worldPos = BuildManager.Instance.GridToWorld(gridPos);
|
||||
|
||||
// 1. 생성 시 부모(Container)를 지정하여 Hierarchy 정리
|
||||
GameObject block = Instantiate(prefab, worldPos, Quaternion.identity);
|
||||
|
||||
// 2. 네트워크 스폰 (Netcode for GameObjects)
|
||||
block.GetComponent<NetworkObject>().Spawn();
|
||||
}
|
||||
|
||||
private float Get3DNoise(int x, int y, int z)
|
||||
{
|
||||
// 대칭 방지를 위한 10000 오프셋 및 시드 적용
|
||||
float xCoord = (x + _seedX + 10000f) * noiseScale;
|
||||
float yCoord = (y + _seedY + 10000f) * noiseScale;
|
||||
float zCoord = (z + _seedZ + 10000f) * noiseScale;
|
||||
|
||||
float ab = Mathf.PerlinNoise(xCoord, yCoord);
|
||||
float bc = Mathf.PerlinNoise(yCoord, zCoord);
|
||||
float ac = Mathf.PerlinNoise(xCoord, zCoord);
|
||||
|
||||
return (ab + bc + ac) / 3f;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
BuildManager bm = BuildManager.Instance;
|
||||
if (bm == null) bm = FindFirstObjectByType<BuildManager>(); // 에러 방지 안전장치
|
||||
if (bm == null) return;
|
||||
|
||||
Vector3Int originGrid = bm.WorldToGrid3D(transform.position);
|
||||
|
||||
for (int x = 0; x < generationRange.x; x += 2)
|
||||
{
|
||||
for (int y = -generationRange.y; y < 0; y += 2)
|
||||
{
|
||||
for (int z = 0; z < generationRange.z; z += 2)
|
||||
{
|
||||
Vector3Int targetGridPos = originGrid + new Vector3Int(x, y, z);
|
||||
float noise = Get3DNoise(targetGridPos.x, targetGridPos.y, targetGridPos.z);
|
||||
|
||||
if (noise < hollowThreshold) continue;
|
||||
|
||||
Gizmos.color = (noise > baseResourceThreshold) ? Color.yellow : new Color(0.5f, 0.5f, 0.5f, 0.2f);
|
||||
Vector3 pos = bm.GridToWorld(targetGridPos);
|
||||
Gizmos.DrawSphere(pos, 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/GameBase/UndergroundGenerator.cs.meta
Normal file
2
Assets/Scripts/GameBase/UndergroundGenerator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7fc80ebb61fb70d4fa694d3a1f81d2ab
|
||||
Reference in New Issue
Block a user