지하 최적화
블록 프리팹 단위 -> 블록 청크 단위 스폰 기타 건설, 조준 관련 사이드이펙트 버그 수정
This commit is contained in:
@@ -99,3 +99,13 @@ MonoBehaviour:
|
||||
SourcePrefabToOverride: {fileID: 0}
|
||||
SourceHashToOverride: 0
|
||||
OverridingTargetPrefab: {fileID: 0}
|
||||
- Override: 0
|
||||
Prefab: {fileID: 5063655529130579611, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
SourcePrefabToOverride: {fileID: 0}
|
||||
SourceHashToOverride: 0
|
||||
OverridingTargetPrefab: {fileID: 0}
|
||||
- Override: 0
|
||||
Prefab: {fileID: 5156816586410934540, guid: 29ad9c03b79f43f42859005ce707dff2, type: 3}
|
||||
SourcePrefabToOverride: {fileID: 0}
|
||||
SourceHashToOverride: 0
|
||||
OverridingTargetPrefab: {fileID: 0}
|
||||
|
||||
167
Assets/Prefabs/MineableChunk.prefab
Normal file
167
Assets/Prefabs/MineableChunk.prefab
Normal file
@@ -0,0 +1,167 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &5063655529130579611
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3520563273759020846}
|
||||
- component: {fileID: 7358912368512722974}
|
||||
- component: {fileID: 3942884241032403340}
|
||||
- component: {fileID: 6314136871395425781}
|
||||
- component: {fileID: 7711355090271435980}
|
||||
- component: {fileID: 5287829808763682365}
|
||||
m_Layer: 12
|
||||
m_Name: MineableChunk
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &3520563273759020846
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5063655529130579611}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: -7.62841, y: 3.2298, z: -4}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &7358912368512722974
|
||||
MeshFilter:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5063655529130579611}
|
||||
m_Mesh: {fileID: 0}
|
||||
--- !u!23 &3942884241032403340
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5063655529130579611}
|
||||
m_Enabled: 1
|
||||
m_CastShadows: 1
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
m_StaticShadowCaster: 0
|
||||
m_MotionVectors: 1
|
||||
m_LightProbeUsage: 1
|
||||
m_ReflectionProbeUsage: 1
|
||||
m_RayTracingMode: 2
|
||||
m_RayTraceProcedural: 0
|
||||
m_RayTracingAccelStructBuildFlagsOverride: 0
|
||||
m_RayTracingAccelStructBuildFlags: 1
|
||||
m_SmallMeshCulling: 1
|
||||
m_ForceMeshLod: -1
|
||||
m_MeshLodSelectionBias: 0
|
||||
m_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
- {fileID: 2100000, guid: 07059e320ce76b044bf5140668021f21, type: 2}
|
||||
- {fileID: 2100000, guid: 4930c6ffa5b9b9f4098249a43abf8506, type: 2}
|
||||
m_StaticBatchInfo:
|
||||
firstSubMesh: 0
|
||||
subMeshCount: 0
|
||||
m_StaticBatchRoot: {fileID: 0}
|
||||
m_ProbeAnchor: {fileID: 0}
|
||||
m_LightProbeVolumeOverride: {fileID: 0}
|
||||
m_ScaleInLightmap: 1
|
||||
m_ReceiveGI: 1
|
||||
m_PreserveUVs: 0
|
||||
m_IgnoreNormalsForChartDetection: 0
|
||||
m_ImportantGI: 0
|
||||
m_StitchLightmapSeams: 1
|
||||
m_SelectedEditorRenderState: 3
|
||||
m_MinimumChartSize: 4
|
||||
m_AutoUVMaxDistance: 0.5
|
||||
m_AutoUVMaxAngle: 89
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_MaskInteraction: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!64 &6314136871395425781
|
||||
MeshCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5063655529130579611}
|
||||
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: 5
|
||||
m_Convex: 0
|
||||
m_CookingOptions: 30
|
||||
m_Mesh: {fileID: 0}
|
||||
--- !u!114 &7711355090271435980
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5063655529130579611}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 2335500207
|
||||
InScenePlacedSourceGlobalObjectIdHash: 2335500207
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
AlwaysReplicateAsRoot: 0
|
||||
SynchronizeTransform: 1
|
||||
ActiveSceneSynchronization: 0
|
||||
SceneMigrationSynchronization: 0
|
||||
SpawnWithObservers: 1
|
||||
DontDestroyWithOwner: 0
|
||||
AutoObjectParentSync: 1
|
||||
SyncOwnerTransformWhenParented: 1
|
||||
AllowOwnerToParent: 0
|
||||
--- !u!114 &5287829808763682365
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5063655529130579611}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 53bb340e9008b024ba035f6ee8fa21a4, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::MineableChunk
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
normalBlockHealth: 100
|
||||
resourceBlockHealth: 150
|
||||
normalDropItem: {fileID: 0}
|
||||
resourceDropItem: {fileID: 11400000, guid: 953ceca9a25978549a56f0a4ff5d6a2c, type: 2}
|
||||
genericDropPrefab: {fileID: 1253970051563370359, guid: 1d7655b1088c3ea46b8f52f6c6760047, type: 3}
|
||||
normalBlockMaterial: {fileID: 2100000, guid: 8aa7a33ec074d5f429b993c5c857614b, type: 2}
|
||||
resourceBlockMaterial: {fileID: 2100000, guid: 4930c6ffa5b9b9f4098249a43abf8506, type: 2}
|
||||
meshRebuildDelay: 0.1
|
||||
highlightMaterial: {fileID: 0}
|
||||
visibilityTimeout: 0.5
|
||||
discoveredTint: {r: 0.3, g: 0.3, b: 0.3, a: 1}
|
||||
7
Assets/Prefabs/MineableChunk.prefab.meta
Normal file
7
Assets/Prefabs/MineableChunk.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa605604c7558cd41abc2fc25fc28e8f
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -223,11 +223,6 @@ PrefabInstance:
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 3912054216608015251}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: ffaf1ddb2ff58d2448ccfdd357387f63, type: 3}
|
||||
--- !u!4 &2152733048352974824 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: -5515783359193845756, guid: ffaf1ddb2ff58d2448ccfdd357387f63, type: 3}
|
||||
m_PrefabInstance: {fileID: 3356319783404427244}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &2473992278589500093 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: ffaf1ddb2ff58d2448ccfdd357387f63, type: 3}
|
||||
@@ -341,7 +336,7 @@ MonoBehaviour:
|
||||
m_Bits: 25600
|
||||
constructionLayer:
|
||||
serializedVersion: 2
|
||||
m_Bits: 256
|
||||
m_Bits: 384
|
||||
buildSpeedMultiplier: 2
|
||||
itemLayer:
|
||||
serializedVersion: 2
|
||||
@@ -359,7 +354,7 @@ MonoBehaviour:
|
||||
crosshairUI: {fileID: 0}
|
||||
idleCrosshair: {fileID: 2628378444897590106, guid: 174f7cb20aaa6d4409b788a700a925ad, type: 3}
|
||||
targetCrosshair: {fileID: -5662625722731528258, guid: 7652364ca249c3144813de7eb3d1b129, type: 3}
|
||||
visionRadius: 2
|
||||
visionRadius: 5
|
||||
--- !u!114 &106528027568436521
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -506,4 +501,8 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::PlayerEquipmentHandler
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
toolAnchor: {fileID: 2152733048352974824}
|
||||
mainHandAnchor: {fileID: 0}
|
||||
offHandAnchor: {fileID: 0}
|
||||
mainHandSlot:
|
||||
SlotType: 0
|
||||
AttachPoint: {fileID: 0}
|
||||
|
||||
82
Assets/Prefabs/UndergroundGenerator.prefab
Normal file
82
Assets/Prefabs/UndergroundGenerator.prefab
Normal file
@@ -0,0 +1,82 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &5156816586410934540
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 69091081815009223}
|
||||
- component: {fileID: 5648469769714403381}
|
||||
- component: {fileID: 1129818542040768727}
|
||||
m_Layer: 0
|
||||
m_Name: UndergroundGenerator
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &69091081815009223
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5156816586410934540}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: -14, y: -6, z: -3}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &5648469769714403381
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5156816586410934540}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 1220698054
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
AlwaysReplicateAsRoot: 0
|
||||
SynchronizeTransform: 1
|
||||
ActiveSceneSynchronization: 0
|
||||
SceneMigrationSynchronization: 0
|
||||
SpawnWithObservers: 1
|
||||
DontDestroyWithOwner: 0
|
||||
AutoObjectParentSync: 1
|
||||
SyncOwnerTransformWhenParented: 1
|
||||
AllowOwnerToParent: 0
|
||||
--- !u!114 &1129818542040768727
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5156816586410934540}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7fc80ebb61fb70d4fa694d3a1f81d2ab, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::UndergroundGenerator
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
generationRange: {x: 50, y: 30, z: 6}
|
||||
noiseScale: 0.14
|
||||
hollowThreshold: 0.299
|
||||
baseResourceThreshold: 0.563
|
||||
increaseResourceWithDepth: 1
|
||||
depthFactor: 0.005
|
||||
normalBlockPrefab: {fileID: 989066657509100432, guid: dbb1cfcb3d9e3844e8d9cdf09b0a1660, type: 3}
|
||||
resourceBlockPrefab: {fileID: 989066657509100432, guid: 17532917e1ada23469c573abf64905f0, type: 3}
|
||||
containerName: UndergroundBlocks
|
||||
7
Assets/Prefabs/UndergroundGenerator.prefab.meta
Normal file
7
Assets/Prefabs/UndergroundGenerator.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29ad9c03b79f43f42859005ce707dff2
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1771,6 +1771,87 @@ PrefabInstance:
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 445606028}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
||||
--- !u!1 &1026559330
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1026559333}
|
||||
- component: {fileID: 1026559332}
|
||||
- component: {fileID: 1026559331}
|
||||
m_Layer: 0
|
||||
m_Name: ChunkedUndergroundGenerator
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1026559331
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1026559330}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 964628114
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
AlwaysReplicateAsRoot: 0
|
||||
SynchronizeTransform: 1
|
||||
ActiveSceneSynchronization: 0
|
||||
SceneMigrationSynchronization: 0
|
||||
SpawnWithObservers: 1
|
||||
DontDestroyWithOwner: 0
|
||||
AutoObjectParentSync: 1
|
||||
SyncOwnerTransformWhenParented: 1
|
||||
AllowOwnerToParent: 0
|
||||
--- !u!114 &1026559332
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1026559330}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3d90f6724f14d49489261782c9672f11, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::ChunkedUndergroundGenerator
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
generationRange: {x: 20, y: 30, z: 10}
|
||||
noiseScale: 0.12
|
||||
hollowThreshold: 0.35
|
||||
baseResourceThreshold: 0.8
|
||||
increaseResourceWithDepth: 1
|
||||
depthFactor: 0.005
|
||||
normalBlockHealth: 100
|
||||
resourceBlockHealth: 150
|
||||
chunkPrefab: {fileID: 0}
|
||||
containerName: UndergroundChunks
|
||||
--- !u!4 &1026559333
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1026559330}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: -7.62834, y: 3.07751, z: -4}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1044242050
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -3031,86 +3112,6 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1600458457}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1634635642
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1634635645}
|
||||
- component: {fileID: 1634635643}
|
||||
- component: {fileID: 1634635644}
|
||||
m_Layer: 0
|
||||
m_Name: UndergroundGenerator
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1634635643
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1634635642}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 1220698054
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
AlwaysReplicateAsRoot: 0
|
||||
SynchronizeTransform: 1
|
||||
ActiveSceneSynchronization: 0
|
||||
SceneMigrationSynchronization: 0
|
||||
SpawnWithObservers: 1
|
||||
DontDestroyWithOwner: 0
|
||||
AutoObjectParentSync: 1
|
||||
SyncOwnerTransformWhenParented: 1
|
||||
AllowOwnerToParent: 0
|
||||
--- !u!114 &1634635644
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1634635642}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7fc80ebb61fb70d4fa694d3a1f81d2ab, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::UndergroundGenerator
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
generationRange: {x: 20, y: 30, z: 6}
|
||||
noiseScale: 0.14
|
||||
hollowThreshold: 0.3
|
||||
baseResourceThreshold: 0.63
|
||||
increaseResourceWithDepth: 1
|
||||
depthFactor: 0.005
|
||||
normalBlockPrefab: {fileID: 989066657509100432, guid: dbb1cfcb3d9e3844e8d9cdf09b0a1660, type: 3}
|
||||
resourceBlockPrefab: {fileID: 989066657509100432, guid: 17532917e1ada23469c573abf64905f0, type: 3}
|
||||
containerName: UndergroundBlocks
|
||||
--- !u!4 &1634635645
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1634635642}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: -14, y: -6, z: -3}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!20 &1648146738 stripped
|
||||
Camera:
|
||||
m_CorrespondingSourceObject: {fileID: 5650099317679730308, guid: 2b08dd32e48ef5e4aa65a6122099152e, type: 3}
|
||||
@@ -3809,6 +3810,71 @@ Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 2338240775821095493, guid: 1955bdf7dd2940f44aa117fbcf6eb626, type: 3}
|
||||
m_PrefabInstance: {fileID: 497942047}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1001 &320979913694462859
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: -7.62841
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalPosition.y
|
||||
value: 3.2298
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: -4
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalRotation.w
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalRotation.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalRotation.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalRotation.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3520563273759020846, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5063655529130579611, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: MineableChunk
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7711355090271435980, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: GlobalObjectIdHash
|
||||
value: 1536139134
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7711355090271435980, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
propertyPath: InScenePlacedSourceGlobalObjectIdHash
|
||||
value: 2335500207
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
m_SourcePrefab: {fileID: 100100000, guid: fa605604c7558cd41abc2fc25fc28e8f, type: 3}
|
||||
--- !u!1001 &3690888448170635710
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -3930,10 +3996,11 @@ SceneRoots:
|
||||
- {fileID: 743367988}
|
||||
- {fileID: 670724422}
|
||||
- {fileID: 1409253547}
|
||||
- {fileID: 1634635645}
|
||||
- {fileID: 437093202}
|
||||
- {fileID: 1489230404}
|
||||
- {fileID: 556982644}
|
||||
- {fileID: 2067098344}
|
||||
- {fileID: 1384281111}
|
||||
- {fileID: 1782529044}
|
||||
- {fileID: 320979913694462859}
|
||||
- {fileID: 1026559333}
|
||||
|
||||
@@ -2,6 +2,7 @@ using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class BuildManager : NetworkBehaviour
|
||||
@@ -36,6 +37,9 @@ public class BuildManager : NetworkBehaviour
|
||||
private Vector3Int _currentGridPos;
|
||||
private PlayerInputActions _inputActions;
|
||||
|
||||
// Public property to check if currently in build mode
|
||||
public bool IsBuildMode => _isBuildMode;
|
||||
|
||||
private Dictionary<Vector3Int, TunnelNode> _tunnelRegistry = new Dictionary<Vector3Int, TunnelNode>();
|
||||
private HashSet<Vector3Int> _occupiedNodes = new HashSet<Vector3Int>();
|
||||
|
||||
@@ -139,10 +143,59 @@ public class BuildManager : NetworkBehaviour
|
||||
return _tunnelRegistry.GetValueOrDefault(pos);
|
||||
}
|
||||
|
||||
// Helper method to properly check if pointer is over UI with New Input System
|
||||
private bool IsPointerOverUI()
|
||||
{
|
||||
if (EventSystem.current == null) return false;
|
||||
|
||||
// Use the new input system's pointer position
|
||||
Vector2 pointerPosition = Mouse.current.position.ReadValue();
|
||||
|
||||
PointerEventData eventData = new PointerEventData(EventSystem.current)
|
||||
{
|
||||
position = pointerPosition
|
||||
};
|
||||
|
||||
List<RaycastResult> results = new List<RaycastResult>();
|
||||
EventSystem.current.RaycastAll(eventData, results);
|
||||
|
||||
// Filter out non-interactive UI elements (crosshair, HUD, etc.)
|
||||
foreach (var result in results)
|
||||
{
|
||||
GameObject uiObject = result.gameObject;
|
||||
|
||||
// Ignore non-interactive UI elements by name
|
||||
if (uiObject.name == "Crosshair" ||
|
||||
uiObject.name.Contains("HUD") ||
|
||||
uiObject.name.Contains("Display"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the UI element is actually interactive (has a Selectable component)
|
||||
UnityEngine.UI.Selectable selectable = uiObject.GetComponent<UnityEngine.UI.Selectable>();
|
||||
if (selectable != null && selectable.interactable)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check parent objects for Selectable components (in case we hit a child element)
|
||||
selectable = uiObject.GetComponentInParent<UnityEngine.UI.Selectable>();
|
||||
if (selectable != null && selectable.interactable)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No interactive UI elements found
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. 건설 요청 시 실제 계산된 worldPos를 넘겨줍니다.
|
||||
private void OnBuildRequested()
|
||||
{
|
||||
if (!_isBuildMode || EventSystem.current.IsPointerOverGameObject()) return;
|
||||
if (!_isBuildMode) return;
|
||||
if (IsPointerOverUI()) return;
|
||||
|
||||
// 고스트가 현재 위치한 '그 좌표'를 그대로 보냅니다.
|
||||
RequestBuildRpc(_selectedTurretIndex, _currentGridPos, _ghostInstance.transform.position);
|
||||
@@ -153,12 +206,30 @@ public class BuildManager : NetworkBehaviour
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestBuildRpc(int index, Vector3Int gridPos, Vector3 worldPos)
|
||||
{
|
||||
if (constructionSitePrefab == null)
|
||||
{
|
||||
Debug.LogError("[BuildManager] Construction site prefab is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
// GridToWorld를 다시 계산하지 않고 전달받은 worldPos를 그대로 사용합니다.
|
||||
GameObject siteObj = Instantiate(constructionSitePrefab, worldPos, Quaternion.identity);
|
||||
siteObj.GetComponent<NetworkObject>().Spawn();
|
||||
|
||||
NetworkObject netObj = siteObj.GetComponent<NetworkObject>();
|
||||
if (netObj == null)
|
||||
{
|
||||
Debug.LogError("[BuildManager] Construction site has no NetworkObject component!");
|
||||
Destroy(siteObj);
|
||||
return;
|
||||
}
|
||||
|
||||
netObj.Spawn();
|
||||
|
||||
ConstructionSite site = siteObj.GetComponent<ConstructionSite>();
|
||||
if (site != null) site.Initialize(index, gridPos);
|
||||
if (site != null)
|
||||
{
|
||||
site.Initialize(index, gridPos);
|
||||
}
|
||||
}
|
||||
|
||||
public void SelectTurret(int index)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
8
Assets/Scripts/Underground.meta
Normal file
8
Assets/Scripts/Underground.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0bcafb0d0408ab4f893e9c51c9e60e5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
193
Assets/Scripts/Underground/BlockData.cs
Normal file
193
Assets/Scripts/Underground/BlockData.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using Unity.Netcode;
|
||||
|
||||
/// <summary>
|
||||
/// Compact block state struct for chunk-based storage.
|
||||
/// 3 bytes per block: type (1), health (1), flags (1)
|
||||
/// </summary>
|
||||
public struct BlockData : INetworkSerializable
|
||||
{
|
||||
/// <summary>
|
||||
/// Block type: 0=empty/air, 1=normal stone, 2=resource ore
|
||||
/// </summary>
|
||||
public byte blockType;
|
||||
|
||||
/// <summary>
|
||||
/// Block health: 0-255 (0 = destroyed)
|
||||
/// </summary>
|
||||
public byte health;
|
||||
|
||||
/// <summary>
|
||||
/// Block flags: bit 0 = isDiscovered (fog of war)
|
||||
/// </summary>
|
||||
public byte flags;
|
||||
|
||||
// Block type constants
|
||||
public const byte TYPE_EMPTY = 0;
|
||||
public const byte TYPE_NORMAL = 1;
|
||||
public const byte TYPE_RESOURCE = 2;
|
||||
|
||||
// Flag bit masks
|
||||
public const byte FLAG_DISCOVERED = 1 << 0; // Has been seen at least once (networked/permanent)
|
||||
|
||||
/// <summary>
|
||||
/// Whether this block is empty (air or destroyed)
|
||||
/// </summary>
|
||||
public bool IsEmpty => blockType == TYPE_EMPTY || health == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this block has been discovered by players
|
||||
/// </summary>
|
||||
public bool IsDiscovered
|
||||
{
|
||||
get => (flags & FLAG_DISCOVERED) != 0;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
flags |= FLAG_DISCOVERED;
|
||||
else
|
||||
flags &= unchecked((byte)~FLAG_DISCOVERED);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this block is a resource block
|
||||
/// </summary>
|
||||
public bool IsResource => blockType == TYPE_RESOURCE;
|
||||
|
||||
/// <summary>
|
||||
/// Create an empty block
|
||||
/// </summary>
|
||||
public static BlockData Empty => new BlockData
|
||||
{
|
||||
blockType = TYPE_EMPTY,
|
||||
health = 0,
|
||||
flags = 0
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a normal stone block with full health
|
||||
/// </summary>
|
||||
public static BlockData Normal(byte maxHealth = 100) => new BlockData
|
||||
{
|
||||
blockType = TYPE_NORMAL,
|
||||
health = maxHealth,
|
||||
flags = 0
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a resource ore block with full health
|
||||
/// </summary>
|
||||
public static BlockData Resource(byte maxHealth = 150) => new BlockData
|
||||
{
|
||||
blockType = TYPE_RESOURCE,
|
||||
health = maxHealth,
|
||||
flags = 0
|
||||
};
|
||||
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||
{
|
||||
serializer.SerializeValue(ref blockType);
|
||||
serializer.SerializeValue(ref health);
|
||||
serializer.SerializeValue(ref flags);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Network-serializable container for entire chunk state.
|
||||
/// Used for initial sync when clients connect.
|
||||
/// </summary>
|
||||
public struct ChunkState : INetworkSerializable
|
||||
{
|
||||
public BlockData[] blocks;
|
||||
|
||||
public const int CHUNK_SIZE = 4;
|
||||
public const int BLOCKS_PER_CHUNK = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; // 64
|
||||
|
||||
public ChunkState(int size)
|
||||
{
|
||||
blocks = new BlockData[size];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure blocks array is initialized
|
||||
/// </summary>
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (blocks == null)
|
||||
{
|
||||
blocks = new BlockData[BLOCKS_PER_CHUNK];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get block at local coordinates within the chunk
|
||||
/// </summary>
|
||||
public BlockData GetBlock(int x, int y, int z)
|
||||
{
|
||||
if (blocks == null) return BlockData.Empty;
|
||||
int index = x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE;
|
||||
if (index < 0 || index >= blocks.Length) return BlockData.Empty;
|
||||
return blocks[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set block at local coordinates within the chunk
|
||||
/// </summary>
|
||||
public void SetBlock(int x, int y, int z, BlockData block)
|
||||
{
|
||||
EnsureInitialized();
|
||||
int index = x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE;
|
||||
if (index >= 0 && index < blocks.Length)
|
||||
{
|
||||
blocks[index] = block;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert local index to local 3D coordinates
|
||||
/// </summary>
|
||||
public static (int x, int y, int z) IndexToLocal(int index)
|
||||
{
|
||||
int x = index % CHUNK_SIZE;
|
||||
int y = (index / CHUNK_SIZE) % CHUNK_SIZE;
|
||||
int z = index / (CHUNK_SIZE * CHUNK_SIZE);
|
||||
return (x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert local 3D coordinates to index
|
||||
/// </summary>
|
||||
public static int LocalToIndex(int x, int y, int z)
|
||||
{
|
||||
return x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE;
|
||||
}
|
||||
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||
{
|
||||
// Serialize array length first
|
||||
int length = BLOCKS_PER_CHUNK;
|
||||
serializer.SerializeValue(ref length);
|
||||
|
||||
if (serializer.IsReader)
|
||||
{
|
||||
blocks = new BlockData[length];
|
||||
}
|
||||
else if (blocks == null)
|
||||
{
|
||||
blocks = new BlockData[length];
|
||||
}
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
blocks[i].NetworkSerialize(serializer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a default initialized ChunkState
|
||||
/// </summary>
|
||||
public static ChunkState CreateEmpty()
|
||||
{
|
||||
return new ChunkState(BLOCKS_PER_CHUNK);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Underground/BlockData.cs.meta
Normal file
2
Assets/Scripts/Underground/BlockData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2b004a2f2115024ea956fdccbb62cf1
|
||||
215
Assets/Scripts/Underground/ChunkCoord.cs
Normal file
215
Assets/Scripts/Underground/ChunkCoord.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Utility struct for chunk coordinate conversions.
|
||||
/// Handles conversions between world, grid, chunk, and local block coordinates.
|
||||
/// </summary>
|
||||
public struct ChunkCoord
|
||||
{
|
||||
public const int CHUNK_SIZE = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Chunk position in chunk coordinates (not world or grid)
|
||||
/// </summary>
|
||||
public Vector3Int chunkPos;
|
||||
|
||||
public ChunkCoord(Vector3Int pos)
|
||||
{
|
||||
chunkPos = pos;
|
||||
}
|
||||
|
||||
public ChunkCoord(int x, int y, int z)
|
||||
{
|
||||
chunkPos = new Vector3Int(x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the grid position of the chunk's origin (corner with smallest coordinates)
|
||||
/// </summary>
|
||||
public Vector3Int GridOrigin => new Vector3Int(
|
||||
chunkPos.x * CHUNK_SIZE,
|
||||
chunkPos.y * CHUNK_SIZE,
|
||||
chunkPos.z * CHUNK_SIZE
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Get world position of chunk origin using BuildManager's grid system
|
||||
/// </summary>
|
||||
public Vector3 WorldOrigin
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BuildManager.Instance != null)
|
||||
{
|
||||
return BuildManager.Instance.GridToWorld(GridOrigin);
|
||||
}
|
||||
// Fallback if BuildManager not available
|
||||
return new Vector3(
|
||||
GridOrigin.x + 0.5f,
|
||||
GridOrigin.y + 0.5f,
|
||||
GridOrigin.z + 0.5f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert grid coordinates to chunk coordinates
|
||||
/// </summary>
|
||||
public static ChunkCoord FromGridPos(Vector3Int gridPos)
|
||||
{
|
||||
return new ChunkCoord(
|
||||
Mathf.FloorToInt((float)gridPos.x / CHUNK_SIZE),
|
||||
Mathf.FloorToInt((float)gridPos.y / CHUNK_SIZE),
|
||||
Mathf.FloorToInt((float)gridPos.z / CHUNK_SIZE)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert world position to chunk coordinates
|
||||
/// </summary>
|
||||
public static ChunkCoord FromWorldPos(Vector3 worldPos)
|
||||
{
|
||||
Vector3Int gridPos;
|
||||
if (BuildManager.Instance != null)
|
||||
{
|
||||
gridPos = BuildManager.Instance.WorldToGrid3D(worldPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
gridPos = new Vector3Int(
|
||||
Mathf.RoundToInt(worldPos.x - 0.5f),
|
||||
Mathf.RoundToInt(worldPos.y - 0.5f),
|
||||
Mathf.RoundToInt(worldPos.z - 0.5f)
|
||||
);
|
||||
}
|
||||
return FromGridPos(gridPos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get local block coordinates within chunk from grid position
|
||||
/// </summary>
|
||||
public static Vector3Int GridToLocal(Vector3Int gridPos)
|
||||
{
|
||||
// Use modulo to get local position, handling negative coords
|
||||
int x = ((gridPos.x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
||||
int y = ((gridPos.y % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
||||
int z = ((gridPos.z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
||||
return new Vector3Int(x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert local block coordinates to grid position
|
||||
/// </summary>
|
||||
public Vector3Int LocalToGrid(Vector3Int localPos)
|
||||
{
|
||||
return GridOrigin + localPos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert local block coordinates to world position
|
||||
/// </summary>
|
||||
public Vector3 LocalToWorld(Vector3Int localPos)
|
||||
{
|
||||
Vector3Int gridPos = LocalToGrid(localPos);
|
||||
if (BuildManager.Instance != null)
|
||||
{
|
||||
return BuildManager.Instance.GridToWorld(gridPos);
|
||||
}
|
||||
return new Vector3(gridPos.x + 0.5f, gridPos.y + 0.5f, gridPos.z + 0.5f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert local index to local 3D coordinates
|
||||
/// </summary>
|
||||
public static Vector3Int IndexToLocal(int index)
|
||||
{
|
||||
int x = index % CHUNK_SIZE;
|
||||
int y = (index / CHUNK_SIZE) % CHUNK_SIZE;
|
||||
int z = index / (CHUNK_SIZE * CHUNK_SIZE);
|
||||
return new Vector3Int(x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert local 3D coordinates to index
|
||||
/// </summary>
|
||||
public static int LocalToIndex(Vector3Int localPos)
|
||||
{
|
||||
return localPos.x + localPos.y * CHUNK_SIZE + localPos.z * CHUNK_SIZE * CHUNK_SIZE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert local coordinates (int) to index
|
||||
/// </summary>
|
||||
public static int LocalToIndex(int x, int y, int z)
|
||||
{
|
||||
return x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if local coordinates are within valid range
|
||||
/// </summary>
|
||||
public static bool IsValidLocal(int x, int y, int z)
|
||||
{
|
||||
return x >= 0 && x < CHUNK_SIZE &&
|
||||
y >= 0 && y < CHUNK_SIZE &&
|
||||
z >= 0 && z < CHUNK_SIZE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if local position is within valid range
|
||||
/// </summary>
|
||||
public static bool IsValidLocal(Vector3Int local)
|
||||
{
|
||||
return IsValidLocal(local.x, local.y, local.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert world hit point to local block index within chunk
|
||||
/// </summary>
|
||||
public int WorldPointToLocalIndex(Vector3 worldPoint)
|
||||
{
|
||||
// Get chunk origin in world space
|
||||
Vector3 chunkWorldOrigin = WorldOrigin;
|
||||
|
||||
// Calculate offset from chunk origin (in grid units, assuming 1 unit per block)
|
||||
float cellSize = BuildManager.Instance != null ? BuildManager.Instance.cellSize : 1f;
|
||||
Vector3 offset = (worldPoint - chunkWorldOrigin) / cellSize;
|
||||
|
||||
// Convert to local coordinates (add small epsilon to handle edge cases)
|
||||
int lx = Mathf.Clamp(Mathf.FloorToInt(offset.x + 0.5f), 0, CHUNK_SIZE - 1);
|
||||
int ly = Mathf.Clamp(Mathf.FloorToInt(offset.y + 0.5f), 0, CHUNK_SIZE - 1);
|
||||
int lz = Mathf.Clamp(Mathf.FloorToInt(offset.z + 0.5f), 0, CHUNK_SIZE - 1);
|
||||
|
||||
return LocalToIndex(lx, ly, lz);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is ChunkCoord other)
|
||||
{
|
||||
return chunkPos == other.chunkPos;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return chunkPos.GetHashCode();
|
||||
}
|
||||
|
||||
public static bool operator ==(ChunkCoord a, ChunkCoord b)
|
||||
{
|
||||
return a.chunkPos == b.chunkPos;
|
||||
}
|
||||
|
||||
public static bool operator !=(ChunkCoord a, ChunkCoord b)
|
||||
{
|
||||
return a.chunkPos != b.chunkPos;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"ChunkCoord({chunkPos.x}, {chunkPos.y}, {chunkPos.z})";
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Underground/ChunkCoord.cs.meta
Normal file
2
Assets/Scripts/Underground/ChunkCoord.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 08589d2f166b446418cc8b61105e6d01
|
||||
126
Assets/Scripts/Underground/ChunkInteractionHandler.cs
Normal file
126
Assets/Scripts/Underground/ChunkInteractionHandler.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Handles interaction between players and chunk-based blocks.
|
||||
/// Converts raycast hits to block indices and manages damage/reveal requests.
|
||||
/// </summary>
|
||||
public static class ChunkInteractionHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Result of a chunk interaction query
|
||||
/// </summary>
|
||||
public struct ChunkHitResult
|
||||
{
|
||||
public bool hasHit;
|
||||
public MineableChunk chunk;
|
||||
public int blockIndex;
|
||||
public Vector3 hitPoint;
|
||||
public Vector3 blockWorldPosition;
|
||||
public BlockData blockData;
|
||||
|
||||
public static ChunkHitResult None => new ChunkHitResult { hasHit = false };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to get chunk and block info from a raycast hit
|
||||
/// </summary>
|
||||
public static ChunkHitResult GetChunkHit(RaycastHit hit)
|
||||
{
|
||||
// Try to get MineableChunk from hit collider
|
||||
MineableChunk chunk = hit.collider.GetComponentInParent<MineableChunk>();
|
||||
if (chunk == null)
|
||||
{
|
||||
return ChunkHitResult.None;
|
||||
}
|
||||
|
||||
// Convert hit point to block index
|
||||
// Push slightly into the surface using the normal to get the correct block
|
||||
Vector3 insidePoint = hit.point - hit.normal * 0.01f;
|
||||
int blockIndex = chunk.WorldPointToBlockIndex(insidePoint);
|
||||
BlockData blockData = chunk.GetBlock(blockIndex);
|
||||
|
||||
// Skip if block is empty
|
||||
if (blockData.IsEmpty)
|
||||
{
|
||||
return ChunkHitResult.None;
|
||||
}
|
||||
|
||||
return new ChunkHitResult
|
||||
{
|
||||
hasHit = true,
|
||||
chunk = chunk,
|
||||
blockIndex = blockIndex,
|
||||
hitPoint = hit.point,
|
||||
blockWorldPosition = chunk.GetBlockWorldPosition(blockIndex),
|
||||
blockData = blockData
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to damage a block at a raycast hit point
|
||||
/// </summary>
|
||||
public static bool TryDamageAtPoint(RaycastHit hit, float damage)
|
||||
{
|
||||
ChunkHitResult result = GetChunkHit(hit);
|
||||
if (!result.hasHit) return false;
|
||||
|
||||
// Request damage on server
|
||||
result.chunk.DamageBlockServerRpc(result.blockIndex, (byte)Mathf.Min(255, damage));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to damage a block directly on a known chunk
|
||||
/// </summary>
|
||||
public static bool TryDamageBlock(MineableChunk chunk, int blockIndex, float damage)
|
||||
{
|
||||
if (chunk == null) return false;
|
||||
|
||||
BlockData block = chunk.GetBlock(blockIndex);
|
||||
if (block.IsEmpty) return false;
|
||||
|
||||
chunk.DamageBlockServerRpc(blockIndex, (byte)Mathf.Min(255, damage));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to reveal a block at a raycast hit point
|
||||
/// </summary>
|
||||
public static bool TryRevealAtPoint(RaycastHit hit)
|
||||
{
|
||||
MineableChunk chunk = hit.collider.GetComponentInParent<MineableChunk>();
|
||||
if (chunk == null) return false;
|
||||
|
||||
int blockIndex = chunk.WorldPointToBlockIndex(hit.point - hit.normal * 0.1f);
|
||||
BlockData blockData = chunk.GetBlock(blockIndex);
|
||||
|
||||
if (blockData.IsEmpty || blockData.IsDiscovered) return false;
|
||||
|
||||
chunk.RevealBlockServerRpc(blockIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a sphere cast to find a targetable block in a chunk
|
||||
/// </summary>
|
||||
public static ChunkHitResult SphereCastForBlock(Vector3 origin, Vector3 direction, float radius, float maxDistance, LayerMask chunkLayer)
|
||||
{
|
||||
if (Physics.SphereCast(origin, radius, direction, out RaycastHit hit, maxDistance, chunkLayer))
|
||||
{
|
||||
return GetChunkHit(hit);
|
||||
}
|
||||
return ChunkHitResult.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a raycast to find a targetable block in a chunk
|
||||
/// </summary>
|
||||
public static ChunkHitResult RaycastForBlock(Vector3 origin, Vector3 direction, float maxDistance, LayerMask chunkLayer)
|
||||
{
|
||||
if (Physics.Raycast(origin, direction, out RaycastHit hit, maxDistance, chunkLayer))
|
||||
{
|
||||
return GetChunkHit(hit);
|
||||
}
|
||||
return ChunkHitResult.None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fc7fb23f18204f4eafbf4395b12aba5
|
||||
330
Assets/Scripts/Underground/ChunkMeshBuilder.cs
Normal file
330
Assets/Scripts/Underground/ChunkMeshBuilder.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Builds optimized meshes for chunks with face culling.
|
||||
/// Only renders faces that are exposed (adjacent to empty blocks or chunk boundaries).
|
||||
/// Uses submeshes for different block types (normal/resource).
|
||||
/// </summary>
|
||||
public static class ChunkMeshBuilder
|
||||
{
|
||||
private const int CHUNK_SIZE = ChunkCoord.CHUNK_SIZE;
|
||||
|
||||
// Face directions
|
||||
private static readonly Vector3Int[] FaceDirections = new Vector3Int[]
|
||||
{
|
||||
Vector3Int.right, // +X
|
||||
Vector3Int.left, // -X
|
||||
Vector3Int.up, // +Y
|
||||
Vector3Int.down, // -Y
|
||||
new Vector3Int(0, 0, 1), // +Z
|
||||
new Vector3Int(0, 0, -1) // -Z
|
||||
};
|
||||
|
||||
// Face normals matching directions
|
||||
private static readonly Vector3[] FaceNormals = new Vector3[]
|
||||
{
|
||||
Vector3.right,
|
||||
Vector3.left,
|
||||
Vector3.up,
|
||||
Vector3.down,
|
||||
Vector3.forward,
|
||||
Vector3.back
|
||||
};
|
||||
|
||||
// Vertices for each face (relative to block center)
|
||||
private static readonly Vector3[][] FaceVertices = new Vector3[][]
|
||||
{
|
||||
// +X face
|
||||
new Vector3[] {
|
||||
new Vector3(0.5f, -0.5f, -0.5f),
|
||||
new Vector3(0.5f, 0.5f, -0.5f),
|
||||
new Vector3(0.5f, 0.5f, 0.5f),
|
||||
new Vector3(0.5f, -0.5f, 0.5f)
|
||||
},
|
||||
// -X face
|
||||
new Vector3[] {
|
||||
new Vector3(-0.5f, -0.5f, 0.5f),
|
||||
new Vector3(-0.5f, 0.5f, 0.5f),
|
||||
new Vector3(-0.5f, 0.5f, -0.5f),
|
||||
new Vector3(-0.5f, -0.5f, -0.5f)
|
||||
},
|
||||
// +Y face
|
||||
new Vector3[] {
|
||||
new Vector3(-0.5f, 0.5f, -0.5f),
|
||||
new Vector3(-0.5f, 0.5f, 0.5f),
|
||||
new Vector3( 0.5f, 0.5f, 0.5f),
|
||||
new Vector3( 0.5f, 0.5f, -0.5f)
|
||||
},
|
||||
// -Y face
|
||||
new Vector3[] {
|
||||
new Vector3(-0.5f, -0.5f, 0.5f),
|
||||
new Vector3(-0.5f, -0.5f, -0.5f),
|
||||
new Vector3( 0.5f, -0.5f, -0.5f),
|
||||
new Vector3( 0.5f, -0.5f, 0.5f)
|
||||
},
|
||||
// +Z face
|
||||
new Vector3[] {
|
||||
new Vector3(-0.5f, -0.5f, 0.5f),
|
||||
new Vector3( 0.5f, -0.5f, 0.5f),
|
||||
new Vector3( 0.5f, 0.5f, 0.5f),
|
||||
new Vector3(-0.5f, 0.5f, 0.5f)
|
||||
},
|
||||
// -Z face
|
||||
new Vector3[] {
|
||||
new Vector3( 0.5f, -0.5f, -0.5f),
|
||||
new Vector3(-0.5f, -0.5f, -0.5f),
|
||||
new Vector3(-0.5f, 0.5f, -0.5f),
|
||||
new Vector3( 0.5f, 0.5f, -0.5f)
|
||||
}
|
||||
};
|
||||
|
||||
// UV coordinates for each face
|
||||
private static readonly Vector2[] FaceUVs = new Vector2[]
|
||||
{
|
||||
new Vector2(0, 0),
|
||||
new Vector2(1, 0),
|
||||
new Vector2(1, 1),
|
||||
new Vector2(0, 1)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Interface for querying neighboring chunks for cross-chunk face culling
|
||||
/// </summary>
|
||||
public interface INeighborProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if block at world grid position is solid (for face culling)
|
||||
/// </summary>
|
||||
bool IsBlockSolid(Vector3Int gridPos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visibility check delegate for fog of war
|
||||
/// </summary>
|
||||
public delegate bool VisibilityChecker(int blockIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Build mesh for a chunk with face culling.
|
||||
/// Returns a mesh with two submeshes: 0=normal blocks, 1=resource blocks
|
||||
/// </summary>
|
||||
public static Mesh BuildMesh(ChunkState state, ChunkCoord coord, INeighborProvider neighborProvider = null, bool onlyDiscovered = true)
|
||||
{
|
||||
return BuildMeshWithVisibility(state, coord, neighborProvider, onlyDiscovered, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build mesh for a chunk with face culling and fog of war support.
|
||||
/// Returns a mesh with 4 submeshes: 0=normal-visible, 1=resource-visible, 2=normal-dark, 3=resource-dark
|
||||
/// </summary>
|
||||
public static Mesh BuildMeshWithVisibility(ChunkState state, ChunkCoord coord, INeighborProvider neighborProvider, bool onlyDiscovered, VisibilityChecker isVisible)
|
||||
{
|
||||
// Lists for 4 submeshes: normal-visible, resource-visible, normal-dark, resource-dark
|
||||
var meshData = new MeshData[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
meshData[i] = new MeshData();
|
||||
}
|
||||
|
||||
// Handle null blocks array
|
||||
if (state.blocks == null)
|
||||
{
|
||||
return CreateEmptyMesh(coord, 4);
|
||||
}
|
||||
|
||||
// Process each block
|
||||
for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++)
|
||||
{
|
||||
BlockData block = state.blocks[i];
|
||||
|
||||
// Skip empty blocks
|
||||
if (block.IsEmpty) continue;
|
||||
|
||||
// Skip undiscovered blocks if onlyDiscovered is true
|
||||
if (onlyDiscovered && !block.IsDiscovered) continue;
|
||||
|
||||
Vector3Int localPos = ChunkCoord.IndexToLocal(i);
|
||||
Vector3 blockOffset = new Vector3(localPos.x, localPos.y, localPos.z);
|
||||
|
||||
// Determine which submesh to use based on block type and visibility
|
||||
bool currentlyVisible = isVisible != null ? isVisible(i) : true;
|
||||
int submeshIndex;
|
||||
if (block.IsResource)
|
||||
{
|
||||
submeshIndex = currentlyVisible ? 1 : 3; // resource-visible or resource-dark
|
||||
}
|
||||
else
|
||||
{
|
||||
submeshIndex = currentlyVisible ? 0 : 2; // normal-visible or normal-dark
|
||||
}
|
||||
|
||||
var data = meshData[submeshIndex];
|
||||
|
||||
// Check each face
|
||||
for (int f = 0; f < 6; f++)
|
||||
{
|
||||
Vector3Int neighborLocal = localPos + FaceDirections[f];
|
||||
|
||||
bool shouldRenderFace = false;
|
||||
|
||||
if (ChunkCoord.IsValidLocal(neighborLocal))
|
||||
{
|
||||
int neighborIndex = ChunkCoord.LocalToIndex(neighborLocal);
|
||||
BlockData neighborBlock = state.blocks[neighborIndex];
|
||||
shouldRenderFace = neighborBlock.IsEmpty || (onlyDiscovered && !neighborBlock.IsDiscovered);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (neighborProvider != null)
|
||||
{
|
||||
Vector3Int neighborGridPos = coord.LocalToGrid(neighborLocal);
|
||||
shouldRenderFace = !neighborProvider.IsBlockSolid(neighborGridPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldRenderFace = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRenderFace)
|
||||
{
|
||||
AddFace(data.vertices, data.normals, data.uvs, data.triangles, blockOffset, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the mesh
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.name = $"Chunk_{coord.chunkPos.x}_{coord.chunkPos.y}_{coord.chunkPos.z}";
|
||||
|
||||
// Combine all vertices from all submeshes
|
||||
int totalVerts = 0;
|
||||
int[] vertOffsets = new int[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
vertOffsets[i] = totalVerts;
|
||||
totalVerts += meshData[i].vertices.Count;
|
||||
}
|
||||
|
||||
var allVertices = new Vector3[totalVerts];
|
||||
var allNormals = new Vector3[totalVerts];
|
||||
var allUVs = new Vector2[totalVerts];
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
meshData[i].vertices.CopyTo(allVertices, vertOffsets[i]);
|
||||
meshData[i].normals.CopyTo(allNormals, vertOffsets[i]);
|
||||
meshData[i].uvs.CopyTo(allUVs, vertOffsets[i]);
|
||||
}
|
||||
|
||||
mesh.vertices = allVertices;
|
||||
mesh.normals = allNormals;
|
||||
mesh.uv = allUVs;
|
||||
|
||||
// Set submeshes with adjusted triangle indices
|
||||
mesh.subMeshCount = 4;
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var triangles = meshData[i].triangles;
|
||||
for (int t = 0; t < triangles.Count; t++)
|
||||
{
|
||||
triangles[t] += vertOffsets[i];
|
||||
}
|
||||
mesh.SetTriangles(triangles, i);
|
||||
}
|
||||
|
||||
mesh.RecalculateBounds();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to hold mesh data for each submesh
|
||||
/// </summary>
|
||||
private class MeshData
|
||||
{
|
||||
public List<Vector3> vertices = new List<Vector3>();
|
||||
public List<Vector3> normals = new List<Vector3>();
|
||||
public List<Vector2> uvs = new List<Vector2>();
|
||||
public List<int> triangles = new List<int>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an empty mesh with proper submesh configuration
|
||||
/// </summary>
|
||||
private static Mesh CreateEmptyMesh(ChunkCoord coord, int submeshCount = 4)
|
||||
{
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.name = $"Chunk_{coord.chunkPos.x}_{coord.chunkPos.y}_{coord.chunkPos.z}_Empty";
|
||||
mesh.subMeshCount = submeshCount;
|
||||
for (int i = 0; i < submeshCount; i++)
|
||||
{
|
||||
mesh.SetTriangles(new int[0], i);
|
||||
}
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a face to the mesh data
|
||||
/// </summary>
|
||||
private static void AddFace(List<Vector3> vertices, List<Vector3> normals, List<Vector2> uvs, List<int> triangles, Vector3 blockOffset, int faceIndex)
|
||||
{
|
||||
int startVertex = vertices.Count;
|
||||
|
||||
// Add vertices
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
vertices.Add(FaceVertices[faceIndex][i] + blockOffset);
|
||||
normals.Add(FaceNormals[faceIndex]);
|
||||
uvs.Add(FaceUVs[i]);
|
||||
}
|
||||
|
||||
// Add triangles (two triangles per face)
|
||||
triangles.Add(startVertex);
|
||||
triangles.Add(startVertex + 1);
|
||||
triangles.Add(startVertex + 2);
|
||||
|
||||
triangles.Add(startVertex);
|
||||
triangles.Add(startVertex + 2);
|
||||
triangles.Add(startVertex + 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Count visible faces in a chunk (for debugging/stats)
|
||||
/// </summary>
|
||||
public static int CountVisibleFaces(ChunkState state, bool onlyDiscovered = true)
|
||||
{
|
||||
if (state.blocks == null) return 0;
|
||||
|
||||
int faceCount = 0;
|
||||
|
||||
for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++)
|
||||
{
|
||||
BlockData block = state.blocks[i];
|
||||
if (block.IsEmpty) continue;
|
||||
if (onlyDiscovered && !block.IsDiscovered) continue;
|
||||
|
||||
Vector3Int localPos = ChunkCoord.IndexToLocal(i);
|
||||
|
||||
for (int f = 0; f < 6; f++)
|
||||
{
|
||||
Vector3Int neighborLocal = localPos + FaceDirections[f];
|
||||
|
||||
if (ChunkCoord.IsValidLocal(neighborLocal))
|
||||
{
|
||||
int neighborIndex = ChunkCoord.LocalToIndex(neighborLocal);
|
||||
BlockData neighborBlock = state.blocks[neighborIndex];
|
||||
if (neighborBlock.IsEmpty || (onlyDiscovered && !neighborBlock.IsDiscovered))
|
||||
{
|
||||
faceCount++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
faceCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return faceCount;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Underground/ChunkMeshBuilder.cs.meta
Normal file
2
Assets/Scripts/Underground/ChunkMeshBuilder.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02008c1a2b561f049ae0f8b78dd9d22c
|
||||
389
Assets/Scripts/Underground/ChunkedUndergroundGenerator.cs
Normal file
389
Assets/Scripts/Underground/ChunkedUndergroundGenerator.cs
Normal file
@@ -0,0 +1,389 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Generates underground terrain using chunk-based system.
|
||||
/// Replaces individual MineableBlock NetworkObjects with MineableChunk NetworkObjects.
|
||||
/// </summary>
|
||||
public class ChunkedUndergroundGenerator : NetworkBehaviour
|
||||
{
|
||||
public static ChunkedUndergroundGenerator Instance { get; private set; }
|
||||
|
||||
[Header("Generation Range (in blocks)")]
|
||||
[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.85f;
|
||||
|
||||
[Header("Resource Distribution")]
|
||||
[SerializeField] private float resourceNoiseScale = 0.25f; // Larger = more spread out
|
||||
[SerializeField, Range(0, 1)] private float resourceSpawnChance = 0.3f; // Additional random chance
|
||||
|
||||
[Header("Depth Settings")]
|
||||
[SerializeField] private bool increaseResourceWithDepth = true;
|
||||
[SerializeField] private float depthFactor = 0.003f;
|
||||
|
||||
[Header("Block Health")]
|
||||
[SerializeField] private byte normalBlockHealth = 100;
|
||||
[SerializeField] private byte resourceBlockHealth = 150;
|
||||
|
||||
[Header("Chunk Prefab")]
|
||||
[SerializeField] private GameObject chunkPrefab;
|
||||
|
||||
[Header("Organization")]
|
||||
[SerializeField] private string containerName = "UndergroundChunks";
|
||||
private Transform _chunkContainer;
|
||||
|
||||
// Chunk registry for neighbor queries
|
||||
private Dictionary<Vector3Int, MineableChunk> _chunkRegistry = new Dictionary<Vector3Int, MineableChunk>();
|
||||
|
||||
// Noise seeds
|
||||
private float _seedX, _seedY, _seedZ;
|
||||
private float _resourceSeedX, _resourceSeedY, _resourceSeedZ;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
_seedX = Random.Range(0f, 99999f);
|
||||
_seedY = Random.Range(0f, 99999f);
|
||||
_seedZ = Random.Range(0f, 99999f);
|
||||
|
||||
// Separate seeds for resource distribution
|
||||
_resourceSeedX = Random.Range(0f, 99999f);
|
||||
_resourceSeedY = Random.Range(0f, 99999f);
|
||||
_resourceSeedZ = Random.Range(0f, 99999f);
|
||||
|
||||
// Create container for hierarchy organization
|
||||
_chunkContainer = new GameObject(containerName).transform;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// Register with MineableChunk for neighbor queries
|
||||
MineableChunk.SetChunkManager(this);
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
GenerateChunks();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate all chunks for the underground area
|
||||
/// </summary>
|
||||
private void GenerateChunks()
|
||||
{
|
||||
Vector3Int originGrid = BuildManager.Instance.WorldToGrid3D(transform.position);
|
||||
|
||||
// Calculate chunk range
|
||||
int chunkSizeX = Mathf.CeilToInt((float)generationRange.x / ChunkCoord.CHUNK_SIZE);
|
||||
int chunkSizeY = Mathf.CeilToInt((float)generationRange.y / ChunkCoord.CHUNK_SIZE);
|
||||
int chunkSizeZ = Mathf.CeilToInt((float)generationRange.z / ChunkCoord.CHUNK_SIZE);
|
||||
|
||||
Debug.Log($"[ChunkedGenerator] Generating {chunkSizeX}x{chunkSizeY}x{chunkSizeZ} chunks = {chunkSizeX * chunkSizeY * chunkSizeZ} total");
|
||||
|
||||
int chunksGenerated = 0;
|
||||
int chunksSkipped = 0;
|
||||
|
||||
// Generate chunks
|
||||
for (int cx = 0; cx < chunkSizeX; cx++)
|
||||
{
|
||||
for (int cy = 0; cy < chunkSizeY; cy++)
|
||||
{
|
||||
for (int cz = 0; cz < chunkSizeZ; cz++)
|
||||
{
|
||||
// Calculate chunk grid origin (going downward for Y)
|
||||
Vector3Int chunkGridOrigin = originGrid + new Vector3Int(
|
||||
cx * ChunkCoord.CHUNK_SIZE,
|
||||
-cy * ChunkCoord.CHUNK_SIZE, // Negative Y (underground)
|
||||
cz * ChunkCoord.CHUNK_SIZE
|
||||
);
|
||||
|
||||
// Generate block data for this chunk
|
||||
ChunkState state = GenerateChunkState(chunkGridOrigin);
|
||||
|
||||
// Check if chunk has any blocks
|
||||
bool hasBlocks = false;
|
||||
for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++)
|
||||
{
|
||||
if (!state.blocks[i].IsEmpty)
|
||||
{
|
||||
hasBlocks = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasBlocks)
|
||||
{
|
||||
chunksSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Spawn chunk
|
||||
SpawnChunk(chunkGridOrigin, state);
|
||||
chunksGenerated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[ChunkedGenerator] Generated {chunksGenerated} chunks, skipped {chunksSkipped} empty chunks");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate block state for a chunk
|
||||
/// </summary>
|
||||
private ChunkState GenerateChunkState(Vector3Int chunkGridOrigin)
|
||||
{
|
||||
ChunkState state = ChunkState.CreateEmpty();
|
||||
|
||||
for (int lx = 0; lx < ChunkCoord.CHUNK_SIZE; lx++)
|
||||
{
|
||||
for (int ly = 0; ly < ChunkCoord.CHUNK_SIZE; ly++)
|
||||
{
|
||||
for (int lz = 0; lz < ChunkCoord.CHUNK_SIZE; lz++)
|
||||
{
|
||||
Vector3Int gridPos = chunkGridOrigin + new Vector3Int(lx, ly, lz);
|
||||
int index = ChunkCoord.LocalToIndex(lx, ly, lz);
|
||||
|
||||
// Check if within generation range
|
||||
Vector3Int originGrid = BuildManager.Instance.WorldToGrid3D(transform.position);
|
||||
Vector3Int offset = gridPos - originGrid;
|
||||
|
||||
if (offset.x < 0 || offset.x >= generationRange.x ||
|
||||
offset.y > 0 || offset.y <= -generationRange.y ||
|
||||
offset.z < 0 || offset.z >= generationRange.z)
|
||||
{
|
||||
state.blocks[index] = BlockData.Empty;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sample noise
|
||||
float noise = Get3DNoise(gridPos.x, gridPos.y, gridPos.z);
|
||||
|
||||
// Check hollow threshold
|
||||
if (noise < hollowThreshold)
|
||||
{
|
||||
state.blocks[index] = BlockData.Empty;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine block type using separate resource noise for better distribution
|
||||
float resourceNoise = GetResourceNoise(gridPos.x, gridPos.y, gridPos.z);
|
||||
|
||||
float currentThreshold = baseResourceThreshold;
|
||||
if (increaseResourceWithDepth)
|
||||
{
|
||||
currentThreshold += gridPos.y * depthFactor; // Lower threshold = more resources at depth
|
||||
}
|
||||
|
||||
// Resource spawns only if: resource noise > threshold AND random chance passes
|
||||
bool isResource = resourceNoise > currentThreshold &&
|
||||
Random.value < resourceSpawnChance;
|
||||
|
||||
if (isResource)
|
||||
{
|
||||
state.blocks[index] = BlockData.Resource(resourceBlockHealth);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.blocks[index] = BlockData.Normal(normalBlockHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn a chunk NetworkObject
|
||||
/// </summary>
|
||||
private void SpawnChunk(Vector3Int chunkGridOrigin, ChunkState state)
|
||||
{
|
||||
if (chunkPrefab == null)
|
||||
{
|
||||
Debug.LogError("[ChunkedGenerator] Chunk prefab not assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate world position for chunk
|
||||
Vector3 worldPos = BuildManager.Instance.GridToWorld(chunkGridOrigin);
|
||||
|
||||
// Instantiate chunk
|
||||
GameObject chunkObj = Instantiate(chunkPrefab, worldPos, Quaternion.identity, _chunkContainer);
|
||||
|
||||
// Spawn on network
|
||||
NetworkObject netObj = chunkObj.GetComponent<NetworkObject>();
|
||||
netObj.Spawn();
|
||||
|
||||
// Initialize chunk with block data
|
||||
MineableChunk chunk = chunkObj.GetComponent<MineableChunk>();
|
||||
if (chunk != null)
|
||||
{
|
||||
chunk.InitializeBlocks(state);
|
||||
|
||||
// Register in dictionary
|
||||
ChunkCoord coord = ChunkCoord.FromGridPos(chunkGridOrigin);
|
||||
_chunkRegistry[coord.chunkPos] = chunk;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 3D Perlin noise sampling for terrain shape
|
||||
/// </summary>
|
||||
private float Get3DNoise(int x, int y, int z)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Separate 3D noise for resource distribution (more spread out)
|
||||
/// </summary>
|
||||
private float GetResourceNoise(int x, int y, int z)
|
||||
{
|
||||
float xCoord = (x + _resourceSeedX + 10000f) * resourceNoiseScale;
|
||||
float yCoord = (y + _resourceSeedY + 10000f) * resourceNoiseScale;
|
||||
float zCoord = (z + _resourceSeedZ + 10000f) * resourceNoiseScale;
|
||||
|
||||
float ab = Mathf.PerlinNoise(xCoord, yCoord);
|
||||
float bc = Mathf.PerlinNoise(yCoord, zCoord);
|
||||
float ac = Mathf.PerlinNoise(xCoord, zCoord);
|
||||
|
||||
return (ab + bc + ac) / 3f;
|
||||
}
|
||||
|
||||
#region Public Query Methods
|
||||
|
||||
/// <summary>
|
||||
/// Get chunk at grid position
|
||||
/// </summary>
|
||||
public MineableChunk GetChunkAtGrid(Vector3Int gridPos)
|
||||
{
|
||||
ChunkCoord coord = ChunkCoord.FromGridPos(gridPos);
|
||||
_chunkRegistry.TryGetValue(coord.chunkPos, out MineableChunk chunk);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get chunk at world position
|
||||
/// </summary>
|
||||
public MineableChunk GetChunkAtWorld(Vector3 worldPos)
|
||||
{
|
||||
ChunkCoord coord = ChunkCoord.FromWorldPos(worldPos);
|
||||
_chunkRegistry.TryGetValue(coord.chunkPos, out MineableChunk chunk);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if block at grid position is solid (for cross-chunk face culling)
|
||||
/// </summary>
|
||||
public bool IsBlockSolid(Vector3Int gridPos)
|
||||
{
|
||||
MineableChunk chunk = GetChunkAtGrid(gridPos);
|
||||
if (chunk == null) return false;
|
||||
|
||||
Vector3Int localPos = ChunkCoord.GridToLocal(gridPos);
|
||||
int index = ChunkCoord.LocalToIndex(localPos);
|
||||
BlockData block = chunk.GetBlock(index);
|
||||
|
||||
return !block.IsEmpty && block.IsDiscovered;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reveal blocks in radius around a world position (for fog of war)
|
||||
/// </summary>
|
||||
public void RevealBlocksInRadius(Vector3 worldPos, float radius)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
// Find all chunks that could be affected
|
||||
foreach (var chunk in _chunkRegistry.Values)
|
||||
{
|
||||
if (chunk == null) continue;
|
||||
|
||||
// Quick bounds check - chunk center distance
|
||||
float chunkDist = Vector3.Distance(worldPos, chunk.transform.position);
|
||||
float maxChunkRadius = ChunkCoord.CHUNK_SIZE * 0.866f; // diagonal
|
||||
|
||||
if (chunkDist <= radius + maxChunkRadius)
|
||||
{
|
||||
chunk.RevealBlocksInRadius(worldPos, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all active chunks
|
||||
/// </summary>
|
||||
public IEnumerable<MineableChunk> GetAllChunks()
|
||||
{
|
||||
return _chunkRegistry.Values;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gizmos
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
BuildManager bm = BuildManager.Instance;
|
||||
if (bm == null) bm = FindFirstObjectByType<BuildManager>();
|
||||
if (bm == null) return;
|
||||
|
||||
Vector3Int originGrid = bm.WorldToGrid3D(transform.position);
|
||||
|
||||
// Draw chunk boundaries
|
||||
int chunkSizeX = Mathf.CeilToInt((float)generationRange.x / ChunkCoord.CHUNK_SIZE);
|
||||
int chunkSizeY = Mathf.CeilToInt((float)generationRange.y / ChunkCoord.CHUNK_SIZE);
|
||||
int chunkSizeZ = Mathf.CeilToInt((float)generationRange.z / ChunkCoord.CHUNK_SIZE);
|
||||
|
||||
Gizmos.color = new Color(0, 1, 0, 0.3f);
|
||||
|
||||
for (int cx = 0; cx < chunkSizeX; cx++)
|
||||
{
|
||||
for (int cy = 0; cy < chunkSizeY; cy++)
|
||||
{
|
||||
for (int cz = 0; cz < chunkSizeZ; cz++)
|
||||
{
|
||||
Vector3Int chunkGridOrigin = originGrid + new Vector3Int(
|
||||
cx * ChunkCoord.CHUNK_SIZE,
|
||||
-cy * ChunkCoord.CHUNK_SIZE,
|
||||
cz * ChunkCoord.CHUNK_SIZE
|
||||
);
|
||||
|
||||
Vector3 worldPos = bm.GridToWorld(chunkGridOrigin);
|
||||
// Offset to center of chunk
|
||||
worldPos += new Vector3(
|
||||
(ChunkCoord.CHUNK_SIZE - 1) * 0.5f,
|
||||
(ChunkCoord.CHUNK_SIZE - 1) * 0.5f,
|
||||
(ChunkCoord.CHUNK_SIZE - 1) * 0.5f
|
||||
);
|
||||
|
||||
Gizmos.DrawWireCube(worldPos, Vector3.one * ChunkCoord.CHUNK_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d90f6724f14d49489261782c9672f11
|
||||
680
Assets/Scripts/Underground/MineableChunk.cs
Normal file
680
Assets/Scripts/Underground/MineableChunk.cs
Normal file
@@ -0,0 +1,680 @@
|
||||
using System.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// NetworkBehaviour for a mineable chunk containing 4x4x4 blocks.
|
||||
/// Manages block state, mesh generation, damage, and item drops.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(MeshFilter))]
|
||||
[RequireComponent(typeof(MeshRenderer))]
|
||||
[RequireComponent(typeof(MeshCollider))]
|
||||
public class MineableChunk : NetworkBehaviour, ChunkMeshBuilder.INeighborProvider
|
||||
{
|
||||
[Header("Block Settings")]
|
||||
[SerializeField] private byte normalBlockHealth = 100;
|
||||
[SerializeField] private byte resourceBlockHealth = 150;
|
||||
|
||||
[Header("Drop Settings")]
|
||||
[SerializeField] private ItemData normalDropItem;
|
||||
[SerializeField] private ItemData resourceDropItem;
|
||||
[SerializeField] private GameObject genericDropPrefab;
|
||||
|
||||
[Header("Materials")]
|
||||
[SerializeField] private Material normalBlockMaterial;
|
||||
[SerializeField] private Material resourceBlockMaterial;
|
||||
|
||||
[Header("Visual Settings")]
|
||||
[SerializeField] private float meshRebuildDelay = 0.1f;
|
||||
|
||||
[Header("Highlight Settings")]
|
||||
[SerializeField] private Material highlightMaterial;
|
||||
private GameObject _highlightCube;
|
||||
private MeshRenderer _highlightRenderer;
|
||||
|
||||
[Header("Fog of War")]
|
||||
[SerializeField] private float visibilityTimeout = 0.5f; // Time before block goes from "visible" to "discovered"
|
||||
[SerializeField] private Color discoveredTint = new Color(0.3f, 0.3f, 0.3f, 1f); // Darkened color for discovered-but-not-visible
|
||||
private float[] _blockVisibilityTime; // Last time each block was visible (local only)
|
||||
private MaterialPropertyBlock _propBlock;
|
||||
|
||||
// Chunk state synced to all clients
|
||||
private NetworkVariable<ChunkState> _networkState = new NetworkVariable<ChunkState>(
|
||||
default,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
// Local cache for quick access
|
||||
private ChunkState _localState;
|
||||
private ChunkCoord _chunkCoord;
|
||||
private bool _isInitialized = false;
|
||||
|
||||
// Components
|
||||
private MeshFilter _meshFilter;
|
||||
private MeshRenderer _meshRenderer;
|
||||
private MeshCollider _meshCollider;
|
||||
private Outline _outline;
|
||||
|
||||
// Mesh rebuild state
|
||||
private bool _meshDirty = false;
|
||||
private Coroutine _rebuildCoroutine;
|
||||
|
||||
// Highlight state
|
||||
private int _highlightedBlockIndex = -1;
|
||||
|
||||
// Reference to chunk manager for neighbor queries
|
||||
private static ChunkedUndergroundGenerator _chunkManager;
|
||||
|
||||
public ChunkCoord Coord => _chunkCoord;
|
||||
public ChunkState State => _localState;
|
||||
|
||||
// Visibility check interval
|
||||
private float _lastVisibilityCheck;
|
||||
private const float VISIBILITY_CHECK_INTERVAL = 0.3f;
|
||||
private bool _hasVisibleBlocks = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_meshFilter = GetComponent<MeshFilter>();
|
||||
_meshRenderer = GetComponent<MeshRenderer>();
|
||||
_meshCollider = GetComponent<MeshCollider>();
|
||||
_outline = GetComponent<Outline>();
|
||||
|
||||
// Initialize local state
|
||||
_localState = ChunkState.CreateEmpty();
|
||||
|
||||
// Initialize visibility tracking (local only, not networked)
|
||||
_blockVisibilityTime = new float[ChunkState.BLOCKS_PER_CHUNK];
|
||||
_propBlock = new MaterialPropertyBlock();
|
||||
|
||||
// Set materials: 0=normal-visible, 1=resource-visible, 2=normal-dark, 3=resource-dark
|
||||
SetupMaterials();
|
||||
|
||||
// Create highlight cube for per-block highlighting
|
||||
CreateHighlightCube();
|
||||
}
|
||||
|
||||
private void SetupMaterials()
|
||||
{
|
||||
if (normalBlockMaterial != null && resourceBlockMaterial != null)
|
||||
{
|
||||
// Create darkened versions of materials for discovered-but-not-visible state
|
||||
Material normalDark = new Material(normalBlockMaterial);
|
||||
ApplyDarkTint(normalDark, normalBlockMaterial);
|
||||
|
||||
Material resourceDark = new Material(resourceBlockMaterial);
|
||||
ApplyDarkTint(resourceDark, resourceBlockMaterial);
|
||||
|
||||
_meshRenderer.materials = new Material[]
|
||||
{
|
||||
normalBlockMaterial, // Submesh 0: Normal blocks (visible)
|
||||
resourceBlockMaterial, // Submesh 1: Resource blocks (visible)
|
||||
normalDark, // Submesh 2: Normal blocks (discovered/dark)
|
||||
resourceDark // Submesh 3: Resource blocks (discovered/dark)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyDarkTint(Material darkMat, Material sourceMat)
|
||||
{
|
||||
// Try URP _BaseColor first, then fallback to _Color
|
||||
if (sourceMat.HasProperty("_BaseColor"))
|
||||
{
|
||||
Color baseColor = sourceMat.GetColor("_BaseColor");
|
||||
darkMat.SetColor("_BaseColor", baseColor * discoveredTint);
|
||||
}
|
||||
else if (sourceMat.HasProperty("_Color"))
|
||||
{
|
||||
Color baseColor = sourceMat.GetColor("_Color");
|
||||
darkMat.SetColor("_Color", baseColor * discoveredTint);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateHighlightCube()
|
||||
{
|
||||
_highlightCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
_highlightCube.name = "BlockHighlight";
|
||||
_highlightCube.transform.SetParent(transform);
|
||||
_highlightCube.transform.localScale = Vector3.one * 1.02f; // Slightly larger than block
|
||||
|
||||
// Remove collider - this is visual only
|
||||
var collider = _highlightCube.GetComponent<Collider>();
|
||||
if (collider != null) Destroy(collider);
|
||||
|
||||
// Setup renderer
|
||||
_highlightRenderer = _highlightCube.GetComponent<MeshRenderer>();
|
||||
if (highlightMaterial != null)
|
||||
{
|
||||
_highlightRenderer.material = highlightMaterial;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create default highlight material if none assigned
|
||||
var mat = new Material(Shader.Find("Standard"));
|
||||
mat.color = new Color(1f, 1f, 0f, 0.3f); // Yellow transparent
|
||||
mat.SetFloat("_Mode", 3); // Transparent mode
|
||||
mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
mat.SetInt("_ZWrite", 0);
|
||||
mat.DisableKeyword("_ALPHATEST_ON");
|
||||
mat.EnableKeyword("_ALPHABLEND_ON");
|
||||
mat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
|
||||
mat.renderQueue = 3000;
|
||||
_highlightRenderer.material = mat;
|
||||
}
|
||||
|
||||
_highlightCube.SetActive(false);
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// Calculate chunk coordinates from world position
|
||||
_chunkCoord = ChunkCoord.FromWorldPos(transform.position);
|
||||
|
||||
// Subscribe to state changes
|
||||
_networkState.OnValueChanged += OnChunkStateChanged;
|
||||
|
||||
// Initial sync
|
||||
SyncLocalState();
|
||||
RebuildMeshImmediate();
|
||||
|
||||
// Find chunk manager if not set
|
||||
if (_chunkManager == null)
|
||||
{
|
||||
_chunkManager = FindFirstObjectByType<ChunkedUndergroundGenerator>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Only check visibility timeout periodically and if we had visible blocks
|
||||
if (!_hasVisibleBlocks) return;
|
||||
if (Time.time - _lastVisibilityCheck < VISIBILITY_CHECK_INTERVAL) return;
|
||||
|
||||
_lastVisibilityCheck = Time.time;
|
||||
CheckVisibilityTimeout();
|
||||
}
|
||||
|
||||
private void CheckVisibilityTimeout()
|
||||
{
|
||||
bool anyStillVisible = false;
|
||||
bool needsRebuild = false;
|
||||
|
||||
for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++)
|
||||
{
|
||||
if (_blockVisibilityTime[i] <= 0) continue;
|
||||
|
||||
bool wasVisible = (Time.time - _blockVisibilityTime[i]) < visibilityTimeout;
|
||||
|
||||
if (wasVisible)
|
||||
{
|
||||
anyStillVisible = true;
|
||||
}
|
||||
else if (_blockVisibilityTime[i] > 0)
|
||||
{
|
||||
// Block just transitioned from visible to discovered
|
||||
// Check if it was visible last frame (within timeout + check interval)
|
||||
float timeSinceVisible = Time.time - _blockVisibilityTime[i];
|
||||
if (timeSinceVisible < visibilityTimeout + VISIBILITY_CHECK_INTERVAL * 2)
|
||||
{
|
||||
needsRebuild = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_hasVisibleBlocks = anyStillVisible;
|
||||
|
||||
if (needsRebuild)
|
||||
{
|
||||
ScheduleMeshRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
_networkState.OnValueChanged -= OnChunkStateChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize chunk with generated block data (called by generator on server)
|
||||
/// </summary>
|
||||
public void InitializeBlocks(ChunkState initialState)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
_networkState.Value = initialState;
|
||||
SyncLocalState();
|
||||
RebuildMeshImmediate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get block data at local coordinates
|
||||
/// </summary>
|
||||
public BlockData GetBlock(int x, int y, int z)
|
||||
{
|
||||
if (!ChunkCoord.IsValidLocal(x, y, z)) return BlockData.Empty;
|
||||
if (_localState.blocks == null) return BlockData.Empty;
|
||||
int index = ChunkCoord.LocalToIndex(x, y, z);
|
||||
return _localState.blocks[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get block data at local index
|
||||
/// </summary>
|
||||
public BlockData GetBlock(int index)
|
||||
{
|
||||
if (index < 0 || index >= ChunkState.BLOCKS_PER_CHUNK) return BlockData.Empty;
|
||||
if (_localState.blocks == null) return BlockData.Empty;
|
||||
return _localState.blocks[index];
|
||||
}
|
||||
|
||||
#region Damage System
|
||||
|
||||
/// <summary>
|
||||
/// Request damage to a specific block (called by clients)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server)]
|
||||
public void DamageBlockServerRpc(int localIndex, byte damage)
|
||||
{
|
||||
if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return;
|
||||
|
||||
var state = _networkState.Value;
|
||||
BlockData block = state.blocks[localIndex];
|
||||
|
||||
if (block.IsEmpty) return;
|
||||
|
||||
// Apply damage
|
||||
int newHealth = Mathf.Max(0, block.health - damage);
|
||||
block.health = (byte)newHealth;
|
||||
|
||||
// Update state
|
||||
state.blocks[localIndex] = block;
|
||||
_networkState.Value = state;
|
||||
|
||||
// Sync to clients
|
||||
UpdateBlockClientRpc(localIndex, block);
|
||||
|
||||
// Play hit effect
|
||||
PlayHitEffectClientRpc(localIndex);
|
||||
|
||||
// Check for destruction
|
||||
if (newHealth <= 0)
|
||||
{
|
||||
DestroyBlock(localIndex, state.blocks[localIndex].blockType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side damage application (for direct server calls)
|
||||
/// </summary>
|
||||
public void DamageBlock(int localIndex, float damageAmount)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
DamageBlockServerRpc(localIndex, (byte)Mathf.Min(255, damageAmount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a single block on all clients
|
||||
/// </summary>
|
||||
[ClientRpc]
|
||||
private void UpdateBlockClientRpc(int localIndex, BlockData block)
|
||||
{
|
||||
if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return;
|
||||
|
||||
_localState.blocks[localIndex] = block;
|
||||
|
||||
// Schedule mesh rebuild
|
||||
ScheduleMeshRebuild();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play hit visual effect
|
||||
/// </summary>
|
||||
[ClientRpc]
|
||||
private void PlayHitEffectClientRpc(int localIndex)
|
||||
{
|
||||
// Get block world position for effect
|
||||
Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex);
|
||||
Vector3 worldPos = _chunkCoord.LocalToWorld(localPos);
|
||||
|
||||
// TODO: Spawn particle effect at worldPos
|
||||
// For now, just a debug visualization
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle block destruction (server-side)
|
||||
/// </summary>
|
||||
private void DestroyBlock(int localIndex, byte blockType)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
// Spawn dropped item
|
||||
Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex);
|
||||
Vector3 worldPos = _chunkCoord.LocalToWorld(localPos);
|
||||
SpawnDrop(worldPos, blockType);
|
||||
|
||||
// Clear block
|
||||
var state = _networkState.Value;
|
||||
state.blocks[localIndex] = BlockData.Empty;
|
||||
_networkState.Value = state;
|
||||
|
||||
// Full sync and rebuild
|
||||
SyncStateClientRpc(state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn item drop at position
|
||||
/// </summary>
|
||||
private void SpawnDrop(Vector3 position, byte blockType)
|
||||
{
|
||||
if (genericDropPrefab == null) return;
|
||||
|
||||
ItemData dropItem = blockType == BlockData.TYPE_RESOURCE ? resourceDropItem : normalDropItem;
|
||||
if (dropItem == null) return;
|
||||
|
||||
GameObject dropObj = Instantiate(genericDropPrefab, position + Vector3.up * 0.5f, Quaternion.identity);
|
||||
NetworkObject netObj = dropObj.GetComponent<NetworkObject>();
|
||||
netObj.Spawn();
|
||||
|
||||
if (dropObj.TryGetComponent<DroppedItem>(out var droppedItem))
|
||||
{
|
||||
droppedItem.Initialize(dropItem.itemID);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fog of War
|
||||
|
||||
/// <summary>
|
||||
/// Check if block is currently visible (in player's sight right now)
|
||||
/// </summary>
|
||||
public bool IsBlockCurrentlyVisible(int localIndex)
|
||||
{
|
||||
if (_blockVisibilityTime == null || localIndex < 0 || localIndex >= _blockVisibilityTime.Length)
|
||||
return false;
|
||||
return (Time.time - _blockVisibilityTime[localIndex]) < visibilityTimeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update local visibility for blocks in range (called by local player)
|
||||
/// </summary>
|
||||
public void UpdateLocalVisibility(Vector3 playerPos, float radius)
|
||||
{
|
||||
bool needsRebuild = false;
|
||||
bool anyVisible = false;
|
||||
|
||||
for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++)
|
||||
{
|
||||
BlockData block = _localState.blocks[i];
|
||||
if (block.IsEmpty || !block.IsDiscovered) continue;
|
||||
|
||||
Vector3 blockWorldPos = GetBlockWorldPosition(i);
|
||||
bool wasVisible = IsBlockCurrentlyVisible(i);
|
||||
|
||||
if (Vector3.Distance(playerPos, blockWorldPos) <= radius)
|
||||
{
|
||||
_blockVisibilityTime[i] = Time.time;
|
||||
anyVisible = true;
|
||||
|
||||
// If block just became visible, need to rebuild mesh
|
||||
if (!wasVisible)
|
||||
{
|
||||
needsRebuild = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track if we have any visible blocks (for timeout checking in Update)
|
||||
_hasVisibleBlocks = anyVisible || _hasVisibleBlocks;
|
||||
|
||||
if (needsRebuild)
|
||||
{
|
||||
ScheduleMeshRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request reveal of a specific block (called by clients)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server)]
|
||||
public void RevealBlockServerRpc(int localIndex)
|
||||
{
|
||||
if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return;
|
||||
|
||||
var state = _networkState.Value;
|
||||
BlockData block = state.blocks[localIndex];
|
||||
|
||||
if (block.IsEmpty || block.IsDiscovered) return;
|
||||
|
||||
// Mark as discovered
|
||||
block.IsDiscovered = true;
|
||||
state.blocks[localIndex] = block;
|
||||
_networkState.Value = state;
|
||||
|
||||
// Sync to clients
|
||||
UpdateBlockClientRpc(localIndex, block);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reveal all blocks within radius of a world position
|
||||
/// </summary>
|
||||
public void RevealBlocksInRadius(Vector3 worldPos, float radius)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
bool anyRevealed = false;
|
||||
var state = _networkState.Value;
|
||||
|
||||
for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++)
|
||||
{
|
||||
BlockData block = state.blocks[i];
|
||||
if (block.IsEmpty || block.IsDiscovered) continue;
|
||||
|
||||
Vector3Int localPos = ChunkCoord.IndexToLocal(i);
|
||||
Vector3 blockWorldPos = _chunkCoord.LocalToWorld(localPos);
|
||||
|
||||
if (Vector3.Distance(worldPos, blockWorldPos) <= radius)
|
||||
{
|
||||
block.IsDiscovered = true;
|
||||
state.blocks[i] = block;
|
||||
anyRevealed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyRevealed)
|
||||
{
|
||||
_networkState.Value = state;
|
||||
SyncStateClientRpc(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if block at local index is discovered
|
||||
/// </summary>
|
||||
public bool IsBlockDiscovered(int localIndex)
|
||||
{
|
||||
if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return false;
|
||||
return _localState.blocks[localIndex].IsDiscovered;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mesh Building
|
||||
|
||||
/// <summary>
|
||||
/// Full state sync from server
|
||||
/// </summary>
|
||||
[ClientRpc]
|
||||
private void SyncStateClientRpc(ChunkState state)
|
||||
{
|
||||
_localState = state;
|
||||
RebuildMeshImmediate();
|
||||
}
|
||||
|
||||
private void OnChunkStateChanged(ChunkState oldState, ChunkState newState)
|
||||
{
|
||||
SyncLocalState();
|
||||
ScheduleMeshRebuild();
|
||||
}
|
||||
|
||||
private void SyncLocalState()
|
||||
{
|
||||
_localState = _networkState.Value;
|
||||
// Ensure blocks array is initialized
|
||||
if (_localState.blocks == null)
|
||||
{
|
||||
_localState = ChunkState.CreateEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleMeshRebuild()
|
||||
{
|
||||
if (_meshDirty) return;
|
||||
_meshDirty = true;
|
||||
|
||||
if (_rebuildCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_rebuildCoroutine);
|
||||
}
|
||||
_rebuildCoroutine = StartCoroutine(DelayedMeshRebuild());
|
||||
}
|
||||
|
||||
private IEnumerator DelayedMeshRebuild()
|
||||
{
|
||||
yield return new WaitForSeconds(meshRebuildDelay);
|
||||
RebuildMeshImmediate();
|
||||
_meshDirty = false;
|
||||
_rebuildCoroutine = null;
|
||||
}
|
||||
|
||||
private void RebuildMeshImmediate()
|
||||
{
|
||||
if (_meshFilter == null) return;
|
||||
|
||||
// Build visual mesh with fog of war (4 submeshes: normal-visible, resource-visible, normal-dark, resource-dark)
|
||||
Mesh visualMesh = ChunkMeshBuilder.BuildMeshWithVisibility(
|
||||
_localState,
|
||||
_chunkCoord,
|
||||
this,
|
||||
true, // only discovered
|
||||
IsBlockCurrentlyVisible // visibility checker
|
||||
);
|
||||
_meshFilter.mesh = visualMesh;
|
||||
|
||||
// Build collision mesh (all blocks, including undiscovered, no visibility distinction)
|
||||
if (_meshCollider != null)
|
||||
{
|
||||
Mesh collisionMesh = ChunkMeshBuilder.BuildMesh(_localState, _chunkCoord, this, false);
|
||||
_meshCollider.sharedMesh = null; // Force refresh
|
||||
_meshCollider.sharedMesh = collisionMesh;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region INeighborProvider Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Check if block at world grid position is solid (for cross-chunk face culling)
|
||||
/// </summary>
|
||||
public bool IsBlockSolid(Vector3Int gridPos)
|
||||
{
|
||||
// Check if position is in this chunk
|
||||
ChunkCoord coord = ChunkCoord.FromGridPos(gridPos);
|
||||
if (coord == _chunkCoord)
|
||||
{
|
||||
Vector3Int local = ChunkCoord.GridToLocal(gridPos);
|
||||
int index = ChunkCoord.LocalToIndex(local);
|
||||
var block = _localState.blocks[index];
|
||||
return !block.IsEmpty && block.IsDiscovered;
|
||||
}
|
||||
|
||||
// Query chunk manager for other chunks
|
||||
if (_chunkManager != null)
|
||||
{
|
||||
return _chunkManager.IsBlockSolid(gridPos);
|
||||
}
|
||||
|
||||
// Default: assume not solid at chunk boundaries
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interaction Support
|
||||
|
||||
/// <summary>
|
||||
/// Convert world hit point to local block index
|
||||
/// </summary>
|
||||
public int WorldPointToBlockIndex(Vector3 worldPoint)
|
||||
{
|
||||
// Convert world point to local space relative to chunk transform
|
||||
Vector3 localPoint = transform.InverseTransformPoint(worldPoint);
|
||||
|
||||
// Each block is 1 unit, centered at integer coordinates
|
||||
// So block at (0,0,0) spans from -0.5 to 0.5
|
||||
int lx = Mathf.Clamp(Mathf.RoundToInt(localPoint.x), 0, ChunkCoord.CHUNK_SIZE - 1);
|
||||
int ly = Mathf.Clamp(Mathf.RoundToInt(localPoint.y), 0, ChunkCoord.CHUNK_SIZE - 1);
|
||||
int lz = Mathf.Clamp(Mathf.RoundToInt(localPoint.z), 0, ChunkCoord.CHUNK_SIZE - 1);
|
||||
|
||||
return ChunkCoord.LocalToIndex(lx, ly, lz);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get world position of block at local index
|
||||
/// </summary>
|
||||
public Vector3 GetBlockWorldPosition(int localIndex)
|
||||
{
|
||||
Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex);
|
||||
// Convert local position to world using chunk transform
|
||||
return transform.TransformPoint(new Vector3(localPos.x, localPos.y, localPos.z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set highlight state for visual feedback
|
||||
/// </summary>
|
||||
public void SetHighlight(bool isOn, int blockIndex = -1)
|
||||
{
|
||||
_highlightedBlockIndex = isOn ? blockIndex : -1;
|
||||
|
||||
// Use per-block highlight cube instead of outline
|
||||
if (_highlightCube != null)
|
||||
{
|
||||
if (isOn && blockIndex >= 0)
|
||||
{
|
||||
// Position highlight at specific block
|
||||
Vector3Int localPos = ChunkCoord.IndexToLocal(blockIndex);
|
||||
_highlightCube.transform.localPosition = new Vector3(localPos.x, localPos.y, localPos.z);
|
||||
_highlightCube.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_highlightCube.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable outline component if present (we use highlight cube instead)
|
||||
if (_outline != null)
|
||||
{
|
||||
_outline.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the currently highlighted block index
|
||||
/// </summary>
|
||||
public int HighlightedBlockIndex => _highlightedBlockIndex;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Registration
|
||||
|
||||
public static void SetChunkManager(ChunkedUndergroundGenerator manager)
|
||||
{
|
||||
_chunkManager = manager;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
2
Assets/Scripts/Underground/MineableChunk.cs.meta
Normal file
2
Assets/Scripts/Underground/MineableChunk.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53bb340e9008b024ba035f6ee8fa21a4
|
||||
Reference in New Issue
Block a user