지하 최적화
블록 프리팹 단위 -> 블록 청크 단위 스폰 기타 건설, 조준 관련 사이드이펙트 버그 수정
This commit is contained in:
@@ -99,3 +99,13 @@ MonoBehaviour:
|
|||||||
SourcePrefabToOverride: {fileID: 0}
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
SourceHashToOverride: 0
|
SourceHashToOverride: 0
|
||||||
OverridingTargetPrefab: {fileID: 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
|
insertIndex: -1
|
||||||
addedObject: {fileID: 3912054216608015251}
|
addedObject: {fileID: 3912054216608015251}
|
||||||
m_SourcePrefab: {fileID: 100100000, guid: ffaf1ddb2ff58d2448ccfdd357387f63, type: 3}
|
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
|
--- !u!1 &2473992278589500093 stripped
|
||||||
GameObject:
|
GameObject:
|
||||||
m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: ffaf1ddb2ff58d2448ccfdd357387f63, type: 3}
|
m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: ffaf1ddb2ff58d2448ccfdd357387f63, type: 3}
|
||||||
@@ -341,7 +336,7 @@ MonoBehaviour:
|
|||||||
m_Bits: 25600
|
m_Bits: 25600
|
||||||
constructionLayer:
|
constructionLayer:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 256
|
m_Bits: 384
|
||||||
buildSpeedMultiplier: 2
|
buildSpeedMultiplier: 2
|
||||||
itemLayer:
|
itemLayer:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
@@ -359,7 +354,7 @@ MonoBehaviour:
|
|||||||
crosshairUI: {fileID: 0}
|
crosshairUI: {fileID: 0}
|
||||||
idleCrosshair: {fileID: 2628378444897590106, guid: 174f7cb20aaa6d4409b788a700a925ad, type: 3}
|
idleCrosshair: {fileID: 2628378444897590106, guid: 174f7cb20aaa6d4409b788a700a925ad, type: 3}
|
||||||
targetCrosshair: {fileID: -5662625722731528258, guid: 7652364ca249c3144813de7eb3d1b129, type: 3}
|
targetCrosshair: {fileID: -5662625722731528258, guid: 7652364ca249c3144813de7eb3d1b129, type: 3}
|
||||||
visionRadius: 2
|
visionRadius: 5
|
||||||
--- !u!114 &106528027568436521
|
--- !u!114 &106528027568436521
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -506,4 +501,8 @@ MonoBehaviour:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::PlayerEquipmentHandler
|
m_EditorClassIdentifier: Assembly-CSharp::PlayerEquipmentHandler
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
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
|
insertIndex: -1
|
||||||
addedObject: {fileID: 445606028}
|
addedObject: {fileID: 445606028}
|
||||||
m_SourcePrefab: {fileID: 100100000, guid: 443aa97110814434cb36b26656f1884c, type: 3}
|
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
|
--- !u!1 &1044242050
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -3031,86 +3112,6 @@ CanvasRenderer:
|
|||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1600458457}
|
m_GameObject: {fileID: 1600458457}
|
||||||
m_CullTransparentMesh: 1
|
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
|
--- !u!20 &1648146738 stripped
|
||||||
Camera:
|
Camera:
|
||||||
m_CorrespondingSourceObject: {fileID: 5650099317679730308, guid: 2b08dd32e48ef5e4aa65a6122099152e, type: 3}
|
m_CorrespondingSourceObject: {fileID: 5650099317679730308, guid: 2b08dd32e48ef5e4aa65a6122099152e, type: 3}
|
||||||
@@ -3809,6 +3810,71 @@ Transform:
|
|||||||
m_CorrespondingSourceObject: {fileID: 2338240775821095493, guid: 1955bdf7dd2940f44aa117fbcf6eb626, type: 3}
|
m_CorrespondingSourceObject: {fileID: 2338240775821095493, guid: 1955bdf7dd2940f44aa117fbcf6eb626, type: 3}
|
||||||
m_PrefabInstance: {fileID: 497942047}
|
m_PrefabInstance: {fileID: 497942047}
|
||||||
m_PrefabAsset: {fileID: 0}
|
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
|
--- !u!1001 &3690888448170635710
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -3930,10 +3996,11 @@ SceneRoots:
|
|||||||
- {fileID: 743367988}
|
- {fileID: 743367988}
|
||||||
- {fileID: 670724422}
|
- {fileID: 670724422}
|
||||||
- {fileID: 1409253547}
|
- {fileID: 1409253547}
|
||||||
- {fileID: 1634635645}
|
|
||||||
- {fileID: 437093202}
|
- {fileID: 437093202}
|
||||||
- {fileID: 1489230404}
|
- {fileID: 1489230404}
|
||||||
- {fileID: 556982644}
|
- {fileID: 556982644}
|
||||||
- {fileID: 2067098344}
|
- {fileID: 2067098344}
|
||||||
- {fileID: 1384281111}
|
- {fileID: 1384281111}
|
||||||
- {fileID: 1782529044}
|
- {fileID: 1782529044}
|
||||||
|
- {fileID: 320979913694462859}
|
||||||
|
- {fileID: 1026559333}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using UnityEngine;
|
|||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine.InputSystem;
|
using UnityEngine.InputSystem;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
|
using UnityEngine.UI;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
public class BuildManager : NetworkBehaviour
|
public class BuildManager : NetworkBehaviour
|
||||||
@@ -36,6 +37,9 @@ public class BuildManager : NetworkBehaviour
|
|||||||
private Vector3Int _currentGridPos;
|
private Vector3Int _currentGridPos;
|
||||||
private PlayerInputActions _inputActions;
|
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 Dictionary<Vector3Int, TunnelNode> _tunnelRegistry = new Dictionary<Vector3Int, TunnelNode>();
|
||||||
private HashSet<Vector3Int> _occupiedNodes = new HashSet<Vector3Int>();
|
private HashSet<Vector3Int> _occupiedNodes = new HashSet<Vector3Int>();
|
||||||
|
|
||||||
@@ -139,10 +143,59 @@ public class BuildManager : NetworkBehaviour
|
|||||||
return _tunnelRegistry.GetValueOrDefault(pos);
|
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를 넘겨줍니다.
|
// 1. 건설 요청 시 실제 계산된 worldPos를 넘겨줍니다.
|
||||||
private void OnBuildRequested()
|
private void OnBuildRequested()
|
||||||
{
|
{
|
||||||
if (!_isBuildMode || EventSystem.current.IsPointerOverGameObject()) return;
|
if (!_isBuildMode) return;
|
||||||
|
if (IsPointerOverUI()) return;
|
||||||
|
|
||||||
// 고스트가 현재 위치한 '그 좌표'를 그대로 보냅니다.
|
// 고스트가 현재 위치한 '그 좌표'를 그대로 보냅니다.
|
||||||
RequestBuildRpc(_selectedTurretIndex, _currentGridPos, _ghostInstance.transform.position);
|
RequestBuildRpc(_selectedTurretIndex, _currentGridPos, _ghostInstance.transform.position);
|
||||||
@@ -153,12 +206,30 @@ public class BuildManager : NetworkBehaviour
|
|||||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||||
private void RequestBuildRpc(int index, Vector3Int gridPos, Vector3 worldPos)
|
private void RequestBuildRpc(int index, Vector3Int gridPos, Vector3 worldPos)
|
||||||
{
|
{
|
||||||
|
if (constructionSitePrefab == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[BuildManager] Construction site prefab is null!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// GridToWorld를 다시 계산하지 않고 전달받은 worldPos를 그대로 사용합니다.
|
// GridToWorld를 다시 계산하지 않고 전달받은 worldPos를 그대로 사용합니다.
|
||||||
GameObject siteObj = Instantiate(constructionSitePrefab, worldPos, Quaternion.identity);
|
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>();
|
ConstructionSite site = siteObj.GetComponent<ConstructionSite>();
|
||||||
if (site != null) site.Initialize(index, gridPos);
|
if (site != null)
|
||||||
|
{
|
||||||
|
site.Initialize(index, gridPos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SelectTurret(int index)
|
public void SelectTurret(int index)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mining behavior for pickaxes and similar tools.
|
/// Mining behavior for pickaxes and similar tools.
|
||||||
|
/// Supports both legacy MineableBlock and new chunk-based MineableChunk.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CreateAssetMenu(menuName = "Items/Behaviors/Mining Behavior")]
|
[CreateAssetMenu(menuName = "Items/Behaviors/Mining Behavior")]
|
||||||
public class MiningBehavior : ItemBehavior
|
public class MiningBehavior : ItemBehavior
|
||||||
@@ -27,7 +28,25 @@ public class MiningBehavior : ItemBehavior
|
|||||||
{
|
{
|
||||||
if (target == null) return;
|
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))
|
if (target.TryGetComponent<IDamageable>(out var damageable))
|
||||||
{
|
{
|
||||||
damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, user));
|
damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, user));
|
||||||
@@ -37,8 +56,15 @@ public class MiningBehavior : ItemBehavior
|
|||||||
public override string GetBlockedReason(GameObject user, GameObject target)
|
public override string GetBlockedReason(GameObject user, GameObject target)
|
||||||
{
|
{
|
||||||
if (target == null) return "No 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 _))
|
if (!target.TryGetComponent<IDamageable>(out _))
|
||||||
return "Cannot mine this object";
|
return "Cannot mine this object";
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,13 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
private PlayerActionHandler _actionHandler;
|
private PlayerActionHandler _actionHandler;
|
||||||
|
|
||||||
private RectTransform _crosshairRect;
|
private RectTransform _crosshairRect;
|
||||||
private MineableBlock _currentTargetBlock; // 현재 강조 중인 블록 저장
|
private MineableBlock _currentTargetBlock; // 현재 강조 중인 블록 저장 (legacy)
|
||||||
private MineableBlock _lastHighlightedBlock;
|
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 CharacterController _controller;
|
||||||
private PlayerInputActions _inputActions;
|
private PlayerInputActions _inputActions;
|
||||||
@@ -209,13 +214,17 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
{
|
{
|
||||||
if (!IsOwner || _actionHandler.IsBusy) return;
|
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();
|
ItemData selectedItem = _inventory.GetSelectedItemData();
|
||||||
if (selectedItem == null) return;
|
if (selectedItem == null) return;
|
||||||
|
|
||||||
// Check if item has behavior (new system)
|
// Check if item has behavior (new system)
|
||||||
if (selectedItem.behavior != null)
|
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
|
// Use the new behavior system
|
||||||
if (selectedItem.CanUse(gameObject, target))
|
if (selectedItem.CanUse(gameObject, target))
|
||||||
@@ -334,51 +343,87 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
{
|
{
|
||||||
if (!IsOwner || _crosshairRect == null) return;
|
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);
|
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;
|
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. 하이라이트 대상 업데이트
|
// Filter by actual attack range from player
|
||||||
MineableBlock currentTarget = null;
|
|
||||||
if (hasTarget)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 대상이 바뀌었을 때만 아웃라인 갱신 (최적화)
|
// 4. 하이라이트 대상 업데이트 - 청크 시스템과 레거시 블록 모두 지원
|
||||||
if (_lastHighlightedBlock != currentTarget)
|
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 (_lastHighlightedBlock != null) _lastHighlightedBlock.SetHighlight(false);
|
||||||
if (currentTarget != null) currentTarget.SetHighlight(true);
|
if (currentLegacyTarget != null) currentLegacyTarget.SetHighlight(true);
|
||||||
_lastHighlightedBlock = currentTarget;
|
_lastHighlightedBlock = currentLegacyTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 기즈모 디버그 데이터 동기화
|
// 기즈모 디버그 데이터 동기화
|
||||||
_debugOrigin = rayStart;
|
Ray debugRay = Camera.main.ScreenPointToRay(_crosshairRect.position);
|
||||||
_debugDir = direction;
|
_debugOrigin = debugRay.origin;
|
||||||
_debugHit = hasTarget;
|
_debugDir = debugRay.direction;
|
||||||
_debugDist = hasTarget ? blockHit.distance : (attackRange - 0.4f);
|
_debugHit = hasTarget && (currentChunk != null || currentLegacyTarget != null);
|
||||||
|
_debugDist = hasTarget ? blockHit.distance : attackRange;
|
||||||
|
|
||||||
// 크로스헤어 이미지 교체
|
// 크로스헤어 이미지 교체
|
||||||
|
bool hasValidTarget = currentChunk != null || currentLegacyTarget != null;
|
||||||
if (crosshairUI != null)
|
if (crosshairUI != null)
|
||||||
{
|
{
|
||||||
crosshairUI.sprite = hasTarget ? targetCrosshair : idleCrosshair;
|
crosshairUI.sprite = hasValidTarget ? targetCrosshair : idleCrosshair;
|
||||||
crosshairUI.color = hasTarget ? Color.green : Color.white;
|
crosshairUI.color = hasValidTarget ? Color.green : Color.white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,11 +465,33 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
|
|
||||||
private void RevealSurroundings()
|
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)
|
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))
|
if (col.TryGetComponent<MineableBlock>(out var block))
|
||||||
{
|
{
|
||||||
// 1. [로컬] 내 화면에서 이 블록을 보이게 함 (실시간 시야)
|
// 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)
|
private IEnumerator ActionRoutine(float duration, string animTrigger, Action actionLogic)
|
||||||
{
|
{
|
||||||
// 1. 상태 잠금
|
// 1. 상태 잠금
|
||||||
@@ -516,6 +595,9 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
{
|
{
|
||||||
if (_actionHandler.IsBusy) return;
|
if (_actionHandler.IsBusy) return;
|
||||||
|
|
||||||
|
// Don't perform actions when in build mode
|
||||||
|
if (BuildManager.Instance != null && BuildManager.Instance.IsBuildMode) return;
|
||||||
|
|
||||||
ItemData selectedItem = _inventory.GetSelectedItemData();
|
ItemData selectedItem = _inventory.GetSelectedItemData();
|
||||||
if (selectedItem == null || selectedItem.behavior == null) return;
|
if (selectedItem == null || selectedItem.behavior == null) return;
|
||||||
|
|
||||||
@@ -525,7 +607,8 @@ public class PlayerNetworkController : NetworkBehaviour
|
|||||||
// Skip if non-repeatable action already executed once
|
// Skip if non-repeatable action already executed once
|
||||||
if (!actionDesc.CanRepeat && _hasExecutedOnce) return;
|
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))
|
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()
|
private void OnDrawGizmos()
|
||||||
{
|
{
|
||||||
if (!Application.isPlaying || !IsOwner) return;
|
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
|
||||||
@@ -23,7 +23,7 @@ TagManager:
|
|||||||
- Mineable
|
- Mineable
|
||||||
- TunnelNode
|
- TunnelNode
|
||||||
- DroppedItem
|
- DroppedItem
|
||||||
-
|
- Wall
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
|
|||||||
Reference in New Issue
Block a user