지하 최적화

블록 프리팹 단위 -> 블록 청크 단위 스폰
기타 건설, 조준 관련 사이드이펙트 버그 수정
This commit is contained in:
2026-01-21 21:34:05 +09:00
parent db5db4b106
commit 59246a67bd
24 changed files with 2622 additions and 127 deletions

View File

@@ -2,6 +2,7 @@
/// <summary>
/// Mining behavior for pickaxes and similar tools.
/// Supports both legacy MineableBlock and new chunk-based MineableChunk.
/// </summary>
[CreateAssetMenu(menuName = "Items/Behaviors/Mining Behavior")]
public class MiningBehavior : ItemBehavior
@@ -27,7 +28,25 @@ public class MiningBehavior : ItemBehavior
{
if (target == null) return;
// Use IDamageable interface for all damageable objects
// Try chunk-based mining first (new system)
if (target.TryGetComponent<MineableChunk>(out var chunk))
{
// Get the specific block index from PlayerNetworkController
var playerController = user.GetComponent<PlayerNetworkController>();
if (playerController != null)
{
var chunkTarget = playerController.GetCurrentChunkTarget();
if (chunkTarget.hasHit && chunkTarget.chunk == chunk)
{
// Damage the specific block within the chunk
chunk.DamageBlockServerRpc(chunkTarget.blockIndex, (byte)Mathf.Min(255, damage));
return;
}
}
return;
}
// Fallback to legacy IDamageable interface
if (target.TryGetComponent<IDamageable>(out var damageable))
{
damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, user));
@@ -37,8 +56,15 @@ public class MiningBehavior : ItemBehavior
public override string GetBlockedReason(GameObject user, GameObject target)
{
if (target == null) return "No target";
// Check for chunk
if (target.TryGetComponent<MineableChunk>(out _))
return null; // Chunks are always mineable
// Check for legacy damageable
if (!target.TryGetComponent<IDamageable>(out _))
return "Cannot mine this object";
return null;
}
}

View File

@@ -44,8 +44,13 @@ public class PlayerNetworkController : NetworkBehaviour
private PlayerActionHandler _actionHandler;
private RectTransform _crosshairRect;
private MineableBlock _currentTargetBlock; // 현재 강조 중인 블록 저장
private MineableBlock _lastHighlightedBlock;
private MineableBlock _currentTargetBlock; // 현재 강조 중인 블록 저장 (legacy)
private MineableBlock _lastHighlightedBlock; // legacy block targeting
// Chunk-based targeting (new system)
private MineableChunk _lastHighlightedChunk;
private int _lastHighlightedChunkBlockIndex = -1;
private ChunkInteractionHandler.ChunkHitResult _currentChunkTarget;
private CharacterController _controller;
private PlayerInputActions _inputActions;
@@ -209,13 +214,17 @@ public class PlayerNetworkController : NetworkBehaviour
{
if (!IsOwner || _actionHandler.IsBusy) return;
// Don't perform actions when in build mode
if (BuildManager.Instance != null && BuildManager.Instance.IsBuildMode) return;
ItemData selectedItem = _inventory.GetSelectedItemData();
if (selectedItem == null) return;
// Check if item has behavior (new system)
if (selectedItem.behavior != null)
{
GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
// Get target - prioritize chunk system over legacy blocks
GameObject target = GetCurrentMiningTarget();
// Use the new behavior system
if (selectedItem.CanUse(gameObject, target))
@@ -334,51 +343,87 @@ public class PlayerNetworkController : NetworkBehaviour
{
if (!IsOwner || _crosshairRect == null) return;
// 1. 카메라 레이로 조준점 계산 (플레이어 몸통 무시)
// Use direct raycast from camera through crosshair position
// Use longer range (100m) from camera to catch all distances
Ray aimRay = Camera.main.ScreenPointToRay(_crosshairRect.position);
Vector3 worldAimPoint;
if (Physics.Raycast(aimRay, out RaycastHit mouseHit, 100f, ~ignoreDuringAim))
worldAimPoint = mouseHit.point;
else
worldAimPoint = aimRay.GetPoint(50f);
// 2. 캐릭터 가슴에서 조준점을 향하는 방향 계산
Vector3 origin = transform.position + Vector3.up * 1.2f;
Vector3 direction = (worldAimPoint - origin).normalized;
// 자기 자신 충돌 방지용 오프셋
Vector3 rayStart = origin + direction * 0.4f;
// 3. [중요] 실제 공격과 동일한 SphereCast 실행
RaycastHit blockHit;
bool hasTarget = Physics.SphereCast(rayStart, aimRadius, direction, out blockHit, attackRange - 0.4f, mineableLayer);
bool hasTarget = Physics.SphereCast(aimRay, aimRadius, out blockHit, 100f, mineableLayer);
// 4. 하이라이트 대상 업데이트
MineableBlock currentTarget = null;
// Filter by actual attack range from player
if (hasTarget)
{
currentTarget = blockHit.collider.GetComponentInParent<MineableBlock>();
Vector3 playerPos = transform.position + Vector3.up * 1.2f;
float distanceFromPlayer = Vector3.Distance(playerPos, blockHit.point);
if (distanceFromPlayer > attackRange)
{
hasTarget = false; // Too far from player to interact with
}
}
// 대상이 바뀌었을 때만 아웃라인 갱신 (최적화)
if (_lastHighlightedBlock != currentTarget)
// 4. 하이라이트 대상 업데이트 - 청크 시스템과 레거시 블록 모두 지원
MineableBlock currentLegacyTarget = null;
MineableChunk currentChunk = null;
int currentChunkBlockIndex = -1;
if (hasTarget)
{
// Try chunk first (new system)
var chunkHit = ChunkInteractionHandler.GetChunkHit(blockHit);
if (chunkHit.hasHit)
{
currentChunk = chunkHit.chunk;
currentChunkBlockIndex = chunkHit.blockIndex;
_currentChunkTarget = chunkHit;
}
else
{
// Fallback to legacy MineableBlock
currentLegacyTarget = blockHit.collider.GetComponentInParent<MineableBlock>();
_currentChunkTarget = ChunkInteractionHandler.ChunkHitResult.None;
}
}
else
{
_currentChunkTarget = ChunkInteractionHandler.ChunkHitResult.None;
}
// Update chunk highlight
bool chunkTargetChanged = (currentChunk != _lastHighlightedChunk) ||
(currentChunkBlockIndex != _lastHighlightedChunkBlockIndex);
if (chunkTargetChanged)
{
if (_lastHighlightedChunk != null)
_lastHighlightedChunk.SetHighlight(false);
if (currentChunk != null)
currentChunk.SetHighlight(true, currentChunkBlockIndex);
_lastHighlightedChunk = currentChunk;
_lastHighlightedChunkBlockIndex = currentChunkBlockIndex;
}
// Update legacy block highlight
if (_lastHighlightedBlock != currentLegacyTarget)
{
if (_lastHighlightedBlock != null) _lastHighlightedBlock.SetHighlight(false);
if (currentTarget != null) currentTarget.SetHighlight(true);
_lastHighlightedBlock = currentTarget;
if (currentLegacyTarget != null) currentLegacyTarget.SetHighlight(true);
_lastHighlightedBlock = currentLegacyTarget;
}
// 기즈모 디버그 데이터 동기화
_debugOrigin = rayStart;
_debugDir = direction;
_debugHit = hasTarget;
_debugDist = hasTarget ? blockHit.distance : (attackRange - 0.4f);
Ray debugRay = Camera.main.ScreenPointToRay(_crosshairRect.position);
_debugOrigin = debugRay.origin;
_debugDir = debugRay.direction;
_debugHit = hasTarget && (currentChunk != null || currentLegacyTarget != null);
_debugDist = hasTarget ? blockHit.distance : attackRange;
// 크로스헤어 이미지 교체
bool hasValidTarget = currentChunk != null || currentLegacyTarget != null;
if (crosshairUI != null)
{
crosshairUI.sprite = hasTarget ? targetCrosshair : idleCrosshair;
crosshairUI.color = hasTarget ? Color.green : Color.white;
crosshairUI.sprite = hasValidTarget ? targetCrosshair : idleCrosshair;
crosshairUI.color = hasValidTarget ? Color.green : Color.white;
}
}
@@ -420,11 +465,33 @@ public class PlayerNetworkController : NetworkBehaviour
private void RevealSurroundings()
{
// Use FogOfWarManager's revealRadius if available, fallback to visionRadius
float currentRevealRadius = visionRadius;
if (FogOfWarManager.Instance != null)
{
currentRevealRadius = FogOfWarManager.Instance.revealRadius;
}
// 시야 반경 내의 블록 감지
Collider[] hitBlocks = Physics.OverlapSphere(transform.position, visionRadius, mineableLayer);
Collider[] hitBlocks = Physics.OverlapSphere(transform.position, currentRevealRadius, mineableLayer);
foreach (var col in hitBlocks)
{
// Try chunk-based reveal first (new system)
if (col.TryGetComponent<MineableChunk>(out var chunk))
{
// Update local visibility (for fog of war visual states)
chunk.UpdateLocalVisibility(transform.position, currentRevealRadius);
// Request server to mark blocks as discovered (permanent)
if (IsOwner)
{
RequestChunkRevealServerRpc(chunk.GetComponent<NetworkObject>().NetworkObjectId, transform.position, currentRevealRadius);
}
continue;
}
// Fallback to legacy MineableBlock
if (col.TryGetComponent<MineableBlock>(out var block))
{
// 1. [로컬] 내 화면에서 이 블록을 보이게 함 (실시간 시야)
@@ -451,6 +518,18 @@ public class PlayerNetworkController : NetworkBehaviour
}
}
[ServerRpc]
private void RequestChunkRevealServerRpc(ulong chunkNetId, Vector3 playerPos, float radius)
{
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(chunkNetId, out var netObj))
{
if (netObj.TryGetComponent<MineableChunk>(out var chunk))
{
chunk.RevealBlocksInRadius(playerPos, radius);
}
}
}
private IEnumerator ActionRoutine(float duration, string animTrigger, Action actionLogic)
{
// 1. 상태 잠금
@@ -516,6 +595,9 @@ public class PlayerNetworkController : NetworkBehaviour
{
if (_actionHandler.IsBusy) return;
// Don't perform actions when in build mode
if (BuildManager.Instance != null && BuildManager.Instance.IsBuildMode) return;
ItemData selectedItem = _inventory.GetSelectedItemData();
if (selectedItem == null || selectedItem.behavior == null) return;
@@ -525,7 +607,8 @@ public class PlayerNetworkController : NetworkBehaviour
// Skip if non-repeatable action already executed once
if (!actionDesc.CanRepeat && _hasExecutedOnce) return;
GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
// Get target - prioritize chunk system over legacy blocks
GameObject target = GetCurrentMiningTarget();
if (selectedItem.CanUse(gameObject, target))
{
@@ -537,6 +620,29 @@ public class PlayerNetworkController : NetworkBehaviour
}
}
/// <summary>
/// Get the current mining target (chunk or legacy block)
/// </summary>
private GameObject GetCurrentMiningTarget()
{
// Prioritize chunk target
if (_currentChunkTarget.hasHit && _currentChunkTarget.chunk != null)
{
return _currentChunkTarget.chunk.gameObject;
}
// Fallback to legacy block
return _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
}
/// <summary>
/// Get the current chunk target info (for MiningBehavior)
/// </summary>
public ChunkInteractionHandler.ChunkHitResult GetCurrentChunkTarget()
{
return _currentChunkTarget;
}
private void OnDrawGizmos()
{
if (!Application.isPlaying || !IsOwner) return;